diff --git a/.env.deployment.example b/.env.deployment.example new file mode 100644 index 000000000..9ba86c60e --- /dev/null +++ b/.env.deployment.example @@ -0,0 +1,29 @@ +# Used by the multi-chain deployment script + +# General +export MNEMONIC="YOUR_MNEMONIC" + +# RPC URLs +export ARBITRUM_RPC_URL="YOUR_RPC_URL" +export ARBITRUM_SEPOLIA_RPC_URL="YOUR_RPC_URL" +export AVALANCHE_RPC_URL="YOUR_RPC_URL" +export BASE_RPC_URL="YOUR_RPC_URL" +export BNB_RPC_URL="YOUR_RPC_URL" +export GNOSIS_RPC_URL="YOUR_RPC_URL" +export MAINNET_RPC_URL="YOUR_RPC_URL" +export OPTIMISM_RPC_URL="YOUR_RPC_URL" +export POLYGON_RPC_URL="YOUR_RPC_URL" +export SCROLL_RPC_URL="YOUR_RPC_URL" +export SEPOLIA_RPC_URL="YOUR_RPC_URL" + +# Etherscan API keys +export ARBISCAN_API_KEY="YOUR_API_KEY" +export BASESCAN_API_KEY="YOUR_API_KEY" +export BSCSCAN_API_KEY="YOUR_API_KEY" +export ETHERSCAN_API_KEY="YOUR_API_KEY" +export GNOSISSCAN_API_KEY="YOUR_API_KEY" +export OPTIMISTIC_API_KEY="YOUR_API_KEY" +export POLYGONSCAN_API_KEY="YOUR_API_KEY" +export SCROLLSCAN_API_KEY="YOUR_API_KEY" +export SNOWTRACE_API_KEY="YOUR_API_KEY" + diff --git a/.env.example b/.env.example index 8e4a32221..9a1fd9318 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,6 @@ -export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" -export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" -export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" -export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" export API_KEY_INFURA="YOUR_API_KEY_INFURA" -export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" -export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" -export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" -export RPC_URL_MAINNET="YOUR_RPC_URL_MAINNET" +export EOA="YOUR_EOA_ADDRESS" export FOUNDRY_PROFILE="lite" export MNEMONIC="YOUR_MNEMONIC" +export RPC_URL_MAINNET="YOUR_RPC_URL_MAINNET" + diff --git a/.gas-snapshot b/.gas-snapshot deleted file mode 100644 index 22f579f78..000000000 --- a/.gas-snapshot +++ /dev/null @@ -1,413 +0,0 @@ -Burn_LockupDynamic_Integration_Concrete_Test:test_Burn_CallerApprovedOperator() (gas: 87632) -Burn_LockupDynamic_Integration_Concrete_Test:test_Burn_CallerNFTOwner() (gas: 78216) -Burn_LockupDynamic_Integration_Concrete_Test:test_Burn_NonTransferableNFT() (gas: 78225) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_NFTDoesNotExist() (gas: 79430) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11325) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 90312) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusPending() (gas: 14289) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 19525) -Burn_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusStreaming() (gas: 19561) -Burn_LockupLinear_Integration_Concrete_Test:test_Burn_CallerApprovedOperator() (gas: 87770) -Burn_LockupLinear_Integration_Concrete_Test:test_Burn_CallerNFTOwner() (gas: 78343) -Burn_LockupLinear_Integration_Concrete_Test:test_Burn_NonTransferableNFT() (gas: 78352) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_NFTDoesNotExist() (gas: 79539) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11311) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 81013) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusPending() (gas: 14275) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 19511) -Burn_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusStreaming() (gas: 19547) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_CancelMultiple() (gas: 833554) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_CancelMultiple_ArrayCountZero() (gas: 6271) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_AllStreamsCold() (gas: 32346) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_AllStreamsNotCancelable() (gas: 859532) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_OnlyNull() (gas: 12362) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_SomeNull() (gas: 78556) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_SomeStreamsCold() (gas: 341289) -CancelMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_SomeStreamsNotCancelable() (gas: 946037) -CancelMultiple_LockupDynamic_Integration_Fuzz_Test:testFuzz_CancelMultiple(uint256,uint40) (runs: 50, μ: 1197307, ~: 1201972) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_CancelMultiple() (gas: 565501) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_CancelMultiple_ArrayCountZero() (gas: 6294) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_AllStreamsCold() (gas: 32494) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_AllStreamsNotCancelable() (gas: 572744) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_OnlyNull() (gas: 12391) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_SomeNull() (gas: 78577) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_SomeStreamsCold() (gas: 245380) -CancelMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_SomeStreamsNotCancelable() (gas: 657233) -CancelMultiple_LockupLinear_Integration_Fuzz_Test:testFuzz_CancelMultiple(uint256,uint40) (runs: 50, μ: 797110, ~: 798213) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel() (gas: 386274) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel_RecipientDoesNotImplementHook() (gas: 371426) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel_RecipientNotContract() (gas: 97176) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel_RecipientReentrancy() (gas: 373617) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel_RecipientReverts() (gas: 371993) -Cancel_LockupDynamic_Integration_Concrete_Test:test_Cancel_StatusPending() (gas: 76401) -Cancel_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11321) -Cancel_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 87398) -Cancel_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusDepleted() (gas: 68085) -Cancel_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 27022) -Cancel_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamNotCancelable() (gas: 261520) -Cancel_LockupDynamic_Integration_Fuzz_Test:testFuzz_Cancel(uint256,uint128) (runs: 50, μ: 453577, ~: 454727) -Cancel_LockupDynamic_Integration_Fuzz_Test:testFuzz_Cancel_StatusPending(uint256) (runs: 50, μ: 76907, ~: 76982) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel() (gas: 269481) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel_RecipientDoesNotImplementHook() (gas: 254609) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel_RecipientNotContract() (gas: 77523) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel_RecipientReentrancy() (gas: 256789) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel_RecipientReverts() (gas: 255176) -Cancel_LockupLinear_Integration_Concrete_Test:test_Cancel_StatusPending() (gas: 76441) -Cancel_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11307) -Cancel_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 78102) -Cancel_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusDepleted() (gas: 68238) -Cancel_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 27130) -Cancel_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamNotCancelable() (gas: 185647) -Cancel_LockupLinear_Integration_Fuzz_Test:testFuzz_Cancel(uint256,uint128) (runs: 50, μ: 309387, ~: 310054) -Cancel_LockupLinear_Integration_Fuzz_Test:testFuzz_Cancel_StatusPending(uint256) (runs: 50, μ: 76947, ~: 77022) -ClaimProtocolRevenues_LockupDynamic_Integration_Concrete_Test:test_ClaimProtocolRevenues() (gas: 319728) -ClaimProtocolRevenues_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_ProtocolRevenuesZero() (gas: 18907) -ClaimProtocolRevenues_LockupLinear_Integration_Concrete_Test:test_ClaimProtocolRevenues() (gas: 246456) -ClaimProtocolRevenues_LockupLinear_Integration_Concrete_Test:test_RevertGiven_ProtocolRevenuesZero() (gas: 18915) -Constructor_LockupDynamic_Integration_Concrete_Test:test_Constructor() (gas: 5374061) -Constructor_LockupLinear_Integration_Concrete_Test:test_Constructor() (gas: 4181201) -CreateWithDeltas_LockupDynamic_Integration_Concrete_Test:test_CreateWithDeltas() (gas: 380621) -CreateWithDeltas_LockupDynamic_Integration_Fuzz_Test:testFuzz_CreateWithDeltas((uint128,uint64,uint40)[]) (runs: 50, μ: 4100448, ~: 3558725) -CreateWithDurations_LockupLinear_Integration_Concrete_Test:test_CreateWithDurations() (gas: 287592) -CreateWithDurations_LockupLinear_Integration_Fuzz_Test:testFuzz_CreateWithDurations((uint40,uint40)) (runs: 50, μ: 286442, ~: 286543) -CreateWithMilestones_LockupDynamic_Integration_Concrete_Test:test_CreateWithMilestones() (gas: 370968) -CreateWithMilestones_LockupDynamic_Integration_Concrete_Test:test_CreateWithMilestones_AssetMissingReturnValue() (gas: 377756) -CreateWithMilestones_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_EndTimeNotInTheFuture() (gas: 47537) -CreateWithMilestones_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_ProtocolFeeTooHigh() (gas: 58079) -CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test:testFuzz_CreateWithMilestones(address,(address,uint40,bool,bool,address,uint128,address,(address,uint256),(uint128,uint64,uint40)[]),uint256) (runs: 50, μ: 3967005, ~: 3991455) -CreateWithRange_LockupLinear_Integration_Concrete_Test:test_CreateWithRange() (gas: 282990) -CreateWithRange_LockupLinear_Integration_Concrete_Test:test_CreateWithRange_AssetMissingReturnValue() (gas: 289757) -CreateWithRange_LockupLinear_Integration_Concrete_Test:test_RevertGiven_EndTimeNotInTheFuture() (gas: 41154) -CreateWithRange_LockupLinear_Integration_Concrete_Test:test_RevertGiven_ProtocolFeeTooHigh() (gas: 51749) -CreateWithRange_LockupLinear_Integration_Fuzz_Test:testFuzz_CreateWithRange(address,(address,address,uint128,address,bool,bool,(uint40,uint40,uint40),(address,uint256)),uint256) (runs: 50, μ: 371749, ~: 379742) -FlashFee_Integration_Concrete_Test:test_FlashFee() (gas: 50968) -FlashFee_Integration_Concrete_Test:test_RevertGiven_AssetNotFlashLoanable() (gas: 18626) -FlashFee_Integration_Fuzz_Test:testFuzz_FlashFee(uint256,uint256) (runs: 50, μ: 51839, ~: 52081) -FlashLoanFunction_Integration_Concrete_Test:test_FlashLoan() (gas: 402140) -FlashLoanFunction_Integration_Concrete_Test:test_RevertGiven_AssetNotFlashLoanable() (gas: 21603) -FlashLoanFunction_Integration_Fuzz_Test:testFuzz_FlashLoanFunction(uint256,uint128,bytes) (runs: 50, μ: 403153, ~: 406890) -GenerateAccentColor_Integration_Concrete_Test:test_GenerateAccentColor() (gas: 13215) -GetAsset_LockupDynamic_Integration_Concrete_Test:test_GetAsset() (gas: 307759) -GetAsset_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12049) -GetAsset_LockupLinear_Integration_Concrete_Test:test_GetAsset() (gas: 234467) -GetAsset_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12035) -GetCliffTime_LockupLinear_Integration_Concrete_Test:test_GetCliffTime() (gas: 234951) -GetCliffTime_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11392) -GetDepositedAmount_LockupDynamic_Integration_Concrete_Test:test_GetDepositedAmount() (gas: 310556) -GetDepositedAmount_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11682) -GetDepositedAmount_LockupLinear_Integration_Concrete_Test:test_GetDepositedAmount() (gas: 237238) -GetDepositedAmount_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11678) -GetEndTime_LockupDynamic_Integration_Concrete_Test:test_GetEndTime() (gas: 310354) -GetEndTime_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11538) -GetEndTime_LockupLinear_Integration_Concrete_Test:test_GetEndTime() (gas: 237090) -GetEndTime_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11546) -GetRange_LockupDynamic_Integration_Concrete_Test:test_GetRange() (gas: 309780) -GetRange_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 13125) -GetRange_LockupLinear_Integration_Concrete_Test:test_GetRange() (gas: 237308) -GetRange_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 13308) -GetRecipient_LockupDynamic_Integration_Concrete_Test:test_GetRecipient() (gas: 12585) -GetRecipient_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_NFTBurned() (gas: 72524) -GetRecipient_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 10989) -GetRecipient_LockupLinear_Integration_Concrete_Test:test_GetRecipient() (gas: 12565) -GetRecipient_LockupLinear_Integration_Concrete_Test:test_RevertGiven_NFTBurned() (gas: 72651) -GetRecipient_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 10993) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StatusDepleted() (gas: 362867) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StatusPending() (gas: 332698) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StatusSettled() (gas: 337906) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StatusStreaming() (gas: 337920) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StreamHasBeenCanceled_StatusCanceled() (gas: 377552) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_GetRefundedAmount_StreamHasBeenCanceled_StatusDepleted() (gas: 399390) -GetRefundedAmount_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12045) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StatusDepleted() (gas: 287754) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StatusPending() (gas: 257418) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StatusSettled() (gas: 262626) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StatusStreaming() (gas: 262640) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StreamHasBeenCanceled_StatusCanceled() (gas: 298987) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_GetRefundedAmount_StreamHasBeenCanceled_StatusDepleted() (gas: 320748) -GetRefundedAmount_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12031) -GetSegments_LockupDynamic_Integration_Concrete_Test:test_GetSegments() (gas: 315258) -GetSegments_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 13758) -GetSender_LockupDynamic_Integration_Concrete_Test:test_GetSender() (gas: 307477) -GetSender_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11814) -GetSender_LockupLinear_Integration_Concrete_Test:test_GetSender() (gas: 234201) -GetSender_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11816) -GetStartTime_LockupDynamic_Integration_Concrete_Test:test_GetStartTime() (gas: 310683) -GetStartTime_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11823) -GetStartTime_LockupLinear_Integration_Concrete_Test:test_GetStartTime() (gas: 237413) -GetStartTime_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11831) -GetStream_LockupDynamic_Integration_Concrete_Test:test_GetStream() (gas: 278720) -GetStream_LockupDynamic_Integration_Concrete_Test:test_GetStream_StatusSettled() (gas: 52024) -GetStream_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 15544) -GetStream_LockupLinear_Integration_Concrete_Test:test_GetStream() (gas: 34920) -GetStream_LockupLinear_Integration_Concrete_Test:test_GetStream_StatusSettled() (gas: 39431) -GetStream_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 14299) -GetWithdrawnAmount_LockupDynamic_Integration_Concrete_Test:test_GetWithdrawnAmount() (gas: 384696) -GetWithdrawnAmount_LockupDynamic_Integration_Concrete_Test:test_GetWithdrawnAmount_NoPreviousWithdrawals() (gas: 335880) -GetWithdrawnAmount_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12012) -GetWithdrawnAmount_LockupDynamic_Integration_Fuzz_Test:testFuzz_GetWithdrawnAmount(uint256,uint128) (runs: 50, μ: 387702, ~: 388140) -GetWithdrawnAmount_LockupDynamic_Integration_Fuzz_Test:testFuzz_GetWithdrawnAmount_NoPreviousWithdrawals(uint256) (runs: 50, μ: 337638, ~: 337804) -GetWithdrawnAmount_LockupLinear_Integration_Concrete_Test:test_GetWithdrawnAmount() (gas: 282118) -GetWithdrawnAmount_LockupLinear_Integration_Concrete_Test:test_GetWithdrawnAmount_NoPreviousWithdrawals() (gas: 262600) -GetWithdrawnAmount_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11998) -GetWithdrawnAmount_LockupLinear_Integration_Fuzz_Test:testFuzz_GetWithdrawnAmount(uint256,uint128) (runs: 50, μ: 285328, ~: 285260) -GetWithdrawnAmount_LockupLinear_Integration_Fuzz_Test:testFuzz_GetWithdrawnAmount_NoPreviousWithdrawals(uint256) (runs: 50, μ: 264330, ~: 264524) -IsCancelable_LockupDynamic_Integration_Concrete_Test:test_IsCancelable() (gas: 515865) -IsCancelable_LockupDynamic_Integration_Concrete_Test:test_IsCancelable_Cold() (gas: 336103) -IsCancelable_LockupDynamic_Integration_Concrete_Test:test_IsCancelable_StreamCancelable() (gas: 327347) -IsCancelable_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11239) -IsCancelable_LockupLinear_Integration_Concrete_Test:test_IsCancelable() (gas: 372779) -IsCancelable_LockupLinear_Integration_Concrete_Test:test_IsCancelable_Cold() (gas: 262975) -IsCancelable_LockupLinear_Integration_Concrete_Test:test_IsCancelable_StreamCancelable() (gas: 254102) -IsCancelable_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11263) -IsCold_LockupDynamic_Integration_Concrete_Test:test_IsCold_StatusCanceled() (gas: 376250) -IsCold_LockupDynamic_Integration_Concrete_Test:test_IsCold_StatusDepleted() (gas: 362513) -IsCold_LockupDynamic_Integration_Concrete_Test:test_IsCold_StatusPending() (gas: 330604) -IsCold_LockupDynamic_Integration_Concrete_Test:test_IsCold_StatusSettled() (gas: 336374) -IsCold_LockupDynamic_Integration_Concrete_Test:test_IsCold_StatusStreaming() (gas: 352684) -IsCold_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11525) -IsCold_LockupLinear_Integration_Concrete_Test:test_IsCold_StatusCanceled() (gas: 297736) -IsCold_LockupLinear_Integration_Concrete_Test:test_IsCold_StatusDepleted() (gas: 287458) -IsCold_LockupLinear_Integration_Concrete_Test:test_IsCold_StatusPending() (gas: 257382) -IsCold_LockupLinear_Integration_Concrete_Test:test_IsCold_StatusSettled() (gas: 263264) -IsCold_LockupLinear_Integration_Concrete_Test:test_IsCold_StatusStreaming() (gas: 263799) -IsCold_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11568) -IsDepleted_LockupDynamic_Integration_Concrete_Test:test_IsDepleted() (gas: 361951) -IsDepleted_LockupDynamic_Integration_Concrete_Test:test_IsDepleted_StreamNotDepleted() (gas: 326771) -IsDepleted_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11191) -IsDepleted_LockupLinear_Integration_Concrete_Test:test_IsDepleted() (gas: 286858) -IsDepleted_LockupLinear_Integration_Concrete_Test:test_IsDepleted_StreamNotDepleted() (gas: 253511) -IsDepleted_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11212) -IsStream_LockupDynamic_Integration_Concrete_Test:test_IsStream() (gas: 327111) -IsStream_LockupDynamic_Integration_Concrete_Test:test_IsStream_Null() (gas: 8527) -IsStream_LockupLinear_Integration_Concrete_Test:test_IsStream() (gas: 253873) -IsStream_LockupLinear_Integration_Concrete_Test:test_IsStream_Null() (gas: 8570) -IsTransferable_LockupDynamic_Integration_Concrete_Test:test_IsTransferable_Stream() (gas: 327273) -IsTransferable_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11674) -IsTransferable_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamTransferNotEnabled() (gas: 515864) -IsTransferable_LockupLinear_Integration_Concrete_Test:test_IsTransferable_Stream() (gas: 254057) -IsTransferable_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11739) -IsTransferable_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamTransferNotEnabled() (gas: 372834) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_IsWarm_StatusCanceled() (gas: 375788) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_IsWarm_StatusDepleted() (gas: 362027) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_IsWarm_StatusPending() (gas: 330041) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_IsWarm_StatusSettled() (gas: 335985) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_IsWarm_StatusStreaming() (gas: 352169) -IsWarm_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11085) -IsWarm_LockupLinear_Integration_Concrete_Test:test_IsWarm_StatusCanceled() (gas: 297243) -IsWarm_LockupLinear_Integration_Concrete_Test:test_IsWarm_StatusDepleted() (gas: 286934) -IsWarm_LockupLinear_Integration_Concrete_Test:test_IsWarm_StatusPending() (gas: 256781) -IsWarm_LockupLinear_Integration_Concrete_Test:test_IsWarm_StatusSettled() (gas: 262847) -IsWarm_LockupLinear_Integration_Concrete_Test:test_IsWarm_StatusStreaming() (gas: 263246) -IsWarm_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11106) -MapSymbol_Integration_Concrete_Test:test_MapSymbol_LockupDynamic() (gas: 16959) -MapSymbol_Integration_Concrete_Test:test_MapSymbol_LockupLinear() (gas: 16733) -MapSymbol_Integration_Concrete_Test:test_RevertGiven_UnknownNFT() (gas: 1039753) -MaxFlashLoan_Integration_Concrete_Test:test_MaxFlashLoan() (gas: 178987) -MaxFlashLoan_Integration_Concrete_Test:test_MaxFlashLoan_AssetNotFlashLoanable() (gas: 15248) -MaxFlashLoan_Integration_Fuzz_Test:testFuzz_MaxFlashLoan(uint256) (runs: 50, μ: 178984, ~: 179002) -ProtocolFees_Integration_Concrete_Test:test_ProtocolFees() (gas: 41254) -ProtocolFees_Integration_Concrete_Test:test_ProtocolFees_ProtocolFeeNotSet() (gas: 9943) -ProtocolRevenues_LockupDynamic_Integration_Concrete_Test:test_ProtocolRevenues() (gas: 320228) -ProtocolRevenues_LockupDynamic_Integration_Concrete_Test:test_ProtocolRevenues_ProtocolRevenuesZero() (gas: 10125) -ProtocolRevenues_LockupLinear_Integration_Concrete_Test:test_ProtocolRevenues() (gas: 246945) -ProtocolRevenues_LockupLinear_Integration_Concrete_Test:test_ProtocolRevenues_ProtocolRevenuesZero() (gas: 10111) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StatusDepleted() (gas: 361988) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StatusPending() (gas: 335841) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StatusSettled() (gas: 335834) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StatusStreaming() (gas: 342703) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 375697) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 398898) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RefundableAmountOf_StreamNotCancelable() (gas: 523491) -RefundableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11099) -RefundableAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_RefundableAmountOf(uint256) (runs: 50, μ: 45465, ~: 30742) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StatusDepleted() (gas: 286891) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StatusPending() (gas: 262568) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StatusSettled() (gas: 262691) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StatusStreaming() (gas: 264178) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 297148) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 320272) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RefundableAmountOf_StreamNotCancelable() (gas: 380410) -RefundableAmountOf_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11110) -RefundableAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_RefundableAmountOf(uint256) (runs: 50, μ: 30678, ~: 30858) -Renounce_LockupDynamic_Integration_Concrete_Test:test_Renounce() (gas: 694498) -Renounce_LockupDynamic_Integration_Concrete_Test:test_Renounce_RecipientDoesNotImplementHook() (gas: 687573) -Renounce_LockupDynamic_Integration_Concrete_Test:test_Renounce_RecipientNotContract() (gas: 292606) -Renounce_LockupDynamic_Integration_Concrete_Test:test_Renounce_RecipientReentrancy() (gas: 692702) -Renounce_LockupDynamic_Integration_Concrete_Test:test_Renounce_RecipientReverts() (gas: 688180) -Renounce_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11567) -Renounce_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 87385) -Renounce_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusDepleted() (gas: 68389) -Renounce_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 24692) -Renounce_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamNotCancelable() (gas: 649846) -Renounce_LockupLinear_Integration_Concrete_Test:test_Renounce() (gas: 481553) -Renounce_LockupLinear_Integration_Concrete_Test:test_Renounce_RecipientDoesNotImplementHook() (gas: 474596) -Renounce_LockupLinear_Integration_Concrete_Test:test_Renounce_RecipientNotContract() (gas: 219349) -Renounce_LockupLinear_Integration_Concrete_Test:test_Renounce_RecipientReentrancy() (gas: 479733) -Renounce_LockupLinear_Integration_Concrete_Test:test_Renounce_RecipientReverts() (gas: 475203) -Renounce_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11575) -Renounce_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusCanceled() (gas: 78108) -Renounce_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusDepleted() (gas: 68564) -Renounce_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StatusSettled() (gas: 24822) -Renounce_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamNotCancelable() (gas: 436925) -SafeAssetDecimals_Integration_Concrete_Test:test_SafeAssetDecimals() (gas: 12117) -SafeAssetDecimals_Integration_Concrete_Test:test_SafeAssetDecimals_DecimalsNotImplemented() (gas: 10852) -SafeAssetDecimals_Integration_Concrete_Test:test_SafeAssetDecimals_EOA() (gas: 11625) -SafeAssetSymbol_Integration_Concrete_Test:test_SafeAssetSymbol() (gas: 18550) -SafeAssetSymbol_Integration_Concrete_Test:test_SafeAssetSymbol_Bytes32() (gas: 62214) -SafeAssetSymbol_Integration_Concrete_Test:test_SafeAssetSymbol_EOA() (gas: 13222) -SafeAssetSymbol_Integration_Concrete_Test:test_SafeAssetSymbol_LongSymbol() (gas: 624896) -SafeAssetSymbol_Integration_Concrete_Test:test_SafeAssetSymbol_SymbolNotImplemented() (gas: 12399) -SetComptroller_LockupDynamic_Integration_Concrete_Test:test_SetComptroller_NewComptroller() (gas: 311753) -SetComptroller_LockupDynamic_Integration_Concrete_Test:test_SetComptroller_SameComptroller() (gas: 23283) -SetComptroller_LockupLinear_Integration_Concrete_Test:test_SetComptroller_NewComptroller() (gas: 311750) -SetComptroller_LockupLinear_Integration_Concrete_Test:test_SetComptroller_SameComptroller() (gas: 23280) -SetFlashFee_Integration_Fuzz_Test:testFuzz_SetFlashFee(uint256) (runs: 50, μ: 37754, ~: 39448) -SetNFTDescriptor_LockupDynamic_Integration_Concrete_Test:test_SetNFTDescriptor_NewNFTDescriptor() (gas: 6551561) -SetNFTDescriptor_LockupDynamic_Integration_Concrete_Test:test_SetNFTDescriptor_SameNFTDescriptor() (gas: 2258131) -SetNFTDescriptor_LockupLinear_Integration_Concrete_Test:test_SetNFTDescriptor_NewNFTDescriptor() (gas: 6550197) -SetNFTDescriptor_LockupLinear_Integration_Concrete_Test:test_SetNFTDescriptor_SameNFTDescriptor() (gas: 2256602) -SetProtocolFee_Integration_Concrete_Test:test_SetProtocolFee() (gas: 47804) -SetProtocolFee_Integration_Concrete_Test:test_SetProtocolFee_SameFee() (gas: 22636) -SetProtocolFee_Integration_Fuzz_Test:testFuzz_SetProtocolFee(uint256) (runs: 50, μ: 43131, ~: 43074) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11651) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_StatusOf() (gas: 352806) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_StatusOf_AssetsFullyWithdrawn() (gas: 362652) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_StatusOf_RefundableAmountNotZero() (gas: 336586) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_StatusOf_StartTimeInTheFuture() (gas: 330685) -StatusOf_LockupDynamic_Integration_Concrete_Test:test_StatusOf_StreamCanceled() (gas: 376424) -StatusOf_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11681) -StatusOf_LockupLinear_Integration_Concrete_Test:test_StatusOf() (gas: 263297) -StatusOf_LockupLinear_Integration_Concrete_Test:test_StatusOf_AssetsFullyWithdrawn() (gas: 287573) -StatusOf_LockupLinear_Integration_Concrete_Test:test_StatusOf_RefundableAmountNotZero() (gas: 263462) -StatusOf_LockupLinear_Integration_Concrete_Test:test_StatusOf_StartTimeInTheFuture() (gas: 257439) -StatusOf_LockupLinear_Integration_Concrete_Test:test_StatusOf_StreamCanceled() (gas: 297893) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11319) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_CurrentMilestone1st() (gas: 45931) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_CurrentMilestoneNot1st() (gas: 50774) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_OneSegment() (gas: 257024) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StartTimeInTheFuture() (gas: 20230) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StartTimeInThePresent() (gas: 25593) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StatusDepleted() (gas: 68723) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StatusPending() (gas: 20360) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StatusSettled() (gas: 26624) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 87798) -StreamedAmountOf_LockupDynamic_Integration_Concrete_Test:test_StreamedAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 116445) -StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_Calculation((uint128,uint64,uint40)[],uint40) (runs: 50, μ: 3521669, ~: 3127614) -StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_Monotonicity((uint128,uint64,uint40)[],uint40,uint40) (runs: 50, μ: 3971180, ~: 4105954) -StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_OneSegment((uint128,uint64,uint40),uint40) (runs: 50, μ: 276351, ~: 270631) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 11349) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_CliffTimeInTheFuture() (gas: 26236) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_CliffTimeInThePast() (gas: 17291) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_CliffTimeInThePresent() (gas: 27121) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_StatusDepleted() (gas: 68877) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_StatusPending() (gas: 20305) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_StatusSettled() (gas: 26688) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 78522) -StreamedAmountOf_LockupLinear_Integration_Concrete_Test:test_StreamedAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 107059) -StreamedAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_Calculation(uint40,uint128) (runs: 50, μ: 234550, ~: 235267) -StreamedAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_CliffTimeInTheFuture(uint40) (runs: 50, μ: 27341, ~: 27604) -StreamedAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_StreamedAmountOf_Monotonicity(uint40,uint40,uint128) (runs: 50, μ: 239192, ~: 241446) -ToggleFlashAsset_Integration_Concrete_Test:test_ToggleFlashAsset() (gas: 31848) -ToggleFlashAsset_Integration_Concrete_Test:test_ToggleFlashAsset_FlagNotEnabled() (gas: 41868) -TokenURI_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_NFTDoesNotExist() (gas: 13542) -TokenURI_LockupDynamic_Integration_Concrete_Test:test_TokenURI_Decoded() (gas: 6624) -TokenURI_LockupDynamic_Integration_Concrete_Test:test_TokenURI_Full() (gas: 6601) -TokenURI_LockupLinear_Integration_Concrete_Test:test_RevertGiven_NFTDoesNotExist() (gas: 13525) -TokenURI_LockupLinear_Integration_Concrete_Test:test_TokenURI_Decoded() (gas: 6624) -TokenURI_LockupLinear_Integration_Concrete_Test:test_TokenURI_Full() (gas: 6601) -TransferFrom_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamNotTransferable() (gas: 314210) -TransferFrom_LockupDynamic_Integration_Concrete_Test:test_TransferFrom() (gas: 326549) -TransferFrom_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamNotTransferable() (gas: 240360) -TransferFrom_LockupLinear_Integration_Concrete_Test:test_TransferFrom() (gas: 253204) -WasCanceled_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12048) -WasCanceled_LockupDynamic_Integration_Concrete_Test:test_WasCanceled() (gas: 364519) -WasCanceled_LockupDynamic_Integration_Concrete_Test:test_WasCanceled_StreamNotCanceled() (gas: 327661) -WasCanceled_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12069) -WasCanceled_LockupLinear_Integration_Concrete_Test:test_WasCanceled() (gas: 289228) -WasCanceled_LockupLinear_Integration_Concrete_Test:test_WasCanceled_StreamNotCanceled() (gas: 254401) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_NFTBurned() (gas: 75443) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 14165) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamNotTransferable() (gas: 265059) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Concrete_Test:test_WithdrawMaxAndTransfer() (gas: 158734) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Concrete_Test:test_WithdrawMaxAndTransfer_WithdrawableAmountZero() (gas: 100587) -WithdrawMaxAndTransfer_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawMaxAndTransfer(uint256,address) (runs: 50, μ: 140673, ~: 156073) -WithdrawMaxAndTransfer_LockupLinear_Integration_Concrete_Test:test_RevertGiven_NFTBurned() (gas: 75583) -WithdrawMaxAndTransfer_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 14179) -WithdrawMaxAndTransfer_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamNotTransferable() (gas: 189290) -WithdrawMaxAndTransfer_LockupLinear_Integration_Concrete_Test:test_WithdrawMaxAndTransfer() (gas: 111578) -WithdrawMaxAndTransfer_LockupLinear_Integration_Concrete_Test:test_WithdrawMaxAndTransfer_WithdrawableAmountZero() (gas: 100775) -WithdrawMaxAndTransfer_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawMaxAndTransfer(uint256,address) (runs: 50, μ: 99101, ~: 109924) -WithdrawMax_LockupDynamic_Integration_Concrete_Test:test_WithdrawMax() (gas: 135217) -WithdrawMax_LockupDynamic_Integration_Concrete_Test:test_WithdrawMax_EndTimeNotInTheFuture() (gas: 80287) -WithdrawMax_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawMax(uint256) (runs: 50, μ: 117564, ~: 120638) -WithdrawMax_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawMax_EndTimeNotInTheFuture(uint256) (runs: 50, μ: 82928, ~: 83069) -WithdrawMax_LockupLinear_Integration_Concrete_Test:test_WithdrawMax() (gas: 74491) -WithdrawMax_LockupLinear_Integration_Concrete_Test:test_WithdrawMax_EndTimeNotInTheFuture() (gas: 80494) -WithdrawMax_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawMax(uint256) (runs: 50, μ: 73574, ~: 73703) -WithdrawMax_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawMax_EndTimeNotInTheFuture(uint256) (runs: 50, μ: 83080, ~: 83218) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_AllStatusesDepleted() (gas: 73894) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_OnlyNull() (gas: 21063) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_SomeNull() (gas: 124687) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_SomeStatusesDepleted() (gas: 83367) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_WithdrawMultiple() (gas: 1831194) -WithdrawMultiple_LockupDynamic_Integration_Concrete_Test:test_WithdrawMultiple_ArrayCountsZero() (gas: 9109) -WithdrawMultiple_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawMultiple(uint256,address,uint128) (runs: 50, μ: 2745706, ~: 2745970) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_AllStatusesDepleted() (gas: 74018) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_OnlyNull() (gas: 21020) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_SomeNull() (gas: 105130) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_RevertGiven_SomeStatusesDepleted() (gas: 83491) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_WithdrawMultiple() (gas: 1264004) -WithdrawMultiple_LockupLinear_Integration_Concrete_Test:test_WithdrawMultiple_ArrayCountsZero() (gas: 9165) -WithdrawMultiple_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawMultiple(uint256,address,uint128) (runs: 50, μ: 1773592, ~: 1773439) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 19918) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_StreamDepleted() (gas: 67854) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw() (gas: 385175) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_CallerApprovedOperator() (gas: 112713) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_CallerRecipient() (gas: 81272) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_EndTimeNotInTheFuture() (gas: 72502) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_RecipientDoesNotImplementHook() (gas: 362822) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_RecipientNotContract() (gas: 122683) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_RecipientReentrancy() (gas: 390163) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_RecipientReverts() (gas: 363377) -Withdraw_LockupDynamic_Integration_Concrete_Test:test_Withdraw_StreamHasBeenCanceled() (gas: 382351) -Withdraw_LockupDynamic_Integration_Fuzz_Test:testFuzz_Withdraw(uint256,address,uint128) (runs: 50, μ: 121478, ~: 98629) -Withdraw_LockupDynamic_Integration_Fuzz_Test:testFuzz_Withdraw_CallerApprovedOperator(address) (runs: 50, μ: 145456, ~: 145456) -Withdraw_LockupDynamic_Integration_Fuzz_Test:testFuzz_Withdraw_SegmentFuzing(((uint128,uint64,uint40)[],uint256,address)) (runs: 50, μ: 3965055, ~: 4005165) -Withdraw_LockupDynamic_Integration_Fuzz_Test:testFuzz_Withdraw_StreamHasBeenCanceled(uint256,address,uint128) (runs: 50, μ: 160774, ~: 160963) -Withdraw_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 19917) -Withdraw_LockupLinear_Integration_Concrete_Test:test_RevertGiven_StreamDepleted() (gas: 68020) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw() (gas: 268395) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_CallerApprovedOperator() (gas: 93092) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_CallerRecipient() (gas: 61640) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_EndTimeNotInTheFuture() (gas: 72719) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_RecipientDoesNotImplementHook() (gas: 259694) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_RecipientNotContract() (gas: 75746) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_RecipientReentrancy() (gas: 273430) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_RecipientReverts() (gas: 260249) -Withdraw_LockupLinear_Integration_Concrete_Test:test_Withdraw_StreamHasBeenCanceled() (gas: 292858) -Withdraw_LockupLinear_Integration_Fuzz_Test:testFuzz_Withdraw(uint256,address,uint128) (runs: 50, μ: 99389, ~: 99269) -Withdraw_LockupLinear_Integration_Fuzz_Test:testFuzz_Withdraw_CallerApprovedOperator(address) (runs: 50, μ: 112211, ~: 112211) -Withdraw_LockupLinear_Integration_Fuzz_Test:testFuzz_Withdraw_StreamHasBeenCanceled(uint256,address,uint128) (runs: 50, μ: 141114, ~: 141030) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12045) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf() (gas: 378164) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_NoPreviousWithdrawals() (gas: 347681) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StartTimeInThePresent() (gas: 337258) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusDepleted() (gas: 363782) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusPending() (gas: 334007) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusSettled() (gas: 340230) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 378537) -WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test:test_WithdrawableAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 400691) -WithdrawableAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawableAmountOf(uint40,uint128) (runs: 50, μ: 335154, ~: 352469) -WithdrawableAmountOf_LockupDynamic_Integration_Fuzz_Test:testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40) (runs: 50, μ: 298041, ~: 289776) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_RevertGiven_Null() (gas: 12076) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_CliffTimeInTheFuture() (gas: 253622) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_NoPreviousWithdrawals() (gas: 263493) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusDepleted() (gas: 288666) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusPending() (gas: 258694) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_StatusSettled() (gas: 265047) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_StreamHasBeenCanceled_StatusCanceled() (gas: 300014) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_StreamHasBeenCanceled_StatusDepleted() (gas: 322024) -WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test:test_WithdrawableAmountOf_WithWithdrawals() (gas: 287044) -WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawableAmountOf(uint40,uint128,uint128) (runs: 50, μ: 463735, ~: 463093) -WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawableAmountOf_CliffTimeInTheFuture(uint40) (runs: 50, μ: 263728, ~: 263963) -WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test:testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40,uint128) (runs: 50, μ: 440141, ~: 440973) \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index d5e5d2770..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -lib/** linguist-vendored diff --git a/.github/workflows/ci-deep.yml b/.github/workflows/ci-deep.yml index 66f525ad6..16d942d5a 100644 --- a/.github/workflows/ci-deep.yml +++ b/.github/workflows/ci-deep.yml @@ -1,7 +1,6 @@ name: "CI Deep" env: - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} @@ -11,11 +10,11 @@ on: workflow_dispatch: inputs: unitFuzzRuns: - default: "100000" + default: "50000" description: "Unit: number of fuzz runs." required: false integrationFuzzRuns: - default: "100000" + default: "50000" description: "Integration: number of fuzz runs." required: false invariantRuns: @@ -33,204 +32,46 @@ on: jobs: lint: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Lint the contracts" - run: "pnpm lint" - - - name: "Add lint summary" - run: | - echo "## Lint result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" build: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Show the Foundry config" - run: "forge config" - - - name: "Produce an optimized build with --via-ir" - run: "FOUNDRY_PROFILE=optimized forge build" - - - name: "Build the test contracts" - run: "FOUNDRY_PROFILE=test-optimized forge build" - - - name: "Cache the build and the node modules so that they can be re-used by the other jobs" - uses: "actions/cache/save@v3" - with: - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Add build summary" - run: | - echo "## Build result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" test-unit: - env: - FOUNDRY_FUZZ_RUNS: ${{ inputs.unitFuzzRuns || '100000' }} needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the unit tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/unit\"" - - - name: "Add test summary" - run: | - echo "## Unit tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: ${{ inputs.unitFuzzRuns || 50000 }} + foundry-profile: "test-optimized" + match-path: "test/unit/**/*.sol" + name: "Unit tests" test-integration: - env: - FOUNDRY_FUZZ_RUNS: ${{ inputs.integrationFuzzRuns || '100000' }} needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the integration tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/integration/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Integration tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: ${{ inputs.integrationFuzzRuns || 50000 }} + foundry-profile: "test-optimized" + match-path: "test/integration/**/*.sol" + name: "Integration tests" test-invariant: - env: - FOUNDRY_INVARIANT_DEPTH: ${{ inputs.invariantDepth || '100' }} - FOUNDRY_INVARIANT_RUNS: ${{ inputs.invariantRuns || '100' }} needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the invariant tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/invariant/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Invariant tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-invariant-depth: ${{ inputs.invariantDepth || 100 }} + foundry-invariant-runs: ${{ inputs.invariantRuns || 100 }} + foundry-profile: "test-optimized" + match-path: "test/invariant/**/*.sol" + name: "Invariant tests" test-fork: - env: - FOUNDRY_FUZZ_RUNS: ${{ inputs.forkFuzzRuns || '1000' }} needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the fork tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/fork/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Fork tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + secrets: + RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: ${{ inputs.forkFuzzRuns || 1000 }} + foundry-profile: "test-optimized" + match-path: "test/fork/**/*.sol" + name: "Fork tests" diff --git a/.github/workflows/ci-fork.yml b/.github/workflows/ci-fork.yml new file mode 100644 index 000000000..1de1165e0 --- /dev/null +++ b/.github/workflows/ci-fork.yml @@ -0,0 +1,32 @@ +name: "CI Fork and Util tests" + +on: + schedule: + - cron: "0 3 * * 1,3,5" # at 3:00 AM UTC on Monday, Wednesday and Friday + +jobs: + lint: + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" + + build: + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" + + test-fork: + needs: ["lint", "build"] + secrets: + RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 100 + foundry-profile: "test-optimized" + fuzz-seed: true + match-path: "test/fork/**/*.sol" + name: "Fork tests" + + test-utils: + needs: ["lint", "build"] + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-profile: "test-optimized" + match-path: "test/utils/**/*.sol" + name: "Utils tests" diff --git a/.github/workflows/ci-slither.yml b/.github/workflows/ci-slither.yml index d2e2f57d5..aef1c9066 100644 --- a/.github/workflows/ci-slither.yml +++ b/.github/workflows/ci-slither.yml @@ -1,83 +1,12 @@ name: "CI Slither" -env: - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} - on: schedule: - cron: "0 3 * * 0" # at 3:00am UTC every Sunday jobs: lint: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Lint the contracts" - run: "pnpm lint" - - - name: "Add lint summary" - run: | - echo "## Lint result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" slither-analyze: - runs-on: "ubuntu-latest" - permissions: - actions: "read" - contents: "read" - security-events: "write" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Run Slither analysis" - uses: "crytic/slither-action@v0.3.0" - id: "slither" - with: - fail-on: "none" - sarif: "results.sarif" - - - name: "Upload SARIF file to GitHub code scanning" - uses: "github/codeql-action/upload-sarif@v2" - with: - sarif_file: ${{ steps.slither.outputs.sarif }} - - - name: "Add Slither summary" - run: | - echo "## Slither result" >> $GITHUB_STEP_SUMMARY - echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/slither-analyze.yml@main" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c424484e..4d1e4703c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,298 +4,63 @@ concurrency: cancel-in-progress: true group: ${{github.workflow}}-${{github.ref}} -env: - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} - on: workflow_dispatch: pull_request: push: branches: - "main" + - "staging" + - "staging-blast" jobs: lint: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Lint the contracts" - run: "pnpm lint" - - - name: "Add lint summary" - run: | - echo "## Lint result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-lint.yml@main" build: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Install the Node.js dependencies" - run: "pnpm install" - - - name: "Show the Foundry config" - run: "forge config" - - - name: "Generate and prepare the contract artifacts" - run: "./shell/prepare-artifacts.sh" - - - name: "Build the test contracts" - run: "FOUNDRY_PROFILE=test-optimized forge build" - - - name: "Cache the build and the node modules so that they can be re-used by the other jobs" - uses: "actions/cache/save@v3" - with: - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Store the contract artifacts in CI" - uses: "actions/upload-artifact@v3" - with: - name: "contract-artifacts" - path: "artifacts" - - name: "Add build summary" - run: | - echo "## Build result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-build.yml@main" test-unit: needs: ["lint", "build"] - env: - FOUNDRY_FUZZ_RUNS: "5000" - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Pnpm" - uses: "pnpm/action-setup@v2" - with: - version: "8" - - - name: "Install Node.js" - uses: "actions/setup-node@v3" - with: - cache: "pnpm" - node-version: "lts/*" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the unit tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/unit/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Unit tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 5000 + foundry-profile: "test-optimized" + match-path: "test/unit/**/*.sol" + name: "Unit tests" test-integration: needs: ["lint", "build"] - env: - FOUNDRY_FUZZ_RUNS: "5000" - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the integration tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/integration/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Integration tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - test-utils: - needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the utils tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/utils/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Utils tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 5000 + foundry-profile: "test-optimized" + match-path: "test/integration/**/*.sol" + name: "Integration tests" test-invariant: needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Run the invariant tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/invariant/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Invariant tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-profile: "test-optimized" + match-path: "test/invariant/**/*.sol" + name: "Invariant tests" test-fork: needs: ["lint", "build"] - env: - FOUNDRY_FUZZ_RUNS: "100" - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Generate fuzz seed that changes weekly to avoid burning through RPC allowance" - run: | - echo "FOUNDRY_FUZZ_SEED=$(echo $(($EPOCHSECONDS / 604800)))" >> $GITHUB_ENV - - - name: "Run the fork tests against the optimized build" - run: "FOUNDRY_PROFILE=test-optimized forge test --match-path \"test/fork/**/*.sol\"" - - - name: "Add test summary" - run: | - echo "## Fork tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + secrets: + RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }} + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main" + with: + foundry-fuzz-runs: 20 + foundry-profile: "test-optimized" + match-path: "test/fork/**/*.sol" + name: "Fork tests" coverage: needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Restore the cached build and the node modules" - uses: "actions/cache/restore@v3" - with: - fail-on-cache-miss: true - key: "build-and-modules-${{ github.sha }}" - path: | - cache - node_modules - out - out-optimized - - - name: "Generate the coverage report using the unit and the integration tests" - run: "forge coverage --match-path \"test/{unit,integration}/**/*.sol\" --report lcov" - - - name: "Upload coverage report to Codecov" - uses: "codecov/codecov-action@v3" - with: - files: "./lcov.info" - - - name: "Add coverage summary" - run: | - echo "## Coverage result" >> $GITHUB_STEP_SUMMARY - echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: "sablier-labs/reusable-workflows/.github/workflows/forge-coverage.yml@main" + with: + match-path: "test/{integration,unit}/**/*.sol" diff --git a/.github/workflows/deploy-comptroller.yml b/.github/workflows/deploy-comptroller.yml deleted file mode 100644 index 03eff8a05..000000000 --- a/.github/workflows/deploy-comptroller.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Deploy Comptroller" - -env: - API_KEY_ARBISCAN: ${{ secrets.API_KEY_ARBISCAN }} - API_KEY_BSCSCAN: ${{ secrets.API_KEY_BSCSCAN }} - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_GNOSISSCAN: ${{ secrets.API_KEY_GNOSISSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - API_KEY_OPTIMISTIC_ETHERSCAN: ${{ secrets.API_KEY_OPTIMISTIC_ETHERSCAN }} - API_KEY_POLYGONSCAN: ${{ secrets.API_KEY_POLYGONSCAN }} - API_KEY_SNOWTRACE: ${{ secrets.API_KEY_SNOWTRACE }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.MNEMONIC }} - -on: - workflow_dispatch: - inputs: - admin: - default: "0xF3663da48030b6c88535413Fd643aB0B5F3496ff" - description: "Initial contract admin." - required: false - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - -jobs: - deploy-comptroller: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Deploy the SablierV2Comptroller contract" - run: >- - forge script script/DeployComptroller.s.sol - --broadcast - --rpc-url "${{ inputs.chain }}" - --sig "run(address)" - --verify - -vvvv - "${{ inputs.admin }}" - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-core.yml b/.github/workflows/deploy-core.yml deleted file mode 100644 index 24159ddd5..000000000 --- a/.github/workflows/deploy-core.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: "Deploy Core" - -env: - API_KEY_ARBISCAN: ${{ secrets.API_KEY_ARBISCAN }} - API_KEY_BSCSCAN: ${{ secrets.API_KEY_BSCSCAN }} - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_GNOSISSCAN: ${{ secrets.API_KEY_GNOSISSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - API_KEY_OPTIMISTIC_ETHERSCAN: ${{ secrets.API_KEY_OPTIMISTIC_ETHERSCAN }} - API_KEY_POLYGONSCAN: ${{ secrets.API_KEY_POLYGONSCAN }} - API_KEY_SNOWTRACE: ${{ secrets.API_KEY_SNOWTRACE }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.MNEMONIC }} - -on: - workflow_dispatch: - inputs: - admin: - default: "0xF3663da48030b6c88535413Fd643aB0B5F3496ff" - description: "Initial protocol admin." - required: false - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - max-segment-count: - default: "300" - description: "Maximum number of segments allowed in a stream." - required: false - -jobs: - deploy-core: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Deploy the Sablier V2 Core protocol" - run: >- - forge script script/DeployCore.s.sol - --broadcast - --rpc-url "${{ inputs.chain }}" - --sig "run(address,uint256)" - --verify - "${{ inputs.admin }}" - "${{ inputs.max-segment-count }}" - -vvvv - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-lockup-dynamic.yml b/.github/workflows/deploy-lockup-dynamic.yml deleted file mode 100644 index f2d7c72a5..000000000 --- a/.github/workflows/deploy-lockup-dynamic.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: "Deploy Lockup Dynamic" - -env: - API_KEY_ARBISCAN: ${{ secrets.API_KEY_ARBISCAN }} - API_KEY_BSCSCAN: ${{ secrets.API_KEY_BSCSCAN }} - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_GNOSISSCAN: ${{ secrets.API_KEY_GNOSISSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - API_KEY_OPTIMISTIC_ETHERSCAN: ${{ secrets.API_KEY_OPTIMISTIC_ETHERSCAN }} - API_KEY_POLYGONSCAN: ${{ secrets.API_KEY_POLYGONSCAN }} - API_KEY_SNOWTRACE: ${{ secrets.API_KEY_SNOWTRACE }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.MNEMONIC }} - -on: - workflow_dispatch: - inputs: - admin: - default: "0xF3663da48030b6c88535413Fd643aB0B5F3496ff" - description: "Initial contract admin." - required: false - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - comptroller: - description: "Address of an already deployed comptroller." - required: true - nft-descriptor: - description: "Address of an NFT descriptor contract." - required: true - max-segment-count: - default: "300" - description: "Maximum number of segments allowed in a stream." - required: false - -jobs: - deploy-lockup-dynamic: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Deploy the SablierV2LockupDynamic contract" - run: >- - forge script script/DeployLockupDynamic.s.sol - --broadcast - --rpc-url "${{ inputs.chain }}" - --sig "run(address,address,address,uint256)" - --verify - "${{ inputs.admin }}" - "${{ inputs.comptroller }}" - "${{ inputs.nft-descriptor }}" - "${{ inputs.max-segment-count }}" - -vvvv - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-lockup-linear.yml b/.github/workflows/deploy-lockup-linear.yml deleted file mode 100644 index 9d51e5855..000000000 --- a/.github/workflows/deploy-lockup-linear.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: "Deploy Lockup Linear" - -env: - API_KEY_ARBISCAN: ${{ secrets.API_KEY_ARBISCAN }} - API_KEY_BSCSCAN: ${{ secrets.API_KEY_BSCSCAN }} - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_GNOSISSCAN: ${{ secrets.API_KEY_GNOSISSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - API_KEY_OPTIMISTIC_ETHERSCAN: ${{ secrets.API_KEY_OPTIMISTIC_ETHERSCAN }} - API_KEY_POLYGONSCAN: ${{ secrets.API_KEY_POLYGONSCAN }} - API_KEY_SNOWTRACE: ${{ secrets.API_KEY_SNOWTRACE }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.MNEMONIC }} - -on: - workflow_dispatch: - inputs: - admin: - default: "0xF3663da48030b6c88535413Fd643aB0B5F3496ff" - description: "Initial contract admin." - required: false - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - comptroller: - description: "Address of an already deployed comptroller." - required: true - nft-descriptor: - description: "Address of an NFT descriptor contract." - required: true - -jobs: - deploy-lockup-linear: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Deploy the SablierV2LockupLinear contract" - run: >- - forge script script/DeployLockupLinear.s.sol - --broadcast - --rpc-url ${{ inputs.chain }} - --sig "run(address,address,address)" - --verify - "${{ inputs.admin }}" - "${{ inputs.comptroller }}" - "${{ inputs.nft-descriptor }}" - -vvvv - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-nft-descriptor.yml b/.github/workflows/deploy-nft-descriptor.yml deleted file mode 100644 index ef5ee19f4..000000000 --- a/.github/workflows/deploy-nft-descriptor.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "Deploy NFT Descriptor" - -env: - API_KEY_ARBISCAN: ${{ secrets.API_KEY_ARBISCAN }} - API_KEY_BSCSCAN: ${{ secrets.API_KEY_BSCSCAN }} - API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} - API_KEY_GNOSISSCAN: ${{ secrets.API_KEY_GNOSISSCAN }} - API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} - API_KEY_OPTIMISTIC_ETHERSCAN: ${{ secrets.API_KEY_OPTIMISTIC_ETHERSCAN }} - API_KEY_POLYGONSCAN: ${{ secrets.API_KEY_POLYGONSCAN }} - API_KEY_SNOWTRACE: ${{ secrets.API_KEY_SNOWTRACE }} - FOUNDRY_PROFILE: "optimized" - MNEMONIC: ${{ secrets.MNEMONIC }} - -on: - workflow_dispatch: - inputs: - chain: - default: "sepolia" - description: "Chain name as defined in the Foundry config." - required: false - -jobs: - deploy-nft-descriptor: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Deploy the NFT Descriptor" - run: >- - forge script script/DeployNFTDescriptor.s.sol - --broadcast - --rpc-url "${{ inputs.chain }}" - --verify - -vvvv - - - name: "Add workflow summary" - run: | - echo "## Result" >> $GITHUB_STEP_SUMMARY - echo "✅ Done" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/generate-svg.yml b/.github/workflows/generate-svg.yml index f97355082..6c2f0d0c9 100644 --- a/.github/workflows/generate-svg.yml +++ b/.github/workflows/generate-svg.yml @@ -21,10 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - token: ${{ secrets.CI_TOKEN }} + uses: "actions/checkout@v4" - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" diff --git a/.github/workflows/multibuild.yml b/.github/workflows/multibuild.yml new file mode 100644 index 000000000..bd42ec6f0 --- /dev/null +++ b/.github/workflows/multibuild.yml @@ -0,0 +1,26 @@ +name: "Multibuild" + +on: + workflow_dispatch: + schedule: + - cron: "0 3 * * 0" # at 3:00am UTC every Sunday + +jobs: + multibuild: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install --frozen-lockfile" + + - name: "Check that the project can be built with multiple Solidity versions" + uses: "PaulRBerg/foundry-multibuild@v1" + with: + min: "0.8.22" + max: "0.8.26" + skip-test: "true" diff --git a/.github/workflows/run-smtchecker.yml b/.github/workflows/run-smtchecker.yml index a6aaa12bb..9df7b5718 100644 --- a/.github/workflows/run-smtchecker.yml +++ b/.github/workflows/run-smtchecker.yml @@ -7,9 +7,7 @@ jobs: runs-on: "macos-latest" steps: - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" + uses: "actions/checkout@v4" - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" @@ -18,7 +16,7 @@ jobs: run: "FOUNDRY_PROFILE=smt forge build > smtchecker-report.txt" - name: "Store the report as an artifact" - uses: "actions/upload-artifact@v3" + uses: "actions/upload-artifact@v4" with: name: smtchecker-report path: "smtchecker-report.txt" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..a717e9753 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,10 @@ +name: "Close stale issues and PRs" + +on: + workflow_dispatch: + schedule: + - cron: "0 3 * * 0" # at 3:00am UTC every Sunday + +jobs: + stale: + uses: "sablier-labs/reusable-workflows/.github/workflows/stale.yml@main" diff --git a/.gitignore b/.gitignore index 9d4d1426a..7a74302fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ artifacts broadcast cache coverage +deployments docs node_modules out @@ -11,9 +12,11 @@ out-svg # files *.env +*.env.deployment *.log .DS_Store .pnp.* lcov.info package-lock.json +pnpm-lock.yaml yarn.lock diff --git a/.prettierignore b/.prettierignore index b3ec58260..0fba7f6e2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,7 +4,6 @@ broadcast cache coverage docs -lib node_modules out out-optimized @@ -16,6 +15,7 @@ out-svg *.sol .DS_Store .pnp.* +bun.lockb lcov.info package-lock.json pnpm-lock.yaml diff --git a/.solhint.json b/.solhint.json index 31a4d5b8c..d9bf4b91a 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,13 +3,13 @@ "rules": { "avoid-low-level-calls": "off", "code-complexity": ["error", 9], - "compiler-version": ["error", ">=0.8.19"], + "compiler-version": ["error", ">=0.8.22"], "contract-name-camelcase": "off", "const-name-snakecase": "off", - "custom-errors": "off", "func-name-mixedcase": "off", "func-visibility": ["error", { "ignoreConstructors": true }], - "max-line-length": ["error", 123], + "gas-custom-errors": "off", + "max-line-length": ["error", 124], "named-parameters-mapping": "warn", "no-empty-blocks": "off", "not-rely-on-time": "off", diff --git a/.vscode/settings.json b/.vscode/settings.json index be6b5a15e..436b490ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,11 +9,9 @@ ".gas-snapshot": "julia" }, "editor.formatOnSave": true, - "npm.exclude": "**/lib/**", "prettier.documentSelectors": ["**/*.svg"], "search.exclude": { - "**/node_modules": true, - "lib": true + "**/node_modules": true }, "solidity.formatter": "forge" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4513f1a16..fb01c70e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Common Changelog](https://common-changelog.org/). +[1.2.0]: https://github.com/sablier-labs/v2-core/compare/v1.1.2...v1.2.0 [1.1.2]: https://github.com/sablier-labs/v2-core/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/sablier-labs/v2-core/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/sablier-labs/v2-core/compare/v1.0.2...v1.1.0 @@ -11,11 +12,47 @@ The format is based on [Common Changelog](https://common-changelog.org/). [1.0.1]: https://github.com/sablier-labs/v2-core/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/sablier-labs/v2-core/releases/tag/v1.0.0 +## [1.2.0] - 2024-07-04 + +### Changed + +- **Breaking:** move common logic into `Lockup` contract ([#784](https://github.com/sablier-labs/v2-core/pull/784), + [#813](https://github.com/sablier-labs/v2-core/pull/813), [#850](https://github.com/sablier-labs/v2-core/pull/850), + [#941](https://github.com/sablier-labs/v2-core/pull/941)) +- **Breaking:** use a new hook system ([#951](https://github.com/sablier-labs/v2-core/pull/951)) + - Replace `ISablierV2Recipient` with `ISablierLockupRecipient` hook interface + - Remove `try..catch` block from hook calls +- Allow only supported characters in NFT Descriptor asset symbols + ([#945](https://github.com/sablier-labs/v2-core/pull/945), [#960](https://github.com/sablier-labs/v2-core/pull/960), + [#949](https://github.com/sablier-labs/v2-core/pull/949)) +- Bump build dependencies ([#806](https://github.com/sablier-labs/v2-core/pull/806), + [#942](https://github.com/sablier-labs/v2-core/pull/942), [#944](https://github.com/sablier-labs/v2-core/pull/944)) +- Change permissions of `withdraw` function to public ([#785](https://github.com/sablier-labs/v2-core/pull/785)) +- Disallow zero `startTime` ([#813](https://github.com/sablier-labs/v2-core/pull/813), + [#852](https://github.com/sablier-labs/v2-core/pull/852)) +- Rename create functions `createWithTimestamps` and `createWithDurations` across all lockup contracts + ([#798](https://github.com/sablier-labs/v2-core/pull/798)) +- Switch to Bun ([#775](https://github.com/sablier-labs/v2-core/pull/775)) +- Use Solidity v0.8.26 ([#944](https://github.com/sablier-labs/v2-core/pull/944)) + +### Added + +- Add Lockup Tranched contract ([#817](https://github.com/sablier-labs/v2-core/pull/817)) +- Add `precompiles` in the NPM release ([#841](https://github.com/sablier-labs/v2-core/pull/841)) +- Add return value in `withdrawMax` and `withdrawMaxAndTransfer` + ([#961](https://github.com/sablier-labs/v2-core/pull/961)) + +### Removed + +- **Breaking:** remove protocol fee ([#839](https://github.com/sablier-labs/v2-core/pull/839)) +- Remove flash loan abstract contract ([#779](https://github.com/sablier-labs/v2-core/pull/779)) +- Remove `to` from `withdrawMultiple` function ([#785](https://github.com/sablier-labs/v2-core/pull/785)) + ## [1.1.2] - 2023-12-19 ### Changed -- Upgrade Solidity to `0.8.23` ([#758](https://github.com/sablier-labs/v2-core/pull/758)) +- Use Solidity v0.8.23 ([#758](https://github.com/sablier-labs/v2-core/pull/758)) ## [1.1.1] - 2023-12-16 @@ -35,7 +72,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). - Simplify `renounce` and `withdraw` implementations ([#683](https://github.com/sablier-labs/v2-core/pull/683), [#705](https://github.com/sablier-labs/v2-core/pull/705)) - Update import paths to use Node.js dependencies ([#734](https://github.com/sablier-labs/v2-core/pull/734)) -- Upgrade Solidity to `0.8.21` ([#688](https://github.com/sablier-labs/v2-core/pull/688)) +- Use Solidity v0.8.21 ([#688](https://github.com/sablier-labs/v2-core/pull/688)) ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f7ea57fc..3f0810fa5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ You will need the following software on your machine: - [Git](https://git-scm.com/downloads) - [Foundry](https://github.com/foundry-rs/foundry) - [Node.Js](https://nodejs.org/en/download/) -- [Pnpm](https://pnpm.io/) +- [Bun](https://bun.sh/) In addition, familiarity with [Solidity](https://soliditylang.org/) is requisite. @@ -26,14 +26,21 @@ Clone this repository including submodules: $ git clone --recurse-submodules -j8 git@github.com:sablier-labs/v2-core.git ``` -Then, inside the project's directory, run this to install the Node.js dependencies: +Then, inside the project's directory, run this to install the Node.js dependencies and build the contracts: ```shell -$ pnpm install +$ bun install +$ bun run build ``` Now you can start making changes. +To see a list of all available scripts: + +```shell +$ bun run +``` + ## Pull Requests When making a pull request, ensure that: @@ -48,7 +55,6 @@ When making a pull request, ensure that: - Gas snapshots are provided and demonstrate an improvement (or an acceptable deficit given other improvements). - Reference contracts are modified correspondingly if relevant. - New tests are included for all new features or code paths. -- If making a modification to third-party Node.js dependencies, `pnpm audit` passes. - A descriptive summary of the PR has been provided. ## Environment Variables @@ -63,7 +69,6 @@ populate it with the appropriate environment values. You need to provide your mn To make CI work in your pull request, ensure that the necessary environment variables are configured in your forked repository's secrets. Please add the following variables in your GitHub Secrets: -- API_KEY_ETHERSCAN - API_KEY_INFURA - RPC_URL_MAINNET diff --git a/LICENSE.md b/LICENSE.md index be1df6c0a..f89bb709d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -9,11 +9,11 @@ Parameters Licensor: Sablier Labs Ltd -Licensed Work: Sablier V2 Core The Licensed Work is (C) 2023 Sablier Labs Ltd +Licensed Work: Sablier V2 Core The Licensed Work is (C) 2024 Sablier Labs Ltd Additional Use Grant: Any uses listed and defined at v2-core-license-grants.sablier.eth -Change Date: The earlier of 2027-07-01 or a date specified at v2-core-license-date.sablier.eth +Change Date: The earlier of 2028-07-03 or a date specified at v2-core-license-date.sablier.eth Change License: GNU General Public License v3.0 or later diff --git a/README.md b/README.md index 44fc6ce53..781609aca 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ of tokens deposited. This is the recommended approach. -Install Sablier V2 Core as a Node.js package: +Install Sablier V2 Core using your favorite package manager, e.g., with Bun: ```shell -yarn add @sablier/v2-core +bun add @sablier/v2-core ``` -Then, if you are using Foundry, add these to your `remappings.txt` file: +Then, if you are using Foundry, you need to add these to your `remappings.txt` file: ```text @sablier/v2-core/=node_modules/@sablier/v2-core/ @@ -46,16 +46,18 @@ Then, if you are using Foundry, add these to your `remappings.txt` file: ### Git Submodules +This installation method is not recommended, but it is available for those who prefer it. + First, install the submodule using Forge: ```shell -forge install sablier-labs/v2-core +forge install --no-commit sablier-labs/v2-core ``` Second, install the project's dependencies: ```shell -forge install --no-commit OpenZeppelin/openzeppelin-contracts@v4.9.2 PaulRBerg/prb-math@v4 +forge install --no-commit OpenZeppelin/openzeppelin-contracts@v5.0.2 PaulRBerg/prb-math@v4.0.3 ``` Finally, add these to your `remappings.txt` file: @@ -85,9 +87,9 @@ contract MyContract { ## Architecture -V2 Core uses a singleton-style architecture, where all streams are managed in the `LockupLinear` and `LockupDynamic` -contracts. That is, Sablier does not deploy a new contract for each stream. It bundles all streams into a single -contract, which is more gas-efficient and easier to maintain. +V2 Core uses a singleton-style architecture, where all streams are managed in the `LockupLinear`, `LockupDynamic` and +`LockupTranched` contracts. That is, Sablier does not deploy a new contract for each stream. It bundles all streams into +a single contract, which is more gas-efficient and easier to maintain. For more information, see the [Technical Overview](https://docs.sablier.com/contracts/v2/reference/overview) in our docs, as well as these [diagrams](https://docs.sablier.com/contracts/v2/reference/diagrams). @@ -102,10 +104,6 @@ it is explained in depth [here](https://github.com/sablier-labs/v2-core/wiki/Tes The list of all deployment addresses can be found [here](https://docs.sablier.com). For guidance on the deploy scripts, see the [Deployments wiki](https://github.com/sablier-labs/v2-core/wiki/Deployments). -It is worth noting that not every file in this repository is included in the current deployments. For instance, the -`SablierV2FlashLoan` abstract is not inherited by any contract on the `main` branch, but we have kept it in version -control because we may decide to use it in the future. - ## Security The codebase has undergone rigorous audits by leading security experts from Cantina, as well as independent auditors. diff --git a/SECURITY.md b/SECURITY.md index 8ae591510..dbf5eb603 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,8 +12,8 @@ report it to us. Starting on July 1, 2023, the [sablier-labs/v2-core](https://github.com/sablier-labs/v2-core) repository is subject to the Sablier V2 Bug Bounty (the "Program") to incentivize responsible bug disclosure. -We are limiting the scope of the Program to critical and high severity bugs, and are offering a reward of up to $50,000. -Happy hunting! +We are limiting the scope of the Program to critical and high severity bugs, and are offering a reward of up to +$100,000. Happy hunting! ### Scope @@ -22,8 +22,8 @@ The scope of the Program is limited to bugs that result in the draining of funds The Program does NOT cover the following: - Code located in the [test](./test) or [script](./script) directories. -- External code in the [lib](./lib) directory, except for code that is explicitly used by a deployed contract located in - the [src](./src) directory. +- External code in `node_modules`, except for code that is explicitly used by a deployed contract located in the + [src](./src) directory. - Contract deployments on test networks, such as Sepolia. - Bugs in third-party contracts or platforms interacting with Sablier V2 Core. - Previously reported or discovered vulnerabilities in contracts built by third parties on Sablier V2 Core. @@ -31,8 +31,8 @@ The Program does NOT cover the following: Vulnerabilities contingent upon the occurrence of any of the following also are outside the scope of this Program: -- Front-end bugs -- DDOS attacks +- Front-end bugs (clickjacking etc.) +- DDoS attacks - Spamming - Phishing - Social engineering attacks @@ -45,19 +45,23 @@ Vulnerabilities contingent upon the occurrence of any of the following also are Sablier V2 Core has been developed with a number of technical assumptions in mind. For a disclosure to qualify as a vulnerability, it must adhere to these assumptions as well: -- The immutable variable `MAX_SEGMENT_COUNT` has a low value that cannot lead to an overflow of the block gas limit. -- The total supply of any ERC-20 token remains below 2128 - 1, i.e. `type(uint128).max`. +- The immutable variables `MAX_SEGMENT_COUNT` and `MAX_TRANCHE_COUNT` have values that cannot lead to an overflow of the + block gas limit. +- The total supply of any ERC-20 token remains below 2128 - 1, i.e., `type(uint128).max`. - The `transfer` and `transferFrom` methods of any ERC-20 token strictly reduce the sender's balance by the transfer amount and increase the recipient's balance by the same amount. In other words, tokens that charge fees on transfers are not supported. - An address' ERC-20 balance can only change as a result of a `transfer` call by the sender or a `transferFrom` call by an approved address. This excludes rebase tokens and interest-bearing tokens. - The token contract does not allow callbacks (e.g. ERC-777 is not supported). +- There is no need for exponents greater than ~18.44 in `LockupDynamic` segments. +- Recipient contracts on the hook allowlist have gone through due diligence and are assumed to expose no risk to the + Sablier protocol. ### Rewards Rewards will be allocated based on the severity of the bug disclosed and will be evaluated and rewarded at the -discretion of the Sablier Labs team. For critical bugs that lead to any loss of user funds, rewards of up to $50,000 +discretion of the Sablier Labs team. For critical bugs that lead to any loss of user funds, rewards of up to $100,000 will be granted. Lower severity bugs will be rewarded at the discretion of the team. ### Disclosure diff --git a/benchmark/Benchmark.t.sol b/benchmark/Benchmark.t.sol new file mode 100644 index 000000000..78856032f --- /dev/null +++ b/benchmark/Benchmark.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { ISablierV2Lockup } from "../src/interfaces/ISablierV2Lockup.sol"; + +import { Base_Test } from "../test/Base.t.sol"; + +/// @notice Benchmark contract with common logic needed by all tests. +abstract contract Benchmark_Test is Base_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint128 internal immutable AMOUNT_PER_SEGMENT = 100e18; + uint128 internal immutable AMOUNT_PER_TRANCHE = 100e18; + uint256[7] internal streamIds = [50, 51, 52, 53, 54, 55, 56]; + + /// @dev The directory where the benchmark files are stored. + string internal benchmarkResults = "benchmark/results/"; + + /// @dev The path to the file where the benchmark results are stored. + string internal benchmarkResultsFile; + + /// @dev A variable used to store the content to append to the results file. + string internal contentToAppend; + + ISablierV2Lockup internal lockup; + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + super.setUp(); + + deal({ token: address(dai), to: users.sender, give: type(uint256).max }); + resetPrank({ msgSender: users.sender }); + + // Create the first streams in each Lockup contract to initialize all the variables. + _createFewStreams(); + } + + /*////////////////////////////////////////////////////////////////////////// + GAS FUNCTIONS FOR SHARED IMPLEMENTATIONS + //////////////////////////////////////////////////////////////////////////*/ + + function gasBurn() internal { + // Set the caller to the Recipient for `burn` and change timestamp to the end time. + resetPrank({ msgSender: users.recipient }); + + vm.warp({ newTimestamp: defaults.END_TIME() }); + + lockup.withdrawMax(streamIds[0], users.recipient); + + uint256 initialGas = gasleft(); + lockup.burn(streamIds[0]); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat("| `burn` | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCancel() internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time + resetPrank({ msgSender: users.sender }); + + uint256 initialGas = gasleft(); + lockup.cancel(streamIds[1]); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat("| `cancel` | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasRenounce() internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + uint256 initialGas = gasleft(); + lockup.renounce(streamIds[2]); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + contentToAppend = string.concat("| `renounce` | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasWithdraw(uint256 streamId, address caller, address to, string memory extraInfo) internal { + resetPrank({ msgSender: caller }); + + uint128 withdrawAmount = lockup.withdrawableAmountOf(streamId); + + uint256 initialGas = gasleft(); + lockup.withdraw(streamId, to, withdrawAmount); + string memory gasUsed = vm.toString(initialGas - gasleft()); + + // Check if caller is recipient or not. + bool isCallerRecipient = caller == users.recipient; + + string memory s = isCallerRecipient + ? string.concat("| `withdraw` ", extraInfo, " (by Recipient) | ") + : string.concat("| `withdraw` ", extraInfo, " (by Anyone) | "); + contentToAppend = string.concat(s, gasUsed, " |"); + + // Append the data to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasWithdraw_AfterEndTime(uint256 streamId, address caller, address to, string memory extraInfo) internal { + extraInfo = string.concat(extraInfo, " (After End Time)"); + + uint256 warpTime = lockup.getEndTime(streamId) + 1; + vm.warp({ newTimestamp: warpTime }); + + gasWithdraw(streamId, caller, to, extraInfo); + } + + function gasWithdraw_BeforeEndTime( + uint256 streamId, + address caller, + address to, + string memory extraInfo + ) + internal + { + extraInfo = string.concat(extraInfo, " (Before End Time)"); + + uint256 warpTime = lockup.getEndTime(streamId) - 1; + vm.warp({ newTimestamp: warpTime }); + + gasWithdraw(streamId, caller, to, extraInfo); + } + + function gasWithdraw_ByAnyone(uint256 streamId1, uint256 streamId2, string memory extraInfo) internal { + gasWithdraw_AfterEndTime(streamId1, users.sender, users.recipient, extraInfo); + gasWithdraw_BeforeEndTime(streamId2, users.sender, users.recipient, extraInfo); + } + + function gasWithdraw_ByRecipient(uint256 streamId1, uint256 streamId2, string memory extraInfo) internal { + gasWithdraw_AfterEndTime(streamId1, users.recipient, users.alice, extraInfo); + gasWithdraw_BeforeEndTime(streamId2, users.recipient, users.alice, extraInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Append a line to the file at given path. + function _appendToFile(string memory path, string memory line) internal { + vm.writeLine({ path: path, data: line }); + } + + /// @dev Calculates the total amount to be deposited in the stream, by accounting for the broker fee. + function _calculateTotalAmount(uint128 depositAmount, UD60x18 brokerFee) internal pure returns (uint128) { + UD60x18 factor = ud(1e18); + UD60x18 totalAmount = ud(depositAmount).mul(factor).div(factor.sub(brokerFee)); + return totalAmount.intoUint128(); + } + + /// @dev Internal function to creates a few streams in each Lockup contract. + function _createFewStreams() internal { + for (uint128 i = 0; i < 100; ++i) { + lockupDynamic.createWithTimestamps(defaults.createWithTimestampsLD()); + lockupLinear.createWithTimestamps(defaults.createWithTimestampsLL()); + lockupTranched.createWithTimestamps(defaults.createWithTimestampsLT()); + } + } +} diff --git a/benchmark/EstimateMaxCount.t.sol b/benchmark/EstimateMaxCount.t.sol new file mode 100644 index 000000000..792f8ad80 --- /dev/null +++ b/benchmark/EstimateMaxCount.t.sol @@ -0,0 +1,91 @@ +// solhint-disable no-console +pragma solidity >=0.8.22 <0.9.0; + +import { console2 } from "forge-std/src/console2.sol"; +import { Test } from "forge-std/src/Test.sol"; + +import { LockupDynamic_Gas_Test } from "./LockupDynamic.Gas.t.sol"; +import { LockupTranched_Gas_Test } from "./LockupTranched.Gas.t.sol"; + +/// @notice Structure to group the block gas limit and chain id. +struct ChainInfo { + uint256 blockGasLimit; + uint256 chainId; +} + +contract EstimateMaxCount is Test { + // Buffer gas units to be deducted from the block gas limit so that the max count never exceeds the block limit. + uint256 public constant BUFFER_GAS = 1_000_000; + + // Initial guess for the maximum number of segments/tranches. + uint128 public constant INITIAL_GUESS = 240; + + /// @dev List of chains with their block gas limit. + ChainInfo[] public chains; + + constructor() { + chains.push(ChainInfo({ blockGasLimit: 32_000_000, chainId: 42_161 })); // Arbitrum + chains.push(ChainInfo({ blockGasLimit: 15_000_000, chainId: 43_114 })); // Avalanche + chains.push(ChainInfo({ blockGasLimit: 60_000_000, chainId: 8453 })); // Base + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 238 })); // Blast + chains.push(ChainInfo({ blockGasLimit: 138_000_000, chainId: 56 })); // BNB + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 1 })); // Ethereum + chains.push(ChainInfo({ blockGasLimit: 17_000_000, chainId: 100 })); // Gnosis + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 10 })); // Optimism + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 137 })); // Polygon + chains.push(ChainInfo({ blockGasLimit: 10_000_000, chainId: 534_352 })); // Scroll + chains.push(ChainInfo({ blockGasLimit: 30_000_000, chainId: 11_155_111 })); // Sepolia + } + + /// @notice Estimate the maximum number of segments allowed in LockupDynamic. + function test_EstimateSegments() public { + LockupDynamic_Gas_Test lockupDynamicGasTest = new LockupDynamic_Gas_Test(); + lockupDynamicGasTest.setUp(); + + for (uint256 i = 0; i < chains.length; ++i) { + uint128 count = INITIAL_GUESS; + + // Subtract `BUFFER_GAS` from `blockGasLimit` as an additional precaution to account for the dynamic gas for + // ether transfer on different chains. + uint256 blockGasLimit = chains[i].blockGasLimit - BUFFER_GAS; + + uint256 gasConsumed = 0; + uint256 lastGasConsumed = 0; + while (blockGasLimit > gasConsumed) { + count += 10; + lastGasConsumed = gasConsumed; + + // Estimate the gas consumed by adding 10 segments. + gasConsumed = lockupDynamicGasTest.computeGas_CreateWithDurations(count + 10); + } + + console2.log("count: %d and gasUsed: %d and chainId: %d", count, lastGasConsumed, chains[i].chainId); + } + } + + /// @notice Estimate the maximum number of tranches allowed in LockupTranched. + function test_EstimateTranches() public { + LockupTranched_Gas_Test lockupTranchedGasTest = new LockupTranched_Gas_Test(); + lockupTranchedGasTest.setUp(); + + for (uint256 i = 0; i < chains.length; ++i) { + uint128 count = INITIAL_GUESS; + + // Subtract `BUFFER_GAS` from `blockGasLimit` as an additional precaution to account for the dynamic gas for + // ether transfer on different chains. + uint256 blockGasLimit = chains[i].blockGasLimit - BUFFER_GAS; + + uint256 gasConsumed = 0; + uint256 lastGasConsumed = 0; + while (blockGasLimit > gasConsumed) { + count += 10; + lastGasConsumed = gasConsumed; + + // Estimate the gas consumed by adding 10 tranches. + gasConsumed = lockupTranchedGasTest.computeGas_CreateWithDurations(count + 10); + } + + console2.log("count: %d and gasUsed: %d and chainId: %d", count, lastGasConsumed, chains[i].chainId); + } + } +} diff --git a/benchmark/LockupDynamic.Gas.t.sol b/benchmark/LockupDynamic.Gas.t.sol new file mode 100644 index 000000000..afac9403d --- /dev/null +++ b/benchmark/LockupDynamic.Gas.t.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; +import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; + +import { Broker, LockupDynamic } from "../src/types/DataTypes.sol"; +import { Benchmark_Test } from "./Benchmark.t.sol"; + +/// @notice Tests used to benchmark LockupDynamic. +/// @dev This contract creates a Markdown file with the gas usage of each function. +contract LockupDynamic_Gas_Test is Benchmark_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint128[] internal _segments = [2, 10, 100]; + uint256[] internal _streamIdsForWithdraw = new uint256[](4); + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + function setUp() public override { + super.setUp(); + + lockup = lockupDynamic; + } + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function testGas_Implementations() external { + // Set the file path. + benchmarkResultsFile = string.concat(benchmarkResults, "SablierV2LockupDynamic.md"); + + // Create the file if it doesn't exist, otherwise overwrite it. + vm.writeFile({ + path: benchmarkResultsFile, + data: string.concat("# Benchmarks for LockupDynamic\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n") + }); + + vm.warp({ newTimestamp: defaults.END_TIME() }); + gasBurn(); + + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + gasCancel(); + + gasRenounce(); + + // Create streams with different number of segments. + for (uint256 i; i < _segments.length; ++i) { + gasCreateWithDurations({ totalSegments: _segments[i] }); + gasCreateWithTimestamps({ totalSegments: _segments[i] }); + + gasWithdraw_ByRecipient( + _streamIdsForWithdraw[0], + _streamIdsForWithdraw[1], + string.concat("(", vm.toString(_segments[i]), " segments)") + ); + gasWithdraw_ByAnyone( + _streamIdsForWithdraw[2], + _streamIdsForWithdraw[3], + string.concat("(", vm.toString(_segments[i]), " segments)") + ); + } + } + + /*////////////////////////////////////////////////////////////////////////// + GAS BENCHMARKS FOR CREATE FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + // The following function is used in the estimations of `MAX_SEGMENT_COUNT`. + function computeGas_CreateWithDurations(uint128 totalSegments) public returns (uint256 gasUsed) { + LockupDynamic.CreateWithDurations memory params = + _createWithDurationParams(totalSegments, defaults.BROKER_FEE()); + + uint256 beforeGas = gasleft(); + lockupDynamic.createWithDurations(params); + + gasUsed = beforeGas - gasleft(); + } + + function gasCreateWithDurations(uint128 totalSegments) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + LockupDynamic.CreateWithDurations memory params = + _createWithDurationParams(totalSegments, defaults.BROKER_FEE()); + + uint256 beforeGas = gasleft(); + lockupDynamic.createWithDurations(params); + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurations` (", vm.toString(totalSegments), " segments) (Broker fee set) | ", gasUsed, " |" + ); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Calculate gas usage without broker fee. + params = _createWithDurationParams(totalSegments, ud(0)); + + beforeGas = gasleft(); + lockupDynamic.createWithDurations(params); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurations` (", vm.toString(totalSegments), " segments) (Broker fee not set) | ", gasUsed, " |" + ); + + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Store the last 2 streams IDs for withdraw gas benchmark. + _streamIdsForWithdraw[0] = lockupDynamic.nextStreamId() - 2; + _streamIdsForWithdraw[1] = lockupDynamic.nextStreamId() - 1; + + // Create 2 more streams for withdraw gas benchmark. + _streamIdsForWithdraw[2] = lockupDynamic.createWithDurations(params); + _streamIdsForWithdraw[3] = lockupDynamic.createWithDurations(params); + } + + function gasCreateWithTimestamps(uint128 totalSegments) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time + resetPrank({ msgSender: users.sender }); + + LockupDynamic.CreateWithTimestamps memory params = + _createWithTimestampParams(totalSegments, defaults.BROKER_FEE()); + + uint256 beforeGas = gasleft(); + lockupDynamic.createWithTimestamps(params); + + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestamps` (", vm.toString(totalSegments), " segments) (Broker fee set) | ", gasUsed, " |" + ); + + // Append the data to the file + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Calculate gas usage without broker fee. + params = _createWithTimestampParams(totalSegments, ud(0)); + + beforeGas = gasleft(); + lockupDynamic.createWithTimestamps(params); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestamps` (", + vm.toString(totalSegments), + " segments) (Broker fee not set) | ", + gasUsed, + " |" + ); + + // Append the data to the file + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _createWithDurationParams( + uint128 totalSegments, + UD60x18 brokerFee + ) + private + view + returns (LockupDynamic.CreateWithDurations memory) + { + LockupDynamic.SegmentWithDuration[] memory segments_ = new LockupDynamic.SegmentWithDuration[](totalSegments); + + // Populate segments. + for (uint256 i = 0; i < totalSegments; ++i) { + segments_[i] = ( + LockupDynamic.SegmentWithDuration({ + amount: AMOUNT_PER_SEGMENT, + exponent: ud2x18(0.5e18), + duration: defaults.CLIFF_DURATION() + }) + ); + } + + uint128 depositAmount = AMOUNT_PER_SEGMENT * totalSegments; + + return LockupDynamic.CreateWithDurations({ + sender: users.sender, + recipient: users.recipient, + totalAmount: _calculateTotalAmount(depositAmount, brokerFee), + asset: dai, + cancelable: true, + transferable: true, + segments: segments_, + broker: Broker({ account: users.broker, fee: brokerFee }) + }); + } + + function _createWithTimestampParams( + uint128 totalSegments, + UD60x18 brokerFee + ) + private + view + returns (LockupDynamic.CreateWithTimestamps memory) + { + LockupDynamic.Segment[] memory segments_ = new LockupDynamic.Segment[](totalSegments); + + // Populate segments. + for (uint256 i = 0; i < totalSegments; ++i) { + segments_[i] = ( + LockupDynamic.Segment({ + amount: AMOUNT_PER_SEGMENT, + exponent: ud2x18(0.5e18), + timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) + }) + ); + } + + uint128 depositAmount = AMOUNT_PER_SEGMENT * totalSegments; + + return LockupDynamic.CreateWithTimestamps({ + sender: users.sender, + recipient: users.recipient, + totalAmount: _calculateTotalAmount(depositAmount, brokerFee), + asset: dai, + cancelable: true, + transferable: true, + startTime: getBlockTimestamp(), + segments: segments_, + broker: Broker({ account: users.broker, fee: brokerFee }) + }); + } +} diff --git a/benchmark/LockupLinear.Gas.t.sol b/benchmark/LockupLinear.Gas.t.sol new file mode 100644 index 000000000..f4d35e961 --- /dev/null +++ b/benchmark/LockupLinear.Gas.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { ud } from "@prb/math/src/UD60x18.sol"; + +import { LockupLinear } from "../src/types/DataTypes.sol"; + +import { Benchmark_Test } from "./Benchmark.t.sol"; + +/// @notice Tests used to benchmark LockupLinear. +/// @dev This contract creates a Markdown file with the gas usage of each function. +contract LockupLinear_Gas_Test is Benchmark_Test { + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public override { + super.setUp(); + + lockup = lockupLinear; + } + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function testGas_Implementations() external { + // Set the file path. + benchmarkResultsFile = string.concat(benchmarkResults, "SablierV2LockupLinear.md"); + + // Create the file if it doesn't exist, otherwise overwrite it. + vm.writeFile({ + path: benchmarkResultsFile, + data: string.concat("# Benchmarks for LockupLinear\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n") + }); + + vm.warp({ newTimestamp: defaults.END_TIME() }); + gasBurn(); + + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + gasCancel(); + + gasRenounce(); + + gasCreateWithDurations({ cliffDuration: 0 }); + gasCreateWithDurations({ cliffDuration: defaults.CLIFF_DURATION() }); + + gasCreateWithTimestamps({ cliffTime: 0 }); + gasCreateWithTimestamps({ cliffTime: defaults.CLIFF_TIME() }); + + gasWithdraw_ByRecipient(streamIds[3], streamIds[4], ""); + gasWithdraw_ByAnyone(streamIds[5], streamIds[6], ""); + } + + /*////////////////////////////////////////////////////////////////////////// + GAS BENCHMARKS FOR CREATE FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function gasCreateWithDurations(uint40 cliffDuration) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + LockupLinear.CreateWithDurations memory params = defaults.createWithDurationsLL(); + params.durations.cliff = cliffDuration; + + uint256 beforeGas = gasleft(); + lockupLinear.createWithDurations(params); + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + string memory cliffSetOrNot = cliffDuration == 0 ? " (cliff not set)" : " (cliff set)"; + + contentToAppend = string.concat("| `createWithDurations` (Broker fee set)", cliffSetOrNot, " | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Calculate gas usage without broker fee. + params.broker.fee = ud(0); + params.totalAmount = _calculateTotalAmount(defaults.DEPOSIT_AMOUNT(), ud(0)); + + beforeGas = gasleft(); + lockupLinear.createWithDurations(params); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = + string.concat("| `createWithDurations` (Broker fee not set)", cliffSetOrNot, " | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + function gasCreateWithTimestamps(uint40 cliffTime) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + LockupLinear.CreateWithTimestamps memory params = defaults.createWithTimestampsLL(); + params.timestamps.cliff = cliffTime; + + uint256 beforeGas = gasleft(); + lockupLinear.createWithTimestamps(params); + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + string memory cliffSetOrNot = cliffTime == 0 ? " (cliff not set)" : " (cliff set)"; + + contentToAppend = + string.concat("| `createWithTimestamps` (Broker fee set)", cliffSetOrNot, " | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Calculate gas usage without broker fee. + params.broker.fee = ud(0); + params.totalAmount = _calculateTotalAmount(defaults.DEPOSIT_AMOUNT(), ud(0)); + + beforeGas = gasleft(); + lockupLinear.createWithTimestamps(params); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = + string.concat("| `createWithTimestamps` (Broker fee not set)", cliffSetOrNot, " | ", gasUsed, " |"); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } +} diff --git a/benchmark/LockupTranched.Gas.t.sol b/benchmark/LockupTranched.Gas.t.sol new file mode 100644 index 000000000..591e80c89 --- /dev/null +++ b/benchmark/LockupTranched.Gas.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; + +import { Broker, LockupTranched } from "../src/types/DataTypes.sol"; + +import { Benchmark_Test } from "./Benchmark.t.sol"; + +/// @notice Tests used to benchmark LockupTranched. +/// @dev This contract creates a Markdown file with the gas usage of each function. +contract LockupTranched_Gas_Test is Benchmark_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint128[] internal _tranches = [2, 10, 100]; + uint256[] internal _streamIdsForWithdraw = new uint256[](4); + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public override { + super.setUp(); + + lockup = lockupTranched; + } + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function testGas_Implementations() external { + // Set the file path. + benchmarkResultsFile = string.concat(benchmarkResults, "SablierV2LockupTranched.md"); + + // Create the file if it doesn't exist, otherwise overwrite it. + vm.writeFile({ + path: benchmarkResultsFile, + data: string.concat( + "# Benchmarks for LockupTranched\n\n", "| Implementation | Gas Usage |\n", "| --- | --- |\n" + ) + }); + + vm.warp({ newTimestamp: defaults.END_TIME() }); + gasBurn(); + + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + gasCancel(); + + gasRenounce(); + + // Create streams with different number of tranches. + for (uint256 i; i < _tranches.length; ++i) { + gasCreateWithDurations({ totalTranches: _tranches[i] }); + gasCreateWithTimestamps({ totalTranches: _tranches[i] }); + + gasWithdraw_ByRecipient( + _streamIdsForWithdraw[0], + _streamIdsForWithdraw[1], + string.concat("(", vm.toString(_tranches[i]), " tranches)") + ); + gasWithdraw_ByAnyone( + _streamIdsForWithdraw[2], + _streamIdsForWithdraw[3], + string.concat("(", vm.toString(_tranches[i]), " tranches)") + ); + } + } + + /*////////////////////////////////////////////////////////////////////////// + GAS BENCHMARKS FOR CREATE FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + // The following function is used in the estimation of `MAX_TRANCHE_COUNT` + function computeGas_CreateWithDurations(uint128 totalTranches) public returns (uint256 gasUsed) { + LockupTranched.CreateWithDurations memory params = + _createWithDurationParams(totalTranches, defaults.BROKER_FEE()); + + uint256 beforeGas = gasleft(); + lockupTranched.createWithDurations(params); + + gasUsed = beforeGas - gasleft(); + } + + function gasCreateWithDurations(uint128 totalTranches) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + LockupTranched.CreateWithDurations memory params = + _createWithDurationParams(totalTranches, defaults.BROKER_FEE()); + + uint256 beforeGas = gasleft(); + lockupTranched.createWithDurations(params); + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurations` (", vm.toString(totalTranches), " tranches) (Broker fee set) | ", gasUsed, " |" + ); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Calculate gas usage without broker fee. + params = _createWithDurationParams(totalTranches, ud(0)); + + beforeGas = gasleft(); + lockupTranched.createWithDurations(params); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithDurations` (", vm.toString(totalTranches), " tranches) (Broker fee not set) | ", gasUsed, " |" + ); + + _appendToFile(benchmarkResultsFile, contentToAppend); + + // Store the last 2 streams IDs for withdraw gas benchmark. + _streamIdsForWithdraw[0] = lockupTranched.nextStreamId() - 2; + _streamIdsForWithdraw[1] = lockupTranched.nextStreamId() - 1; + + // Create 2 more streams for withdraw gas benchmark. + _streamIdsForWithdraw[2] = lockupTranched.createWithDurations(params); + _streamIdsForWithdraw[3] = lockupTranched.createWithDurations(params); + } + + function gasCreateWithTimestamps(uint128 totalTranches) internal { + // Set the caller to the Sender for the next calls and change timestamp to before end time. + resetPrank({ msgSender: users.sender }); + + uint256 beforeGas = gasleft(); + lockupTranched.createWithTimestamps({ params: _createWithTimestampParams(totalTranches, defaults.BROKER_FEE()) }); + + string memory gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestamps` (", vm.toString(totalTranches), " tranches) (Broker fee set) | ", gasUsed, " |" + ); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + + beforeGas = gasleft(); + lockupTranched.createWithTimestamps({ params: _createWithTimestampParams(totalTranches, ud(0)) }); + gasUsed = vm.toString(beforeGas - gasleft()); + + contentToAppend = string.concat( + "| `createWithTimestamps` (", + vm.toString(totalTranches), + " tranches) (Broker fee not set) | ", + gasUsed, + " |" + ); + + // Append the content to the file. + _appendToFile(benchmarkResultsFile, contentToAppend); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _createWithDurationParams( + uint128 totalTranches, + UD60x18 brokerFee + ) + private + view + returns (LockupTranched.CreateWithDurations memory) + { + LockupTranched.TrancheWithDuration[] memory tranches_ = new LockupTranched.TrancheWithDuration[](totalTranches); + + // Populate tranches + for (uint256 i = 0; i < totalTranches; ++i) { + tranches_[i] = ( + LockupTranched.TrancheWithDuration({ amount: AMOUNT_PER_TRANCHE, duration: defaults.CLIFF_DURATION() }) + ); + } + + uint128 depositAmount = AMOUNT_PER_SEGMENT * totalTranches; + + return LockupTranched.CreateWithDurations({ + sender: users.sender, + recipient: users.recipient, + totalAmount: _calculateTotalAmount(depositAmount, brokerFee), + asset: dai, + cancelable: true, + transferable: true, + tranches: tranches_, + broker: Broker({ account: users.broker, fee: brokerFee }) + }); + } + + function _createWithTimestampParams( + uint128 totalTranches, + UD60x18 brokerFee + ) + private + view + returns (LockupTranched.CreateWithTimestamps memory) + { + LockupTranched.Tranche[] memory tranches_ = new LockupTranched.Tranche[](totalTranches); + + // Populate tranches. + for (uint256 i = 0; i < totalTranches; ++i) { + tranches_[i] = ( + LockupTranched.Tranche({ + amount: AMOUNT_PER_TRANCHE, + timestamp: getBlockTimestamp() + uint40(defaults.CLIFF_DURATION() * (1 + i)) + }) + ); + } + + uint128 depositAmount = AMOUNT_PER_SEGMENT * totalTranches; + + return LockupTranched.CreateWithTimestamps({ + sender: users.sender, + recipient: users.recipient, + totalAmount: _calculateTotalAmount(depositAmount, brokerFee), + asset: dai, + cancelable: true, + transferable: true, + startTime: getBlockTimestamp(), + tranches: tranches_, + broker: Broker({ account: users.broker, fee: brokerFee }) + }); + } +} diff --git a/benchmark/results/SablierV2LockupDynamic.md b/benchmark/results/SablierV2LockupDynamic.md new file mode 100644 index 000000000..de8e2145b --- /dev/null +++ b/benchmark/results/SablierV2LockupDynamic.md @@ -0,0 +1,31 @@ +# Benchmarks for LockupDynamic + +| Implementation | Gas Usage | +| ---------------------------------------------------------- | --------- | +| `burn` | 15716 | +| `cancel` | 74341 | +| `renounce` | 39007 | +| `createWithDurations` (2 segments) (Broker fee set) | 200602 | +| `createWithDurations` (2 segments) (Broker fee not set) | 185037 | +| `createWithTimestamps` (2 segments) (Broker fee set) | 184780 | +| `createWithTimestamps` (2 segments) (Broker fee not set) | 180015 | +| `withdraw` (2 segments) (After End Time) (by Recipient) | 19108 | +| `withdraw` (2 segments) (Before End Time) (by Recipient) | 27554 | +| `withdraw` (2 segments) (After End Time) (by Anyone) | 14239 | +| `withdraw` (2 segments) (Before End Time) (by Anyone) | 27485 | +| `createWithDurations` (10 segments) (Broker fee set) | 395084 | +| `createWithDurations` (10 segments) (Broker fee not set) | 390326 | +| `createWithTimestamps` (10 segments) (Broker fee set) | 385125 | +| `createWithTimestamps` (10 segments) (Broker fee not set) | 380375 | +| `withdraw` (10 segments) (After End Time) (by Recipient) | 14295 | +| `withdraw` (10 segments) (Before End Time) (by Recipient) | 32545 | +| `withdraw` (10 segments) (After End Time) (by Anyone) | 14246 | +| `withdraw` (10 segments) (Before End Time) (by Anyone) | 32476 | +| `createWithDurations` (100 segments) (Broker fee set) | 2740781 | +| `createWithDurations` (100 segments) (Broker fee not set) | 2736987 | +| `createWithTimestamps` (100 segments) (Broker fee set) | 2642946 | +| `createWithTimestamps` (100 segments) (Broker fee not set) | 2639185 | +| `withdraw` (100 segments) (After End Time) (by Recipient) | 14295 | +| `withdraw` (100 segments) (Before End Time) (by Recipient) | 88968 | +| `withdraw` (100 segments) (After End Time) (by Anyone) | 14226 | +| `withdraw` (100 segments) (Before End Time) (by Anyone) | 88899 | diff --git a/benchmark/results/SablierV2LockupLinear.md b/benchmark/results/SablierV2LockupLinear.md new file mode 100644 index 000000000..d53fa08f9 --- /dev/null +++ b/benchmark/results/SablierV2LockupLinear.md @@ -0,0 +1,19 @@ +# Benchmarks for LockupLinear + +| Implementation | Gas Usage | +| ----------------------------------------------------------- | --------- | +| `burn` | 15694 | +| `cancel` | 56829 | +| `renounce` | 19381 | +| `createWithDurations` (Broker fee set) (cliff not set) | 129276 | +| `createWithDurations` (Broker fee not set) (cliff not set) | 113680 | +| `createWithDurations` (Broker fee set) (cliff set) | 138071 | +| `createWithDurations` (Broker fee not set) (cliff set) | 133273 | +| `createWithTimestamps` (Broker fee set) (cliff not set) | 115334 | +| `createWithTimestamps` (Broker fee not set) (cliff not set) | 110530 | +| `createWithTimestamps` (Broker fee set) (cliff set) | 137629 | +| `createWithTimestamps` (Broker fee not set) (cliff set) | 132827 | +| `withdraw` (After End Time) (by Recipient) | 29701 | +| `withdraw` (Before End Time) (by Recipient) | 19104 | +| `withdraw` (After End Time) (by Anyone) | 24799 | +| `withdraw` (Before End Time) (by Anyone) | 19002 | diff --git a/benchmark/results/SablierV2LockupTranched.md b/benchmark/results/SablierV2LockupTranched.md new file mode 100644 index 000000000..ec7ea9494 --- /dev/null +++ b/benchmark/results/SablierV2LockupTranched.md @@ -0,0 +1,31 @@ +# Benchmarks for LockupTranched + +| Implementation | Gas Usage | +| ---------------------------------------------------------- | --------- | +| `burn` | 15738 | +| `cancel` | 63994 | +| `renounce` | 26501 | +| `createWithDurations` (2 tranches) (Broker fee set) | 199536 | +| `createWithDurations` (2 tranches) (Broker fee not set) | 183969 | +| `createWithTimestamps` (2 tranches) (Broker fee set) | 189410 | +| `createWithTimestamps` (2 tranches) (Broker fee not set) | 183945 | +| `withdraw` (2 tranches) (After End Time) (by Recipient) | 20100 | +| `withdraw` (2 tranches) (Before End Time) (by Recipient) | 14797 | +| `withdraw` (2 tranches) (After End Time) (by Anyone) | 15199 | +| `withdraw` (2 tranches) (Before End Time) (by Anyone) | 14695 | +| `createWithDurations` (10 tranches) (Broker fee set) | 388757 | +| `createWithDurations` (10 tranches) (Broker fee not set) | 383998 | +| `createWithTimestamps` (10 tranches) (Broker fee set) | 397102 | +| `createWithTimestamps` (10 tranches) (Broker fee not set) | 391750 | +| `withdraw` (10 tranches) (After End Time) (by Recipient) | 17855 | +| `withdraw` (10 tranches) (Before End Time) (by Recipient) | 19616 | +| `withdraw` (10 tranches) (After End Time) (by Anyone) | 17760 | +| `withdraw` (10 tranches) (Before End Time) (by Anyone) | 19514 | +| `createWithDurations` (100 tranches) (Broker fee set) | 2672918 | +| `createWithDurations` (100 tranches) (Broker fee not set) | 2668643 | +| `createWithTimestamps` (100 tranches) (Broker fee set) | 2738297 | +| `createWithTimestamps` (100 tranches) (Broker fee not set) | 2734635 | +| `withdraw` (100 tranches) (After End Time) (by Recipient) | 46746 | +| `withdraw` (100 tranches) (Before End Time) (by Recipient) | 73989 | +| `withdraw` (100 tranches) (After End Time) (by Anyone) | 46644 | +| `withdraw` (100 tranches) (Before End Time) (by Anyone) | 73887 | diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 000000000..c7900ccb4 Binary files /dev/null and b/bun.lockb differ diff --git a/codecov.yml b/codecov.yml index eca34cf1b..1ab56482e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,6 +5,7 @@ coverage: status: patch: off ignore: + - "precompiles" - "script" - "src/libraries/NFTSVG.sol" - "src/libraries/SVGElements.sol" diff --git a/foundry.toml b/foundry.toml index be5079296..a95fb8642 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,13 +1,17 @@ [profile.default] auto_detect_solc = false bytecode_hash = "none" - emv_version = "paris" - fs_permissions = [{ access = "read", path = "out-optimized" }] - libs = ["lib"] + evm_version = "shanghai" + fs_permissions = [ + { access = "read", path = "./out-optimized" }, + { access = "read", path = "package.json" }, + { access = "read-write", path = "./benchmark/results" }, + ] + gas_limit = 9223372036854775807 gas_reports = [ - "SablierV2Comptroller", "SablierV2LockupDynamic", "SablierV2LockupLinear", + "SablierV2LockupTranched", "SablierV2NFTDescriptor", ] optimizer = true @@ -15,7 +19,7 @@ out = "out" script = "script" sender = "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38" - solc = "0.8.23" + solc = "0.8.26" src = "src" test = "test" @@ -29,6 +33,10 @@ fail_on_revert = true runs = 20 +# Run only the code inside benchmark directory +[profile.benchmark] + test = "benchmark" + # Speed up compilation and tests during development [profile.lite] optimizer = false @@ -68,6 +76,7 @@ [profile.smt.model_checker.contracts] "src/SablierV2LockupDynamic.sol" = ["SablierV2LockupDynamic"] "src/SablierV2LockupLinear.sol" = ["SablierV2LockupLinear"] + "src/SablierV2LockupTranched.sol" = ["SablierV2LockupTranched"] "src/SablierV2NFTDescriptor.sol" = ["SablierV2NFTDescriptor"] # Test the optimized contracts without re-compiling them @@ -79,16 +88,6 @@ out = "docs" repository = "https://github.com/sablier-labs/v2-core" -[etherscan] - arbitrum = { key = "${API_KEY_ARBISCAN}" } - avalanche = { key = "${API_KEY_SNOWTRACE" } - bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } - gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } - mainnet = { key = "${API_KEY_ETHERSCAN}" } - optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } - polygon = { key = "${API_KEY_POLYGONSCAN}" } - sepolia = { key = "${API_KEY_ETHERSCAN}" } - [fmt] bracket_spacing = true int_types = "long" @@ -102,9 +101,11 @@ [rpc_endpoints] arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" - bnb_smart_chain = "https://bsc-dataseed.binance.org" - gnosis_chain = "https://rpc.gnosischain.com" + base = "https://mainnet.base.org" + bnb = "https://bsc-dataseed.binance.org" + gnosis = "https://rpc.gnosischain.com" localhost = "http://localhost:8545" + ethereum = "${RPC_URL_MAINNET}" mainnet = "${RPC_URL_MAINNET}" optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" diff --git a/funding.json b/funding.json new file mode 100644 index 000000000..c47fccfee --- /dev/null +++ b/funding.json @@ -0,0 +1,5 @@ +{ + "opRetro": { + "projectId": "0x7262ed9c020b3b41ac7ba405aab4ff37575f8b6f975ebed2e65554a08419f8f4" + } +} diff --git a/package.json b/package.json index 968765499..c8138bf07 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@sablier/v2-core", "description": "Core smart contracts of the Sablier V2 token distribution protocol", "license": "BUSL-1.1", - "version": "1.1.2", + "version": "1.2.0", "author": { "name": "Sablier Labs Ltd", "url": "https://sablier.com" @@ -11,19 +11,19 @@ "url": "https://github.com/sablier-labs/v2-core/issues" }, "dependencies": { - "@openzeppelin/contracts": "4.9.2", - "@prb/math": "4.0.2" + "@openzeppelin/contracts": "5.0.2", + "@prb/math": "4.0.3" }, "devDependencies": { - "@prb/test": "0.6.4", - "forge-std": "github:foundry-rs/forge-std#v1.5.6", - "prettier": "^2.8.8", - "solady": "0.0.129", - "solarray": "github:evmcheb/solarray#0625e7e", - "solhint": "^4.0.0" + "forge-std": "github:foundry-rs/forge-std#v1.8.2", + "prettier": "^3.3.2", + "solady": "0.0.208", + "solarray": "github:evmcheb/solarray#a547630", + "solhint": "^5.0.1" }, "files": [ "artifacts", + "precompiles", "src", "test/utils", "CHANGELOG.md", @@ -58,20 +58,18 @@ }, "repository": "github.com/sablier-labs/v2-core", "scripts": { + "benchmark": "bun run build:optimized && FOUNDRY_PROFILE=benchmark forge test --mt testGas && bun run prettier:write", "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "build:smt": "FOUNDRY_PROFILE=smt forge build", "clean": "rm -rf artifacts broadcast cache docs out out-optimized out-svg", - "gas:report": "forge test --gas-report --mp \"./test/integration/**/*.sol\" --nmt \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", - "gas:snapshot": "forge snapshot --mp \"./test/integration/**/*.sol\" --nmt \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", - "gas:snapshot:optimized": "pnpm build:optimized && FOUNDRY_PROFILE=test-optimized forge snapshot --mp \"./test/integration/**/*.sol\" --nmt \"test(Fork)?(Fuzz)?_RevertWhen_\\w{1,}?\"", - "lint": "pnpm lint:sol && pnpm prettier:check", - "lint:sol": "forge fmt --check && pnpm solhint \"{script,src,test}/**/*.sol\"", - "prepack": "bash ./shell/prepare-artifacts.sh", + "lint": "bun run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && bun solhint \"{precompiles,script,src,test}/**/*.sol\"", + "prepack": "bun install && bash ./shell/prepare-artifacts.sh", "prettier:check": "prettier --check \"**/*.{json,md,svg,yml}\"", "prettier:write": "prettier --write \"**/*.{json,md,svg,yml}\"", "test": "forge test", "test:lite": "FOUNDRY_PROFILE=lite forge test", - "test:optimized": "pnpm build:optimized && FOUNDRY_PROFILE=test-optimized forge test" + "test:optimized": "bun run build:optimized && FOUNDRY_PROFILE=test-optimized forge test" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 1b136c4d2..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,738 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@openzeppelin/contracts': - specifier: 4.9.2 - version: 4.9.2 - '@prb/math': - specifier: 4.0.2 - version: 4.0.2 - -devDependencies: - '@prb/test': - specifier: 0.6.4 - version: 0.6.4 - forge-std: - specifier: github:foundry-rs/forge-std#v1.5.6 - version: github.com/foundry-rs/forge-std/e8a047e3f40f13fa37af6fe14e6e06283d9a060e - prettier: - specifier: ^2.8.8 - version: 2.8.8 - solady: - specifier: 0.0.129 - version: 0.0.129 - solarray: - specifier: github:evmcheb/solarray#0625e7e - version: github.com/evmcheb/solarray/0625e7e - solhint: - specifier: ^4.0.0 - version: 4.0.0 - -packages: - - /@babel/code-frame@7.22.5: - resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.5 - dev: true - - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/highlight@7.22.5: - resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true - - /@openzeppelin/contracts@4.9.2: - resolution: {integrity: sha512-mO+y6JaqXjWeMh9glYVzVu8HYPGknAAnWyxTRhGeckOruyXQMNnlcW6w/Dx9ftLeIQk6N+ZJFuVmTwF7lEIFrg==} - dev: false - - /@pnpm/config.env-replace@1.1.0: - resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} - engines: {node: '>=12.22.0'} - dev: true - - /@pnpm/network.ca-file@1.0.2: - resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} - engines: {node: '>=12.22.0'} - dependencies: - graceful-fs: 4.2.10 - dev: true - - /@pnpm/npm-conf@2.2.2: - resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} - engines: {node: '>=12'} - dependencies: - '@pnpm/config.env-replace': 1.1.0 - '@pnpm/network.ca-file': 1.0.2 - config-chain: 1.1.13 - dev: true - - /@prb/math@4.0.2: - resolution: {integrity: sha512-kJgqvXR6iyU7+N959RzggSFhBdnRuSDnc/bs8u6MzdWw7aYIUaAr+uMVdpP6Dheypjerd7sfJgFOs19FRFhscg==} - dev: false - - /@prb/test@0.6.4: - resolution: {integrity: sha512-P0tTMsB6XQ0Wp61EYdXJYFhsOVGyZvcOFub2y9yk0sF+GYDusctR7DzEI+vOP0SILm3knFkEJASjewHEBppdRQ==} - dev: true - - /@sindresorhus/is@5.6.0: - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - dev: true - - /@solidity-parser/parser@0.16.0: - resolution: {integrity: sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==} - dependencies: - antlr4ts: 0.5.0-alpha.4 - dev: true - - /@szmarczak/http-timer@5.0.1: - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - dependencies: - defer-to-connect: 2.0.1 - dev: true - - /@types/http-cache-semantics@4.0.4: - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /antlr4@4.13.0: - resolution: {integrity: sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==} - engines: {node: '>=16'} - dev: true - - /antlr4ts@0.5.0-alpha.4: - resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /ast-parents@0.0.1: - resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} - dev: true - - /astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: true - - /cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} - dev: true - - /cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} - dependencies: - '@types/http-cache-semantics': 4.0.4 - get-stream: 6.0.1 - http-cache-semantics: 4.1.1 - keyv: 4.5.4 - mimic-response: 4.0.0 - normalize-url: 8.0.0 - responselike: 3.0.0 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: true - - /config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - dependencies: - ini: 1.3.8 - proto-list: 1.2.4 - dev: true - - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} - engines: {node: '>=14'} - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 - dev: true - - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dependencies: - mimic-response: 3.1.0 - dev: true - - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: true - - /defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - dev: true - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: true - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true - - /glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 - dev: true - - /got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} - dependencies: - '@sindresorhus/is': 5.6.0 - '@szmarczak/http-timer': 5.0.1 - cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 - decompress-response: 6.0.0 - form-data-encoder: 2.1.4 - get-stream: 6.0.1 - http2-wrapper: 2.2.1 - lowercase-keys: 3.0.0 - p-cancelable: 3.0.0 - responselike: 3.0.0 - dev: true - - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true - - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: true - - /http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - dev: true - - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true - - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true - - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true - - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - dependencies: - json-buffer: 3.0.1 - dev: true - - /latest-version@7.0.0: - resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} - engines: {node: '>=14.16'} - dependencies: - package-json: 8.1.1 - dev: true - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true - - /lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: true - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - - /lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - dev: true - - /mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: true - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true - - /normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} - dev: true - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - dev: true - - /package-json@8.1.1: - resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} - engines: {node: '>=14.16'} - dependencies: - got: 12.6.1 - registry-auth-token: 5.0.2 - registry-url: 6.0.1 - semver: 7.5.4 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.22.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true - - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true - - /proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - dev: true - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true - - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - dev: true - - /rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - dev: true - - /registry-auth-token@5.0.2: - resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} - engines: {node: '>=14'} - dependencies: - '@pnpm/npm-conf': 2.2.2 - dev: true - - /registry-url@6.0.1: - resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} - engines: {node: '>=12'} - dependencies: - rc: 1.2.8 - dev: true - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: true - - /resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} - dependencies: - lowercase-keys: 3.0.0 - dev: true - - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: true - - /solady@0.0.129: - resolution: {integrity: sha512-2i+8lsLLT7nAED+A9C+ZLi8YmpSnUNKGKozkesN2Qm3P3iMvorXAsD5LyT1MAC3eyVfhY3PuvBkvgd31nUzkoQ==} - dev: true - - /solhint@4.0.0: - resolution: {integrity: sha512-bFViMcFvhqVd/HK3Roo7xZXX5nbujS7Bxeg5vnZc9QvH0yCWCrQ38Yrn1pbAY9tlKROc6wFr+rK1mxYgYrjZgA==} - hasBin: true - dependencies: - '@solidity-parser/parser': 0.16.0 - ajv: 6.12.6 - antlr4: 4.13.0 - ast-parents: 0.0.1 - chalk: 4.1.2 - commander: 10.0.1 - cosmiconfig: 8.2.0 - fast-diff: 1.3.0 - glob: 8.1.0 - ignore: 5.2.4 - js-yaml: 4.1.0 - latest-version: 7.0.0 - lodash: 4.17.21 - pluralize: 8.0.0 - semver: 7.5.4 - strip-ansi: 6.0.1 - table: 6.8.1 - text-table: 0.2.0 - optionalDependencies: - prettier: 2.8.8 - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: true - - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /table@6.8.1: - resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} - engines: {node: '>=10.0.0'} - dependencies: - ajv: 8.12.0 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - github.com/evmcheb/solarray/0625e7e: - resolution: {tarball: https://codeload.github.com/evmcheb/solarray/tar.gz/0625e7e} - name: solarray#0625e7e - version: 0.0.0 - dev: true - - github.com/foundry-rs/forge-std/e8a047e3f40f13fa37af6fe14e6e06283d9a060e: - resolution: {tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/e8a047e3f40f13fa37af6fe14e6e06283d9a060e} - name: forge-std - version: 1.5.6 - dev: true diff --git a/precompiles/Precompiles.sol b/precompiles/Precompiles.sol new file mode 100644 index 000000000..a2fea12b5 --- /dev/null +++ b/precompiles/Precompiles.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: BUSL-1.1 +// solhint-disable max-line-length,no-inline-assembly,reason-string +pragma solidity >=0.8.22; + +import { ISablierV2LockupDynamic } from "../src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierV2LockupLinear } from "../src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "../src/interfaces/ISablierV2LockupTranched.sol"; +import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; +import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; + +/// @notice This is useful for external integrations seeking to test against the exact deployed bytecode, as recompiling +/// with via IR enabled would be time-consuming. +/// +/// The BUSL-1.1 license permits non-production usage of this file. This prohibits running the code on mainnet, +/// but allows for execution in test environments, such as a local development network or a testnet. +contract Precompiles { + /*////////////////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////////////////*/ + + uint256 public constant MAX_SEGMENT_COUNT = 500; + uint256 public constant MAX_TRANCHE_COUNT = 500; + + /*////////////////////////////////////////////////////////////////////////// + BYTECODES + //////////////////////////////////////////////////////////////////////////*/ + + bytes public constant BYTECODE_LOCKUP_DYNAMIC = + hex"60c0604052346103e457615a506060813803918261001c816103e8565b9384928339810103126103e45780516001600160a01b038116908190036103e45760208201516001600160a01b03811692908390036103e4576040015161006360406103e8565b92601d84527f5361626c696572205632204c6f636b75702044796e616d6963204e4654000000602085015261009860406103e8565b601181527029a0a116ab1916a627a1a5aaa816a22ca760791b602082015230608052845190946001600160401b0382116102e75760015490600182811c921680156103da575b60208310146102c95781601f84931161036c575b50602090601f8311600114610306575f926102fb575b50508160011b915f199060031b1c1916176001555b83516001600160401b0381116102e757600254600181811c911680156102dd575b60208210146102c957601f8111610266575b50602094601f8211600114610203579481929394955f926101f8575b50508160011b915f199060031b1c1916176002555b5f80546001600160a01b031990811685178255600880549091169290921790915560405192907fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a360a0526001600755615642908161040e823960805181613935015260a051818181610bca01526139ff0152f35b015190505f8061016c565b601f1982169560025f52805f20915f5b88811061024e57508360019596979810610236575b505050811b01600255610181565b01515f1960f88460031b161c191690555f8080610228565b91926020600181928685015181550194019201610213565b60025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace601f830160051c810191602084106102bf575b601f0160051c01905b8181106102b45750610150565b5f81556001016102a7565b909150819061029e565b634e487b7160e01b5f52602260045260245ffd5b90607f169061013e565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610108565b60015f9081528281209350601f198516905b818110610354575090846001959493921061033c575b505050811b0160015561011d565b01515f1960f88460031b161c191690555f808061032e565b92936020600181928786015181550195019301610318565b60015f529091507fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f840160051c810191602085106103d0575b90601f859493920160051c01905b8181106103c257506100f2565b5f81558493506001016103b5565b90915081906103a7565b91607f16916100de565b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176102e75760405256fe6080806040526004361015610012575f80fd5b5f3560e01c90816301ffc9a7146127c657508063027b6744146127a457806306fdde03146126e9578063081812fc146126cb578063095ea7b3146125c65780631400ecec146125155780631c1cdd4c146124b15780631e99d5691461249457806323b872dd1461247d578063303acc851461244057806331df3d481461232d578063406887cb146121be57806340e58ee514611ea5578063425d30dd14611e5557806342842e0e14611e2c57806342966c6814611c685780634426757014611c425780634857501f14611bd15780634869e12d14611b975780634cc55e1114611a9f57806354c02292146117ee57806357404b12146117605780636352211e146117315780636d0cee751461173157806370a08231146116c757806375829def146116595780637cad6cd1146115685780637de6b1db146113e95780638659c27014610feb578063894e9a0d14610cbc5780638f69b99314610c3c5780639067b67714610bed5780639188ec8414610bb357806395d89b4114610aab578063a22cb465146109f7578063a80fc071146109a6578063ad35efd414610947578063b2564569146108f7578063b637b8651461089e578063b88d4fde14610816578063b8a3be66146107e1578063b971302a14610793578063bc2be1be14610744578063c156a11d1461060e578063c87b56dd146104f8578063d4dbd20b146104a7578063d511609f1461045c578063d975dfed14610422578063e985e9c5146103c9578063ea5ead1914610385578063eac8f5b814610334578063f590c176146102d9578063f851a440146102b45763fdd46d601461026e575f80fd5b346102b05760603660031901126102b0576102876128f3565b6044356001600160801b03811681036102b0576102ae916102a661392b565b6004356133df565b005b5f80fd5b346102b0575f3660031901126102b05760206001600160a01b035f5416604051908152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a602052602060405f205460f81c6040519015158152f35b62b8e7e760e51b5f5260045260245ffd5b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a60205260206001600160a01b03600160405f20015416604051908152f35b346102b05760403660031901126102b05760206004356103b86103a66128f3565b916103b081614206565b928391613082565b6001600160801b0360405191168152f35b346102b05760403660031901126102b0576103e26128dd565b6001600160a01b036103f26128f3565b91165f5260066020526001600160a01b0360405f2091165f52602052602060ff60405f2054166040519015158152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323576103b8602091614206565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a6020526020600260405f20015460801c604051908152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a60205260206001600160801b03600360405f20015416604051908152f35b346102b05760203660031901126102b057600435610515816136ad565b505f6001600160a01b0360085416916044604051809481937fe9dc637500000000000000000000000000000000000000000000000000000000835230600484015260248301525afa8015610603575f90610586575b610582906040519182916020835260208301906128b8565b0390f35b503d805f833e6105968183612a76565b8101906020818303126102b05780519067ffffffffffffffff82116102b057019080601f830112156102b0578151916105ce83612a98565b916105dc6040519384612a76565b838352602084830101116102b057610582926105fe9160208085019101612897565b61056a565b6040513d5f823e3d90fd5b346102b05760403660031901126102b05760043561062a6128f3565b61063261392b565b815f52600a60205260ff600160405f20015460a81c161561073257815f5260036020526001600160a01b0360405f205416908133036107125761067483614206565b6001600160801b0381169182610702575b6001600160a01b038116156106ef576106a6856001600160a01b03926137f1565b1692836106c05784637e27328960e01b5f5260045260245ffd5b80859185036106d457602084604051908152f35b8492506364283d7b60e01b5f5260045260245260445260645ffd5b633250574960e11b5f525f60045260245ffd5b61070d828587613082565b610685565b8263216caf0d60e01b5f526004526001600160a01b03331660245260445ffd5b5062b8e7e760e51b5f5260045260245ffd5b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a602052602064ffffffffff60405f205460a01c16604051908152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a60205260206001600160a01b0360405f205416604051908152f35b346102b05760203660031901126102b0576004355f52600a602052602060ff600160405f20015460a81c166040519015158152f35b346102b05760803660031901126102b05761082f6128dd565b6108376128f3565b6064359167ffffffffffffffff83116102b057366023840112156102b05782600401359161086483612a98565b926108726040519485612a76565b80845236602482870101116102b0576020815f9260246102ae9801838801378501015260443591612f92565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600b6020526105826108e360405f20612f0b565b604051918291602083526020830190612988565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a602052602060ff600160405f20015460b01c166040519015158152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c16156103235761097f9061375d565b6040516005821015610992576020918152f35b634e487b7160e01b5f52602160045260245ffd5b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a60205260206001600160801b03600260405f20015416604051908152f35b346102b05760403660031901126102b057610a106128dd565b602435908115158092036102b0576001600160a01b0316908115610a7f57335f52600660205260405f20825f5260205260405f2060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b507f5b08ba18000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346102b0575f3660031901126102b0576040515f6002548060011c90600181168015610ba9575b602083108114610b9557828552908115610b715750600114610b13575b61058283610aff81850382612a76565b6040519182916020835260208301906128b8565b91905060025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace915f905b808210610b5757509091508101602001610aff610aef565b919260018160209254838588010152019101909291610b3f565b60ff191660208086019190915291151560051b84019091019150610aff9050610aef565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610ad2565b346102b0575f3660031901126102b05760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a602052602064ffffffffff60405f205460c81c16604051908152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c161561032357610c749061375d565b6005811015806109925760028214908115610cb0575b8115610c9e575b6020826040519015158152f35b90506109925760046020911482610c91565b5050600381145f610c8a565b346102b05760203660031901126102b057600435604051610180810181811067ffffffffffffffff821117610fd757606091610160916040525f81525f60208201525f60408201525f838201525f60808201525f60a08201525f60c08201525f60e08201525f6101008201525f610120820152610d37612ebb565b6101408201520152805f52600a60205260ff600160405f20015460a81c161561032357805f52600a60205260405f20604051610d7281612a59565b8154906001600160a01b0382168152602081019264ffffffffff8360a01c1684526040820164ffffffffff8460c81c168152606083019160ff8560f01c1615158352608084019460f81c1515855260018101549160a08501946001600160a01b038416865260c0810160ff8560a01c1615158152610e11600260e084019560ff8860a81c161515875260ff61010086019860b01c161515885201612ed9565b61012083019081526002610e248c61375d565b610e2d816129fa565b14610fcf575b5196516001600160a01b0316925164ffffffffff1695511515905115159351151594511515958a5f52600360205260405f20546001600160a01b03169a5f52600b60205260405f2092516001600160a01b0316995164ffffffffff1698511515926040519a610ea46101808d612a76565b8b5260208b019b8c5260408b01998a5260608b0191825260808b0192835260a08b0193845260c08b0194855260e08b019586526101008b019687526101208b019788526101408b01988952610ef890612f0b565b986101608b01998a526040519b8c9b60208d52516001600160a01b031660208d0152516001600160a01b031660408c01525164ffffffffff1660608b01525164ffffffffff1660808a015251151560a089015251151560c0880152516001600160a01b031660e08701525115156101008601525115156101208501525115156101408401525180516001600160801b031661016084015260208101516001600160801b0316610180840152604001516001600160801b03166101a0830152516101c082016101c090526101e0820161058291612988565b5f8752610e33565b634e487b7160e01b5f52604160045260245ffd5b346102b05760203660031901126102b05760043567ffffffffffffffff81116102b05761101c903690600401612957565b9061102561392b565b5f915b80831061103157005b61103c838284612e4a565b359261104661392b565b835f52600a60205260ff600160405f20015460a81c16156113d657835f52600a60205260ff600160405f20015460a01c165f146110905783634a5541ef60e01b5f5260045260245ffd5b909192805f52600a60205260405f205460f81c6113c4576110c5815f52600a6020526001600160a01b0360405f205416331490565b156113ae576110d3816136ce565b90805f52600a6020526110eb600260405f2001612ed9565b916001600160801b038351166001600160801b038216101561139a57815f52600a60205260ff60405f205460f01c161561138657806001600160801b0360208161113f948188511603169501511690612aea565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115611361575b825f52600a602052600360405f20016001600160801b0382166fffffffffffffffffffffffffffffffff19825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa5061125a6001600160a01b03600160405f2001541694611232888588614660565b604080518b81526001600160801b03808b166020830152909216908201529081906060820190565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a160ff6112a4866001600160a01b03165f52600960205260405f2090565b54166112ba575b50505050506001019190611028565b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af190811561060357630d4af11f60e31b916001600160e01b0319915f91611333575b50160361131757808080806112ab565b632187e5e760e21b5f526001600160a01b03602491166004525ffd5b611354915060203d811161135a575b61134c8183612a76565b81019061368d565b87611307565b503d611342565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055611189565b506339c6dc7360e21b5f526024906004525ffd5b506322cad1af60e11b5f526024906004525ffd5b63216caf0d60e01b5f526004523360245260445ffd5b63fe19f19f60e01b5f5260045260245ffd5b8362b8e7e760e51b5f526024906004525ffd5b346102b05760203660031901126102b05760043561140561392b565b805f52600a60205260ff600160405f20015460a81c1615610323576114298161375d565b611432816129fa565b6004810361144d5750634a5541ef60e01b5f5260045260245ffd5b611456816129fa565b60038103611471575063fe19f19f60e01b5f5260045260245ffd5b60029061147d816129fa565b14611556576114a0815f52600a6020526001600160a01b0360405f205416331490565b1561153757805f52600a60205260ff60405f205460f01c1615611525576020817ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7925f52600a825260405f2060ff60f01b19815416905560405190807f0eb069207093cd3e51cd1370d2d369770057fbe29947e577e5fb428c6c6fc78f5f80a28152a1005b6339c6dc7360e21b5f5260045260245ffd5b63216caf0d60e01b5f526004526001600160a01b03331660245260445ffd5b6322cad1af60e11b5f5260045260245ffd5b346102b05760203660031901126102b0576004356001600160a01b0381168091036102b0576001600160a01b035f5416338103611643575060085490806001600160a01b03198316176008556001600160a01b036040519216825260208201527fa2548bd4b805e907c1558a47b5858324fe8bb4a2e1ddfca647eecbf65610eebc60403392a26007545f19810190811161162f5760407f6bd5c950a8d8df17f772f5af37cb3655737899cbf903264b9795592da439661c91815190600182526020820152a1005b634e487b7160e01b5f52601160045260245ffd5b6331b339a960e21b5f526004523360245260445ffd5b346102b05760203660031901126102b0576116726128dd565b5f546001600160a01b03811633810361164357506001600160a01b036001600160a01b0319921691829116175f55337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf805f80a3005b346102b05760203660031901126102b0576001600160a01b036116e86128dd565b168015611705575f526004602052602060405f2054604051908152f35b7f89c62b64000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b346102b05760203660031901126102b057602061174f6004356136ad565b6001600160a01b0360405191168152f35b346102b05760203660031901126102b05760043561177c612ea3565b50805f52600a60205260ff600160405f20015460a81c1615610323575f908152600a6020526040908190205481519064ffffffffff60c882901c81169160a01c166117c683612a3d565b825260208201526117ec8251809264ffffffffff60208092828151168552015116910152565bf35b346102b05760203660031901126102b05760043567ffffffffffffffff81116102b0578036036101206003198201126102b05761182961392b565b60c482013590602219018112156102b057810160048101359067ffffffffffffffff82116102b05760240160608202360381136102b05761186b913691612d66565b9081519161187883612d4e565b926118866040519485612a76565b808452601f1961189582612d4e565b015f5b818110611a8857505064ffffffffff4216916001600160801b036118bb82613985565b51511667ffffffffffffffff60206118d284613985565b5101511664ffffffffff8060406118e886613985565b5101511686011690604051926118fd84612a21565b83526020830152604082015261191286613985565b5261191c85613985565b5060015b8281106119f45750505061193682600401612e82565b9261194360248401612e82565b9261195060448201612e6e565b916064820135936001600160a01b0385168095036102b0576020966119ec966119ac966001600160801b036119e1976001600160a01b0361199360848a01612e96565b94816119a160a48c01612e96565b976040519d8e612a04565b168c52168c8b0152166040890152606088015215156080870152151560a086015260c085015260e084015260e4369101612e03565b6101008201526139a6565b604051908152f35b806001600160801b03611a0960019385613992565b51511667ffffffffffffffff6020611a218487613992565b5101511664ffffffffff806040611a3b5f1987018d613992565b51015116816040611a4c878a613992565b5101511601169060405192611a6084612a21565b835260208301526040820152611a768289613992565b52611a818188613992565b5001611920565b602090611a93612ebb565b82828901015201611898565b346102b05760403660031901126102b05760043567ffffffffffffffff81116102b057611ad0903690600401612957565b9060243567ffffffffffffffff81116102b057611af1903690600401612957565b9091611afb61392b565b818403611b67575f5b848110611b0d57005b80611b61611b1e6001938886612e4a565b35611b2a838987612e4a565b355f5260036020526001600160a01b0360405f205416611b53611b4e85898b612e4a565b612e6e565b91611b5c61392b565b6133df565b01611b04565b50827faec93440000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323576103b8602091614156565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f611c0a8261375d565b600581101561099257600203611c28575b6020906040519015158152f35b505f52600a602052602060ff60405f205460f01c16611c1b565b346102b0575f3660031901126102b05760206001600160a01b0360085416604051908152f35b346102b05760203660031901126102b057600435611c8461392b565b805f52600a60205260ff600160405f20015460a81c161561032357805f52600a60205260ff600160405f20015460a01c1615611e0157611cc3816140e4565b156113ae57805f5260036020526001600160a01b0360405f205416151580611dfa575b80611ddd575b611dcb577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b0360405f2054168015908115611d94575b825f52600360205260405f206001600160a01b03198154169055825f827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8280a450611d8257005b637e27328960e01b5f5260045260245ffd5b611db3835f52600560205260405f206001600160a01b03198154169055565b805f52600460205260405f205f198154019055611d3a565b630da9b01360e01b5f5260045260245ffd5b50805f52600a60205260ff600160405f20015460b01c1615611cec565b505f611ce6565b7f817cd639000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346102b0576102ae611e3d3661291d565b9060405192611e4d602085612a76565b5f8452612f92565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323575f52600a602052602060ff600160405f20015460a01c166040519015158152f35b346102b05760203660031901126102b057600435611ec161392b565b805f52600a60205260ff600160405f20015460a81c161561032357805f52600a60205260ff600160405f20015460a01c165f14611f0a57634a5541ef60e01b5f5260045260245ffd5b805f52600a60205260405f205460f81c6113c457611f3c815f52600a6020526001600160a01b0360405f205416331490565b1561153757611f4a816136ce565b90805f52600a602052611f62600260405f2001612ed9565b916001600160801b038351166001600160801b03821610156121ab57815f52600a60205260ff60405f205460f01c161561219857806001600160801b03602081611fb6948188511603169501511690612aea565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115612173575b825f52600a602052600360405f20016001600160801b0382166fffffffffffffffffffffffffffffffff19825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa506120a96001600160a01b03600160405f2001541694611232888588614660565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a1845f52600960205260ff60405f2054166120ec57005b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af190811561060357630d4af11f60e31b916001600160e01b0319915f91612154575b50160361214257005b632187e5e760e21b5f5260045260245ffd5b61216d915060203d60201161135a5761134c8183612a76565b84612139565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055612000565b506339c6dc7360e21b5f5260045260245ffd5b506322cad1af60e11b5f5260045260245ffd5b346102b05760203660031901126102b0576121d76128dd565b6001600160a01b035f54169033820361231657806001600160a01b03913b156122ea57166040516301ffc9a760e01b81527ff8ee98d3000000000000000000000000000000000000000000000000000000006004820152602081602481855afa908115610603575f916122bb575b501561229057805f52600960205260405f20600160ff198254161790556040519081527fb4378d4e289cb3f40f4f75a99c9cafa76e3df1c4dc31309babc23dc91bd7280160203392a2005b7f7fb843ea000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6122dd915060203d6020116122e3575b6122d58183612a76565b810190612e32565b82612245565b503d6122cb565b7f5a2b2d83000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b506331b339a960e21b5f526004523360245260445ffd5b346102b05760203660031901126102b05760043567ffffffffffffffff81116102b05761014060031982360301126102b05761236761392b565b6040519061237482612a04565b61238081600401612909565b825261238e60248201612909565b602083015261239f60448201612ab4565b604083015260648101356001600160a01b03811681036102b05760608301526123ca608482016129ed565b60808301526123db60a482016129ed565b60a08301526123ec60c48201612d3c565b60c083015260e481013567ffffffffffffffff81116102b057810191366023840112156102b0576119e16119ec926124306020953690602460048201359101612d66565b60e0840152610104369101612e03565b346102b05760203660031901126102b0576001600160a01b036124616128dd565b165f526009602052602060ff60405f2054166040519015158152f35b346102b0576102ae61248e3661291d565b91612b0a565b346102b0575f3660031901126102b0576020600754604051908152f35b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323576124e99061375d565b600581101561099257806020911590811561250a575b506040519015158152f35b6001915014826124ff565b346102b05760203660031901126102b057600435805f52600a60205260ff600160405f20015460a81c1615610323576020905f90805f52600a835260ff60405f205460f01c16806125aa575b612578575b506001600160801b0360405191168152f35b6125a49150805f52600a835261259e6001600160801b03600260405f20015416916136ce565b90612aea565b82612566565b50805f52600a835260ff600160405f20015460a01c1615612561565b346102b05760403660031901126102b0576125df6128dd565b6024356125eb816136ad565b331515806126b8575b80612685575b6126595781906001600160a01b0380851691167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9255f80a45f5260056020526001600160a01b0360405f2091166001600160a01b03198254161790555f80f35b7fa9fbf51f000000000000000000000000000000000000000000000000000000005f523360045260245ffd5b506001600160a01b0381165f52600660205260405f206001600160a01b0333165f5260205260ff60405f205416156125fa565b50336001600160a01b03821614156125f4565b346102b05760203660031901126102b057602061174f600435612ac8565b346102b0575f3660031901126102b0576040515f6001548060011c9060018116801561279a575b602083108114610b9557828552908115610b71575060011461273c5761058283610aff81850382612a76565b91905060015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6915f905b80821061278057509091508101602001610aff610aef565b919260018160209254838588010152019101909291612768565b91607f1691612710565b346102b0575f3660031901126102b057602060405167016345785d8a00008152f35b346102b05760203660031901126102b057600435906001600160e01b031982168092036102b057817f490649060000000000000000000000000000000000000000000000000000000060209314908115612822575b5015158152f35b7f80ac58cd0000000000000000000000000000000000000000000000000000000081149150811561286d575b811561285c575b508361281b565b6301ffc9a760e01b91501483612855565b7f5b5e139f000000000000000000000000000000000000000000000000000000008114915061284e565b5f5b8381106128a85750505f910152565b8181015183820152602001612899565b906020916128d181518092818552858086019101612897565b601f01601f1916010190565b600435906001600160a01b03821682036102b057565b602435906001600160a01b03821682036102b057565b35906001600160a01b03821682036102b057565b60609060031901126102b0576004356001600160a01b03811681036102b057906024356001600160a01b03811681036102b0579060443590565b9181601f840112156102b05782359167ffffffffffffffff83116102b0576020808501948460051b0101116102b057565b90602080835192838152019201905f5b8181106129a55750505090565b9091926020606060019264ffffffffff604088516001600160801b03815116845267ffffffffffffffff86820151168685015201511660408201520194019101919091612998565b359081151582036102b057565b6005111561099257565b610120810190811067ffffffffffffffff821117610fd757604052565b6060810190811067ffffffffffffffff821117610fd757604052565b6040810190811067ffffffffffffffff821117610fd757604052565b610140810190811067ffffffffffffffff821117610fd757604052565b90601f8019910116810190811067ffffffffffffffff821117610fd757604052565b67ffffffffffffffff8111610fd757601f01601f191660200190565b35906001600160801b03821682036102b057565b612ad1816136ad565b505f5260056020526001600160a01b0360405f20541690565b906001600160801b03809116911603906001600160801b03821161162f57565b91906001600160a01b031680156106ef57815f5260036020526001600160a01b0360405f205416151580612d34575b80612d17575b612d04577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051848152a1815f5260036020526001600160a01b0360405f20541692823315159283612c4f575b6001600160a01b03935085612c18575b805f52600460205260405f2060018154019055815f52600360205260405f20816001600160a01b0319825416179055857fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a416808303612c0057505050565b6364283d7b60e01b5f5260045260245260445260645ffd5b612c37825f52600560205260405f206001600160a01b03198154169055565b855f52600460205260405f205f198154019055612b9f565b9192905080612cad575b15612c6657828291612b8f565b8284612c7e57637e27328960e01b5f5260045260245ffd5b7f177e802f000000000000000000000000000000000000000000000000000000005f523360045260245260445ffd5b503384148015612cdb575b80612c595750825f526005602052336001600160a01b0360405f20541614612c59565b50835f52600660205260405f206001600160a01b0333165f5260205260ff60405f205416612cb8565b50630da9b01360e01b5f5260045260245ffd5b50815f52600a60205260ff600160405f20015460b01c1615612b3f565b506001612b39565b359064ffffffffff821682036102b057565b67ffffffffffffffff8111610fd75760051b60200190565b929192612d7282612d4e565b93612d806040519586612a76565b60606020868581520193028201918183116102b057925b828410612da45750505050565b6060848303126102b05760405190612dbb82612a21565b612dc485612ab4565b825260208501359067ffffffffffffffff821682036102b05782602092836060950152612df360408801612d3c565b6040820152815201930192612d97565b91908260409103126102b057604051612e1b81612a3d565b6020808294612e2981612909565b84520135910152565b908160209103126102b0575180151581036102b05790565b9190811015612e5a5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160801b03811681036102b05790565b356001600160a01b03811681036102b05790565b3580151581036102b05790565b60405190612eb082612a3d565b5f6020838281520152565b60405190612ec882612a21565b5f6040838281528260208201520152565b90604051612ee681612a21565b60406001600160801b03600183958054838116865260801c6020860152015416910152565b908154612f1781612d4e565b92612f256040519485612a76565b81845260208401905f5260205f205f915b838310612f435750505050565b600160208192604051612f5581612a21565b64ffffffffff86546001600160801b038116835267ffffffffffffffff8160801c168584015260c01c166040820152815201920192019190612f36565b90612f9e838284612b0a565b803b612fab575b50505050565b602091612ff16001600160a01b03809316956040519586948594630a85bd0160e11b865233600487015216602485015260448401526080606484015260848301906128b8565b03815f865af15f9181613061575b5061302d575061300d6141d7565b805190816130285782633250574960e11b5f5260045260245ffd5b602001fd5b6001600160e01b0319630a85bd0160e11b91160361304f57505f808080612fa5565b633250574960e11b5f5260045260245ffd5b61307b91925060203d60201161135a5761134c8183612a76565b905f612fff565b9061308b61392b565b815f52600a60205260ff600160405f20015460a81c161561073257815f52600a60205260ff600160405f20015460a01c166133cc576001600160a01b03811680156133a0576001600160801b03841691821561337457835f5260036020526001600160a01b0360405f205416948583141580613364575b613330576001600160801b0361311786614206565b168085116132fd575061313c90855f52600a602052600260405f20015460801c61422c565b5f858152600a6020526040902060020180546001600160801b031660809290921b6fffffffffffffffffffffffffffffffff191691909117815561317f90612ed9565b6001600160801b036131a38160208401511692826040818351169201511690612aea565b1611156132cb575b835f52600a6020526131cf836001600160a01b03600160405f200154169283614660565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a183331415806132b5575b6132395750505050565b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af1908115610603576392b9102b60e01b916001600160e01b0319915f91613296575b50160361214257808080612fa5565b6132af915060203d60201161135a5761134c8183612a76565b5f613287565b50835f52600960205260ff60405f20541661322f565b5f848152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b191690556131ab565b84867fa1fb2bbc000000000000000000000000000000000000000000000000000000005f5260045260245260445260645ffd5b82857fb34359d3000000000000000000000000000000000000000000000000000000005f526004523360245260445260645ffd5b5061336e856140e4565b15613102565b837fd2aabcd9000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b827f7fbf7168000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b50634a5541ef60e01b5f5260045260245ffd5b90815f52600a60205260ff600160405f20015460a81c161561073257815f52600a60205260ff600160405f20015460a01c166133cc576001600160a01b03811680156133a0576001600160801b03841691821561337457835f5260036020526001600160a01b0360405f20541694858314158061367d575b613330576001600160801b0361346c86614206565b168085116132fd575061349190855f52600a602052600260405f20015460801c61422c565b5f858152600a6020526040902060020180546001600160801b031660809290921b6fffffffffffffffffffffffffffffffff19169190911781556134d490612ed9565b6001600160801b036134f88160208401511692826040818351169201511690612aea565b16111561364b575b835f52600a602052613524836001600160a01b03600160405f200154169283614660565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a18333141580613626575b61358e5750505050565b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af1908115610603576392b9102b60e01b916001600160e01b0319915f91613607575b5016036135eb57808080612fa5565b6001600160a01b0390632187e5e760e21b5f521660045260245ffd5b613620915060203d60201161135a5761134c8183612a76565b5f6135dc565b5060ff613644856001600160a01b03165f52600960205260405f2090565b5416613584565b5f848152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b19169055613500565b50613687856140e4565b15613457565b908160209103126102b057516001600160e01b0319811681036102b05790565b805f5260036020526001600160a01b0360405f205416908115611d82575090565b64ffffffffff4216815f52600a6020528064ffffffffff60405f205460a01c16101561375757815f52600a60205264ffffffffff60405f205460c81c16111561373c57805f52600b602052600160405f2054115f14613733576137309061430c565b90565b6137309061424c565b5f52600a6020526001600160801b03600260405f2001541690565b50505f90565b805f52600a60205260ff600160405f20015460a01c165f1461377f5750600490565b805f52600a60205260405f205460f81c6137eb57805f52600a60205264ffffffffff60405f205460a01c1642106137e6576137b9816136ce565b905f52600a6020526001600160801b0380600260405f200154169116105f146137e157600190565b600290565b505f90565b50600390565b90805f5260036020526001600160a01b0360405f205416151580613919575b806138fc575b611dcb577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b038060405f20541692836138c5575b16806138ad575b815f52600360205260405f20816001600160a01b0319825416179055827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a490565b805f52600460205260405f2060018154019055613869565b6138e4835f52600560205260405f206001600160a01b03198154169055565b835f52600460205260405f205f198154019055613862565b50805f52600a60205260ff600160405f20015460b01c1615613816565b506001600160a01b0382161515613810565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016300361395d57565b7fa1c0d6e5000000000000000000000000000000000000000000000000000000005f5260045ffd5b805115612e5a5760200190565b8051821015612e5a5760209160051b010190565b906139c86001600160801b036040840151166020610100850151015190614522565b916001600160801b038351169060e08101519160c082019264ffffffffff84511682156140bc578015614094578151801561406c577f00000000000000000000000000000000000000000000000000000000000000008111614041575064ffffffffff6040613a3684613985565b51015116811015613ffd57505f905f905f81515f905b808210613f75575050505064ffffffffff80421691169081811015613f475750506001600160801b031690818103613f1957505060075493845f52600a60205260405f20916001600160801b038251166001600160801b036002850191166fffffffffffffffffffffffffffffffff198254161790556001600160a01b03606082015116916001600160a01b036001850193166001600160a01b031984541617835560808201948551151560ff60f01b197eff00000000000000000000000000000000000000000000000000000000000087549260f01b169116178555835493750100000000000000000000000000000000000000000060a08501957fffffffffffffffffff0000ffffffffffffffffffffffffffffffffffffffffff76ff000000000000000000000000000000000000000000008851151560b01b169116171790556001600160a01b0380845116166001600160a01b03198654161785555184549060e0840151917fffff00000000000000000000ffffffffffffffffffffffffffffffffffffffff78ffffffffff00000000000000000000000000000000000000007dffffffffff000000000000000000000000000000000000000000000000006040613c218751975f19890190613992565b51015160c81b169360a01b169116171785555f5b818110613e0b575050600187016007556001600160a01b0360208301511680156106ef57613c6b886001600160a01b03926137f1565b16613ddf578682613cb96001600160a01b0360607f33eb09bbf19ea3fb22c760d5164234f8bf62ca07dcf5a437ad389e96b0bd6443960151166001600160801b0385511690309033906145ff565b6001600160801b0360208401511680613daf575b506001600160a01b0381511694613da4613d866001600160a01b03602085015116986001600160a01b036060860151169a511515935115156001600160a01b0361010060e088015193549764ffffffffff60405199613d2b8b612a3d565b818160a01c168b5260c81c1660208a015201515116946001600160801b0360206040519a8b9a8b5233828c01528281511660408c01520151166060890152608088015260a087015261014060c0870152610140860190612988565b9260e085019064ffffffffff60208092828151168552015116910152565b6101208301520390a4565b613dd9906001600160a01b036060840151166001600160a01b0361010085015151169033906145ff565b5f613ccd565b7f73c6ac6e000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b885f52600b60205260405f2090613e268160e0870151613992565b51825468010000000000000000811015610fd75760018101808555811015612e5a576001935f5260205f2001906001600160801b0380825116166fffffffffffffffffffffffffffffffff198354161782556020810151907fffffff00000000000000000000000000ffffffffffffffffffffffffffffffff7cffffffffff000000000000000000000000000000000000000000000000604077ffffffffffffffff0000000000000000000000000000000086549560801b1693847fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff8716178755015160c01b1692161717905501613c35565b7fd90b7e39000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7f210aec0e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b9193509193613f99906001600160801b03613f908588613992565b5151169061422c565b9364ffffffffff806040613fad8685613992565b51015116941680851115613fc957506001849301909291613a4c565b8490847f9588ac09000000000000000000000000000000000000000000000000000000005f5260045260245260445260645ffd5b64ffffffffff604061400e84613985565b51015116907ff539a17c000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7f4757689b000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f3952c64e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd572dbcb000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f6095d3bc000000000000000000000000000000000000000000000000000000005f5260045ffd5b805f5260036020526001600160a01b0360405f2054169081331491821561412a575b508115614111575090565b90506001600160a01b036141253392612ac8565b161490565b9091505f52600660205260405f206001600160a01b0333165f5260205260ff60405f205416905f614106565b805f52600a60205261416d600260405f2001612ed9565b90805f52600a60205260ff600160405f20015460a01c165f1461419b5750602001516001600160801b031690565b90815f52600a60205260405f205460f81c6141ba5750613730906136ce565b61373091506001600160801b036040818351169201511690612aea565b3d15614201573d906141e882612a98565b916141f66040519384612a76565b82523d5f602084013e565b606090565b6137309061421381614156565b905f52600a602052600260405f20015460801c90612aea565b906001600160801b03809116911601906001600160801b03821161162f57565b5f818152600a60205260409020546142839064ffffffffff60a082901c811660c89290921c811682900381169142821603166146b0565b90805f52600b60205260405f20805415612e5a575f526142d867ffffffffffffffff60205f205460801c1692825f52600a6020526142d36001600160801b03600260405f20015416948592614790565b614803565b9182136142f557506142f16001600160801b03916148de565b1690565b90505f52600a602052600260405f20015460801c90565b9064ffffffffff421691805f52600a60205260405f20906040519061433082612a59565b6101206143c360028554956001600160a01b0387168652602086019664ffffffffff8160a01c16885264ffffffffff8160c81c16604088015260ff8160f01c161515606088015260f81c1515608087015260ff60018201546001600160a01b03811660a0890152818160a01c16151560c0890152818160a81c16151560e089015260b01c16151561010087015201612ed9565b92019182525f52600b6020526143db60405f20612f0b565b915f9264ffffffffff60406143ef83613985565b510151168664ffffffffff5f925b16106144e3578161447464ffffffffff9697988784816001600160801b0361442c6142d3986144799b9a613992565b5151169a8b9867ffffffffffffffff6020614447868b613992565b51015116978260406144598784613992565b5101511694806144c6575050511680925b03169203166146b0565b614790565b91821361449a5750906001600160801b0361449481936148de565b16011690565b6001600160801b03915060209051015116806001600160801b038316115f146144c1575090565b905090565b60409250906144d8915f190190613992565b51015116809261446a565b936001600160801b03600191816144fa8886613992565b51511601169401958064ffffffffff8060406145168b87613992565b510151169892986143fd565b91909160405161453181612a3d565b5f81525f6020820152926001600160801b0382169081156145e25767016345785d8a000081116145ab5761456d6001600160801b0391836154fb565b1660208501918183521115614597576001600160801b03918261459292511690612aea565b168252565b634e487b7160e01b5f52600160045260245ffd5b7f4fea5c1a000000000000000000000000000000000000000000000000000000005f5260045267016345785d8a000060245260445ffd5b50505090506040516145f381612a3d565b5f81525f602082015290565b9091926001600160a01b0361465e9481604051957f23b872dd000000000000000000000000000000000000000000000000000000006020880152166024860152166044840152606483015260648252614659608483612a76565b614913565b565b61465e926001600160a01b03604051937fa9059cbb000000000000000000000000000000000000000000000000000000006020860152166024840152604483015260448252614659606483612a76565b600160ff1b81148015614783575b61475b575f811215614752576146e2815f035b5f84121561474b57835f0390614998565b917f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831161471c575f19911813156147175790565b5f0390565b907fd49c26b3000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b8390614998565b6146e2816146d1565b7f9fe2b450000000000000000000000000000000000000000000000000000000005f5260045ffd5b50600160ff1b82146146be565b806147aa57506147a657670de0b6b3a764000090565b5f90565b90670de0b6b3a764000082146147f557806147cd575050670de0b6b3a764000090565b670de0b6b3a764000081146147f1576147ec906142d361373093614a9e565b614bf5565b5090565b5050670de0b6b3a764000090565b600160ff1b811480156148d1575b6148a9575f8112156148a057614835815f035b5f84121561489957835f03906154fb565b917f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831161486a575f19911813156147175790565b907f120b5b43000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b83906154fb565b61483581614824565b7fa6070c25000000000000000000000000000000000000000000000000000000005f5260045ffd5b50600160ff1b8214614811565b5f81126148e85790565b7f2463f3d5000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b5f806001600160a01b0361493c93169360208151910182865af16149356141d7565b90836155a9565b805190811515918261497d575b50506149525750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6149909250602080918301019101612e32565b155f80614949565b5f19670de0b6b3a7640000820991670de0b6b3a7640000820291828085109403938085039414614a635781841015614a2957670de0b6b3a7640000829109600182190182168092046002816003021880820260020302808202600203028082026002030280820260020302808202600203028091026002030293600183805f03040190848311900302920304170290565b7f63a05778000000000000000000000000000000000000000000000000000000005f52600452670de0b6b3a764000060245260445260645ffd5b5091508115614a70570490565b634e487b7160e01b5f52601260045260245ffd5b8015614a70576ec097ce7bc90715b34b9f10000000000590565b805f811315614bca57670de0b6b3a76400008112614baa57506001905b670de0b6b3a764000081056001600160801b03811160071b90811c67ffffffffffffffff811160061b90811c63ffffffff811160051b90811c61ffff811160041b90811c9060ff821160031b91821c92600f841160021b93841c94600160038711811b96871c119617171717171717670de0b6b3a7640000810291811d90670de0b6b3a76400008214614b9757506706f05b59d3b20000905b5f8213614b615750500290565b80670de0b6b3a764000091020590671bc16d674ec80000821215614b89575b60011d90614b54565b809192019160011d90614b80565b9050670de0b6b3a7640000929150020290565b5f1991508015614a70576ec097ce7bc90715b34b9f100000000005614abb565b7f059b101b000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b5f811215614c225768033dd1780914b971141981126137e657614c19905f03614bf5565b61373090614a84565b680a688906bd8affffff81136154d057670de0b6b3a76400009060401b057780000000000000000000000000000000000000000000000067ff00000000000000821661539b575b670de0b6b3a76400009066ff000000000000831661528b575b65ff00000000008316615183575b64ff000000008316615083575b63ff0000008316614f8b575b62ff00008316614e9b575b61ff008316614db3575b60ff8316614cd3575b029060401c60bf031c90565b60808316614da0575b60408316614d8d575b60208316614d7a575b60108316614d67575b60088316614d54575b60048316614d41575b60028316614d2e575b6001831615614cc757680100000000000000010260401c614cc7565b680100000000000000010260401c614d12565b680100000000000000030260401c614d09565b680100000000000000060260401c614d00565b6801000000000000000b0260401c614cf7565b680100000000000000160260401c614cee565b6801000000000000002c0260401c614ce5565b680100000000000000590260401c614cdc565b6180008316614e88575b6140008316614e75575b6120008316614e62575b6110008316614e4f575b6108008316614e3c575b6104008316614e29575b6102008316614e16575b610100831615614cbe57680100000000000000b10260401c614cbe565b680100000000000001630260401c614df9565b680100000000000002c60260401c614def565b6801000000000000058c0260401c614de5565b68010000000000000b170260401c614ddb565b6801000000000000162e0260401c614dd1565b68010000000000002c5d0260401c614dc7565b680100000000000058b90260401c614dbd565b628000008316614f78575b624000008316614f65575b622000008316614f52575b621000008316614f3f575b620800008316614f2c575b620400008316614f19575b620200008316614f06575b62010000831615614cb4576801000000000000b1720260401c614cb4565b680100000000000162e40260401c614ee8565b6801000000000002c5c80260401c614edd565b68010000000000058b910260401c614ed2565b680100000000000b17210260401c614ec7565b68010000000000162e430260401c614ebc565b680100000000002c5c860260401c614eb1565b6801000000000058b90c0260401c614ea6565b63800000008316615070575b6340000000831661505d575b6320000000831661504a575b63100000008316615037575b63080000008316615024575b63040000008316615011575b63020000008316614ffe575b6301000000831615614ca95768010000000000b172180260401c614ca9565b6801000000000162e4300260401c614fdf565b68010000000002c5c8600260401c614fd3565b680100000000058b90c00260401c614fc7565b6801000000000b17217f0260401c614fbb565b680100000000162e42ff0260401c614faf565b6801000000002c5c85fe0260401c614fa3565b68010000000058b90bfc0260401c614f97565b6480000000008316615170575b644000000000831661515d575b642000000000831661514a575b6410000000008316615137575b6408000000008316615124575b6404000000008316615111575b64020000000083166150fe575b640100000000831615614c9d57680100000000b17217f80260401c614c9d565b68010000000162e42ff10260401c6150de565b680100000002c5c85fe30260401c6150d1565b6801000000058b90bfce0260401c6150c4565b68010000000b17217fbb0260401c6150b7565b6801000000162e42fff00260401c6150aa565b68010000002c5c8601cc0260401c61509d565b680100000058b90c0b490260401c615090565b658000000000008316615278575b654000000000008316615265575b652000000000008316615252575b65100000000000831661523f575b65080000000000831661522c575b650400000000008316615219575b650200000000008316615206575b65010000000000831615614c90576801000000b1721835510260401c614c90565b680100000162e430e5a20260401c6151e5565b6801000002c5c863b73f0260401c6151d7565b68010000058b90cf1e6e0260401c6151c9565b680100000b1721bcfc9a0260401c6151bb565b68010000162e43f4f8310260401c6151ad565b680100002c5c89d5ec6d0260401c61519f565b6801000058b91b5bc9ae0260401c615191565b66800000000000008316615388575b66400000000000008316615375575b66200000000000008316615362575b6610000000000000831661534f575b6608000000000000831661533c575b66040000000000008316615329575b66020000000000008316615316575b6601000000000000831615614c825768010000b17255775c040260401c614c82565b6801000162e525ee05470260401c6152f4565b68010002c5cc37da94920260401c6152e5565b680100058ba01fb9f96d0260401c6152d6565b6801000b175effdc76ba0260401c6152c7565b680100162f3904051fa10260401c6152b8565b6801002c605e2e8cec500260401c6152a9565b68010058c86da1c09ea20260401c61529a565b67800000000000000082166154b1575b670de0b6b3a764000090674000000000000000831661549e575b672000000000000000831661548b575b6710000000000000008316615478575b6708000000000000008316615465575b6704000000000000008316615452575b670200000000000000831661543f575b670100000000000000831661542c575b9050614c69565b680100b1afa5abcbed610260401c615425565b68010163da9fb33356d80260401c615415565b680102c9a3e778060ee70260401c615405565b6801059b0d31585743ae0260401c6153f5565b68010b5586cf9890f62a0260401c6153e5565b6801172b83c7d517adce0260401c6153d5565b6801306fe0a31b7152df0260401c6153c5565b5077b504f333f9de6484800000000000000000000000000000006153ab565b7f0360d028000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9091905f198382098382029182808310920391808303921461559857670de0b6b3a7640000821015615568577faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac106699394670de0b6b3a7640000910990828211900360ee1b910360121c170290565b84907f5173648d000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5050670de0b6b3a764000090049150565b906155e657508051156155be57805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b8151158061562c575b6155f7575090565b6001600160a01b03907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b156155ef56fea164736f6c634300081a000a"; + bytes public constant BYTECODE_LOCKUP_LINEAR = + hex"60a0604052346103bf57614b706040813803918261001c816103c3565b9384928339810103126103bf5780516001600160a01b03811691908290036103bf57602001516001600160a01b038116908190036103bf5761005e60406103c3565b91601c83527f5361626c696572205632204c6f636b7570204c696e656172204e465400000000602084015261009360406103c3565b601181527029a0a116ab1916a627a1a5aaa816a624a760791b60208201523060805283519092906001600160401b0381116102d057600154600181811c911680156103b5575b60208210146102b257601f8111610352575b50602094601f82116001146102ef579481929394955f926102e4575b50508160011b915f199060031b1c1916176001555b82516001600160401b0381116102d057600254600181811c911680156102c6575b60208210146102b257601f811161024f575b506020601f82116001146101ec57819293945f926101e1575b50508160011b915f199060031b1c1916176002555b5f80546001600160a01b031990811684178255600880549091169290921790915560405191907fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a3600160075561478790816103e9823960805181613adb0152f35b015190505f80610168565b601f1982169060025f52805f20915f5b8181106102375750958360019596971061021f575b505050811b0160025561017d565b01515f1960f88460031b161c191690555f8080610211565b9192602060018192868b0151815501940192016101fc565b60025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace601f830160051c810191602084106102a8575b601f0160051c01905b81811061029d575061014f565b5f8155600101610290565b9091508190610287565b634e487b7160e01b5f52602260045260245ffd5b90607f169061013d565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610107565b601f1982169560015f52805f20915f5b88811061033a57508360019596979810610322575b505050811b0160015561011c565b01515f1960f88460031b161c191690555f8080610314565b919260206001819286850151815501940192016102ff565b60015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f830160051c810191602084106103ab575b601f0160051c01905b8181106103a057506100eb565b5f8155600101610393565b909150819061038a565b90607f16906100d9565b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176102d05760405256fe6080806040526004361015610012575f80fd5b5f3560e01c90816301ffc9a71461313757508063027b67441461311557806306fdde031461305a578063081812fc1461303c578063095ea7b314612f375780631400ecec14612e865780631c1cdd4c14612e225780631e99d56914612e0557806323b872dd14612dee578063303acc8514612db1578063406887cb14612c4257806340e58ee51461296b578063425d30dd1461291b57806342842e0e146128f257806342966c681461272e57806344267570146127085780634857501f146126975780634869e12d1461265d5780634cc55e11146122b657806353b157271461218b57806357404b12146120c55780636352211e146120965780636d0cee751461209657806370a082311461202c57806375829def14611fbe578063780a82c814611f725780637cad6cd114611e955780637de6b1db14611d485780638659c27014611991578063894e9a0d146116a95780638f69b993146116295780639067b677146115da57806395d89b41146114d2578063a22cb4651461141e578063a80fc071146113cd578063ab167ccc1461125c578063ad35efd4146111fd578063b2564569146111ad578063b88d4fde14611123578063b8a3be66146110ee578063b971302a146110a0578063bc2be1be14611051578063c156a11d14610c3c578063c87b56dd14610b31578063d4dbd20b14610ae0578063d511609f14610a95578063d975dfed14610a4a578063e985e9c5146109f1578063ea5ead19146106ac578063eac8f5b81461065b578063f590c17614610600578063f851a440146105db5763fdd46d6014610263575f80fd5b346105d75760603660031901126105d75760043561027f613264565b906102886133c6565b610290613ad1565b815f52600a60205260ff600160405f20015460a81c16156105c557815f52600a60205260ff600160405f20015460a01c166105b2576001600160a01b03831690811561059f576001600160801b031690811561058c57825f5260036020526001600160a01b0360405f20541693848214158061057c575b610561576001600160801b0361031c8561431b565b168084116105475750835f52600a60205282600260405f20015460801c016001600160801b0381116105335761037b90855f52600a602052600260405f2001906001600160801b036001600160801b031983549260801b169116179055565b835f52600a602052610392600260405f20016136aa565b6001600160801b036103b681602084015116928260408183511692015116906133fe565b161115610501575b835f52600a6020526103e2836001600160a01b03600160405f200154169283614341565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a183331415806104eb575b61044857005b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af19081156104e0576392b9102b60e01b916001600160e01b0319915f916104b1575b50160361049f57005b632187e5e760e21b5f5260045260245ffd5b6104d3915060203d6020116104d9575b6104cb8183613388565b8101906137e0565b5f610496565b503d6104c1565b6040513d5f823e3d90fd5b50835f52600960205260ff60405f205416610442565b5f848152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b191690556103be565b634e487b7160e01b5f52601160045260245ffd5b838563287ecaef60e21b5f5260045260245260445260645ffd5b508263b34359d360e01b5f526004523360245260445260645ffd5b5061058684613b2b565b15610307565b8263d2aabcd960e01b5f5260045260245ffd5b82630ff7ee2d60e31b5f5260045260245ffd5b50634a5541ef60e01b5f5260045260245ffd5b5062b8e7e760e51b5f5260045260245ffd5b5f80fd5b346105d7575f3660031901126105d75760206001600160a01b035f5416604051908152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a602052602060405f205460f81c6040519015158152f35b62b8e7e760e51b5f5260045260245ffd5b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a60205260206001600160a01b03600160405f20015416604051908152f35b346105d75760403660031901126105d7576004356106c8613264565b906106d28161431b565b906106db613ad1565b805f52600a60205260ff600160405f20015460a81c161561064a57805f52600a60205260ff600160405f20015460a01c166109df576001600160a01b0383169182156109cc576001600160801b03169182156109b957815f5260036020526001600160a01b0360405f2054169384821415806109a9575b61098e576001600160801b036107678461431b565b168085116109745750825f52600a60205283600260405f20015460801c016001600160801b038111610533576107c690845f52600a602052600260405f2001906001600160801b036001600160801b031983549260801b169116179055565b825f52600a6020526107dd600260405f20016136aa565b6001600160801b0361080181602084015116928260408183511692015116906133fe565b161115610942575b825f52600a60205261082d846001600160a01b03600160405f200154169283614341565b81837f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051888152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051848152a1833314158061092c575b61089d575b602083604051908152f35b604051916392b9102b60e01b8352600483015233602483015260448201528160648201526020816084815f875af19081156104e0576392b9102b60e01b916001600160e01b0319915f9161090d575b5016036108fa578180610892565b50632187e5e760e21b5f5260045260245ffd5b610926915060203d6020116104d9576104cb8183613388565b856108ec565b50835f52600960205260ff60405f20541661088d565b5f838152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b19169055610809565b848463287ecaef60e21b5f5260045260245260445260645ffd5b509063b34359d360e01b5f526004523360245260445260645ffd5b506109b383613b2b565b15610752565b5063d2aabcd960e01b5f5260045260245ffd5b50630ff7ee2d60e31b5f5260045260245ffd5b634a5541ef60e01b5f5260045260245ffd5b346105d75760403660031901126105d757610a0a61324e565b6001600160a01b03610a1a613264565b91165f5260066020526001600160a01b0360405f2091165f52602052602060ff60405f2054166040519015158152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a57610a8460209161431b565b6001600160801b0360405191168152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a6020526020600260405f20015460801c604051908152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a60205260206001600160801b03600360405f20015416604051908152f35b346105d75760203660031901126105d757600435610b4e81613800565b505f6001600160a01b0360085416916044604051809481937fe9dc637500000000000000000000000000000000000000000000000000000000835230600484015260248301525afa80156104e0575f90610bbf575b610bbb90604051918291602083526020830190613229565b0390f35b503d805f833e610bcf8183613388565b8101906020818303126105d75780519067ffffffffffffffff82116105d757019080601f830112156105d757815191610c07836133aa565b91610c156040519384613388565b838352602084830101116105d757610bbb92610c379160208085019101613208565b610ba3565b346105d75760403660031901126105d757600435610c58613264565b610c60613ad1565b815f52600a60205260ff600160405f20015460a81c16156105c557815f5260036020526001600160a01b0360405f2054169081330361103a576001600160801b03610caa8461431b565b169081158015610d33575b506001600160a01b03811615610d2057610cd7846001600160a01b0392613997565b169182610cf15783637e27328960e01b5f5260045260245ffd5b8084918403610d0557602083604051908152f35b9091506364283d7b60e01b5f5260045260245260445260645ffd5b633250574960e11b5f525f60045260245ffd5b610d3b613ad1565b845f52600a60205260ff600160405f20015460a81c161561102857845f52600a60205260ff600160405f20015460a01c1661101557831561100257610fef57835f5260036020526001600160a01b0360405f2054168084141580610fdf575b610fc4576001600160801b03610daf8661431b565b16808411610faa5750845f52600a60205282600260405f20015460801c016001600160801b03811161053357610e0e90865f52600a602052600260405f2001906001600160801b036001600160801b031983549260801b169116179055565b845f52600a602052610e25600260405f20016136aa565b6001600160801b03610e4981602084015116928260408183511692015116906133fe565b161115610f78575b845f52600a6020526001600160a01b03600160405f20015416610e75848683614341565b84867f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051888152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051878152a18033141580610f62575b15610cb5576040516392b9102b60e01b81528560048201523360248201528460448201528360648201526020816084815f865af19081156104e0576392b9102b60e01b916001600160e01b0319915f91610f43575b501614610cb557632187e5e760e21b5f5260045260245ffd5b610f5c915060203d6020116104d9576104cb8183613388565b88610f2a565b50805f52600960205260ff60405f205416610ed5565b5f858152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b19169055610e51565b838663287ecaef60e21b5f5260045260245260445260645ffd5b838563b34359d360e01b5f526004523360245260445260645ffd5b50610fe985613b2b565b15610d9a565b8363d2aabcd960e01b5f5260045260245ffd5b84630ff7ee2d60e31b5f5260045260245ffd5b84634a5541ef60e01b5f5260045260245ffd5b8462b8e7e760e51b5f5260045260245ffd5b8263216caf0d60e01b5f526004523360245260445ffd5b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a602052602064ffffffffff60405f205460a01c16604051908152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a60205260206001600160a01b0360405f205416604051908152f35b346105d75760203660031901126105d7576004355f52600a602052602060ff600160405f20015460a81c166040519015158152f35b346105d75760803660031901126105d75761113c61324e565b611144613264565b6064359167ffffffffffffffff83116105d757366023840112156105d757826004013591611171836133aa565b9261117f6040519485613388565b80845236602482870101116105d7576020815f9260246111ab98018388013785010152604435916136f0565b005b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a602052602060ff600160405f20015460b01c166040519015158152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a5761123590613903565b6040516005821015611248576020918152f35b634e487b7160e01b5f52602160045260245ffd5b346105d7576101403660031901126105d757611276613ad1565b61127e61368c565b64ffffffffff421680825264ffffffffff6112976136dc565b166113b2575b60e43564ffffffffff811681036105d75764ffffffffff9101166040820152600435906001600160a01b038216918281036105d757506024356001600160a01b038116908181036105d757506044356001600160801b038116908181036105d757506064356001600160a01b0381168091036105d75760843591821515928381036105d7575060a43593841515948581036105d7575060405196611340886132e5565b8752602087015260408601526060850152608084015260a083015260c08201526040610103193601126105d7576040519061137a8261336c565b61010435906001600160a01b03821682036105d757826113aa9260209452610124358482015260e0820152613c21565b604051908152f35b64ffffffffff6113c06136dc565b820116602083015261129d565b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a60205260206001600160801b03600260405f20015416604051908152f35b346105d75760403660031901126105d75761143761324e565b602435908115158092036105d7576001600160a01b03169081156114a657335f52600660205260405f20825f5260205260405f2060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b507f5b08ba18000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346105d7575f3660031901126105d7576040515f6002548060011c906001811680156115d0575b6020831081146115bc57828552908115611598575060011461153a575b610bbb8361152681850382613388565b604051918291602083526020830190613229565b91905060025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace915f905b80821061157e57509091508101602001611526611516565b919260018160209254838588010152019101909291611566565b60ff191660208086019190915291151560051b840190910191506115269050611516565b634e487b7160e01b5f52602260045260245ffd5b91607f16916114f9565b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a602052602064ffffffffff60405f205460c81c16604051908152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a5761166190613903565b600581101580611248576002821490811561169d575b811561168b575b6020826040519015158152f35b9050611248576004602091148261167e565b5050600381145f611677565b346105d75760203660031901126105d7576004355f6101606040516116cd81613332565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e0820152826101008201528261012082015261171061368c565b6101408201520152805f52600a60205260ff600160405f20015460a81c161561064a57805f52600a60205260405f2060405161174b8161334f565b81546001600160a01b0381168252602082019364ffffffffff8260a01c168552604083019364ffffffffff8360c81c1685526060840160ff8460f01c1615158152608085019360f81c1515845260018201549360a08601956001600160a01b038616875260c081019560ff8160a01c16151587526117ea600260e084019660ff8460a81c161515885260ff61010086019460b01c1615158452016136aa565b61012083019081526117fb87613903565b600581101561124857600214611989575b5197516001600160a01b031692865f52600b60205260405f205464ffffffffff16995164ffffffffff1694511515915115159751151595511515965f52600360205260405f20546001600160a01b031692516001600160a01b03169a5164ffffffffff16905115159260405161188181613332565b8c81526020810191825260408101928352606081019384526080810194855260a0810195865260c0810196875260e0810197885261010081019889526101208101998a5261014081019a8b52610160019a8b526040519b8c52516001600160a01b031660208c01525164ffffffffff1660408b015251151560608a01525115156080890152516001600160a01b031660a08801525164ffffffffff1660c087015251151560e08601525115156101008501525115156101208401525180516001600160801b031661014084015260208101516001600160801b0316610160840152604001516001600160801b03166101808301525164ffffffffff166101a08201526101c090f35b5f855261180c565b346105d75760203660031901126105d75760043567ffffffffffffffff81116105d7576119c29036906004016132b4565b906119cb613ad1565b5f915b8083106119d757005b6119e2838284613668565b35926119ec613ad1565b835f52600a60205260ff600160405f20015460a81c1615611d3657835f52600a60205260ff600160405f20015460a01c165f14611a365783634a5541ef60e01b5f5260045260245ffd5b909192805f52600a60205260405f205460f81c611d2457611a6b815f52600a6020526001600160a01b0360405f205416331490565b15611d0e57611a7981613821565b90805f52600a602052611a91600260405f20016136aa565b916001600160801b038351166001600160801b0382161015611cfb57815f52600a60205260ff60405f205460f01c1615611ce857806001600160801b03602081611ae59481885116031695015116906133fe565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115611cc3575b825f52600a602052600360405f20016001600160801b0382166001600160801b0319825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50611bf76001600160a01b03600160405f2001541694611bcf888588614341565b604080518b81526001600160801b03808b166020830152909216908201529081906060820190565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a1845f52600960205260ff60405f205416611c48575b505050505060010191906119ce565b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af19081156104e057630d4af11f60e31b916001600160e01b0319915f91611ca5575b50160361049f5780808080611c39565b611cbd915060203d81116104d9576104cb8183613388565b87611c95565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055611b2f565b506339c6dc7360e21b5f5260045260245ffd5b506322cad1af60e11b5f5260045260245ffd5b63216caf0d60e01b5f526004523360245260445ffd5b63fe19f19f60e01b5f5260045260245ffd5b8362b8e7e760e51b5f5260045260245ffd5b346105d75760203660031901126105d757600435611d64613ad1565b805f52600a60205260ff600160405f20015460a81c161561064a57611d8881613903565b60058110156112485760048103611dac5750634a5541ef60e01b5f5260045260245ffd5b60038103611dc7575063fe19f19f60e01b5f5260045260245ffd5b600214611e8357611dec815f52600a6020526001600160a01b0360405f205416331490565b15611d0e57805f52600a60205260ff60405f205460f01c1615611e71576020817ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7925f52600a825260405f2060ff60f01b19815416905560405190807f0eb069207093cd3e51cd1370d2d369770057fbe29947e577e5fb428c6c6fc78f5f80a28152a1005b6339c6dc7360e21b5f5260045260245ffd5b6322cad1af60e11b5f5260045260245ffd5b346105d75760203660031901126105d7576004356001600160a01b0381168091036105d7576001600160a01b035f5416338103611f5c575060085490806001600160a01b03198316176008556001600160a01b036040519216825260208201527fa2548bd4b805e907c1558a47b5858324fe8bb4a2e1ddfca647eecbf65610eebc60403392a26007545f1981019081116105335760407f6bd5c950a8d8df17f772f5af37cb3655737899cbf903264b9795592da439661c91815190600182526020820152a1005b6331b339a960e21b5f526004523360245260445ffd5b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600b602052602064ffffffffff60405f205416604051908152f35b346105d75760203660031901126105d757611fd761324e565b5f546001600160a01b038116338103611f5c57506001600160a01b036001600160a01b0319921691829116175f55337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf805f80a3005b346105d75760203660031901126105d7576001600160a01b0361204d61324e565b16801561206a575f526004602052602060405f2054604051908152f35b7f89c62b64000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b346105d75760203660031901126105d75760206120b4600435613800565b6001600160a01b0360405191168152f35b346105d75760203660031901126105d7576004356120e161368c565b50805f52600a60205260ff600160405f20015460a81c161561064a57806060915f52600a60205264ffffffffff60405f205460a01c1690805f52600b60205264ffffffffff60405f205416905f52600a60205264ffffffffff60405f205460c81c16906040519261215184613316565b835260208301526040820152612189604051809264ffffffffff60408092828151168552826020820151166020860152015116910152565bf35b346105d7576101603660031901126105d7576121a5613ad1565b6040516121b1816132e5565b6121b961324e565b81526121c3613264565b60208201526121d06133c6565b60408201526064356001600160a01b03811681036105d757606082015260843580151581036105d757608082015260a43580151581036105d75760a082015260603660c31901126105d75760405161222781613316565b60c43564ffffffffff811681036105d757815260e43564ffffffffff811681036105d75760208201526101043564ffffffffff811681036105d757604082015260c08201526040610123193601126105d757604051906122868261336c565b61012435906001600160a01b03821682036105d757826113aa9260209452610144358482015260e0820152613c21565b346105d75760403660031901126105d75760043567ffffffffffffffff81116105d7576122e79036906004016132b4565b60243567ffffffffffffffff81116105d7576123079036906004016132b4565b612312939193613ad1565b80830361262e575f5b83811061232457005b61232f818585613668565b3561233b828686613668565b355f5260036020526001600160a01b0360405f2054169061235d838589613668565b356001600160801b038116908181036105d75750612379613ad1565b815f52600a60205260ff600160405f20015460a81c16156105c557815f52600a60205260ff600160405f20015460a01c166105b25782156109cc5780156109b957815f5260036020526001600160a01b0360405f20541692838114158061261e575b612604576001600160801b036123f08461431b565b168083116125ea5750825f52600a60205281600260405f20015460801c016001600160801b0381116105335761244f90845f52600a602052600260405f2001906001600160801b036001600160801b031983549260801b169116179055565b825f52600a602052612466600260405f20016136aa565b6001600160801b0361248a81602084015116928260408183511692015116906133fe565b1611156125b8575b825f52600a6020526001600160a01b03600160405f200154166124b6838383614341565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a183331415806125a2575b612527575b5050505060010161231b565b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af19081156104e0576392b9102b60e01b916001600160e01b0319915f91612584575b50160361049f5780808061251b565b61259c915060203d81116104d9576104cb8183613388565b89612575565b50835f52600960205260ff60405f205416612516565b5f838152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b19169055612492565b828463287ecaef60e21b5f5260045260245260445260645ffd5b8263b34359d360e01b5f526004523360245260445260645ffd5b5061262883613b2b565b156123db565b827faec93440000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a57610a84602091613b9d565b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f6126d082613903565b6005811015611248576002036126ee575b6020906040519015158152f35b505f52600a602052602060ff60405f205460f01c166126e1565b346105d7575f3660031901126105d75760206001600160a01b0360085416604051908152f35b346105d75760203660031901126105d75760043561274a613ad1565b805f52600a60205260ff600160405f20015460a81c161561064a57805f52600a60205260ff600160405f20015460a01c16156128c75761278981613b2b565b15611d0e57805f5260036020526001600160a01b0360405f2054161515806128c0575b806128a3575b612891577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b0360405f205416801590811561285a575b825f52600360205260405f206001600160a01b03198154169055825f827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8280a45061284857005b637e27328960e01b5f5260045260245ffd5b612879835f52600560205260405f206001600160a01b03198154169055565b805f52600460205260405f205f198154019055612800565b630da9b01360e01b5f5260045260245ffd5b50805f52600a60205260ff600160405f20015460b01c16156127b2565b505f6127ac565b7f817cd639000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346105d7576111ab6129033661327a565b9060405192612913602085613388565b5f84526136f0565b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a575f52600a602052602060ff600160405f20015460a01c166040519015158152f35b346105d75760203660031901126105d757600435612987613ad1565b805f52600a60205260ff600160405f20015460a81c161561064a57805f52600a60205260ff600160405f20015460a01c165f146129d057634a5541ef60e01b5f5260045260245ffd5b805f52600a60205260405f205460f81c611d2457612a02815f52600a6020526001600160a01b0360405f205416331490565b15611d0e57612a1081613821565b90805f52600a602052612a28600260405f20016136aa565b916001600160801b038351166001600160801b0382161015611cfb57815f52600a60205260ff60405f205460f01c1615611ce857806001600160801b03602081612a7c9481885116031695015116906133fe565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115612c1d575b825f52600a602052600360405f20016001600160801b0382166001600160801b0319825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50612b666001600160a01b03600160405f2001541694611bcf888588614341565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a1845f52600960205260ff60405f205416612ba957005b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af19081156104e057630d4af11f60e31b916001600160e01b0319915f91612bfe5750160361049f57005b612c17915060203d6020116104d9576104cb8183613388565b84610496565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055612ac6565b346105d75760203660031901126105d757612c5b61324e565b6001600160a01b035f541690338203612d9a57806001600160a01b03913b15612d6e57166040516301ffc9a760e01b81527ff8ee98d3000000000000000000000000000000000000000000000000000000006004820152602081602481855afa9081156104e0575f91612d3f575b5015612d1457805f52600960205260405f20600160ff198254161790556040519081527fb4378d4e289cb3f40f4f75a99c9cafa76e3df1c4dc31309babc23dc91bd7280160203392a2005b7f7fb843ea000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b612d61915060203d602011612d67575b612d598183613388565b810190613650565b82612cc9565b503d612d4f565b7f5a2b2d83000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b506331b339a960e21b5f526004523360245260445ffd5b346105d75760203660031901126105d7576001600160a01b03612dd261324e565b165f526009602052602060ff60405f2054166040519015158152f35b346105d7576111ab612dff3661327a565b9161341e565b346105d7575f3660031901126105d7576020600754604051908152f35b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a57612e5a90613903565b6005811015611248578060209115908115612e7b575b506040519015158152f35b600191501482612e70565b346105d75760203660031901126105d757600435805f52600a60205260ff600160405f20015460a81c161561064a576020905f90805f52600a835260ff60405f205460f01c1680612f1b575b612ee9575b506001600160801b0360405191168152f35b612f159150805f52600a8352612f0f6001600160801b03600260405f2001541691613821565b906133fe565b82612ed7565b50805f52600a835260ff600160405f20015460a01c1615612ed2565b346105d75760403660031901126105d757612f5061324e565b602435612f5c81613800565b33151580613029575b80612ff6575b612fca5781906001600160a01b0380851691167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9255f80a45f5260056020526001600160a01b0360405f2091166001600160a01b03198254161790555f80f35b7fa9fbf51f000000000000000000000000000000000000000000000000000000005f523360045260245ffd5b506001600160a01b0381165f52600660205260405f206001600160a01b0333165f5260205260ff60405f20541615612f6b565b50336001600160a01b0382161415612f65565b346105d75760203660031901126105d75760206120b46004356133dc565b346105d7575f3660031901126105d7576040515f6001548060011c9060018116801561310b575b6020831081146115bc5782855290811561159857506001146130ad57610bbb8361152681850382613388565b91905060015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6915f905b8082106130f157509091508101602001611526611516565b9192600181602092548385880101520191019092916130d9565b91607f1691613081565b346105d7575f3660031901126105d757602060405167016345785d8a00008152f35b346105d75760203660031901126105d757600435906001600160e01b031982168092036105d757817f490649060000000000000000000000000000000000000000000000000000000060209314908115613193575b5015158152f35b7f80ac58cd000000000000000000000000000000000000000000000000000000008114915081156131de575b81156131cd575b508361318c565b6301ffc9a760e01b915014836131c6565b7f5b5e139f00000000000000000000000000000000000000000000000000000000811491506131bf565b5f5b8381106132195750505f910152565b818101518382015260200161320a565b9060209161324281518092818552858086019101613208565b601f01601f1916010190565b600435906001600160a01b03821682036105d757565b602435906001600160a01b03821682036105d757565b60609060031901126105d7576004356001600160a01b03811681036105d757906024356001600160a01b03811681036105d7579060443590565b9181601f840112156105d75782359167ffffffffffffffff83116105d7576020808501948460051b0101116105d757565b610100810190811067ffffffffffffffff82111761330257604052565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761330257604052565b610180810190811067ffffffffffffffff82111761330257604052565b610140810190811067ffffffffffffffff82111761330257604052565b6040810190811067ffffffffffffffff82111761330257604052565b90601f8019910116810190811067ffffffffffffffff82111761330257604052565b67ffffffffffffffff811161330257601f01601f191660200190565b604435906001600160801b03821682036105d757565b6133e581613800565b505f5260056020526001600160a01b0360405f20541690565b906001600160801b03809116911603906001600160801b03821161053357565b91906001600160a01b03168015610d2057815f5260036020526001600160a01b0360405f205416151580613648575b8061362b575b613618577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051848152a1815f5260036020526001600160a01b0360405f20541692823315159283613563575b6001600160a01b0393508561352c575b805f52600460205260405f2060018154019055815f52600360205260405f20816001600160a01b0319825416179055857fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a41680830361351457505050565b6364283d7b60e01b5f5260045260245260445260645ffd5b61354b825f52600560205260405f206001600160a01b03198154169055565b855f52600460205260405f205f1981540190556134b3565b91929050806135c1575b1561357a578282916134a3565b828461359257637e27328960e01b5f5260045260245ffd5b7f177e802f000000000000000000000000000000000000000000000000000000005f523360045260245260445ffd5b5033841480156135ef575b8061356d5750825f526005602052336001600160a01b0360405f2054161461356d565b50835f52600660205260405f206001600160a01b0333165f5260205260ff60405f2054166135cc565b50630da9b01360e01b5f5260045260245ffd5b50815f52600a60205260ff600160405f20015460b01c1615613453565b50600161344d565b908160209103126105d7575180151581036105d75790565b91908110156136785760051b0190565b634e487b7160e01b5f52603260045260245ffd5b6040519061369982613316565b5f6040838281528260208201520152565b906040516136b781613316565b60406001600160801b03600183958054838116865260801c6020860152015416910152565b60c43564ffffffffff811681036105d75790565b906136fc83828461341e565b803b613709575b50505050565b60209161374f6001600160a01b03809316956040519586948594630a85bd0160e11b86523360048701521660248501526044840152608060648401526084830190613229565b03815f865af15f91816137bf575b5061378b575061376b6142ec565b805190816137865782633250574960e11b5f5260045260245ffd5b602001fd5b6001600160e01b0319630a85bd0160e11b9116036137ad57505f808080613703565b633250574960e11b5f5260045260245ffd5b6137d991925060203d6020116104d9576104cb8183613388565b905f61375d565b908160209103126105d757516001600160e01b0319811681036105d75790565b805f5260036020526001600160a01b0360405f205416908115612848575090565b805f52600b60205264ffffffffff60405f205416815f52600a60205264ffffffffff60405f205460a01c1690421080156138f9575b6138f357815f52600a60205264ffffffffff60405f205460c81c1690814210156138d6578061388892039042036144cf565b815f52600a6020526138ab6001600160801b03600260405f2001541680926145bb565b9081116138c0576001600160801b0391501690565b505f52600a602052600260405f20015460801c90565b50505f52600a6020526001600160801b03600260405f2001541690565b50505f90565b5042811015613856565b805f52600a60205260ff600160405f20015460a01c165f146139255750600490565b805f52600a60205260405f205460f81c61399157805f52600a60205264ffffffffff60405f205460a01c16421061398c5761395f81613821565b905f52600a6020526001600160801b0380600260405f200154169116105f1461398757600190565b600290565b505f90565b50600390565b90805f5260036020526001600160a01b0360405f205416151580613abf575b80613aa2575b612891577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b038060405f2054169283613a6b575b1680613a53575b815f52600360205260405f20816001600160a01b0319825416179055827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a490565b805f52600460205260405f2060018154019055613a0f565b613a8a835f52600560205260405f206001600160a01b03198154169055565b835f52600460205260405f205f198154019055613a08565b50805f52600a60205260ff600160405f20015460b01c16156139bc565b506001600160a01b03821615156139b6565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003613b0357565b7fa1c0d6e5000000000000000000000000000000000000000000000000000000005f5260045ffd5b805f5260036020526001600160a01b0360405f20541690813314918215613b71575b508115613b58575090565b90506001600160a01b03613b6c33926133dc565b161490565b9091505f52600660205260405f206001600160a01b0333165f5260205260ff60405f205416905f613b4d565b805f52600a602052613bb4600260405f20016136aa565b90805f52600a60205260ff600160405f20015460a01c165f14613be25750602001516001600160801b031690565b90815f52600a60205260405f205460f81c613c045750613c0190613821565b90565b613c0191506001600160801b0360408183511692015116906133fe565b90613c426001600160801b03604084015116602060e0850151015190614398565b916001600160801b0383511660c082015190156142c45764ffffffffff8151161561429c576020810164ffffffffff81511680614210575b5050604064ffffffffff82511691019064ffffffffff82511690818110156141e257505064ffffffffff80421691511690818110156141b45750506007549280516001600160801b03169160405192613cd284613316565b8352602083015f9052604083015f905260608101516001600160a01b03169260c082015190604082015164ffffffffff16946080840195888751151560a087015115159287516001600160a01b0316965164ffffffffff169160405197613d388961334f565b885260208801928352604088019182526060880190815260808801915f835260a0890196875260c08901935f855260e08a0195600187526101008b019788526101208b01998a525f52600a60205260405f2099516001600160a01b03166001600160a01b03168a546001600160a01b031916178a5551908954905160c81b7dffffffffff00000000000000000000000000000000000000000000000000169160a01b78ffffffffff000000000000000000000000000000000000000016907fffff00000000000000000000ffffffffffffffffffffffffffffffffffffffff161717885551151587549060f01b7eff000000000000000000000000000000000000000000000000000000000000169060ff60f01b191617875551151586549060f81b7fff0000000000000000000000000000000000000000000000000000000000000016907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff161786556001860193516001600160a01b03166001600160a01b031684546001600160a01b03191617845551151583549060a01b74ff0000000000000000000000000000000000000000169060ff60a01b19161783555115159082549051151560b01b76ff00000000000000000000000000000000000000000000169160a81b75ff00000000000000000000000000000000000000000016907fffffffffffffffffff0000ffffffffffffffffffffffffffffffffffffffffff16171790556002820190519081516001600160801b03166001600160801b031681546001600160801b03191617815560208201516001600160801b0316613fb891906001600160801b036001600160801b031983549260801b169116179055565b604001516001600160801b031690600301906001600160801b031681546001600160801b03191617905560c08101516020015164ffffffffff1680614194575b50600185016007556001600160a01b036020820151168015610d2057614026866001600160a01b0392613997565b16614168576140516001600160a01b036060830151166001600160801b038451169030903390614475565b7f44cb432df42caa86b7ec73644ab8aec922bc44c71c98fc330addc75b88adbc7c6101408660208501946001600160801b0386511680614139575b506141306001600160a01b03865116956001600160a01b03602082015116976001600160a01b03606083015116995115156001600160801b0360a0840151151592816001600160a01b0360e060c0880151970151511697604051998a523360208b01525116604089015251166060870152608086015260a085015260c084019064ffffffffff60408092828151168552826020820151166020860152015116910152565b610120820152a4565b614162906001600160a01b036060880151166001600160a01b0360e08901515116903390614475565b5f61408c565b7f73c6ac6e000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b855f52600b60205260405f209064ffffffffff198254161790555f613ff8565b7f210aec0e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7f5057f084000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b64ffffffffff8351168181101561426e57505064ffffffffff90511664ffffffffff60408301511690818110613c7a577f9fee2691000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7fb39831ea000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7fd572dbcb000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f6095d3bc000000000000000000000000000000000000000000000000000000005f5260045ffd5b3d15614316573d906142fd826133aa565b9161430b6040519384613388565b82523d5f602084013e565b606090565b613c019061432881613b9d565b905f52600a602052600260405f20015460801c906133fe565b614396926001600160a01b03604051937fa9059cbb000000000000000000000000000000000000000000000000000000006020860152166024840152604483015260448252614391606483613388565b614669565b565b9190916040516143a78161336c565b5f81525f6020820152926001600160801b0382169081156144585767016345785d8a00008111614421576143e36001600160801b0391836145bb565b166020850191818352111561440d576001600160801b039182614408925116906133fe565b168252565b634e487b7160e01b5f52600160045260245ffd5b7f4fea5c1a000000000000000000000000000000000000000000000000000000005f5260045267016345785d8a000060245260445ffd5b50505090506040516144698161336c565b5f81525f602082015290565b9091926001600160a01b036143969481604051957f23b872dd000000000000000000000000000000000000000000000000000000006020880152166024860152166044840152606483015260648252614391608483613388565b5f19670de0b6b3a7640000820991670de0b6b3a764000082029182808510940393808503941461459a578184101561456057670de0b6b3a7640000829109600182190182168092046002816003021880820260020302808202600203028082026002030280820260020302808202600203028091026002030293600183805f03040190848311900302920304170290565b7f63a05778000000000000000000000000000000000000000000000000000000005f52600452670de0b6b3a764000060245260445260645ffd5b50915081156145a7570490565b634e487b7160e01b5f52601260045260245ffd5b9091905f198382098382029182808310920391808303921461465857670de0b6b3a7640000821015614628577faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac106699394670de0b6b3a7640000910990828211900360ee1b910360121c170290565b84907f5173648d000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5050670de0b6b3a764000090049150565b5f806001600160a01b0361469293169360208151910182865af161468b6142ec565b90836146ee565b80519081151591826146d3575b50506146a85750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6146e69250602080918301019101613650565b155f8061469f565b9061472b575080511561470357805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b81511580614771575b61473c575090565b6001600160a01b03907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b1561473456fea164736f6c634300081a000a"; + bytes public constant BYTECODE_LOCKUP_TRANCHED = + hex"60c0604052346103e457614e2e6060813803918261001c816103e8565b9384928339810103126103e45780516001600160a01b038116908190036103e45760208201516001600160a01b03811692908390036103e4576040015161006360406103e8565b92601e84527f5361626c696572205632204c6f636b7570205472616e63686564204e46540000602085015261009860406103e8565b60118152705341422d56322d4c4f434b55502d54524160781b602082015230608052845190946001600160401b0382116102e75760015490600182811c921680156103da575b60208310146102c95781601f84931161036c575b50602090601f8311600114610306575f926102fb575b50508160011b915f199060031b1c1916176001555b83516001600160401b0381116102e757600254600181811c911680156102dd575b60208210146102c957601f8111610266575b50602094601f8211600114610203579481929394955f926101f8575b50508160011b915f199060031b1c1916176002555b5f80546001600160a01b031990811685178255600880549091169290921790915560405192907fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a360a0526001600755614a20908161040e823960805181613e32015260a051818181612fa10152613edb0152f35b015190505f8061016c565b601f1982169560025f52805f20915f5b88811061024e57508360019596979810610236575b505050811b01600255610181565b01515f1960f88460031b161c191690555f8080610228565b91926020600181928685015181550194019201610213565b60025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace601f830160051c810191602084106102bf575b601f0160051c01905b8181106102b45750610150565b5f81556001016102a7565b909150819061029e565b634e487b7160e01b5f52602260045260245ffd5b90607f169061013e565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610108565b60015f9081528281209350601f198516905b818110610354575090846001959493921061033c575b505050811b0160015561011d565b01515f1960f88460031b161c191690555f808061032e565b92936020600181928786015181550195019301610318565b60015f529091507fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6601f840160051c810191602085106103d0575b90601f859493920160051c01905b8181106103c257506100f2565b5f81558493506001016103b5565b90915081906103a7565b91607f16916100de565b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176102e75760405256fe6080806040526004361015610012575f80fd5b5f3560e01c90816301ffc9a71461330d57508063027b6744146132eb57806306fdde0314613230578063081812fc14613212578063095ea7b31461310d5780631400ecec1461305c5780631c1cdd4c14612ff85780631e99d56914612fdb57806323b872dd14612fc45780632fe4304114612f8a578063303acc8514612f4d57806332fbe22b14612df0578063406887cb14612c8157806340e58ee5146129aa578063425d30dd1461295a57806342842e0e1461293157806342966c681461276d57806344267570146127475780634857501f146126d65780634869e12d1461269c5780634cc55e11146122f657806357404b12146122685780636352211e146122395780636d0cee751461223957806370a08231146121cf57806375829def146121615780637cad6cd1146120705780637de6b1db14611f235780637f5799f914611eca5780638659c27014611b13578063894e9a0d146117d4578063897f362b146115095780638f69b993146114895780639067b6771461143a57806395d89b4114611332578063a22cb4651461127e578063a80fc0711461122d578063ad35efd4146111ce578063b25645691461117e578063b88d4fde146110f4578063b8a3be66146110bf578063b971302a14611071578063bc2be1be14611022578063c156a11d14610c08578063c87b56dd14610afd578063d4dbd20b14610aac578063d511609f14610a61578063d975dfed14610a16578063e985e9c5146109bd578063ea5ead1914610690578063eac8f5b81461063f578063f590c176146105e4578063f851a440146105bf5763fdd46d601461026e575f80fd5b346105bb5760603660031901126105bb5760043561028a61343a565b90604435916001600160801b038316908184036105bb576102a9613e28565b825f52600a60205260ff600160405f20015460a81c16156105a957825f52600a60205260ff600160405f20015460a01c16610596576001600160a01b03811690811561058357821561057057835f5260036020526001600160a01b0360405f205416948583141580610560575b610545576001600160801b0361032b86614680565b1680851161052b575061035090855f52600a602052600260405f20015460801c6146a6565b5f858152600a6020526040902060020180546001600160801b031660809290921b6001600160801b03191691909117815561038a906139ce565b6001600160801b036103ae8160208401511692826040818351169201511690613611565b1611156104f9575b835f52600a6020526103da836001600160a01b03600160405f200154169283614804565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a183331415806104e3575b61044057005b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af19081156104d8576392b9102b60e01b916001600160e01b0319915f916104a9575b50160361049757005b632187e5e760e21b5f5260045260245ffd5b6104cb915060203d6020116104d1575b6104c3818361359d565b810190613b11565b5f61048e565b503d6104b9565b6040513d5f823e3d90fd5b50835f52600960205260ff60405f20541661043a565b5f848152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b191690556103b6565b848663287ecaef60e21b5f5260045260245260445260645ffd5b828563b34359d360e01b5f526004523360245260445260645ffd5b5061056a8561455b565b15610316565b8363d2aabcd960e01b5f5260045260245ffd5b83630ff7ee2d60e31b5f5260045260245ffd5b82634a5541ef60e01b5f5260045260245ffd5b8262b8e7e760e51b5f5260045260245ffd5b5f80fd5b346105bb575f3660031901126105bb5760206001600160a01b035f5416604051908152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a602052602060405f205460f81c6040519015158152f35b62b8e7e760e51b5f5260045260245ffd5b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a60205260206001600160a01b03600160405f20015416604051908152f35b346105bb5760403660031901126105bb576004356106ac61343a565b6106b582614680565b916106be613e28565b805f52600a60205260ff600160405f20015460a81c161561062e57805f52600a60205260ff600160405f20015460a01c166109ab576001600160a01b0382168015610998576001600160801b03841692831561098557825f5260036020526001600160a01b0360405f205416948583141580610975575b61095a576001600160801b0361074a85614680565b16808611610940575061076f90845f52600a602052600260405f20015460801c6146a6565b5f848152600a6020526040902060020180546001600160801b031660809290921b6001600160801b0319169190911781556107a9906139ce565b6001600160801b036107cd8160208401511692826040818351169201511690613611565b16111561090e575b825f52600a6020526107f9846001600160a01b03600160405f200154169283614804565b81837f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051888152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051848152a183331415806108f8575b610869575b602083604051908152f35b604051916392b9102b60e01b8352600483015233602483015260448201528160648201526020816084815f875af19081156104d8576392b9102b60e01b916001600160e01b0319915f916108d9575b5016036108c657818061085e565b50632187e5e760e21b5f5260045260245ffd5b6108f2915060203d6020116104d1576104c3818361359d565b856108b8565b50835f52600960205260ff60405f205416610859565b5f838152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b191690556107d5565b858563287ecaef60e21b5f5260045260245260445260645ffd5b828463b34359d360e01b5f526004523360245260445260645ffd5b5061097f8461455b565b15610735565b8263d2aabcd960e01b5f5260045260245ffd5b50630ff7ee2d60e31b5f5260045260245ffd5b634a5541ef60e01b5f5260045260245ffd5b346105bb5760403660031901126105bb576109d6613424565b6001600160a01b036109e661343a565b91165f5260066020526001600160a01b0360405f2091165f52602052602060ff60405f2054166040519015158152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e57610a50602091614680565b6001600160801b0360405191168152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a6020526020600260405f20015460801c604051908152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a60205260206001600160801b03600360405f20015416604051908152f35b346105bb5760203660031901126105bb57600435610b1a81613b31565b505f6001600160a01b0360085416916044604051809481937fe9dc637500000000000000000000000000000000000000000000000000000000835230600484015260248301525afa80156104d8575f90610b8b575b610b87906040519182916020835260208301906133ff565b0390f35b503d805f833e610b9b818361359d565b8101906020818303126105bb5780519067ffffffffffffffff82116105bb57019080601f830112156105bb57815191610bd3836135bf565b91610be1604051938461359d565b838352602084830101116105bb57610b8792610c0391602080850191016133de565b610b6f565b346105bb5760403660031901126105bb57600435610c2461343a565b610c2c613e28565b815f52600a60205260ff600160405f20015460a81c161561101057815f5260036020526001600160a01b0360405f20541690813303610ff957610c6e83614680565b906001600160801b0382169182158015610d02575b50506001600160a01b03811615610cef57610ca6846001600160a01b0392613cee565b169182610cc05783637e27328960e01b5f5260045260245ffd5b8084918403610cd457602083604051908152f35b9091506364283d7b60e01b5f5260045260245260445260645ffd5b633250574960e11b5f525f60045260245ffd5b610d0a613e28565b855f52600a60205260ff600160405f20015460a81c1615610fe757855f52600a60205260ff600160405f20015460a01c16610fd4578415610fc157610fae57845f5260036020526001600160a01b0360405f205416908185141580610f9e575b610f83576001600160801b03610d7f87614680565b16808511610f695750610da490865f52600a602052600260405f20015460801c6146a6565b5f868152600a6020526040902060020180546001600160801b031660809290921b6001600160801b031916919091178155610dde906139ce565b6001600160801b03610e028160208401511692826040818351169201511690613611565b161115610f37575b845f52600a6020526001600160a01b03600160405f20015416610e2e848683614804565b84867f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051888152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051878152a18033141580610f21575b610e99575b80610c83565b6040516392b9102b60e01b81528560048201523360248201528460448201528360648201526020816084815f865af19081156104d8576392b9102b60e01b916001600160e01b0319915f91610f02575b501614610e9357632187e5e760e21b5f5260045260245ffd5b610f1b915060203d6020116104d1576104c3818361359d565b88610ee9565b50805f52600960205260ff60405f205416610e8e565b5f858152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b19169055610e0a565b848763287ecaef60e21b5f5260045260245260445260645ffd5b848663b34359d360e01b5f526004523360245260445260645ffd5b50610fa88661455b565b15610d6a565b8463d2aabcd960e01b5f5260045260245ffd5b85630ff7ee2d60e31b5f5260045260245ffd5b85634a5541ef60e01b5f5260045260245ffd5b8562b8e7e760e51b5f5260045260245ffd5b8263216caf0d60e01b5f526004523360245260445ffd5b5062b8e7e760e51b5f5260045260245ffd5b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a602052602064ffffffffff60405f205460a01c16604051908152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a60205260206001600160a01b0360405f205416604051908152f35b346105bb5760203660031901126105bb576004355f52600a602052602060ff600160405f20015460a81c166040519015158152f35b346105bb5760803660031901126105bb5761110d613424565b61111561343a565b6064359167ffffffffffffffff83116105bb57366023840112156105bb57826004013591611142836135bf565b92611150604051948561359d565b80845236602482870101116105bb576020815f92602461117c9801838801378501015260443591613a21565b005b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a602052602060ff600160405f20015460b01c166040519015158152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e5761120690613c5a565b6040516005821015611219576020918152f35b634e487b7160e01b5f52602160045260245ffd5b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a60205260206001600160801b03600260405f20015416604051908152f35b346105bb5760403660031901126105bb57611297613424565b602435908115158092036105bb576001600160a01b031690811561130657335f52600660205260405f20825f5260205260405f2060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b507f5b08ba18000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346105bb575f3660031901126105bb576040515f6002548060011c90600181168015611430575b60208310811461141c578285529081156113f8575060011461139a575b610b87836113868185038261359d565b6040519182916020835260208301906133ff565b91905060025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace915f905b8082106113de57509091508101602001611386611376565b9192600181602092548385880101520191019092916113c6565b60ff191660208086019190915291151560051b840190910191506113869050611376565b634e487b7160e01b5f52602260045260245ffd5b91607f1691611359565b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a602052602064ffffffffff60405f205460c81c16604051908152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e576114c190613c5a565b60058110158061121957600282149081156114fd575b81156114eb575b6020826040519015158152f35b905061121957600460209114826114de565b5050600381145f6114d7565b346105bb5760203660031901126105bb5760043567ffffffffffffffff81116105bb578036036101206003198201126105bb57611544613e28565b60c482013590602219018112156105bb5781019060048201359167ffffffffffffffff83116105bb5760248101908360061b80360383136105bb57600460209161158d87613875565b9661159b604051988961359d565b875282870193010101913683116105bb57905b8282106117ba575050508151916115c483613875565b926115d2604051948561359d565b808452601f196115e182613875565b015f5b81811061179757505064ffffffffff4216916001600160801b0361160782613b52565b51511664ffffffffff80602061161c85613b52565b510151168501166040519161163083613548565b8252602082015261164086613b52565b5261164a85613b52565b5060015b8281106117225750505061166482600401613a00565b9261167160248401613a00565b9261167e6044820161392e565b916064820135936001600160a01b0385168095036105bb5760209661171a966116da966001600160801b0361170f976001600160a01b036116c160848a01613a14565b94816116cf60a48c01613a14565b976040519d8e61352b565b168c52168c8b0152166040890152606088015215156080870152151560a086015260c085015260e084015260e43691016138c3565b610100820152613e82565b604051908152f35b806001600160801b0361173760019385613b5f565b51511664ffffffffff8060206117505f1986018c613b5f565b510151168160206117618689613b5f565b5101511601166040519161177483613548565b825260208201526117858289613b5f565b526117908188613b5f565b500161164e565b6020906040516117a681613548565b5f81525f83820152828289010152016115e4565b60206040916117c9368561388d565b8152019101906115ae565b346105bb5760203660031901126105bb5760043560606101606040516117f981613564565b5f81525f60208201525f60408201525f838201525f60808201525f60a08201525f60c08201525f60e08201525f6101008201525f61012082015260405161183f81613581565b5f81525f60208201525f60408201526101408201520152805f52600a60205260ff600160405f20015460a81c161561062e57805f52600a60205260405f2060405191610140830183811067ffffffffffffffff821117611aff576040528154916001600160a01b0383168452602084019264ffffffffff8160a01c168452604085019064ffffffffff8160c81c16825285606081019260ff8360f01c1615158452608082019260f81c1515835260018501549260a08301956001600160a01b038516875261193c600260c086019260ff8860a01c161515845260ff61010060e0890198828b60a81c1615158a52019860b01c1615158852016139ce565b6101208b0190815261194d89613c5a565b600581101561121957600214611af7575b5196516001600160a01b0316925164ffffffffff169551151590511515935115159451151595885f52600360205260405f20546001600160a01b03169a516001600160a01b0316995164ffffffffff16985f52600b60205260405f2092511515926040519a6119cc8c613564565b8b5260208b019b8c5260408b01998a5260608b0191825260808b0192835260a08b0193845260c08b0194855260e08b019586526101008b019687526101208b019788526101408b01988952611a209061395a565b986101608b01998a526040519b8c9b60208d52516001600160a01b031660208d0152516001600160a01b031660408c01525164ffffffffff1660608b01525164ffffffffff1660808a015251151560a089015251151560c0880152516001600160a01b031660e08701525115156101008601525115156101208501525115156101408401525180516001600160801b031661016084015260208101516001600160801b0316610180840152604001516001600160801b03166101a0830152516101c082016101c090526101e08201610b87916134cf565b5f875261195e565b634e487b7160e01b5f52604160045260245ffd5b346105bb5760203660031901126105bb5760043567ffffffffffffffff81116105bb57611b4490369060040161349e565b90611b4d613e28565b5f915b808310611b5957005b611b6483828461390a565b3592611b6e613e28565b835f52600a60205260ff600160405f20015460a81c1615611eb857835f52600a60205260ff600160405f20015460a01c165f14611bb85783634a5541ef60e01b5f5260045260245ffd5b909192805f52600a60205260405f205460f81c611ea657611bed815f52600a6020526001600160a01b0360405f205416331490565b15611e9057611bfb81613b73565b90805f52600a602052611c13600260405f20016139ce565b916001600160801b038351166001600160801b0382161015611e7d57815f52600a60205260ff60405f205460f01c1615611e6a57806001600160801b03602081611c67948188511603169501511690613611565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115611e45575b825f52600a602052600360405f20016001600160801b0382166001600160801b0319825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50611d796001600160a01b03600160405f2001541694611d51888588614804565b604080518b81526001600160801b03808b166020830152909216908201529081906060820190565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a1845f52600960205260ff60405f205416611dca575b50505050506001019190611b50565b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af19081156104d857630d4af11f60e31b916001600160e01b0319915f91611e27575b5016036104975780808080611dbb565b611e3f915060203d81116104d1576104c3818361359d565b87611e17565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055611cb1565b506339c6dc7360e21b5f5260045260245ffd5b506322cad1af60e11b5f5260045260245ffd5b63216caf0d60e01b5f526004523360245260445ffd5b63fe19f19f60e01b5f5260045260245ffd5b8362b8e7e760e51b5f5260045260245ffd5b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600b602052610b87611f0f60405f2061395a565b6040519182916020835260208301906134cf565b346105bb5760203660031901126105bb57600435611f3f613e28565b805f52600a60205260ff600160405f20015460a81c161561062e57611f6381613c5a565b60058110156112195760048103611f875750634a5541ef60e01b5f5260045260245ffd5b60038103611fa2575063fe19f19f60e01b5f5260045260245ffd5b60021461205e57611fc7815f52600a6020526001600160a01b0360405f205416331490565b15611e9057805f52600a60205260ff60405f205460f01c161561204c576020817ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7925f52600a825260405f2060ff60f01b19815416905560405190807f0eb069207093cd3e51cd1370d2d369770057fbe29947e577e5fb428c6c6fc78f5f80a28152a1005b6339c6dc7360e21b5f5260045260245ffd5b6322cad1af60e11b5f5260045260245ffd5b346105bb5760203660031901126105bb576004356001600160a01b0381168091036105bb576001600160a01b035f541633810361214b575060085490806001600160a01b03198316176008556001600160a01b036040519216825260208201527fa2548bd4b805e907c1558a47b5858324fe8bb4a2e1ddfca647eecbf65610eebc60403392a26007545f1981019081116121375760407f6bd5c950a8d8df17f772f5af37cb3655737899cbf903264b9795592da439661c91815190600182526020820152a1005b634e487b7160e01b5f52601160045260245ffd5b6331b339a960e21b5f526004523360245260445ffd5b346105bb5760203660031901126105bb5761217a613424565b5f546001600160a01b03811633810361214b57506001600160a01b036001600160a01b0319921691829116175f55337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf805f80a3005b346105bb5760203660031901126105bb576001600160a01b036121f0613424565b16801561220d575f526004602052602060405f2054604051908152f35b7f89c62b64000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b346105bb5760203660031901126105bb576020612257600435613b31565b6001600160a01b0360405191168152f35b346105bb5760203660031901126105bb57600435612284613942565b50805f52600a60205260ff600160405f20015460a81c161561062e575f908152600a6020526040908190205481519064ffffffffff60c882901c81169160a01c166122ce83613548565b825260208201526122f48251809264ffffffffff60208092828151168552015116910152565bf35b346105bb5760403660031901126105bb5760043567ffffffffffffffff81116105bb5761232790369060040161349e565b9060243567ffffffffffffffff81116105bb5761234890369060040161349e565b919092612353613e28565b82810361266c575f5b81811061236557005b61237081838561390a565b3561237c82848661390a565b355f5260036020526001600160a01b0360405f205416906123a66123a184888a61390a565b61392e565b916123af613e28565b815f52600a60205260ff600160405f20015460a81c161561101057815f52600a60205260ff600160405f20015460a01c16612659578015610998576001600160801b03831690811561098557825f5260036020526001600160a01b0360405f205416938482141580612649575b61262e576001600160801b0361243185614680565b16808411612614575061245690845f52600a602052600260405f20015460801c6146a6565b5f848152600a6020526040902060020180546001600160801b031660809290921b6001600160801b031916919091178155612490906139ce565b6001600160801b036124b48160208401511692826040818351169201511690613611565b1611156125e2575b825f52600a6020526001600160a01b03600160405f200154166124e0838383614804565b81847f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d6020604051878152a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051858152a183331415806125cc575b612551575b5050505060010161235c565b604051926392b9102b60e01b84526004840152336024840152604483015260648201526020816084815f865af19081156104d8576392b9102b60e01b916001600160e01b0319915f916125ae575b50160361049757808080612545565b6125c6915060203d81116104d1576104c3818361359d565b8961259f565b50835f52600960205260ff60405f205416612540565b5f838152600a6020526040902060018101805460ff60a01b1916600160a01b179055805460ff60f01b191690556124bc565b838563287ecaef60e21b5f5260045260245260445260645ffd5b508263b34359d360e01b5f526004523360245260445260645ffd5b506126538461455b565b1561241c565b50634a5541ef60e01b5f5260045260245ffd5b90507faec93440000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e57610a506020916145cd565b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f61270f82613c5a565b60058110156112195760020361272d575b6020906040519015158152f35b505f52600a602052602060ff60405f205460f01c16612720565b346105bb575f3660031901126105bb5760206001600160a01b0360085416604051908152f35b346105bb5760203660031901126105bb57600435612789613e28565b805f52600a60205260ff600160405f20015460a81c161561062e57805f52600a60205260ff600160405f20015460a01c1615612906576127c88161455b565b15611e9057805f5260036020526001600160a01b0360405f2054161515806128ff575b806128e2575b6128d0577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b0360405f2054168015908115612899575b825f52600360205260405f206001600160a01b03198154169055825f827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8280a45061288757005b637e27328960e01b5f5260045260245ffd5b6128b8835f52600560205260405f206001600160a01b03198154169055565b805f52600460205260405f205f19815401905561283f565b630da9b01360e01b5f5260045260245ffd5b50805f52600a60205260ff600160405f20015460b01c16156127f1565b505f6127eb565b7f817cd639000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b346105bb5761117c61294236613464565b906040519261295260208561359d565b5f8452613a21565b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e575f52600a602052602060ff600160405f20015460a01c166040519015158152f35b346105bb5760203660031901126105bb576004356129c6613e28565b805f52600a60205260ff600160405f20015460a81c161561062e57805f52600a60205260ff600160405f20015460a01c165f14612a0f57634a5541ef60e01b5f5260045260245ffd5b805f52600a60205260405f205460f81c611ea657612a41815f52600a6020526001600160a01b0360405f205416331490565b15611e9057612a4f81613b73565b90805f52600a602052612a67600260405f20016139ce565b916001600160801b038351166001600160801b0382161015611e7d57815f52600a60205260ff60405f205460f01c1615611e6a57806001600160801b03602081612abb948188511603169501511690613611565b5f828152600a6020526040902080547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16600160f81b179055916001600160801b038316908115612c5c575b825f52600a602052600360405f20016001600160801b0382166001600160801b0319825416179055825f52600a6020526001600160a01b0360405f205416835f5260036020526001600160a01b0360405f20541694845f52600a60205285827f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50612ba56001600160a01b03600160405f2001541694611d51888588614804565b0390a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051868152a1845f52600960205260ff60405f205416612be857005b60405193630d4af11f60e31b855260048501526024840152604483015260648201526020816084815f865af19081156104d857630d4af11f60e31b916001600160e01b0319915f91612c3d5750160361049757005b612c56915060203d6020116104d1576104c3818361359d565b8461048e565b825f52600a602052600160405f2001600160a01b60ff60a01b19825416179055612b05565b346105bb5760203660031901126105bb57612c9a613424565b6001600160a01b035f541690338203612dd957806001600160a01b03913b15612dad57166040516301ffc9a760e01b81527ff8ee98d3000000000000000000000000000000000000000000000000000000006004820152602081602481855afa9081156104d8575f91612d7e575b5015612d5357805f52600960205260405f20600160ff198254161790556040519081527fb4378d4e289cb3f40f4f75a99c9cafa76e3df1c4dc31309babc23dc91bd7280160203392a2005b7f7fb843ea000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b612da0915060203d602011612da6575b612d98818361359d565b8101906138f2565b82612d08565b503d612d8e565b7f5a2b2d83000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b506331b339a960e21b5f526004523360245260445ffd5b346105bb5760203660031901126105bb5760043567ffffffffffffffff81116105bb5761014060031982360301126105bb57612e2a613e28565b604051612e368161352b565b612e4282600401613450565b8152612e5060248301613450565b6020820152612e61604483016135db565b604082015260648201356001600160a01b03811681036105bb576060820152612e8c6084830161351e565b6080820152612e9d60a4830161351e565b60a0820152612eae60c48301613863565b60c082015260e482013567ffffffffffffffff81116105bb57820191366023840112156105bb57600483013592612ee484613875565b90612ef2604051928361359d565b848252602060048184019660061b83010101903682116105bb57602401945b818610612f3357602061171a8661170f878760e08401526101043691016138c3565b6020604091612f42368961388d565b815201950194612f11565b346105bb5760203660031901126105bb576001600160a01b03612f6e613424565b165f526009602052602060ff60405f2054166040519015158152f35b346105bb575f3660031901126105bb5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346105bb5761117c612fd536613464565b91613631565b346105bb575f3660031901126105bb576020600754604051908152f35b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e5761303090613c5a565b6005811015611219578060209115908115613051575b506040519015158152f35b600191501482613046565b346105bb5760203660031901126105bb57600435805f52600a60205260ff600160405f20015460a81c161561062e576020905f90805f52600a835260ff60405f205460f01c16806130f1575b6130bf575b506001600160801b0360405191168152f35b6130eb9150805f52600a83526130e56001600160801b03600260405f2001541691613b73565b90613611565b826130ad565b50805f52600a835260ff600160405f20015460a01c16156130a8565b346105bb5760403660031901126105bb57613126613424565b60243561313281613b31565b331515806131ff575b806131cc575b6131a05781906001600160a01b0380851691167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9255f80a45f5260056020526001600160a01b0360405f2091166001600160a01b03198254161790555f80f35b7fa9fbf51f000000000000000000000000000000000000000000000000000000005f523360045260245ffd5b506001600160a01b0381165f52600660205260405f206001600160a01b0333165f5260205260ff60405f20541615613141565b50336001600160a01b038216141561313b565b346105bb5760203660031901126105bb5760206122576004356135ef565b346105bb575f3660031901126105bb576040515f6001548060011c906001811680156132e1575b60208310811461141c578285529081156113f8575060011461328357610b87836113868185038261359d565b91905060015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6915f905b8082106132c757509091508101602001611386611376565b9192600181602092548385880101520191019092916132af565b91607f1691613257565b346105bb575f3660031901126105bb57602060405167016345785d8a00008152f35b346105bb5760203660031901126105bb57600435906001600160e01b031982168092036105bb57817f490649060000000000000000000000000000000000000000000000000000000060209314908115613369575b5015158152f35b7f80ac58cd000000000000000000000000000000000000000000000000000000008114915081156133b4575b81156133a3575b5083613362565b6301ffc9a760e01b9150148361339c565b7f5b5e139f0000000000000000000000000000000000000000000000000000000081149150613395565b5f5b8381106133ef5750505f910152565b81810151838201526020016133e0565b90602091613418815180928185528580860191016133de565b601f01601f1916010190565b600435906001600160a01b03821682036105bb57565b602435906001600160a01b03821682036105bb57565b35906001600160a01b03821682036105bb57565b60609060031901126105bb576004356001600160a01b03811681036105bb57906024356001600160a01b03811681036105bb579060443590565b9181601f840112156105bb5782359167ffffffffffffffff83116105bb576020808501948460051b0101116105bb57565b90602080835192838152019201905f5b8181106134ec5750505090565b825180516001600160801b0316855260209081015164ffffffffff1681860152604090940193909201916001016134df565b359081151582036105bb57565b610120810190811067ffffffffffffffff821117611aff57604052565b6040810190811067ffffffffffffffff821117611aff57604052565b610180810190811067ffffffffffffffff821117611aff57604052565b6060810190811067ffffffffffffffff821117611aff57604052565b90601f8019910116810190811067ffffffffffffffff821117611aff57604052565b67ffffffffffffffff8111611aff57601f01601f191660200190565b35906001600160801b03821682036105bb57565b6135f881613b31565b505f5260056020526001600160a01b0360405f20541690565b906001600160801b03809116911603906001600160801b03821161213757565b91906001600160a01b03168015610cef57815f5260036020526001600160a01b0360405f20541615158061385b575b8061383e575b61382b577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051848152a1815f5260036020526001600160a01b0360405f20541692823315159283613776575b6001600160a01b0393508561373f575b805f52600460205260405f2060018154019055815f52600360205260405f20816001600160a01b0319825416179055857fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a41680830361372757505050565b6364283d7b60e01b5f5260045260245260445260645ffd5b61375e825f52600560205260405f206001600160a01b03198154169055565b855f52600460205260405f205f1981540190556136c6565b91929050806137d4575b1561378d578282916136b6565b82846137a557637e27328960e01b5f5260045260245ffd5b7f177e802f000000000000000000000000000000000000000000000000000000005f523360045260245260445ffd5b503384148015613802575b806137805750825f526005602052336001600160a01b0360405f20541614613780565b50835f52600660205260405f206001600160a01b0333165f5260205260ff60405f2054166137df565b50630da9b01360e01b5f5260045260245ffd5b50815f52600a60205260ff600160405f20015460b01c1615613666565b506001613660565b359064ffffffffff821682036105bb57565b67ffffffffffffffff8111611aff5760051b60200190565b91908260409103126105bb576040516138a581613548565b60206138be8183956138b6816135db565b855201613863565b910152565b91908260409103126105bb576040516138db81613548565b60208082946138e981613450565b84520135910152565b908160209103126105bb575180151581036105bb5790565b919081101561391a5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160801b03811681036105bb5790565b6040519061394f82613548565b5f6020838281520152565b90815461396681613875565b92613974604051948561359d565b81845260208401905f5260205f205f915b8383106139925750505050565b6001602081926040516139a481613548565b64ffffffffff86546001600160801b038116835260801c1683820152815201920192019190613985565b906040516139db81613581565b60406001600160801b03600183958054838116865260801c6020860152015416910152565b356001600160a01b03811681036105bb5790565b3580151581036105bb5790565b90613a2d838284613631565b803b613a3a575b50505050565b602091613a806001600160a01b03809316956040519586948594630a85bd0160e11b865233600487015216602485015260448401526080606484015260848301906133ff565b03815f865af15f9181613af0575b50613abc5750613a9c614651565b80519081613ab75782633250574960e11b5f5260045260245ffd5b602001fd5b6001600160e01b0319630a85bd0160e11b911603613ade57505f808080613a34565b633250574960e11b5f5260045260245ffd5b613b0a91925060203d6020116104d1576104c3818361359d565b905f613a8e565b908160209103126105bb57516001600160e01b0319811681036105bb5790565b805f5260036020526001600160a01b0360405f205416908115612887575090565b80511561391a5760200190565b805182101561391a5760209160051b010190565b9064ffffffffff421691805f52600b602052613b9160405f2061395a565b908364ffffffffff6020613ba485613b52565b5101511611613c5357805f52600a6020528364ffffffffff60405f205460c81c161115613c3457506001600160801b03613bdd82613b52565b515116916001925b8251841015613c2d578464ffffffffff6020613c018787613b5f565b5101511611613c2d576001600160801b0360019181613c208787613b5f565b5151160116930192613be5565b9350915050565b919250505f52600a6020526001600160801b03600260405f2001541690565b505f925050565b805f52600a60205260ff600160405f20015460a01c165f14613c7c5750600490565b805f52600a60205260405f205460f81c613ce857805f52600a60205264ffffffffff60405f205460a01c164210613ce357613cb681613b73565b905f52600a6020526001600160801b0380600260405f200154169116105f14613cde57600190565b600290565b505f90565b50600390565b90805f5260036020526001600160a01b0360405f205416151580613e16575b80613df9575b6128d0577ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020604051838152a1805f5260036020526001600160a01b038060405f2054169283613dc2575b1680613daa575b815f52600360205260405f20816001600160a01b0319825416179055827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a490565b805f52600460205260405f2060018154019055613d66565b613de1835f52600560205260405f206001600160a01b03198154169055565b835f52600460205260405f205f198154019055613d5f565b50805f52600a60205260ff600160405f20015460b01c1615613d13565b506001600160a01b0382161515613d0d565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003613e5a57565b7fa1c0d6e5000000000000000000000000000000000000000000000000000000005f5260045ffd5b90613ea46001600160801b0360408401511660206101008501510151906146c6565b916001600160801b038351169060e08101519160c082019264ffffffffff845116821561453357801561450b57815180156144e3577f000000000000000000000000000000000000000000000000000000000000000081116144b8575064ffffffffff6020613f1284613b52565b5101511681101561447457505f905f905f81515f905b8082106143ec575050505064ffffffffff804216911690818110156143be5750506001600160801b03169081810361439057505060075493845f52600a60205260405f20916001600160801b038251166001600160801b036002850191166001600160801b03198254161790556001600160a01b03606082015116916001600160a01b036001850193166001600160a01b031984541617835560808201948551151560ff60f01b197eff00000000000000000000000000000000000000000000000000000000000087549260f01b169116178555835493750100000000000000000000000000000000000000000060a08501957fffffffffffffffffff0000ffffffffffffffffffffffffffffffffffffffffff76ff000000000000000000000000000000000000000000008851151560b01b169116171790556001600160a01b0380845116166001600160a01b03198654161785555184549060e0840151917fffff00000000000000000000ffffffffffffffffffffffffffffffffffffffff78ffffffffff00000000000000000000000000000000000000007dffffffffff0000000000000000000000000000000000000000000000000060206140f48751975f19890190613b5f565b51015160c81b169360a01b169116171785555f5b8181106142de575050600187016007556001600160a01b036020830151168015610cef5761413e886001600160a01b0392613cee565b166142b257868261418c6001600160a01b0360607ffeb1cb9ce021c8bd5fb1eb836e6284c68866fa32d1d844238de19955238f8076960151166001600160801b0385511690309033906147a3565b6001600160801b0360208401511680614282575b506001600160a01b03815116946142776142596001600160a01b03602085015116986001600160a01b036060860151169a511515935115156001600160a01b0361010060e088015193549764ffffffffff604051996141fe8b613548565b818160a01c168b5260c81c1660208a015201515116946001600160801b0360206040519a8b9a8b5233828c01528281511660408c01520151166060890152608088015260a087015261014060c08701526101408601906134cf565b9260e085019064ffffffffff60208092828151168552015116910152565b6101208301520390a4565b6142ac906001600160a01b036060840151166001600160a01b0361010085015151169033906147a3565b5f6141a0565b7f73c6ac6e000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b885f52600b60205260405f20906142f98160e0870151613b5f565b5182549268010000000000000000841015611aff576001840180825584101561391a576001936020915f52815f2001916001600160801b0380825116166001600160801b031984541617835501517fffffffffffffffffffffff0000000000ffffffffffffffffffffffffffffffff74ffffffffff0000000000000000000000000000000083549260801b16911617905501614108565b7f6375ff13000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7f210aec0e000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b9193509193614410906001600160801b036144078588613b5f565b515116906146a6565b9364ffffffffff8060206144248685613b5f565b5101511694168085111561444057506001849301909291613f28565b8490847fd97494c6000000000000000000000000000000000000000000000000000000005f5260045260245260445260645ffd5b64ffffffffff602061448584613b52565b51015116907ff1fb2cc5000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b7f73627f74000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7ea4ccdf000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd572dbcb000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f6095d3bc000000000000000000000000000000000000000000000000000000005f5260045ffd5b805f5260036020526001600160a01b0360405f205416908133149182156145a1575b508115614588575090565b90506001600160a01b0361459c33926135ef565b161490565b9091505f52600660205260405f206001600160a01b0333165f5260205260ff60405f205416905f61457d565b805f52600a6020526145e4600260405f20016139ce565b90805f52600a60205260ff600160405f20015460a01c165f146146125750602001516001600160801b031690565b90815f52600a60205260405f205460f81c614634575061463190613b73565b90565b61463191506001600160801b036040818351169201511690613611565b3d1561467b573d90614662826135bf565b91614670604051938461359d565b82523d5f602084013e565b606090565b6146319061468d816145cd565b905f52600a602052600260405f20015460801c90613611565b906001600160801b03809116911601906001600160801b03821161213757565b9190916040516146d581613548565b5f81525f6020820152926001600160801b0382169081156147865767016345785d8a0000811161474f576147116001600160801b0391836148d9565b166020850191818352111561473b576001600160801b03918261473692511690613611565b168252565b634e487b7160e01b5f52600160045260245ffd5b7f4fea5c1a000000000000000000000000000000000000000000000000000000005f5260045267016345785d8a000060245260445ffd5b505050905060405161479781613548565b5f81525f602082015290565b9091926001600160a01b036148029481604051957f23b872dd0000000000000000000000000000000000000000000000000000000060208801521660248601521660448401526064830152606482526147fd60848361359d565b614854565b565b614802926001600160a01b03604051937fa9059cbb0000000000000000000000000000000000000000000000000000000060208601521660248401526044830152604482526147fd60648361359d565b5f806001600160a01b0361487d93169360208151910182865af1614876614651565b9083614987565b80519081151591826148be575b50506148935750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6148d192506020809183010191016138f2565b155f8061488a565b9091905f198382098382029182808310920391808303921461497657670de0b6b3a7640000821015614946577faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac106699394670de0b6b3a7640000910990828211900360ee1b910360121c170290565b84907f5173648d000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5050670de0b6b3a764000090049150565b906149c4575080511561499c57805190602001fd5b7f1425ea42000000000000000000000000000000000000000000000000000000005f5260045ffd5b81511580614a0a575b6149d5575090565b6001600160a01b03907f9996b315000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b50803b156149cd56fea164736f6c634300081a000a"; + bytes public constant BYTECODE_NFT_DESCRIPTOR = + hex"60808060405234601557615eaf908161001a8239f35b5f80fdfe6102406040526004361015610012575f80fd5b5f3560e01c63e9dc637514610025575f80fd5b346141bf5760403660031901126141bf576001600160a01b036004351680600435036141bf576103e06040525f61024081905260606102608190526102808290526102a08290526102c08190526102e0819052610320819052610340819052610360819052610380526103a08190526103c0526103008190526100b6906100ad600435614827565b61032052614a3d565b61034052610300516040517feac8f5b80000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa9081156145bb575f91614684575b506001600160a01b0361012791168061024052614b39565b61026052610300516040517fa80fc0710000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa80156145bb576fffffffffffffffffffffffffffffffff915f91614665575b501661028052610300516040517fad35efd40000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa80156145bb575f90614628575b6101f59150614cdb565b61036052610300516040517f4869e12d0000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa9081156145bb575f916145f9575b50610280516fffffffffffffffffffffffffffffffff1680156145e5576fffffffffffffffffffffffffffffffff612710819302160416610160610240015260405160208101904682526bffffffffffffffffffffffff1960043560601b1660408201526024356054820152605481526102c9607482614713565b51902061040a60028061016861ffff8560101c160693600161031c63ffffffff601e61031482601461030c82604660ff6050818d60081c16069b16069d16615408565b970116615408565b980116615408565b60246040519788947f68736c2800000000000000000000000000000000000000000000000000000000602087015261035d815180926020868a0191016146cd565b85017f2c00000000000000000000000000000000000000000000000000000000000000838201526103988251809360206025850191016146cd565b01017f252c000000000000000000000000000000000000000000000000000000000000838201526103d38251809360206003850191016146cd565b01017f2529000000000000000000000000000000000000000000000000000000000000838201520301601d19810184520182614713565b6104446fffffffffffffffffffffffffffffffff604061024001511660ff61043d6001600160a01b036102405116614ddb565b1690614f41565b9061045a6001600160a01b036102405116614a3d565b6020610240015190602460206001600160a01b0360c0610240015116604051928380927fbc2be1be000000000000000000000000000000000000000000000000000000008252823560048301525afa80156145bb576024915f916145c6575b5060206001600160a01b0360c0610240015116604051938480927f9067b677000000000000000000000000000000000000000000000000000000008252823560048301525afa80156145bb5764ffffffffff8091610521945f9161458c575b50169116615237565b610340516103a05190939091906105ac600161054a60646105438188066158b3565b9604615408565b6020604051968261056489945180928580880191016146cd565b8301610578825180938580850191016146cd565b01017f2500000000000000000000000000000000000000000000000000000000000000815203601e19810186520184614713565b61016061024001519361012061024001519760e061024001519760405161016052610140610160510161016051811067ffffffffffffffff821117614578576040526101605152602061016051015260406101605101526060610160510152608061016051015260a061016051015260c061016051015260e06101605101526101006101605101526101206101605101526040516101c0810181811067ffffffffffffffff82111761457857604052606081525f60208201525f60408201526060808201525f6080820152606060a08201525f60c08201525f60e082015260606101008201525f6101208201525f61014082015260606101608201525f6101808201525f6101a082015260a06101605101516108eb6109ca60046007602760586106e260c06101605101516101605151906159b4565b60b76106ed5f615ca7565b985f6102205260206102205261071560405161070c6102205182614713565b5f8152846156d2565b1561456e57601b60909a5b6107298c615408565b906040519b8c9889937f3c672069643d220000000000000000000000000000000000000000000000000061022051860152835161076f81846102205188019801886146cd565b8b017f222066696c6c3d2223666666223e000000000000000000000000000000000000838201527f3c726563742077696474683d220000000000000000000000000000000000000060358201526107d282518093604284019061022051016146cd565b0101917f22206865696768743d22313030222066696c6c2d6f7061636974793d222e3033858401527f222072783d223135222072793d22313522207374726f6b653d22236666662220603b8401527f7374726f6b652d6f7061636974793d222e3122207374726f6b652d7769647468605b8401527f3d2234222f3e0000000000000000000000000000000000000000000000000000607b8401527f3c7465787420783d2232302220793d2233342220666f6e742d66616d696c793d60818401527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f7370616365222060a18401527f666f6e742d73697a653d2232327078223e00000000000000000000000000000060c184015251809360d28401906146cd565b0101661e17ba32bc3a1f60c91b838201527f3c7465787420783d2232302220793d2237322220666f6e742d66616d696c793d60be8201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f7370616365222060de8201527f666f6e742d73697a653d2232367078223e00000000000000000000000000000060fe8201526109858251809361010f84019061022051016146cd565b0101661e17ba32bc3a1f60c91b838201526109ac82518093605f84019061022051016146cd565b0101631e17b39f60e11b838201520301601b19810184520182614713565b6101008301526101208201526101206101605101516108eb610a3860046007602760586040516109fd6102205182614713565b5f815260b7610a0c6001615ca7565b98601b6028610a1a8c615db2565b610a2384615e2a565b8082111561456757505b019a6107298c615408565b61016083015261018082015260206101605101516108eb610a796004600760276058604051610a6a6102205182614713565b5f815260b7610a0c6002615ca7565b8252602082015260286080610160510151604051610a9a6102205182614713565b5f81526108eb610ae46004600760276058610ab56003615ca7565b9660b7610ac189615db2565b610aca8b615e2a565b8082111561455f5750995b601b8c8c019a6107298c615408565b60a085015260c0840152602083015101016101208201510161018082015101603081016080830152602f19906103e8030160011c8061014083015261012082015101601081016101a083015261018082015101610220518101604083015260106102205191602084015101010160e0820152610b7361010082015161016083015183519060a085015192614ed4565b60608201526101006101208190526040516101a0819052610b949190614713565b60c76101a051527f3c726563742077696474683d223130302522206865696768743d223130302522610220516101a05101527f2066696c7465723d2275726c28234e6f69736529222f3e3c7265637420783d2260406101a05101527f37302220793d223730222077696474683d2238363022206865696768743d223860606101a05101527f3630222066696c6c3d2223666666222066696c6c2d6f7061636974793d222e3060806101a05101527f33222072783d223435222072793d22343522207374726f6b653d22236666662260a06101a05101527f207374726f6b652d6f7061636974793d222e3122207374726f6b652d7769647460c06101a05101527f683d2234222f3e0000000000000000000000000000000000000000000000000060e06101a05101526101605151610120610160510151906060830151610140525f610200526060610200526040516101e052610cf6610200516101e051614713565b60336101e051527f3c636972636c652069643d22476c6f772220723d22353030222066696c6c3d22610220516101e05101527f75726c282352616469616c476c6f7729222f3e0000000000000000000000000060406101e051015261014060405190610d628183614713565b61011c82527f3c66696c7465722069643d224e6f697365223e3c6665466c6f6f6420783d2230610220518301527f2220793d2230222077696474683d223130302522206865696768743d2231303060408301527f252220666c6f6f642d636f6c6f723d2268736c283233302c3231252c31312529610200518301527f2220666c6f6f642d6f7061636974793d22312220726573756c743d22666c6f6f60808301527f6446696c6c222f3e3c666554757262756c656e6365206261736546726571756560a08301527f6e63793d222e3422206e756d4f6374617665733d22332220726573756c743d2260c08301527f4e6f6973652220747970653d226672616374616c4e6f697365222f3e3c66654260e08301527f6c656e6420696e3d224e6f6973652220696e323d22666c6f6f6446696c6c2220610120518301527f6d6f64653d22736f66742d6c69676874222f3e3c2f66696c7465723e000000006101208301525f6101c0526103a06101c0526119416118bc6073606b60405196610eeb6101c05189614713565b61037b88527f3c706174682069643d224c6f676f222066696c6c3d2223666666222066696c6c610220518901527f2d6f7061636974793d222e312220643d226d3133332e3535392c3132342e303360408901527f34632d2e3031332c322e3431322d312e3035392c342e3834382d322e3932332c610200518901527f362e3430322d322e3535382c312e3831392d352e3136382c332e3433392d372e60808901527f3838382c342e3939362d31342e34342c382e3236322d33312e3034372c31322e60a08901527f3536352d34372e3637342c31322e3536392d382e3835382e3033362d31372e3860c08901527f33382d312e3237322d32362e3332382d332e3636332d392e3830362d322e373660e08901527f362d31392e3038372d372e3131332d32372e3536322d31322e3737382d31332e610120518901527f3834322d382e3032352c392e3436382d32382e3630362c31362e3135332d33356101208901527f2e323635683063322e3033352d312e3833382c342e3235322d332e3534362c36868901527f2e3436332d352e323234683063362e3432392d352e3635352c31362e3231382d6101608901527f322e3833352c32302e3335382c342e31372c342e3134332c352e3035372c382e6101808901527f3831362c392e3634392c31332e39322c31332e373334682e30333763352e37336101a08901527f362c362e3436312c31352e3335372d322e3235332c392e33382d382e34382c306101c08901527f2c302d332e3531352d332e3531352d332e3531352d332e3531352d31312e34396101e08901527f2d31312e3437382d35322e3635362d35322e3636342d36342e3833372d36342e6102008901527f3833376c2e3034392d2e303337632d312e3732352d312e3630362d322e3731396102208901527f2d332e3834372d322e3735312d362e3230346830632d2e3034362d322e3337356102408901527f2c312e3036322d342e3538322c322e3732362d362e32323968306c2e3138352d6102608901527f2e3134386830632e3039392d2e3036322c2e3232322d2e3134382c2e33372d2e6102808901527f323539683063322e30362d312e3336322c332e3935312d322e3632312c362e306102a08901527f34342d332e3834324335372e3736332d332e3437332c39372e37362d322e33346102c08901527f312c3132382e3633372c31382e3333326331362e3637312c392e3934362d32366102e08901527f2e3334342c35342e3831332d33382e3635312c34302e3139392d362e3239392d6103008901527f362e3039362d31382e3036332d31372e3734332d31392e3636382d31382e38316103208901527f312d362e3031362d342e3034372d31332e3036312c342e3737362d372e3735326103408901527f2c392e3735316c36382e3235342c36382e33373163312e3732342c312e3630316103608901527f2c322e3731342c332e38342c322e3733382c362e3139325a222f3e00000000006103808901525f6101805260a06101805260405160a0526113506101805160a051614713565b607560a051527f3c706174682069643d22466c6f6174696e6754657874222066696c6c3d226e6f6102205160a05101527f6e652220643d224d313235203435683735307338302030203830203830763735604060a05101527f307330203830202d3830203830682d373530732d38302030202d3830202d38306102005160a05101527f762d3735307330202d3830203830202d3830222f3e0000000000000000000000608060a051015261193c60146022611409615979565b9360a2604051957f3c72616469616c4772616469656e742069643d2252616469616c476c6f77223e610220518801527f3c73746f70206f66667365743d223025222073746f702d636f6c6f723d220000604088015261153e6025603589605e8751956102205189019661147f818486018a6146cd565b83017f222073746f702d6f7061636974793d222e36222f3e0000000000000000000000838201528f7f3c73746f70206f66667365743d2231303025222073746f702d636f6c6f723d22908201526114e282518093609384019061022051016146cd565b01017f222073746f702d6f7061636974793d2230222f3e000000000000000000000000838201527f3c2f72616469616c4772616469656e743e00000000000000000000000000000060498201520301600581018a520188614713565b61165585602361154c615979565b6040519b8c917f3c6c696e6561724772616469656e742069643d2253616e64546f70222078313d610220518401527f223025222079313d223025223e0000000000000000000000000000000000000060408401527f3c73746f70206f66667365743d223025222073746f702d636f6c6f723d220000604d84015288516115d5818486018a6146cd565b83016211179f60e91b838201527f3c73746f70206f66667365743d2231303025222073746f702d636f6c6f723d22606e82015261161e82518093608e84019061022051016146cd565b01016211179f60e91b83820152701e17b634b732b0b923b930b234b2b73a1f60791b60268201520301600b1981018b520189614713565b6117df60726023611664615979565b6040519c8d917f3c6c696e6561724772616469656e742069643d2253616e64426f74746f6d2220610220518401527f78313d2231303025222079313d2231303025223e00000000000000000000000060408401527f3c73746f70206f66667365743d22313025222073746f702d636f6c6f723d220060548401526116f3815180928486019061022051016146cd565b82016211179f60e91b828201527f3c73746f70206f66667365743d2231303025222073746f702d636f6c6f723d22607682015288519061173782609683018a6146cd565b01016211179f60e91b838201527f3c616e696d617465206174747269627574654e616d653d22783122206475723d60268201527f2236732220726570656174436f756e743d22696e646566696e6974652220766160468201527f6c7565733d223330253b3630253b313230253b3630253b3330253b222f3e00006066820152701e17b634b732b0b923b930b234b2b73a1f60791b60848201520301605281018c52018a614713565b6117e7615979565b906040519a8b947f3c6c696e6561724772616469656e742069643d22486f7572676c617373537472610220518701527f6f6b6522206772616469656e745472616e73666f726d3d22726f74617465283960408701527f302922206772616469656e74556e6974733d227573657253706163654f6e5573610200518701527f65223e000000000000000000000000000000000000000000000000000000000060808701527f3c73746f70206f66667365743d22353025222073746f702d636f6c6f723d22006083870152518092858701906146cd565b83016211179f60e91b838201527f3c73746f70206f66667365743d22383025222073746f702d636f6c6f723d220060a58201526119058251809360c484019061022051016146cd565b01016211179f60e91b83820152701e17b634b732b0b923b930b234b2b73a1f60791b60258201520301600b19810187520185614713565b614ed4565b60e05261195561194f614c65565b856156d2565b938415614544575b5060c061010081905260405191906119759083614713565b609082527f3c7061746820643d224d2035302c3336302061203330302c333030203020312c610220518301527f31203630302c302061203330302c333030203020312c31202d3630302c30222060408301527f66696c6c3d2223666666222066696c6c2d6f7061636974793d222e3032222073610200518301527f74726f6b653d2275726c2823486f7572676c6173735374726f6b65292220737460808301527f726f6b652d77696474683d2234222f3e00000000000000000000000000000000610180518301526102c060405160c052611a528160c051614713565b61029860c051527f3c7061746820643d226d3536362c3136312e323031762d35332e39323463302d6102205160c05101527f31392e3338322d32322e3531332d33372e3536332d36332e3339382d35312e31604060c05101527f39382d34302e3735362d31332e3539322d39342e3934362d32312e3037392d316102005160c05101527f35322e3538372d32312e303739732d3131312e3833382c372e3438372d313532608060c05101527f2e3630322c32312e303739632d34302e3839332c31332e3633362d36332e34316101805160c05101527f332c33312e3831362d36332e3431332c35312e3139387635332e39323463302c6101005160c05101527f31372e3138312c31372e3730342c33332e3432372c35302e3232332c34362e3360e060c05101527f3934763238342e383039632d33322e3531392c31322e39362d35302e3232332c6101205160c05101527f32392e3230362d35302e3232332c34362e3339347635332e39323463302c313961012060c05101527f2e3338322c32322e35322c33372e3536332c36332e3431332c35312e3139382c8260c05101527f34302e3736332c31332e3539322c39342e3935342c32312e3037392c3135322e61016060c05101527f3630322c32312e303739733131312e3833312d372e3438372c3135322e35383761018060c05101527f2d32312e3037396334302e3838362d31332e3633362c36332e3339382d33312e6101a060c05101527f3831362c36332e3339382d35312e313938762d35332e39323463302d31372e316101c060c05101527f39362d31372e3730342d33332e3433352d35302e3232332d34362e34303156326101e060c05101527f30372e3630336333322e3531392d31322e3936372c35302e3232332d32392e3261020060c05101527f30362c35302e3232332d34362e3430315a6d2d3334372e3436322c35372e373961022060c05101527f336c3133302e3935392c3133312e3032372d3133302e3935392c3133312e303161024060c05101527f33563231382e3939345a6d3236322e3932342e303232763236322e3031386c2d61026060c05101527f3133302e3933372d3133312e3030362c3133302e3933372d3133312e3031335a61028060c05101527f222066696c6c3d2223313631383232223e3c2f706174683e00000000000000006102a060c0510152855f1461432f57604051611dcd6102205182614713565b5f8152955b156141dc57604051611de66101e082614713565b6101b181527f3c7061746820643d226d3438312e34362c3438312e35347638312e3031632d32610220518201527f2e33352e37372d342e38322c312e35312d372e33392c322e32332d33302e332c60408201527f382e35342d37342e36352c31332e39322d3132342e30362c31332e39322d3533610200518201527f2e362c302d3130312e32342d362e33332d3133312e34372d31362e3136762d3860808201527f316c34362e332d34362e3331683137302e33336c34362e32392c34362e33315a610180518201527f222066696c6c3d2275726c282353616e64426f74746f6d29222f3e3c70617468610100518201527f20643d226d3433352e31372c3433352e323363302c312e31372d2e34362c322e60e08201527f33322d312e33332c332e34342d372e31312c392e30382d34312e39332c31352e610120518201527f39382d38332e38312c31352e3938732d37362e372d362e392d38332e38322d316101208201527f352e3938632d2e38372d312e31322d312e33332d322e32372d312e33332d332e838201527f3434762d2e30346c382e33342d382e33352e30312d2e30316331332e37322d366101608201527f2e35312c34322e39352d31312e30322c37362e382d31312e30327336322e39376101808201527f2c342e34392c37362e37322c31316c382e34322c382e34325a222066696c6c3d6101a08201527f2275726c282353616e64546f7029222f3e0000000000000000000000000000006101c0820152905b6040519261201f6107e085614713565b6107a7845261022080517f3c672066696c6c3d226e6f6e6522207374726f6b653d2275726c2823486f7572908601527f676c6173735374726f6b652922207374726f6b652d6c696e656361703d22726f60408087019190915261020080517f756e6422207374726f6b652d6d697465726c696d69743d22313022207374726f908801527f6b652d77696474683d2234223e3c7061746820643d226d3536352e3634312c31608088015261018080517f30372e323863302c392e3533372d352e35362c31382e3632392d31352e36373690890152610100517f2c32362e393733682d2e303233632d392e3230342c372e3539362d32322e3139908901527f342c31342e3536322d33382e3139372c32302e3539322d33392e3530342c313460e089015261012080517f2e3933362d39372e3332352c32342e3335352d3136312e3733332c32342e3335908a01527f352d39302e34382c302d3136372e3934382d31382e3538322d3139392e393533908901527f2d34342e393438682d2e303233632d31302e3131352d382e3334342d31352e36948801949094527f37362d31372e3433372d31352e3637362d32362e3937332c302d33392e3733356101608801527f2c39362e3535342d37312e3932312c3231352e3635322d37312e393231733231938701939093527f352e3632392c33322e3138352c3231352e3632392c37312e3932315a222f3e3c6101a08701527f7061746820643d226d3133342e33362c3136312e32303363302c33392e3733356101c0808801919091527f2c39362e3535342c37312e3932312c3231352e3635322c37312e3932317332316101e08801527f352e3632392d33322e3138362c3231352e3632392d37312e393231222f3e3c6c938701939093527f696e652078313d223133342e3336222079313d223136312e323033222078323d828701527f223133342e3336222079323d223130372e3238222f3e3c6c696e652078313d226102408701527f3536352e3634222079313d223136312e323033222078323d223536352e3634226102608701527f2079323d223130372e3238222f3e3c6c696e652078313d223138342e353834226102808701527f2079313d223230362e383233222078323d223138342e353835222079323d22356102a08701527f33372e353739222f3e3c6c696e652078313d223231382e313831222079313d22938601939093527f3231382e313138222078323d223231382e313831222079323d223536322e35336102e08601527f37222f3e3c6c696e652078313d223438312e383138222079313d223231382e316103008601527f3432222078323d223438312e383139222079323d223536322e343238222f3e3c6103208601527f6c696e652078313d223531352e343135222079313d223230372e3335322220786103408601527f323d223531352e343136222079323d223533372e353739222f3e3c70617468206103608601527f643d226d3138342e35382c3533372e353863302c352e34352c342e32372c313061038086015290517f2e36352c31322e30332c31352e3432682e303263352e35312c332e33392c3132908501527f2e37392c362e35352c32312e35352c392e34322c33302e32312c392e392c37386103c08501527f2e30322c31362e32382c3133312e38332c31362e32382c34392e34312c302c396103e08501527f332e37362d352e33382c3132342e30362d31332e39322c322e372d2e37362c356104008501527f2e32392d312e35342c372e37352d322e33352c382e37372d322e38372c31362e6104208501527f30352d362e30342c32312e35362d392e3433683063372e37362d342e37372c316104408501527f322e30342d392e39372c31322e30342d31352e3432222f3e3c7061746820643d6104608501527f226d3138342e3538322c3439322e363536632d33312e3335342c31322e3438356104808501527f2d35302e3232332c32382e35382d35302e3232332c34362e3134322c302c392e6104a08501527f3533362c352e3536342c31382e3632372c31352e3637372c32362e393639682e6104c08501527f30323263382e3530332c372e3030352c32302e3231332c31332e3436332c33346104e08501527f2e3532342c31392e3135392c392e3939392c332e3939312c32312e3236392c376105008501527f2e3630392c33332e3539372c31302e3738382c33362e34352c392e3430372c386105208501527f322e3138312c31352e3030322c3133312e3833352c31352e3030327339352e336105408501527f36332d352e3539352c3133312e3830372d31352e3030326331302e3834372d326105608501527f2e37392c32302e3836372d352e3932362c32392e3932342d392e3334392c312e6105808501527f3234342d2e3436372c322e3437332d2e3934322c332e3637332d312e3432342c6105a08501527f31342e3332362d352e3639362c32362e3033352d31322e3136312c33342e35326105c08501527f342d31392e313733682e3032326331302e3131342d382e3334322c31352e36376105e08501527f372d31372e3433332c31352e3637372d32362e3936392c302d31372e3536322d6106008501527f31382e3836392d33332e3636352d35302e3232332d34362e3135222f3e3c70616106208501527f746820643d226d3133342e33362c3539322e373263302c33392e3733352c39366106408501527f2e3535342c37312e3932312c3231352e3635322c37312e393231733231352e366106608501527f32392d33322e3138362c3231352e3632392d37312e393231222f3e3c6c696e656106808501527f2078313d223133342e3336222079313d223539322e3732222078323d223133346106a08501527f2e3336222079323d223533382e373937222f3e3c6c696e652078313d223536356106c08501527f2e3634222079313d223539322e3732222078323d223536352e3634222079323d6106e08501527f223533382e373937222f3e3c706f6c796c696e6520706f696e74733d223438316107008501527f2e383232203438312e393031203438312e373938203438312e383737203438316107208501527f2e373735203438312e383534203335302e303135203335302e303236203231386107408501527f2e313835203231382e313239222f3e3c706f6c796c696e6520706f696e74733d6107608501527f223231382e313835203438312e393031203231382e323331203438312e3835346107808501527f203335302e303135203335302e303236203438312e383232203231382e3135326107a08501527f222f3e3c2f673e000000000000000000000000000000000000000000000000006107c0850152905181517f3c672069643d22486f7572676c617373223e00000000000000000000000000009082015284519151909788959092916129ee9183916032890191016146cd565b840160c051519060328101826102205160c0510191612a0c926146cd565b016032018082518093610220510191612a24926146cd565b018082518093610220510191612a39926146cd565b018082518093610220510191612a4e926146cd565b01631e17b39f60e11b815203601b1981018452600401612a6e9084614713565b60405160805261022051608051017f3c646566733e000000000000000000000000000000000000000000000000000090526101e0515160805160260181610220516101e0510191612abe926146cd565b60805101815191826026830191610220510191612ada926146cd565b016026018082518093610220510191612af2926146cd565b0160a051519080826102205160a0510191612b0c926146cd565b0160e051519080826102205160e0510191612b26926146cd565b018082518093610220510191612b3b926146cd565b01610140515190808261022051610140510191612b57926146cd565b017f3c2f646566733e000000000000000000000000000000000000000000000000008152608051900360181981016080515260070160805190612b9991614713565b6101605160e0015190610160516101000151916101605160400151906101605160600151612bc78583615bf8565b916040958651612bd78882614713565b600581526102205181017f2d31303025000000000000000000000000000000000000000000000000000000905287519485916102205183017f3c74657874506174682073746172744f66667365743d220000000000000000009052805190816037850191610220510191612c4a926146cd565b7f2220687265663d2223466c6f6174696e6754657874222066696c6c3d222366666037918401918201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e0000000000000000006101098201528151610220519092612d8e918491610120850191016146cd565b0160370160e981016a1e17ba32bc3a2830ba341f60a91b90520360e90160141981018552600b01612dbf9085614713565b612dc891615bf8565b928551612dd58782614713565b60028152610220518101947f3025000000000000000000000000000000000000000000000000000000000000865287519586926102205184017f3c74657874506174682073746172744f66667365743d22000000000000000000905251908160378501612e41926146cd565b7f2220687265663d2223466c6f6174696e6754657874222066696c6c3d222366666037918401918201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e0000000000000000006101098201528151610220519092612f85918491610120850191016146cd565b0160370160e981016a1e17ba32bc3a2830ba341f60a91b90520360e90160141981018552600b01612fb69085614713565b612fc08282615c62565b918651612fcd8882614713565b60048152610220518101937f2d35302500000000000000000000000000000000000000000000000000000000855288519485926102205184017f3c74657874506174682073746172744f66667365743d22000000000000000000905251908160378501613039926146cd565b7f2220687265663d2223466c6f6174696e6754657874222066696c6c3d222366666037918401918201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e000000000000000000610109820152815161022051909261317d918491610120850191016146cd565b0160370160e981016a1e17ba32bc3a2830ba341f60a91b90520360e90160141981018452600b016131ae9084614713565b6131b791615c62565b9085516131c48782614713565b60038152610220518101927f3530250000000000000000000000000000000000000000000000000000000000845287519384926102205184017f3c74657874506174682073746172744f66667365743d22000000000000000000905251908160378501613230926146cd565b7f2220687265663d2223466c6f6174696e6754657874222066696c6c3d222366666037918401918201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e0000000000000000006101098201528151610220519092613374918491610120850191016146cd565b0160370160e981016a1e17ba32bc3a2830ba341f60a91b90520360e90160141981018352600b016133a59083614713565b85519384936102205185017f3c7465787420746578742d72656e646572696e673d226f7074696d697a65537090528785017f656564223e0000000000000000000000000000000000000000000000000000009052805190816045870191610220510191613411926146cd565b840181519182604583019161022051019161342b926146cd565b016045018082518093610220510191613443926146cd565b018082518093610220510191613458926146cd565b01661e17ba32bc3a1f60c91b8152036018198101825260070161347b9082614713565b610140820151916101a08101519060408101519060e001519361349d90615408565b916134a790615408565b906134b190615408565b936134bb90615408565b8551948592610220518401947f3c75736520687265663d2223476c6f77222066696c6c2d6f7061636974793d2286528885017f2e39222f3e0000000000000000000000000000000000000000000000000000009052604585017f3c75736520687265663d2223476c6f772220783d22313030302220793d2231309052606585017f3030222066696c6c2d6f7061636974793d222e39222f3e0000000000000000009052607c85017f3c75736520687265663d22234c6f676f2220783d223137302220793d223137309052609c85017f22207472616e73666f726d3d227363616c65282e3629222f3e3c757365206872905260bc85017f65663d2223486f7572676c6173732220783d223135302220793d223930222074905260dc85017f72616e73666f726d3d22726f746174652831302922207472616e73666f726d2d905260fc85017f6f726967696e3d2235303020353030222f3e0000000000000000000000000000905261010e85017f3c75736520687265663d222350726f67726573732220783d2200000000000000905280519081610127870191610220510191613662926146cd565b840161012781016a11103c9e911b9c9811179f60a91b905261013281017f3c75736520687265663d22235374617475732220783d220000000000000000009052815191826101498301916102205101916136bb926146cd565b0161012701602281016a11103c9e911b9c9811179f60a91b9052602d81017f3c75736520687265663d2223416d6f756e742220783d220000000000000000009052815191826044830191610220510191613714926146cd565b01602201602281016a11103c9e911b9c9811179f60a91b9052602d81017f3c75736520687265663d22234475726174696f6e2220783d2200000000000000905281519182604683019161022051019161376c926146cd565b01602201602481016a11103c9e911b9c9811179f60a91b90520360240160141981018452600b0161379d9084614713565b83519283926102205184017f3c73766720786d6c6e733d22687474703a2f2f7777772e77332e6f72672f323090528584017f30302f737667222077696474683d223130303022206865696768743d2231303090526102005184017f30222076696577426f783d2230203020313030302031303030223e000000000090526101a05151607b850181610220516101a0510191613837926146cd565b84016080515190607b810182610220516080510191613855926146cd565b01607b01808251809361022051019161386d926146cd565b0191829151809361387d926146cd565b017f3c2f7376673e0000000000000000000000000000000000000000000000000000815203601919810182526006016138b69082614713565b61038052610300518151610220517fb25645690000000000000000000000000000000000000000000000000000000090820190815260248035818401528252916001600160a01b03169061390b604482614713565b515a925f93928493fa61391c614796565b6102e0819052901580156103c0526141d45761022051818051810103126141bf5761022051015180151581036141bf575b15156102a052610260516103005182517fb971302a00000000000000000000000000000000000000000000000000000000815260248035600483015261022051919283919082906001600160a01b03165afa9081156141ca575f9161417e575b50600360236139be613ad693614a3d565b938161012061024001518780519788947f5b7b2274726169745f74797065223a224173736574222c2276616c7565223a2261022051870152613a0b815180928589019061022051016146cd565b85017f227d2c7b2274726169745f74797065223a2253656e646572222c2276616c75658382015262111d1160e91b61020051820152613a5682518093606384019061022051016146cd565b01017f227d2c7b2274726169745f74797065223a22537461747573222c2276616c75658382015262111d1160e91b6043820152613a9f82518093604684019061022051016146cd565b01017f227d5d0000000000000000000000000000000000000000000000000000000000838201520301601c19810184520182614713565b6103205161026051610340516102405191939291613afc906001600160a01b0316614a3d565b613b07602435615408565b6102a051909190156140f25761010051875190613b249082614713565b609b81527fe29aa0efb88f205741524e494e473a205472616e7366657272696e6720746865610220518201527f204e4654206d616b657320746865206e6577206f776e65722074686520726563888201527f697069656e74206f66207468652073747265616d2e205468652066756e647320610200518201527f617265206e6f74206175746f6d61746963616c6c792077697468647261776e2060808201527f666f72207468652070726576696f757320726563697069656e742e000000000061018051820152915b8751968794610220518601967f54686973204e465420726570726573656e74732061207061796d656e7420737488528a87017f7265616d20696e2061205361626c6965722056322000000000000000000000009052805190610220518101918060558a0190613c5c91856146cd565b7f20636f6e74726163742e20546865206f776e6572206f662074686973204e46546055918a01918201527f2063616e207769746864726177207468652073747265616d656420617373657460758201527f732c207768696368206172652064656e6f6d696e6174656420696e2000000000609582015284516102205186019691613cea8260b183018a6146cd565b01605501605c81017f2e5c6e5c6e2d2053747265616d2049443a200000000000000000000000000000905281519182606e830191610220510191613d2d926146cd565b01605c0190601282016302e3716960e51b905251918260168301613d50926146cd565b01601201600481016901020b2323932b9b99d160b51b905281519182600e830191610220510191613d80926146cd565b0160040190600a82016302e3716960e51b9052519182600e8301613da3926146cd565b01600a01600481016901020b2323932b9b99d160b51b905281519182600e830191610220510191613dd3926146cd565b01600401600a81017f5c6e5c6e00000000000000000000000000000000000000000000000000000000905281519182600e830191610220510191613e16926146cd565b01600a0103600401601f1981018452613e2f9084614713565b61032051613e3e602435615408565b85518091610220518201936a029b0b13634b2b9102b19160ad1b855280519081602b850191610220510191613e72926146cd565b8201602b81017f2023000000000000000000000000000000000000000000000000000000000000905281519182602d830191610220510191613eb3926146cd565b01602b0103600201601f1981018252613ecc9082614713565b61038051613ed990615567565b9286519586956102205187017f7b2261747472696275746573223a000000000000000000000000000000000000905280519081602e890191610220510191613f20926146cd565b860190602e82017f2c226465736372697074696f6e223a22000000000000000000000000000000009052519182603e8301613f5a926146cd565b01602e0190601082017f222c2265787465726e616c5f75726c223a2268747470733a2f2f7361626c69659052603082017f722e636f6d222c226e616d65223a2200000000000000000000000000000000009052519182603f8301613fbd926146cd565b01601001602f81017f222c22696d616765223a22646174613a696d6167652f7376672b786d6c3b62619052604f81017f736536342c0000000000000000000000000000000000000000000000000000009052815191826054830191610220510191614027926146cd565b01602f01602581017f227d000000000000000000000000000000000000000000000000000000000000905203602501601d198101825260020161406a9082614713565b6102c081905261407990615567565b90805180926102205182017f646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c000000905280519081603d8401916102205101916140bf926146cd565b810103603d01601f19810183526140d69083614713565b5180916102205182526102205182016140ee916146ee565b0390f35b86516140ff608082614713565b605b81527fe29d95494e464f3a2054686973204e4654206973206e6f6e2d7472616e736665610220518201527f7261626c652e2049742063616e6e6f7420626520736f6c64206f72207472616e888201527f7366657272656420746f20616e6f74686572206163636f756e742e00000000006102005182015291613bed565b9050610220513d61022051116141c3575b6141998183614713565b816102205191810103126141bf57516001600160a01b03811681036141bf5760036139ad565b5f80fd5b503d61418f565b83513d5f823e3d90fd5b50600161394d565b6040516141eb61012082614713565b60f881527f3c7061746820643d226d3438312e34362c3530342e3130317635382e34343963610220518201527f2d322e33352e37372d342e38322c312e35312d372e33392c322e32332d33302e60408201527f332c382e35342d37342e36352c31332e39322d3132342e30362c31332e39322d610200518201527f35332e362c302d3130312e32342d362e33332d3133312e34372d31362e31367660808201527f2d35382e343339683236322e39325a222066696c6c3d2275726c282353616e64610180518201527f426f74746f6d29222f3e3c656c6c697073652063783d22333530222063793d22610100518201527f3530342e313031222072783d223133312e343632222072793d2232382e31303860e08201527f222066696c6c3d2275726c282353616e64546f7029222f3e0000000000000000610120518201529061200f565b60405161433e6101c082614713565b61019981527f3c706f6c79676f6e20706f696e74733d22333530203335302e30323620343135610220518201527f2e3033203238342e39373820323835203238342e39373820333530203335302e60408201527f303236222066696c6c3d2275726c282353616e64426f74746f6d29222f3e3c70610200518201527f61746820643d226d3431362e3334312c3238312e39373563302c2e3931342d2e60808201527f3335342c312e3830392d312e3033352c322e36382d352e3534322c372e303736610180518201527f2d33322e3636312c31322e34352d36352e32382c31322e34352d33322e363234610100518201527f2c302d35392e3733382d352e3337342d36352e32382d31322e34352d2e36383160e08201527f2d2e3837322d312e3033352d312e3736372d312e3033352d322e36382c302d2e610120518201527f3931342e3335342d312e3830382c312e3033352d322e3637362c352e3534322d6101208201527f372e3037362c33322e3635362d31322e34352c36352e32382d31322e34352c33838201527f322e3631392c302c35392e3733382c352e3337342c36352e32382c31322e34356101608201527f2e3638312e3836372c312e3033352c312e3736322c312e3033352c322e3637366101808201527f5a222066696c6c3d2275726c282353616e64546f7029222f3e000000000000006101a082015295611dd2565b614558919450614552614ca0565b906156d2565b925f61195d565b905099610ad5565b9050610a2d565b601b60d09a610720565b634e487b7160e01b5f52604160045260245ffd5b6145ae915060203d6020116145b4575b6145a68183614713565b81019061475d565b5f610518565b503d61459c565b6040513d5f823e3d90fd5b6145df915060203d6020116145b4576145a68183614713565b5f6104b9565b634e487b7160e01b5f52601260045260245ffd5b61461b915060203d602011614621575b6146138183614713565b810190614735565b5f61024e565b503d614609565b506020813d60201161465d575b8161464260209383614713565b810103126141bf575160058110156141bf576101f5906101eb565b3d9150614635565b61467e915060203d602011614621576146138183614713565b5f610191565b90506020813d6020116146c5575b8161469f60209383614713565b810103126141bf57516001600160a01b03811681036141bf576001600160a01b0361010f565b3d9150614692565b5f5b8381106146de5750505f910152565b81810151838201526020016146cf565b90602091614707815180928185528580860191016146cd565b601f01601f1916010190565b90601f8019910116810190811067ffffffffffffffff82111761457857604052565b908160209103126141bf57516fffffffffffffffffffffffffffffffff811681036141bf5790565b908160209103126141bf575164ffffffffff811681036141bf5790565b67ffffffffffffffff811161457857601f01601f191660200190565b3d156147c0573d906147a78261477a565b916147b56040519384614713565b82523d5f602084013e565b606090565b6020818303126141bf5780519067ffffffffffffffff82116141bf570181601f820112156141bf5780516147f88161477a565b926148066040519485614713565b818452602082840101116141bf5761482491602080850191016146cd565b90565b6001600160a01b0316604051906395d89b4160e01b82525f82600481845afa9182156145bb575f92614a19575b5060409161489783516148678582614713565b601181527f5341422d56322d4c4f434b55502d4c494e0000000000000000000000000000006020820152826156d2565b156148d75750506148aa81519182614713565b600d81527f4c6f636b7570204c696e65617200000000000000000000000000000000000000602082015290565b61491683516148e68582614713565b601181527f5341422d56322d4c4f434b55502d44594e0000000000000000000000000000006020820152826156d2565b1561495657505061492981519182614713565b600e81527f4c6f636b75702044796e616d6963000000000000000000000000000000000000602082015290565b61499583516149658582614713565b601181527f5341422d56322d4c4f434b55502d5452410000000000000000000000000000006020820152826156d2565b156149d55750506149a881519182614713565b600f81527f4c6f636b7570205472616e636865640000000000000000000000000000000000602082015290565b614a159083519384937f814a8a2e0000000000000000000000000000000000000000000000000000000085526004850152602484015260448301906146ee565b0390fd5b614a369192503d805f833e614a2e8183614713565b8101906147c5565b905f614854565b6001600160a01b03168060405191614a56606084614713565b602a8352602083016040368237835115614b255760309053825160011015614b25576078602184015360295b60018111614ac35750614a93575090565b7fe22e27eb000000000000000000000000000000000000000000000000000000005f52600452601460245260445ffd5b90600f81166010811015614b25577f3031323334353637383961626364656600000000000000000000000000000000901a614afe83866156ff565b5360041c908015614b11575f1901614a82565b634e487b7160e01b5f52601160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b5f809160405160208101906395d89b4160e01b825260048152614b5d602482614713565b51915afa614b69614796565b90158015614c59575b614c1d5780602080614b89935183010191016147c5565b601e8151115f14614bd05750604051614ba3604082614713565b600b81527f4c6f6e672053796d626f6c000000000000000000000000000000000000000000602082015290565b614bd981615710565b15614be15790565b50604051614bf0604082614713565b601281527f556e737570706f727465642053796d626f6c0000000000000000000000000000602082015290565b50604051614c2c604082614713565b600581527f4552433230000000000000000000000000000000000000000000000000000000602082015290565b50604081511115614b72565b60405190614c74604083614713565b600782527f536574746c6564000000000000000000000000000000000000000000000000006020830152565b60405190614caf604083614713565b600882527f4465706c657465640000000000000000000000000000000000000000000000006020830152565b6005811015614dc75760048103614cf55750614824614ca0565b60038103614d395750604051614d0c604082614713565b600881527f43616e63656c6564000000000000000000000000000000000000000000000000602082015290565b60018103614d7d5750604051614d50604082614713565b600981527f53747265616d696e670000000000000000000000000000000000000000000000602082015290565b600203614d8c57614824614c65565b604051614d9a604082614713565b600781527f50656e64696e6700000000000000000000000000000000000000000000000000602082015290565b634e487b7160e01b5f52602160045260245ffd5b5f809160405160208101907f313ce56700000000000000000000000000000000000000000000000000000000825260048152614e18602482614713565b51915afa614e24614796565b9080614e53575b15614e4e576020818051810103126141bf576020015160ff811681036141bf5790565b505f90565b506020815114614e2b565b60405190614e6d604083614713565b600482527f2667743b000000000000000000000000000000000000000000000000000000006020830152565b60405190614ea8604083614713565b600482527f266c743b000000000000000000000000000000000000000000000000000000006020830152565b90614eff9493614f306020614f3f95614f22828096816040519c8d8b83829d519485930191016146cd565b8901614f13825180938580850191016146cd565b010191828151948592016146cd565b0191828151948592016146cd565b0103601f198101845283614713565b565b908115615216578061520657505b806001811015614fb8575050614f63614e99565b6148246002602060405184614f8182965180928580860191016146cd565b81017f2031000000000000000000000000000000000000000000000000000000000000838201520301601d19810184520182614713565b66038d7ea4c6800011156151a8576040519060a0820182811067ffffffffffffffff82111761457857604052602091604051614ff48482614713565b5f8152815260409182516150088482614713565b600181527f4b00000000000000000000000000000000000000000000000000000000000000858201528483015282516150418482614713565b600181527f4d000000000000000000000000000000000000000000000000000000000000008582015283830152825161507a8482614713565b600181527f420000000000000000000000000000000000000000000000000000000000000085820152606083015282516150b48482614713565b600181527f54000000000000000000000000000000000000000000000000000000000000008582015260808301525f905f945b6103e882101561518e578451946150fe8187614713565b600786527f2623383830353b000000000000000000000000000000000000000000000000008287015251945f5b6007811061517b575050600160fd1b602786015250600884526151629061515c90615157602887614713565b615408565b916158b3565b916005851015614b25576148249460051b015192614ed4565b818101830151878201840152820161512b565b9490915060016103e86064600a85040693049101946150e7565b506151b1614e5e565b61482460086020604051846151cf82965180928580860191016146cd565b81017f203939392e393954000000000000000000000000000000000000000000000000838201520301601719810184520182614713565b600a0a9081156145e55704614f4f565b5050604051615226604082614713565b60018152600360fc1b602082015290565b62015180910304806152a1575061524c614e99565b614824600660206040518461526a82965180928580860191016146cd565b81017f2031204461790000000000000000000000000000000000000000000000000000838201520301601919810184520182614713565b61270f81116153785760018103615334576148246152f66040516152c6604082614713565b600481527f2044617900000000000000000000000000000000000000000000000000000000602082015292615408565b6020604051938261531086945180928580880191016146cd565b8301615324825180938580850191016146cd565b010103601f198101835282614713565b6148246152f6604051615348604082614713565b600581527f2044617973000000000000000000000000000000000000000000000000000000602082015292615408565b50615381614e5e565b614824600a60206040518461539f82965180928580860191016146cd565b81017f2039393939204461797300000000000000000000000000000000000000000000838201520301601519810184520182614713565b906153e08261477a565b6153ed6040519182614713565b82815280926153fe601f199161477a565b0190602036910137565b805f917a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000082101561553f575b806d04ee2d6d415b85acef8100000000600a921015615524575b662386f26fc10000811015615510575b6305f5e1008110156154ff575b6127108110156154f0575b60648110156154e2575b10156154d7575b600a6021615492600185016153d6565b938401015b5f1901917f30313233343536373839616263646566000000000000000000000000000000008282061a83530480156154d257600a9091615497565b505090565b600190910190615482565b60646002910493019261547b565b61271060049104930192615471565b6305f5e10060089104930192615466565b662386f26fc1000060109104930192615459565b6d04ee2d6d415b85acef810000000060209104930192615449565b50604091507a184f03e93ff9f4daa797ed6e38ed64bf6a1f010000000000000000810461542f565b908151156156bc576040519161557e606084614713565b604083527f4142434445464748494a4b4c4d4e4f505152535455565758595a61626364656660208401527f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392b2f6040840152805160028101809111614b1157600390047f3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81168103614b11576156149060021b6153d6565b90602082019080815182019560208701908151925f83525b88811061566e575050600393949596505251068060011461565c57600214615652575090565b603d905f19015390565b50603d90815f19820153600119015390565b600360049199969901986001603f8b5182828260121c16870101518453828282600c1c16870101518385015382828260061c168701015160028501531684010151600382015301949761562c565b90506040516156cc602082614713565b5f815290565b90815181519081811493846156e9575b5050505090565b602092939450820120920120145f8080806156e2565b908151811015614b25570160200190565b8051905f5b82811061572457505050600190565b7fff0000000000000000000000000000000000000000000000000000000000000061574f82846156ff565b5116600160fd1b811490600360fc1b81101580615889575b7f4100000000000000000000000000000000000000000000000000000000000000821015908161585e575b7f61000000000000000000000000000000000000000000000000000000000000008310159283615833575b8415615809575b508315615801575b5082156157f9575b5081156157f1575b50156157ea57600101615715565b5050505f90565b90505f6157dc565b91505f6157d4565b92505f6157cc565b7f2d000000000000000000000000000000000000000000000000000000000000001493505f6157c4565b7f7a0000000000000000000000000000000000000000000000000000000000000081111593506157bd565b7f5a000000000000000000000000000000000000000000000000000000000000008311159150615792565b507f3900000000000000000000000000000000000000000000000000000000000000811115615767565b806158c757506040516156cc602082614713565b600a81101561592d576158d990615408565b614824602260405180937f2e30000000000000000000000000000000000000000000000000000000000000602083015261591c81518092602086860191016146cd565b81010301601f198101835282614713565b61593690615408565b614824602160405180937f2e00000000000000000000000000000000000000000000000000000000000000602083015261591c81518092602086860191016146cd565b60405190615988604083614713565b601082527f68736c283233302c3231252c31312529000000000000000000000000000000006020830152565b8015615be8576159c2615979565b9061271003906127108211614b1157602e60619160506159e461482495615408565b60576040519788947f3c672066696c6c3d226e6f6e65223e000000000000000000000000000000000060208701527f3c636972636c652063783d22313636222063793d2235302220723d2232322220602f8701527f7374726f6b653d22000000000000000000000000000000000000000000000000604f870152615a71815180926020868a0191016146cd565b85017f22207374726f6b652d77696474683d223130222f3e0000000000000000000000838201527f3c636972636c652063783d22313636222063793d2235302220706174684c656e606c8201527f6774683d2231303030302220723d22323222207374726f6b653d220000000000608c820152615af882518093602060a7850191016146cd565b01017f22207374726f6b652d6461736861727261793d22313030303022207374726f6b838201527f652d646173686f66667365743d220000000000000000000000000000000000006070820152615b59825180936020607e850191016146cd565b01017f22207374726f6b652d6c696e656361703d22726f756e6422207374726f6b652d838201527f77696474683d223522207472616e73666f726d3d22726f74617465282d393029604e8201527f22207472616e73666f726d2d6f726967696e3d22313636203530222f3e000000606e820152631e17b39f60e11b608b82015203016041810184520182614713565b50506040516156cc602082614713565b6010614f3f9193929360206040519582615c1b88945180928580880191016146cd565b830164010714051160dd1b838201526a029b0b13634b2b9102b19160ad1b6025820152615c5182518093856030850191016146cd565b01010301601f198101845283614713565b6005614f3f9193929360206040519582615c8588945180928580880191016146cd565b830164010714051160dd1b83820152615c5182518093856025850191016146cd565b6004811015614dc75780615cf15750604051615cc4604082614713565b600881527f50726f6772657373000000000000000000000000000000000000000000000000602082015290565b60018103615d355750604051615d08604082614713565b600681527f5374617475730000000000000000000000000000000000000000000000000000602082015290565b600203615d7757604051615d4a604082614713565b600681527f416d6f756e740000000000000000000000000000000000000000000000000000602082015290565b604051615d85604082614713565b600881527f4475726174696f6e000000000000000000000000000000000000000000000000602082015290565b5f90805180156157ea5790600d915f925f925b828410615dd85750505050600d02900390565b90919294603b60f81b7fff00000000000000000000000000000000000000000000000000000000000000615e0c88856156ff565b511614615e22575b820194600101929190615dc5565b859450615e14565b5f90805180156157ea57906010915f925f925b828410615e50575050505060041b900390565b90919294603b60f81b7fff00000000000000000000000000000000000000000000000000000000000000615e8488856156ff565b511614615e9a575b820194600101929190615e3d565b859450615e8c56fea164736f6c634300081a000a"; + + /*////////////////////////////////////////////////////////////////////////// + DEPLOYERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode, passing a default value for the + /// `maxSegmentCount` parameter. + /// @dev Notes: + /// - A default value is passed for `maxSegmentCount`. + /// - A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. + function deployLockupDynamic(address initialAdmin) public returns (ISablierV2LockupDynamic lockupDynamic) { + uint256 maxSegmentCount = MAX_SEGMENT_COUNT; + lockupDynamic = deployLockupDynamic(initialAdmin, maxSegmentCount); + } + + /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. + /// @dev A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. + function deployLockupDynamic( + address initialAdmin, + uint256 maxSegmentCount + ) + public + returns (ISablierV2LockupDynamic lockupDynamic) + { + ISablierV2NFTDescriptor nftDescriptor = new SablierV2NFTDescriptor(); + lockupDynamic = deployLockupDynamic(initialAdmin, nftDescriptor, maxSegmentCount); + } + + /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. + /// @dev A default value is passed for `maxSegmentCount`. + function deployLockupDynamic( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor + ) + public + returns (ISablierV2LockupDynamic lockupDynamic) + { + lockupDynamic = deployLockupDynamic(initialAdmin, nftDescriptor, MAX_SEGMENT_COUNT); + } + + /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. + function deployLockupDynamic( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor, + uint256 maxSegmentCount + ) + public + returns (ISablierV2LockupDynamic lockupDynamic) + { + bytes memory creationBytecode = + bytes.concat(BYTECODE_LOCKUP_DYNAMIC, abi.encode(initialAdmin, nftDescriptor, maxSegmentCount)); + assembly { + lockupDynamic := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) + } + require( + address(lockupDynamic) != address(0), "Sablier V2 Precompiles: deployment failed for LockupDynamic contract" + ); + } + + /// @notice Deploys {SablierV2LockupLinear} from precompiled bytecode. + /// @dev A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. + function deployLockupLinear(address initialAdmin) public returns (ISablierV2LockupLinear lockupLinear) { + ISablierV2NFTDescriptor nftDescriptor = new SablierV2NFTDescriptor(); + lockupLinear = deployLockupLinear(initialAdmin, nftDescriptor); + } + + /// @notice Deploys {SablierV2LockupLinear} from precompiled bytecode. + function deployLockupLinear( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor + ) + public + returns (ISablierV2LockupLinear lockupLinear) + { + bytes memory creationBytecode = bytes.concat(BYTECODE_LOCKUP_LINEAR, abi.encode(initialAdmin, nftDescriptor)); + assembly { + lockupLinear := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) + } + require( + address(lockupLinear) != address(0), "Sablier V2 Precompiles: deployment failed for LockupLinear contract" + ); + } + + /// @notice Deploys {SablierV2LockupTranched} from precompiled bytecode, passing a default value for the + /// `maxTrancheCount` parameter. + /// @dev Notes: + /// - A default value is passed for `maxTrancheCount`. + /// - A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. + function deployLockupTranched(address initialAdmin) public returns (ISablierV2LockupTranched lockupTranched) { + uint256 maxTrancheCount = MAX_TRANCHE_COUNT; + lockupTranched = deployLockupTranched(initialAdmin, maxTrancheCount); + } + + /// @notice Deploys {SablierV2LockupTranched} from precompiled bytecode. + /// @dev A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. + function deployLockupTranched( + address initialAdmin, + uint256 maxTrancheCount + ) + public + returns (ISablierV2LockupTranched lockupTranched) + { + ISablierV2NFTDescriptor nftDescriptor = new SablierV2NFTDescriptor(); + lockupTranched = deployLockupTranched(initialAdmin, nftDescriptor, maxTrancheCount); + } + + /// @notice Deploys {SablierV2LockupTranched} from precompiled bytecode. + /// @dev A default value is passed for `maxTrancheCount`. + function deployLockupTranched( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor + ) + public + returns (ISablierV2LockupTranched lockupTranched) + { + lockupTranched = deployLockupTranched(initialAdmin, nftDescriptor, MAX_TRANCHE_COUNT); + } + + /// @notice Deploys {SablierV2LockupTranched} from precompiled bytecode. + function deployLockupTranched( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor, + uint256 maxTrancheCount + ) + public + returns (ISablierV2LockupTranched lockupTranched) + { + bytes memory creationBytecode = + bytes.concat(BYTECODE_LOCKUP_TRANCHED, abi.encode(initialAdmin, nftDescriptor, maxTrancheCount)); + assembly { + lockupTranched := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) + } + require( + address(lockupTranched) != address(0), + "Sablier V2 Precompiles: deployment failed for LockupTranched contract" + ); + } + + /// @notice Deploys {SablierV2NFTDescriptor} from precompiled bytecode. + function deployNFTDescriptor() public returns (ISablierV2NFTDescriptor nftDescriptor) { + bytes memory bytecode = BYTECODE_NFT_DESCRIPTOR; + assembly { + nftDescriptor := create(0, add(bytecode, 0x20), mload(bytecode)) + } + require( + address(nftDescriptor) != address(0), "Sablier V2 Precompiles: deployment failed for NFTDescriptor contract" + ); + } + + /// @notice Deploys all V2 Core contracts. + function deployCore(address initialAdmin) + public + returns ( + ISablierV2LockupDynamic lockupDynamic, + ISablierV2LockupLinear lockupLinear, + ISablierV2LockupTranched lockupTranched, + ISablierV2NFTDescriptor nftDescriptor + ) + { + nftDescriptor = deployNFTDescriptor(); + lockupDynamic = deployLockupDynamic(initialAdmin); + lockupLinear = deployLockupLinear(initialAdmin); + lockupTranched = deployLockupTranched(initialAdmin); + } +} diff --git a/remappings.txt b/remappings.txt index 167506d32..8f1a7a740 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,5 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ -@prb/test/=node_modules/@prb/test/ forge-std/=node_modules/forge-std/ solady/=node_modules/solady/ -solarray/=node_modules/solarray/ \ No newline at end of file +solarray/=node_modules/solarray/ diff --git a/script/Base.s.sol b/script/Base.s.sol index e9f18740e..35cb9c755 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,9 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +// solhint-disable no-console +pragma solidity >=0.8.22 <0.9.0; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { console2 } from "forge-std/src/console2.sol"; import { Script } from "forge-std/src/Script.sol"; +import { stdJson } from "forge-std/src/StdJson.sol"; + +contract BaseScript is Script { + using Strings for uint256; + using stdJson for string; + + /// @dev The default value for `segmentCountMap` and `trancheCountMap`. + uint256 internal constant DEFAULT_MAX_COUNT = 500; -abstract contract BaseScript is Script { /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; @@ -13,24 +24,41 @@ abstract contract BaseScript is Script { /// @dev The address of the transaction broadcaster. address internal broadcaster; - /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. + /// @dev Used to derive the broadcaster's address if $EOA is not defined. string internal mnemonic; + /// @dev Maximum segment count mapped by the chain Id. + mapping(uint256 chainId => uint256 count) internal segmentCountMap; + + /// @dev Maximum tranche count mapped by the chain Id. + mapping(uint256 chainId => uint256 count) internal trancheCountMap; + /// @dev Initializes the transaction broadcaster like this: /// - /// - If $ETH_FROM is defined, use it. + /// - If $EOA is defined, use it. /// - Otherwise, derive the broadcaster address from $MNEMONIC. /// - If $MNEMONIC is not defined, default to a test mnemonic. /// - /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. + /// The use case for $EOA is to specify the broadcaster key and its address via the command line. constructor() { - address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) }); + address from = vm.envOr({ name: "EOA", defaultValue: address(0) }); if (from != address(0)) { broadcaster = from; } else { mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); } + + // Populate the segment and tranche count map. + populateSegmentAndTrancheCountMap(); + + // If there is no maximum value set for a specific chain, use the default value. + if (segmentCountMap[block.chainid] == 0) { + segmentCountMap[block.chainid] = DEFAULT_MAX_COUNT; + } + if (trancheCountMap[block.chainid] == 0) { + trancheCountMap[block.chainid] = DEFAULT_MAX_COUNT; + } } modifier broadcast() { @@ -38,4 +66,70 @@ abstract contract BaseScript is Script { _; vm.stopBroadcast(); } + + /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: + /// https://github.com/Arachnid/deterministic-deployment-proxy + /// + /// Notes: + /// - The salt format is "ChainID , Version ". + /// - The version is obtained from `package.json`. + function constructCreate2Salt() public view returns (bytes32) { + string memory chainId = block.chainid.toString(); + string memory json = vm.readFile("package.json"); + string memory version = json.readString(".version"); + string memory create2Salt = string.concat("ChainID ", chainId, ", Version ", version); + console2.log("The CREATE2 salt is \"%s\"", create2Salt); + return bytes32(abi.encodePacked(create2Salt)); + } + + /// @dev Populates the segment & tranche count map. Values can be updated using the `update-counts.sh` script. + function populateSegmentAndTrancheCountMap() internal { + // forgefmt: disable-start + + // Arbitrum chain ID + segmentCountMap[42161] = 1160; + trancheCountMap[42161] = 1200; + + // Avalanche chain ID. + segmentCountMap[43114] = 520; + trancheCountMap[43114] = 540; + + // Base chain ID. + segmentCountMap[8453] = 2170; + trancheCountMap[8453] = 2270; + + // Blast chain ID. + segmentCountMap[238] = 1080; + trancheCountMap[238] = 1120; + + // BNB chain ID. + segmentCountMap[56] = 4820; + trancheCountMap[56] = 5130; + + // Ethereum chain ID. + segmentCountMap[1] = 1080; + trancheCountMap[1] = 1120; + + // Gnosis chain ID. + segmentCountMap[100] = 600; + trancheCountMap[100] = 620; + + // Optimism chain ID. + segmentCountMap[10] = 1080; + trancheCountMap[10] = 1120; + + // Polygon chain ID. + segmentCountMap[137] = 1080; + trancheCountMap[137] = 1120; + + // Scroll chain ID. + segmentCountMap[534352] = 330; + trancheCountMap[534352] = 340; + + // Sepolia chain ID. + segmentCountMap[11155111] = 1080; + trancheCountMap[11155111] = 1120; + + // forgefmt: disable-end + } } diff --git a/script/DeployComptroller.s.sol b/script/DeployComptroller.s.sol deleted file mode 100644 index cfeb4769c..000000000 --- a/script/DeployComptroller.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; - -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; - -import { BaseScript } from "./Base.s.sol"; - -contract DeployComptroller is BaseScript { - function run(address initialAdmin) public virtual broadcast returns (SablierV2Comptroller comptroller) { - comptroller = new SablierV2Comptroller(initialAdmin); - } -} diff --git a/script/DeployCore.s.sol b/script/DeployCore.s.sol index cb70fa7a1..df6239c8a 100644 --- a/script/DeployCore.s.sol +++ b/script/DeployCore.s.sol @@ -1,37 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; import { BaseScript } from "./Base.s.sol"; -/// @notice Deploys all V2 Core contract in the following order: -/// -/// 1. {SablierV2Comptroller} -/// 2. {SablierV2NFTDescriptor} -/// 3. {SablierV2LockupDynamic} -/// 4. {SablierV2LockupLinear} +/// @notice Deploys all V2 Core contracts. contract DeployCore is BaseScript { - function run( - address initialAdmin, - uint256 maxSegmentCount - ) + function run(address initialAdmin) public virtual broadcast returns ( - SablierV2Comptroller comptroller, SablierV2LockupDynamic lockupDynamic, SablierV2LockupLinear lockupLinear, + SablierV2LockupTranched lockupTranched, SablierV2NFTDescriptor nftDescriptor ) { - comptroller = new SablierV2Comptroller(initialAdmin); nftDescriptor = new SablierV2NFTDescriptor(); - lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxSegmentCount); - lockupLinear = new SablierV2LockupLinear(initialAdmin, comptroller, nftDescriptor); + lockupDynamic = new SablierV2LockupDynamic(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierV2LockupLinear(initialAdmin, nftDescriptor); + lockupTranched = new SablierV2LockupTranched(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); } } diff --git a/script/DeployCore2.s.sol b/script/DeployCore2.s.sol index 3ce985c69..a8059dabc 100644 --- a/script/DeployCore2.s.sol +++ b/script/DeployCore2.s.sol @@ -1,35 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; import { BaseScript } from "./Base.s.sol"; -/// @notice Deploys these contracts in the following order: -/// -/// 1. {SablierV2Comptroller} -/// 2. {SablierV2LockupDynamic} -/// 3. {SablierV2LockupLinear} contract DeployCore2 is BaseScript { function run( address initialAdmin, - ISablierV2NFTDescriptor nftDescriptor, - uint256 maxSegmentCount + ISablierV2NFTDescriptor nftDescriptor ) public virtual broadcast returns ( - SablierV2Comptroller comptroller, SablierV2LockupDynamic lockupDynamic, - SablierV2LockupLinear lockupLinear + SablierV2LockupLinear lockupLinear, + SablierV2LockupTranched lockupTranched ) { - comptroller = new SablierV2Comptroller(initialAdmin); - lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxSegmentCount); - lockupLinear = new SablierV2LockupLinear(initialAdmin, comptroller, nftDescriptor); + lockupDynamic = new SablierV2LockupDynamic(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierV2LockupLinear(initialAdmin, nftDescriptor); + lockupTranched = new SablierV2LockupTranched(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); } } diff --git a/script/DeployDeterministicComptroller.s.sol b/script/DeployDeterministicComptroller.s.sol deleted file mode 100644 index e609d960c..000000000 --- a/script/DeployDeterministicComptroller.s.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; - -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys {SablierV2Comptroller} at a deterministic address across chains. -/// @dev Reverts if the contract has already been deployed. -contract DeployDeterministicComptroller is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: - /// https://github.com/Arachnid/deterministic-deployment-proxy - function run( - string memory create2Salt, - address initialAdmin - ) - public - virtual - broadcast - returns (SablierV2Comptroller comptroller) - { - comptroller = new SablierV2Comptroller{ salt: bytes32(abi.encodePacked(create2Salt)) }(initialAdmin); - } -} diff --git a/script/DeployDeterministicCore.s.sol b/script/DeployDeterministicCore.s.sol index aac8a7ab9..b65be9ae7 100644 --- a/script/DeployDeterministicCore.s.sol +++ b/script/DeployDeterministicCore.s.sol @@ -1,45 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; import { BaseScript } from "./Base.s.sol"; -/// @notice Deploys all V2 Core contracts at deterministic addresses across chains, in the following order: -/// -/// 1. {SablierV2Comptroller} -/// 2. {SablierV2NFTDescriptor} -/// 3. {SablierV2LockupDynamic} -/// 4. {SablierV2LockupLinear} -/// +/// @notice Deploys all V2 Core contracts at deterministic addresses across chains. /// @dev Reverts if any contract has already been deployed. contract DeployDeterministicCore is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy the contract via a deterministic CREATE2 factory. - /// https://github.com/Arachnid/deterministic-deployment-proxy - function run( - string memory create2Salt, - address initialAdmin, - uint256 maxSegmentCount - ) + function run(address initialAdmin) public virtual broadcast returns ( - SablierV2Comptroller comptroller, SablierV2LockupDynamic lockupDynamic, SablierV2LockupLinear lockupLinear, + SablierV2LockupTranched lockupTranched, SablierV2NFTDescriptor nftDescriptor ) { - bytes32 salt = bytes32(abi.encodePacked(create2Salt)); - comptroller = new SablierV2Comptroller{ salt: salt }(initialAdmin); + bytes32 salt = constructCreate2Salt(); nftDescriptor = new SablierV2NFTDescriptor{ salt: salt }(); - // forgefmt: disable-next-line lockupDynamic = - new SablierV2LockupDynamic{ salt: salt }(initialAdmin, comptroller, nftDescriptor, maxSegmentCount); - lockupLinear = new SablierV2LockupLinear{ salt: salt }(initialAdmin, comptroller, nftDescriptor); + new SablierV2LockupDynamic{ salt: salt }(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierV2LockupLinear{ salt: salt }(initialAdmin, nftDescriptor); + lockupTranched = + new SablierV2LockupTranched{ salt: salt }(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); } } diff --git a/script/DeployDeterministicCore2.s.sol b/script/DeployDeterministicCore2.s.sol index 71015d65e..7b50fb75d 100644 --- a/script/DeployDeterministicCore2.s.sol +++ b/script/DeployDeterministicCore2.s.sol @@ -1,43 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; import { BaseScript } from "./Base.s.sol"; -/// @notice Deploys these contracts at deterministic addresses across chains, in the following order: -/// -/// 1. {SablierV2Comptroller} -/// 2. {SablierV2LockupDynamic} -/// 3. {SablierV2LockupLinear} -/// /// @dev Reverts if any contract has already been deployed. contract DeployDeterministicCore2 is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy the contract via a deterministic CREATE2 factory. - /// https://github.com/Arachnid/deterministic-deployment-proxy function run( - string memory create2Salt, address initialAdmin, - ISablierV2NFTDescriptor nftDescriptor, - uint256 maxSegmentCount + ISablierV2NFTDescriptor nftDescriptor ) public virtual broadcast returns ( - SablierV2Comptroller comptroller, SablierV2LockupDynamic lockupDynamic, - SablierV2LockupLinear lockupLinear + SablierV2LockupLinear lockupLinear, + SablierV2LockupTranched lockupTranched ) { - bytes32 salt = bytes32(abi.encodePacked(create2Salt)); - comptroller = new SablierV2Comptroller{ salt: salt }(initialAdmin); - // forgefmt: disable-next-line + bytes32 salt = constructCreate2Salt(); lockupDynamic = - new SablierV2LockupDynamic{ salt: salt }(initialAdmin, comptroller, nftDescriptor, maxSegmentCount); - lockupLinear = new SablierV2LockupLinear{ salt: salt }(initialAdmin, comptroller, nftDescriptor); + new SablierV2LockupDynamic{ salt: salt }(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierV2LockupLinear{ salt: salt }(initialAdmin, nftDescriptor); + lockupTranched = + new SablierV2LockupTranched{ salt: salt }(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); } } diff --git a/script/DeployDeterministicLockupDynamic.s.sol b/script/DeployDeterministicLockupDynamic.s.sol index 8c25b4a83..512f89223 100644 --- a/script/DeployDeterministicLockupDynamic.s.sol +++ b/script/DeployDeterministicLockupDynamic.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; @@ -10,22 +9,17 @@ import { BaseScript } from "./Base.s.sol"; /// @notice Deploys {SablierV2LockupDynamic} at a deterministic address across chains. /// @dev Reverts if the contract has already been deployed. contract DeployDeterministicLockupDynamic is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: - /// https://github.com/Arachnid/deterministic-deployment-proxy function run( - string memory create2Salt, address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor initialNFTDescriptor, - uint256 maxSegmentCount + ISablierV2NFTDescriptor initialNFTDescriptor ) public virtual broadcast returns (SablierV2LockupDynamic lockupDynamic) { - lockupDynamic = new SablierV2LockupDynamic{ salt: bytes32(abi.encodePacked(create2Salt)) }( - initialAdmin, initialComptroller, initialNFTDescriptor, maxSegmentCount - ); + bytes32 salt = constructCreate2Salt(); + lockupDynamic = + new SablierV2LockupDynamic{ salt: salt }(initialAdmin, initialNFTDescriptor, segmentCountMap[block.chainid]); } } diff --git a/script/DeployDeterministicLockupLinear.s.sol b/script/DeployDeterministicLockupLinear.s.sol index 3526a45fb..de36726ab 100644 --- a/script/DeployDeterministicLockupLinear.s.sol +++ b/script/DeployDeterministicLockupLinear.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; @@ -10,12 +9,8 @@ import { BaseScript } from "./Base.s.sol"; /// @dev Deploys {SablierV2LockupLinear} at a deterministic address across chains. /// @dev Reverts if the contract has already been deployed. contract DeployDeterministicLockupLinear is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: - /// https://github.com/Arachnid/deterministic-deployment-proxy function run( - string memory create2Salt, address initialAdmin, - ISablierV2Comptroller initialComptroller, ISablierV2NFTDescriptor initialNFTDescriptor ) public @@ -23,8 +18,7 @@ contract DeployDeterministicLockupLinear is BaseScript { broadcast returns (SablierV2LockupLinear lockupLinear) { - lockupLinear = new SablierV2LockupLinear{ salt: bytes32(abi.encodePacked(create2Salt)) }( - initialAdmin, initialComptroller, initialNFTDescriptor - ); + bytes32 salt = constructCreate2Salt(); + lockupLinear = new SablierV2LockupLinear{ salt: salt }(initialAdmin, initialNFTDescriptor); } } diff --git a/script/DeployDeterministicLockupTranched.s.sol b/script/DeployDeterministicLockupTranched.s.sol new file mode 100644 index 000000000..9e2641c99 --- /dev/null +++ b/script/DeployDeterministicLockupTranched.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; + +import { BaseScript } from "./Base.s.sol"; + +/// @dev Deploys {SablierV2LockupTranched} at a deterministic address across chains. +/// @dev Reverts if the contract has already been deployed. +contract DeployDeterministicLockupTranched is BaseScript { + function run( + address initialAdmin, + ISablierV2NFTDescriptor initialNFTDescriptor + ) + public + virtual + broadcast + returns (SablierV2LockupTranched lockupTranched) + { + bytes32 salt = constructCreate2Salt(); + lockupTranched = new SablierV2LockupTranched{ salt: salt }( + initialAdmin, initialNFTDescriptor, trancheCountMap[block.chainid] + ); + } +} diff --git a/script/DeployDeterministicNFTDescriptor.s.sol b/script/DeployDeterministicNFTDescriptor.s.sol index f7a348b74..f7a72bff2 100644 --- a/script/DeployDeterministicNFTDescriptor.s.sol +++ b/script/DeployDeterministicNFTDescriptor.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; @@ -8,9 +8,8 @@ import { BaseScript } from "./Base.s.sol"; /// @dev Deploys {SablierV2NFTDescriptor} at a deterministic address across chains. /// @dev Reverts if the contract has already been deployed. contract DeployDeterministicNFTDescriptor is BaseScript { - /// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory: - /// https://github.com/Arachnid/deterministic-deployment-proxy - function run(string memory create2Salt) public virtual broadcast returns (SablierV2NFTDescriptor nftDescriptor) { - nftDescriptor = new SablierV2NFTDescriptor{ salt: bytes32(abi.encodePacked(create2Salt)) }(); + function run() public virtual broadcast returns (SablierV2NFTDescriptor nftDescriptor) { + bytes32 salt = constructCreate2Salt(); + nftDescriptor = new SablierV2NFTDescriptor{ salt: salt }(); } } diff --git a/script/DeployLockupDynamic.s.sol b/script/DeployLockupDynamic.s.sol index b27f75396..ae5a9a7cc 100644 --- a/script/DeployLockupDynamic.s.sol +++ b/script/DeployLockupDynamic.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; @@ -10,16 +9,13 @@ import { BaseScript } from "./Base.s.sol"; contract DeployLockupDynamic is BaseScript { function run( address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor initialNFTDescriptor, - uint256 maxSegmentCount + ISablierV2NFTDescriptor initialNFTDescriptor ) public virtual broadcast returns (SablierV2LockupDynamic lockupDynamic) { - lockupDynamic = - new SablierV2LockupDynamic(initialAdmin, initialComptroller, initialNFTDescriptor, maxSegmentCount); + lockupDynamic = new SablierV2LockupDynamic(initialAdmin, initialNFTDescriptor, segmentCountMap[block.chainid]); } } diff --git a/script/DeployLockupLinear.s.sol b/script/DeployLockupLinear.s.sol index b3af99d1b..940966a2f 100644 --- a/script/DeployLockupLinear.s.sol +++ b/script/DeployLockupLinear.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; @@ -10,7 +9,6 @@ import { BaseScript } from "./Base.s.sol"; contract DeployLockupLinear is BaseScript { function run( address initialAdmin, - ISablierV2Comptroller initialComptroller, ISablierV2NFTDescriptor initialNFTDescriptor ) public @@ -18,6 +16,6 @@ contract DeployLockupLinear is BaseScript { broadcast returns (SablierV2LockupLinear lockupLinear) { - lockupLinear = new SablierV2LockupLinear(initialAdmin, initialComptroller, initialNFTDescriptor); + lockupLinear = new SablierV2LockupLinear(initialAdmin, initialNFTDescriptor); } } diff --git a/script/DeployLockupTranched.s.sol b/script/DeployLockupTranched.s.sol new file mode 100644 index 000000000..adae8f7fe --- /dev/null +++ b/script/DeployLockupTranched.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; + +import { BaseScript } from "./Base.s.sol"; + +contract DeployLockupTranched is BaseScript { + function run( + address initialAdmin, + ISablierV2NFTDescriptor initialNFTDescriptor + ) + public + virtual + broadcast + returns (SablierV2LockupTranched lockupTranched) + { + lockupTranched = new SablierV2LockupTranched(initialAdmin, initialNFTDescriptor, trancheCountMap[block.chainid]); + } +} diff --git a/script/DeployNFTDescriptor.s.sol b/script/DeployNFTDescriptor.s.sol index a84ebb90d..6f3019171 100644 --- a/script/DeployNFTDescriptor.s.sol +++ b/script/DeployNFTDescriptor.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; diff --git a/script/GenerateSVG.s.sol b/script/GenerateSVG.s.sol index df3bbc9d7..9a3b63b98 100644 --- a/script/GenerateSVG.s.sol +++ b/script/GenerateSVG.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; @@ -38,11 +38,11 @@ contract GenerateSVG is BaseScript, SablierV2NFTDescriptor { assetAddress: DAI.toHexString(), assetSymbol: "DAI", duration: calculateDurationInDays({ startTime: 0, endTime: duration * 1 days }), - sablierAddress: LOCKUP_LINEAR.toHexString(), progress: stringifyPercentage(progress), progressNumerical: progress, - status: status, - streamingModel: "Lockup Linear" + sablierAddress: LOCKUP_LINEAR.toHexString(), + sablierModel: "Lockup Linear", + status: status }) ); } diff --git a/script/Init.s.sol b/script/Init.s.sol index 8a99762ac..a944d4a94 100644 --- a/script/Init.s.sol +++ b/script/Init.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud2x18 } from "@prb/math/src/UD2x18.sol"; @@ -7,7 +7,6 @@ import { ud60x18 } from "@prb/math/src/UD60x18.sol"; import { Solarray } from "solarray/src/Solarray.sol"; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2LockupDynamic } from "../src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "../src/interfaces/ISablierV2LockupLinear.sol"; import { Broker, LockupDynamic, LockupLinear } from "../src/types/DataTypes.sol"; @@ -15,13 +14,12 @@ import { Broker, LockupDynamic, LockupLinear } from "../src/types/DataTypes.sol" import { BaseScript } from "./Base.s.sol"; interface IERC20Mint { - function mint(address beneficiary, uint256 amount) external; + function mint(address beneficiary, uint256 value) external; } -/// @notice Initializes the protocol by setting up the comptroller and creating some streams. +/// @notice Initializes the protocol by creating some streams. contract Init is BaseScript { function run( - ISablierV2Comptroller comptroller, ISablierV2LockupLinear lockupLinear, ISablierV2LockupDynamic lockupDynamic, IERC20 asset @@ -32,28 +30,16 @@ contract Init is BaseScript { address sender = broadcaster; address recipient = vm.addr(vm.deriveKey({ mnemonic: mnemonic, index: 1 })); - /*////////////////////////////////////////////////////////////////////////// - COMPTROLLER - //////////////////////////////////////////////////////////////////////////*/ - - // Enable the ERC-20 asset for flash loaning. - if (!comptroller.isFlashAsset(asset)) { - comptroller.toggleFlashAsset(asset); - } - - // Set the flash fee to 0.05%. - comptroller.setFlashFee({ newFlashFee: ud60x18(0.0005e18) }); - /*////////////////////////////////////////////////////////////////////////// LOCKUP-LINEAR //////////////////////////////////////////////////////////////////////////*/ // Mint enough assets to the sender. - IERC20Mint(address(asset)).mint({ beneficiary: sender, amount: 131_601.1e18 + 10_000e18 }); + IERC20Mint(address(asset)).mint({ beneficiary: sender, value: 131_601.1e18 + 10_000e18 }); // Approve the Sablier contracts to transfer the ERC-20 assets from the sender. - asset.approve({ spender: address(lockupLinear), amount: type(uint256).max }); - asset.approve({ spender: address(lockupDynamic), amount: type(uint256).max }); + asset.approve({ spender: address(lockupLinear), value: type(uint256).max }); + asset.approve({ spender: address(lockupDynamic), value: type(uint256).max }); // Create 7 Lockup Linear streams with various amounts and durations. // @@ -92,19 +78,21 @@ contract Init is BaseScript { //////////////////////////////////////////////////////////////////////////*/ // Create the default lockupDynamic stream. - LockupDynamic.SegmentWithDelta[] memory segments = new LockupDynamic.SegmentWithDelta[](2); - segments[0] = LockupDynamic.SegmentWithDelta({ amount: 2500e18, exponent: ud2x18(3.14e18), delta: 1 hours }); - segments[1] = LockupDynamic.SegmentWithDelta({ amount: 7500e18, exponent: ud2x18(0.5e18), delta: 1 weeks }); - lockupDynamic.createWithDeltas( - LockupDynamic.CreateWithDeltas({ + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); + segments[0] = + LockupDynamic.SegmentWithDuration({ amount: 2500e18, exponent: ud2x18(3.14e18), duration: 1 hours }); + segments[1] = + LockupDynamic.SegmentWithDuration({ amount: 7500e18, exponent: ud2x18(0.5e18), duration: 1 weeks }); + lockupDynamic.createWithDurations( + LockupDynamic.CreateWithDurations({ + sender: sender, + recipient: recipient, + totalAmount: 10_000e18, asset: asset, - broker: Broker(address(0), ud60x18(0)), cancelable: true, transferable: true, - recipient: recipient, - sender: sender, segments: segments, - totalAmount: 10_000e18 + broker: Broker(address(0), ud60x18(0)) }) ); } diff --git a/shell/deploy-multi-chain.sh b/shell/deploy-multi-chain.sh new file mode 100755 index 000000000..c21ea7b6c --- /dev/null +++ b/shell/deploy-multi-chain.sh @@ -0,0 +1,376 @@ +#!/usr/bin/env bash + +# Pre-requisites for running this script: +# +# - bash >=4.0.0 +# - foundry (https://getfoundry.sh) + +# Usage: ./shell/deploy-multi-chain.sh [options] [[chain1 chain2 ...]] +# Enters interactive mode if no `.env.deployment` file is found +# +# Options: +# --all Deploy on all chains. +# --broadcast Broadcast the deployment and verify on Etherscan. +# --deterministic Deploy using the deterministic script. +# -h, --help Show available command-line options and exit. +# -i, --interactive Enters interactive mode and ignore .env.deployment. +# --print Simulate and show the deployment command. +# -s, --script Script to run from the `script` folder. +# --with-gas-price Specify gas price for transaction. +# +# Example: ./shell/deploy-multi-chain.sh # By default, deploys to Sepolia only +# Example: ./shell/deploy-multi-chain.sh --broadcast optimism polygon +# Example: ./shell/deploy-multi-chain.sh --broadcast --deterministic --print optimism +# +# Make sure to set up your `.env.deployment` file first. + +# Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + +# Color codes +EC='\033[0;31m' # Error Color +IC='\033[0;36m' # Info Color +NC='\033[0m' # No Color +SC='\033[0;32m' # Success Color +WC='\033[0;33m' # Warn Color + +# Unicode characters for tick +TICK="\xE2\x9C\x94" + +# Check: Bash >=4.0.0 is required for associative arrays +if ((BASH_VERSINFO[0] < 4)); then + echo -e "${EC}Error:\nThis script requires Bash version 4.0.0 or higher. + \nYou are currently using Bash version ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}. + \nPlease upgrade your Bash version and try again.${NC}" + exit 1 +fi + +# Define usage +usage="\nUsage: ./shell/deploy-multi-chain.sh [-h] [--help] [--print] [-i] [--interactive] [-s] [--script] + [--broadcast] [--deterministic] [--with-gas-price] [--all] + [[chain1 chain2 ...]] +Examples: + ./shell/deploy-multi-chain.sh # By default, deploys only to Sepolia + ./shell/deploy-multi-chain.sh --broadcast optimism polygon + ./shell/deploy-multi-chain.sh --broadcast --deterministic optimism +" + +# Create deployments directory +deployments=./deployments +rm -rf ${deployments} +mkdir ${deployments} + +# Addresses taken from https://docs.sablier.com/concepts/governance +export ARBITRUM_ADMIN="0xF34E41a6f6Ce5A45559B1D3Ee92E141a3De96376" +export ARBITRUM_SEPOLIA_ADMIN="0xb1bEF51ebCA01EB12001a639bDBbFF6eEcA12B9F" +export AVALANCHE_ADMIN="0x4735517616373c5137dE8bcCDc887637B8ac85Ce" +export BASE_ADMIN="0x83A6fA8c04420B3F9C7A4CF1c040b63Fbbc89B66" +export BNB_ADMIN="0x6666cA940D2f4B65883b454b7Bc7EEB039f64fa3" +export GNOSIS_ADMIN="0x72ACB57fa6a8fa768bE44Db453B1CDBa8B12A399" +export MAINNET_ADMIN="0x79Fb3e81aAc012c08501f41296CCC145a1E15844" +export OPTIMISM_ADMIN="0x43c76FE8Aec91F63EbEfb4f5d2a4ba88ef880350" +export POLYGON_ADMIN="0x40A518C5B9c1d3D6d62Ba789501CE4D526C9d9C6" +export SCROLL_ADMIN="0x0F7Ad835235Ede685180A5c611111610813457a9" +export SEPOLIA_ADMIN="0xb1bEF51ebCA01EB12001a639bDBbFF6eEcA12B9F" + +# Flag for broadcast deployment +BROADCAST_DEPLOYMENT=false + +# Flag for deterministic deployment +DETERMINISTIC_DEPLOYMENT=false + +# Flags for gas price +GAS_PRICE=0 +WITH_GAS_PRICE=false + +# Flag for all chains +ON_ALL_CHAINS=false + +# Flag for displaying deployment command +READ_ONLY=false + +# Flag to enter interactive mode in case .env.deployment not found or --interactive is provided +INTERACTIVE=false + +# Provided chains +provided_chains=() + +# Script to execute +sol_script="" + +# Declare the chains array +declare -A chains + +# define function to initialize all configurations +function initialize { + chains["arbitrum"]="$ARBITRUM_RPC_URL $ARBISCAN_API_KEY $ARBITRUM_ADMIN" + chains["arbitrum_sepolia"]="$ARBITRUM_SEPOLIA_RPC_URL $ARBISCAN_API_KEY $ARBITRUM_SEPOLIA_ADMIN" + chains["avalanche"]="$AVALANCHE_RPC_URL $SNOWTRACE_API_KEY $AVALANCHE_ADMIN" + chains["base"]="$BASE_RPC_URL $BASESCAN_API_KEY $BASE_ADMIN" + chains["bnb_smart_chain"]="$BNB_RPC_URL $BSCSCAN_API_KEY $BNB_ADMIN" + chains["gnosis"]="$GNOSIS_RPC_URL $GNOSISSCAN_API_KEY $GNOSIS_ADMIN" + chains["mainnet"]="$MAINNET_RPC_URL $ETHERSCAN_API_KEY $MAINNET_ADMIN" + chains["optimism"]="$OPTIMISM_RPC_URL $OPTIMISTIC_API_KEY $OPTIMISM_ADMIN" + chains["polygon"]="$POLYGON_RPC_URL $POLYGONSCAN_API_KEY $POLYGON_ADMIN" + chains["sepolia"]="$SEPOLIA_RPC_URL $ETHERSCAN_API_KEY $SEPOLIA_ADMIN" + chains["scroll"]="$SCROLL_RPC_URL $SCROLLSCAN_API_KEY $SCROLL_ADMIN" +} + +# define function to initialize limited configurations +function initialize_interactive { + # load values from the terminal prompt + echo -e "1. Enter admin address: \c" + read admin + + echo -e "2. Enter Etherscan API key: \c" + read api_key +} + +if [ -f .env.deployment ]; then + # Source the .env.deployment file to load the variables + source .env.deployment + + # initialize chains with all the configurations + initialize +else + # Set bool to enter intaractive mode + INTERACTIVE=true + + # load values from the terminal prompt + echo -e "${WC}Missing '.env.deployment'. Provide details below: ${NC}\n" + + # initialize chains + initialize_interactive + +fi + +# Check for arguments passed to the script +for ((i=1; i<=$#; i++)); do + # Convert the argument to lowercase + arg=${!i,,} + + # Check if '--all' flag is provided in the arguments + if [[ ${arg} == "--all" ]]; then + ON_ALL_CHAINS=true + provided_chains=("${!chains[@]}") + fi + + # Check if '--broadcast' flag is provided the arguments + if [[ ${arg} == "--broadcast" ]]; then + BROADCAST_DEPLOYMENT=true + fi + + # Check if '--deterministic' flag is provided in the arguments + if [[ ${arg} == "--deterministic" ]]; then + DETERMINISTIC_DEPLOYMENT=true + fi + + # Show usage of this command with --help option + if [[ ${arg} == "--help" || ${arg} == "-h" ]]; then + echo -e "${usage}" + # Get all chain names from the chains array + names=("${!chains[@]}") + # Sort the names + sorted_names=($(printf "%s\n" "${names[@]}" | sort)) + # Print the header + printf "\nSupported chains: \n%-20s %-20s\n" "Chain Name" + printf "%-20s %-20s\n" "-----------" + + # Print the supported chains + for chain in "${sorted_names[@]}"; do + IFS=' ' read -r rpc_url api_key admin <<< "${chains[$chain]}" + + # Print the chain + printf "%-20s %-20s\n" "${chain}" + done + exit 0 + fi + + # Check if '--interactive' flag is provided in the arguments + if [[ ${arg} == "--interactive" || ${arg} == "-i" ]]; then + INTERACTIVE=true + echo -e "Interactive mode activated. Provide details below: \n" + + initialize_interactive + fi + + # Check if '--print' flag is provided in the arguments + if [[ ${arg} == "--print" ]]; then + READ_ONLY=true + fi + + # Check if '--script' flag is provided in the arguments + if [[ ${arg} == "--script" || ${arg} == "-s" ]]; then + files=(script/*.s.sol) + + # Present the list of available scripts + echo "Please select a script:" + select file in "${files[@]}"; do + if [[ -n ${file} ]]; then + echo -e "${SC}+${NC} You selected ${IC}${file}${NC}" + sol_script=${file} + break + else + echo -e "${EC}Invalid selection${NC}" + fi + done + fi + + # Check if '--with-gas-price' flag is provided in the arguments + if [[ ${arg} == "--with-gas-price" ]]; then + WITH_GAS_PRICE=true + + # Increment index to get the next argument, which should be the gas price + ((i++)) + GAS_PRICE=${!i} + if ! [[ ${GAS_PRICE} =~ ^[0-9]+$ ]]; then + echo -e "${EC}Error: Invalid value for --with-gas-price, must be number${NC}" + exit 1 + fi + fi + + # Check for passed chains + if [[ ${arg} != "--all" && + ${arg} != "--broadcast" && + ${arg} != "--deterministic" && + ${arg} != "--help" && + ${arg} != "-h" && + ${arg} != "-i" && + ${arg} != "--interactive" && + ${arg} != "--print" && + ${arg} != "-s" && + ${arg} != "--script" && + ${arg} != "--with-gas-price" && + ${ON_ALL_CHAINS} == false + ]]; then + # check for synonyms + if [[ ${arg} == "ethereum" ]]; then + arg="mainnet" + fi + provided_chains+=("${arg}") + fi +done + +# Set the default chain to Sepolia if no chains are provided +if [ ${#provided_chains[@]} -eq 0 ]; then + provided_chains=("sepolia") +fi + +# Compile the contracts +echo "Compiling the contracts..." + +# Deploy to the provided chains +for chain in "${provided_chains[@]}"; do + # Check if the provided chain is defined + if [[ ! -v "chains[${chain}]" ]]; then + printf "\n${WC}Warning for '${chain}': Invalid command or chain name. Get the full list of supported chains: ${NC}" + printf "\n\n\t${IC}./shell/deploy-multi-chain.sh --help${NC}\n" + continue + fi + + echo -e "\n${IC}Deployment on ${chain} started...${NC}" + + if [[ ${INTERACTIVE} == true ]]; then + # load values from the terminal prompt + echo -e "Enter RPC URL for ${chain}: \c" + read rpc_url + else + # Split the configuration into RPC, API key, and admin + IFS=' ' read -r rpc_url api_key admin <<< "${chains[$chain]}" + fi + + # Declare the deployment command + declare -a deployment_command + + deployment_command=("forge") + + # Construct the deployment command + if [[ ${DETERMINISTIC_DEPLOYMENT} == true ]]; then + echo -e "${SC}+${NC} Deterministic address" + if [[ ${sol_script} == "" ]]; then + deployment_command+=("script" "script/DeployDeterministicCore.s.sol" "--ffi") + else + deployment_command+=("script" "${sol_script}") + fi + deployment_command+=("--rpc-url" "${rpc_url}") + + #################################################################### + # Distinct ways to construct command with string elements + # While execution adds single quotes around them while + # echo removes single quotes + #################################################################### + if [[ ${READ_ONLY} == true ]]; then + deployment_command+=("--sig" "'run(address)'") + else + deployment_command+=("--sig" "run(address)") + fi + else + # Construct the command + if [[ ${sol_script} == "" ]]; then + deployment_command+=("script" "script/DeployCore.s.sol") + else + deployment_command+=("script" "${sol_script}") + fi + deployment_command+=("--rpc-url" "${rpc_url}") + + if [[ ${READ_ONLY} == true ]]; then + deployment_command+=("--sig" "'run(address)'") + else + deployment_command+=("--sig" "run(address)") + fi + fi + + deployment_command+=("${admin}") + deployment_command+=("-vvv") + + # Append additional options if gas price is enabled + if [[ ${WITH_GAS_PRICE} == true ]]; then + gas_price_in_gwei=$(echo "scale=2; ${GAS_PRICE} / 1000000000" | bc) + echo -e "${SC}+${NC} Max gas price: ${gas_price_in_gwei} gwei" + deployment_command+=("--with-gas-price" "${GAS_PRICE}") + fi + + # Append additional options if broadcast is enabled + if [[ ${BROADCAST_DEPLOYMENT} == true ]]; then + echo -e "${SC}+${NC} Broadcasting on-chain" + deployment_command+=("--broadcast" "--verify" "--etherscan-api-key" "${api_key}") + else + echo -e "${SC}+${NC} Simulating on forked chain" + fi + + if [[ ${READ_ONLY} == true ]]; then + # Print deployment_command + echo -e "${SC}+${NC} Printing command without action\n" + echo -e "FOUNDRY_PROFILE=optimized ${deployment_command[@]}" + else + # Execute the deployment command and print the logs in real-time + output=$(FOUNDRY_PROFILE=optimized "${deployment_command[@]}" |& tee /dev/fd/2) || true + + # Check for error in output + if [[ ${output} == *"Error"* ]]; then + exit 1 + fi + + # Create a file for the chain + chain_file="${deployments}/${chain}.txt" + touch "${chain_file}" + + # Extract and save contract addresses + lockupDynamic_address=$(echo "${output}" | awk '/lockupDynamic: contract/{print $NF}') + lockupLinear_address=$(echo "${output}" | awk '/lockupLinear: contract/{print $NF}') + lockupTranched_address=$(echo "${output}" | awk '/lockupTranched: contract/{print $NF}') + nftDescriptor_address=$(echo "${output}" | awk '/nftDescriptor: contract/{print $NF}') + + # Save to the chain file + { + echo "SablierV2LockupDynamic = ${lockupDynamic_address}" + echo "SablierV2LockupLinear = ${lockupLinear_address}" + echo "SablierV2LockupTranched = ${lockupTranched_address}" + echo "SablierV2NFTDescriptor = ${nftDescriptor_address}" + } >> "$chain_file" + + echo -e "${SC}${TICK} Deployed on ${chain}. You can find the addresses in ${chain_file}${NC}" + fi +done + +echo -e "\nEnd of it." diff --git a/shell/generate-svg.sh b/shell/generate-svg.sh index 4f5a0d556..7748644d6 100755 --- a/shell/generate-svg.sh +++ b/shell/generate-svg.sh @@ -5,6 +5,7 @@ # Pre-requisites: # - foundry (https://getfoundry.sh) +# - sd (https://github.com/chmln/sd) # Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca set -euo pipefail @@ -24,11 +25,16 @@ output=$( "$arg_amount" \ "$arg_duration" ) -svg=$(echo "$output" | awk -F "svg: string " '{print $2}' | awk 'NF > 0') + +# Forge adds 'svg: string ' as a prefix before the SVG +# - The awk command records everything after the prefix, while filtering out empty lines +# - `sd \\"` '"'` removes the escape backslashes +# - `sd ^\"|\"$' ''` removes the starting and the ending double quotes +svg=$(echo "$output" | awk -F "svg: string " '/svg: string /{print $2; exit}' | sd '\\"' '"' | sd '^"|"$' '') # Generate the file name name="nft-${arg_progress}-${arg_status}-${arg_amount}-${arg_duration}.svg" -sanitized="$(echo "$name" | sed "s/ //g" )" # remove whitespaces +sanitized="$(echo "$name" | sd ' ' '' )" # remove whitespaces # Put the SVG in a file mkdir -p "out-svg" diff --git a/shell/prepare-artifacts.sh b/shell/prepare-artifacts.sh index 06ab4fe97..5aa4f6ab5 100755 --- a/shell/prepare-artifacts.sh +++ b/shell/prepare-artifacts.sh @@ -2,7 +2,7 @@ # Pre-requisites: # - foundry (https://getfoundry.sh) -# - pnpm (https://pnpm.io) +# - bun (https://bun.sh) # Strict mode: https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca set -euo pipefail @@ -16,24 +16,23 @@ mkdir $artifacts \ "$artifacts/interfaces" \ "$artifacts/interfaces/erc20" \ "$artifacts/interfaces/erc721" \ - "$artifacts/interfaces/hooks" \ "$artifacts/libraries" # Generate the artifacts with Forge FOUNDRY_PROFILE=optimized forge build # Copy the production artifacts -cp out-optimized/SablierV2Comptroller.sol/SablierV2Comptroller.json $artifacts cp out-optimized/SablierV2LockupDynamic.sol/SablierV2LockupDynamic.json $artifacts cp out-optimized/SablierV2LockupLinear.sol/SablierV2LockupLinear.json $artifacts +cp out-optimized/SablierV2LockupTranched.sol/SablierV2LockupTranched.json $artifacts cp out-optimized/SablierV2NFTDescriptor.sol/SablierV2NFTDescriptor.json $artifacts interfaces=./artifacts/interfaces -cp out-optimized/ISablierV2Base.sol/ISablierV2Base.json $interfaces -cp out-optimized/ISablierV2Comptroller.sol/ISablierV2Comptroller.json $interfaces +cp out-optimized/ISablierLockupRecipient.sol/ISablierLockupRecipient.json $interfaces cp out-optimized/ISablierV2Lockup.sol/ISablierV2Lockup.json $interfaces cp out-optimized/ISablierV2LockupDynamic.sol/ISablierV2LockupDynamic.json $interfaces cp out-optimized/ISablierV2LockupLinear.sol/ISablierV2LockupLinear.json $interfaces +cp out-optimized/ISablierV2LockupTranched.sol/ISablierV2LockupTranched.json $interfaces cp out-optimized/ISablierV2NFTDescriptor.sol/ISablierV2NFTDescriptor.json $interfaces erc20=./artifacts/interfaces/erc20 @@ -43,11 +42,8 @@ erc721=./artifacts/interfaces/erc721 cp out-optimized/IERC721.sol/IERC721.json $erc721 cp out-optimized/IERC721Metadata.sol/IERC721Metadata.json $erc721 -hooks=./artifacts/interfaces/hooks -cp out-optimized/ISablierV2LockupRecipient.sol/ISablierV2LockupRecipient.json $hooks - libraries=./artifacts/libraries cp out-optimized/Errors.sol/Errors.json $libraries # Format the artifacts with Prettier -pnpm prettier --write ./artifacts +bun prettier --write ./artifacts diff --git a/shell/update-counts.sh b/shell/update-counts.sh new file mode 100755 index 000000000..a865a3f4e --- /dev/null +++ b/shell/update-counts.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Pre-requisites for running this script: +# +# - bun (https://bun.sh) +# - foundry (https://getfoundry.sh) + +# Strict mode +set -euo pipefail + +# Path to the Base Script +BASE_SCRIPT="script/Base.s.sol" + +# Compile the contracts with the optimized profile +bun run build:optimized + +# Generalized function to update counts +update_counts() { + local test_name=$1 + local map_name=$2 + echo -e "\nRunning forge test for estimating $test_name..." + local output=$(FOUNDRY_PROFILE=benchmark forge t --mt "test_Estimate${test_name}" -vv) + echo -e "\nParsing output for $test_name..." + + # Define a table with headers. This table is not put in the Solidity script file, + # but is used to be displayed in the terminal. + local table="Category,Chain ID,New Max Count" + + # Parse the output to extract counts and chain IDs + while IFS= read -r line; do + local count=$(echo $line | awk '{print $2}') + local chain_id=$(echo $line | awk '{print $8}') + + # Add the data to the table + table+="\n$map_name,$chain_id,$count" + + # Update the map for each chain ID using sd + sd "$map_name\[$chain_id\] = [0-9]+;" "$map_name[$chain_id] = $count;" $BASE_SCRIPT + done < <(echo "$output" | grep 'count:') + + # Print the table using the column command + echo -e $table | column -t -s ',' +} + +# Call the function with specific parameters for segments and tranches +update_counts "Segments" "segmentCountMap" +update_counts "Tranches" "trancheCountMap" + +# Reformat the code with Forge +forge fmt $BASE_SCRIPT + +printf "\n\nAll mappings updated." diff --git a/shell/update-precompiles.sh b/shell/update-precompiles.sh index dd2d205b0..008db274a 100755 --- a/shell/update-precompiles.sh +++ b/shell/update-precompiles.sh @@ -12,21 +12,21 @@ set -euo pipefail FOUNDRY_PROFILE=optimized forge build # Retrieve the raw bytecodes, removing the "0x" prefix -comptroller=$(cat out-optimized/SablierV2Comptroller.sol/SablierV2Comptroller.json | jq -r '.bytecode.object' | cut -c 3-) lockup_dynamic=$(cat out-optimized/SablierV2LockupDynamic.sol/SablierV2LockupDynamic.json | jq -r '.bytecode.object' | cut -c 3-) lockup_linear=$(cat out-optimized/SablierV2LockupLinear.sol/SablierV2LockupLinear.json | jq -r '.bytecode.object' | cut -c 3-) +lockup_tranched=$(cat out-optimized/SablierV2LockupTranched.sol/SablierV2LockupTranched.json | jq -r '.bytecode.object' | cut -c 3-) nft_descriptor=$(cat out-optimized/SablierV2NFTDescriptor.sol/SablierV2NFTDescriptor.json | jq -r '.bytecode.object' | cut -c 3-) -precompiles_path="test/utils/Precompiles.sol" +precompiles_path="precompiles/Precompiles.sol" if [ ! -f $precompiles_path ]; then echo "Precompiles file does not exist" exit 1 fi # Replace the current bytecodes -sd "(BYTECODE_COMPTROLLER =)[^;]+;" "\$1 hex\"$comptroller\";" $precompiles_path sd "(BYTECODE_LOCKUP_DYNAMIC =)[^;]+;" "\$1 hex\"$lockup_dynamic\";" $precompiles_path sd "(BYTECODE_LOCKUP_LINEAR =)[^;]+;" "\$1 hex\"$lockup_linear\";" $precompiles_path +sd "(BYTECODE_LOCKUP_TRANCHED =)[^;]+;" "\$1 hex\"$lockup_tranched\";" $precompiles_path sd "(BYTECODE_NFT_DESCRIPTOR =)[^;]+;" "\$1 hex\"$nft_descriptor\";" $precompiles_path # Reformat the code with Forge diff --git a/slither.config.json b/slither.config.json index 16f73fce4..a42619ece 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,10 +1,9 @@ { "detectors_to_exclude": "naming-convention,reentrancy-events,solc-version,timestamp", - "filter_paths": "(test)", + "filter_paths": "(node_modules/,test/)", "solc_remaps": [ "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", "@prb/math/=node_modules/@prb-math/", - "@prb/test/=node_modules/@prb/test/", "forge-std/=node_modules/forge-std/", "solady/=node_modules/solady/", "solarray/=node_modules/solarray/" diff --git a/src/SablierV2Comptroller.sol b/src/SablierV2Comptroller.sol deleted file mode 100644 index 2c7390980..000000000 --- a/src/SablierV2Comptroller.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { Adminable } from "./abstracts/Adminable.sol"; -import { IAdminable } from "./interfaces/IAdminable.sol"; -import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol"; - -/* - -███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██████╗ -██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██║ ██║╚════██╗ -███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██║ ██║ █████╔╝ -╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚██╗ ██╔╝██╔═══╝ -███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ╚████╔╝ ███████╗ -╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝ - - ██████╗ ██████╗ ███╗ ███╗██████╗ ████████╗██████╗ ██████╗ ██╗ ██╗ ███████╗██████╗ -██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚══██╔══╝██╔══██╗██╔═══██╗██║ ██║ ██╔════╝██╔══██╗ -██║ ██║ ██║██╔████╔██║██████╔╝ ██║ ██████╔╝██║ ██║██║ ██║ █████╗ ██████╔╝ -██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██╔══██╗██║ ██║██║ ██║ ██╔══╝ ██╔══██╗ -╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ██║╚██████╔╝███████╗███████╗███████╗██║ ██║ - ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ - -*/ - -/// @title SablierV2Comptroller -/// @notice See the documentation in {ISablierV2Comptroller}. -contract SablierV2Comptroller is - ISablierV2Comptroller, // 1 inherited component - Adminable // 1 inherited component -{ - /*////////////////////////////////////////////////////////////////////////// - PUBLIC STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2Comptroller - UD60x18 public override flashFee; - - /// @inheritdoc ISablierV2Comptroller - mapping(IERC20 asset => bool supported) public override isFlashAsset; - - /// @inheritdoc ISablierV2Comptroller - mapping(IERC20 asset => UD60x18 fee) public override protocolFees; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Emits a {TransferAdmin} event. - /// @param initialAdmin The address of the initial contract admin. - constructor(address initialAdmin) { - admin = initialAdmin; - emit IAdminable.TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin }); - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2Comptroller - function setFlashFee(UD60x18 newFlashFee) external override onlyAdmin { - // Effects: set the new flash fee. - UD60x18 oldFlashFee = flashFee; - flashFee = newFlashFee; - - // Log the change of the flash fee. - emit ISablierV2Comptroller.SetFlashFee({ admin: msg.sender, oldFlashFee: oldFlashFee, newFlashFee: newFlashFee }); - } - - /// @inheritdoc ISablierV2Comptroller - function setProtocolFee(IERC20 asset, UD60x18 newProtocolFee) external override onlyAdmin { - // Effects: set the new global fee. - UD60x18 oldProtocolFee = protocolFees[asset]; - protocolFees[asset] = newProtocolFee; - - // Log the change of the protocol fee. - emit ISablierV2Comptroller.SetProtocolFee({ - admin: msg.sender, - asset: asset, - oldProtocolFee: oldProtocolFee, - newProtocolFee: newProtocolFee - }); - } - - /// @inheritdoc ISablierV2Comptroller - function toggleFlashAsset(IERC20 asset) external override onlyAdmin { - // Effects: enable the ERC-20 asset for flash loaning. - bool oldFlag = isFlashAsset[asset]; - isFlashAsset[asset] = !oldFlag; - - // Log the change of the flash asset flag. - emit ISablierV2Comptroller.ToggleFlashAsset({ admin: msg.sender, asset: asset, newFlag: !oldFlag }); - } -} diff --git a/src/SablierV2LockupDynamic.sol b/src/SablierV2LockupDynamic.sol index e8a15a4cb..b06b15c64 100644 --- a/src/SablierV2LockupDynamic.sol +++ b/src/SablierV2LockupDynamic.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -7,15 +7,10 @@ import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/Uint128.sol"; import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uint40.sol"; import { SD59x18 } from "@prb/math/src/SD59x18.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; -import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol"; import { ISablierV2LockupDynamic } from "./interfaces/ISablierV2LockupDynamic.sol"; -import { ISablierV2LockupRecipient } from "./interfaces/hooks/ISablierV2LockupRecipient.sol"; import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol"; -import { Errors } from "./libraries/Errors.sol"; import { Helpers } from "./libraries/Helpers.sol"; import { Lockup, LockupDynamic } from "./types/DataTypes.sol"; @@ -40,7 +35,7 @@ import { Lockup, LockupDynamic } from "./types/DataTypes.sol"; /// @title SablierV2LockupDynamic /// @notice See the documentation in {ISablierV2LockupDynamic}. contract SablierV2LockupDynamic is - ISablierV2LockupDynamic, // 1 inherited component + ISablierV2LockupDynamic, // 5 inherited components SablierV2Lockup // 14 inherited components { using CastingUint128 for uint128; @@ -48,18 +43,14 @@ contract SablierV2LockupDynamic is using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// - PUBLIC CONSTANTS + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierV2LockupDynamic uint256 public immutable override MAX_SEGMENT_COUNT; - /*////////////////////////////////////////////////////////////////////////// - PRIVATE STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Sablier V2 Lockup Dynamic streams mapped by unsigned integer ids. - mapping(uint256 id => LockupDynamic.Stream stream) private _streams; + /// @dev Stream segments mapped by stream IDs. This complements the `_streams` mapping in {SablierV2Lockup}. + mapping(uint256 id => LockupDynamic.Segment[] segments) internal _segments; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -67,17 +58,15 @@ contract SablierV2LockupDynamic is /// @dev Emits a {TransferAdmin} event. /// @param initialAdmin The address of the initial contract admin. - /// @param initialComptroller The address of the initial comptroller. /// @param initialNFTDescriptor The address of the NFT descriptor contract. /// @param maxSegmentCount The maximum number of segments allowed in a stream. constructor( address initialAdmin, - ISablierV2Comptroller initialComptroller, ISablierV2NFTDescriptor initialNFTDescriptor, uint256 maxSegmentCount ) ERC721("Sablier V2 Lockup Dynamic NFT", "SAB-V2-LOCKUP-DYN") - SablierV2Lockup(initialAdmin, initialComptroller, initialNFTDescriptor) + SablierV2Lockup(initialAdmin, initialNFTDescriptor) { MAX_SEGMENT_COUNT = maxSegmentCount; nextStreamId = 1; @@ -87,49 +76,6 @@ contract SablierV2LockupDynamic is USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Lockup - function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { - asset = _streams[streamId].asset; - } - - /// @inheritdoc ISablierV2Lockup - function getDepositedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 depositedAmount) - { - depositedAmount = _streams[streamId].amounts.deposited; - } - - /// @inheritdoc ISablierV2Lockup - function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { - endTime = _streams[streamId].endTime; - } - - /// @inheritdoc ISablierV2LockupDynamic - function getRange(uint256 streamId) - external - view - override - notNull(streamId) - returns (LockupDynamic.Range memory range) - { - range = LockupDynamic.Range({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); - } - - /// @inheritdoc ISablierV2Lockup - function getRefundedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundedAmount) - { - refundedAmount = _streams[streamId].amounts.refunded; - } - /// @inheritdoc ISablierV2LockupDynamic function getSegments(uint256 streamId) external @@ -138,17 +84,7 @@ contract SablierV2LockupDynamic is notNull(streamId) returns (LockupDynamic.Segment[] memory segments) { - segments = _streams[streamId].segments; - } - - /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) external view override notNull(streamId) returns (address sender) { - sender = _streams[streamId].sender; - } - - /// @inheritdoc ISablierV2Lockup - function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { - startTime = _streams[streamId].startTime; + segments = _segments[streamId]; } /// @inheritdoc ISablierV2LockupDynamic @@ -157,103 +93,41 @@ contract SablierV2LockupDynamic is view override notNull(streamId) - returns (LockupDynamic.Stream memory stream) + returns (LockupDynamic.StreamLD memory stream) { - stream = _streams[streamId]; + // Retrieve the Lockup stream from storage. + Lockup.Stream memory lockupStream = _streams[streamId]; // Settled streams cannot be canceled. if (_statusOf(streamId) == Lockup.Status.SETTLED) { - stream.isCancelable = false; + lockupStream.isCancelable = false; } - } - /// @inheritdoc ISablierV2Lockup - function getWithdrawnAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawnAmount) - { - withdrawnAmount = _streams[streamId].amounts.withdrawn; - } - - /// @inheritdoc ISablierV2Lockup - function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - if (_statusOf(streamId) != Lockup.Status.SETTLED) { - result = _streams[streamId].isCancelable; - } - } - - /// @inheritdoc SablierV2Lockup - function isTransferable(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isTransferable; - } - - /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isDepleted; - } - - /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view override(ISablierV2Lockup, SablierV2Lockup) returns (bool result) { - result = _streams[streamId].isStream; + stream = LockupDynamic.StreamLD({ + amounts: lockupStream.amounts, + asset: lockupStream.asset, + endTime: lockupStream.endTime, + isCancelable: lockupStream.isCancelable, + isDepleted: lockupStream.isDepleted, + isStream: lockupStream.isStream, + isTransferable: lockupStream.isTransferable, + recipient: _ownerOf(streamId), + segments: _segments[streamId], + sender: lockupStream.sender, + startTime: lockupStream.startTime, + wasCanceled: lockupStream.wasCanceled + }); } - /// @inheritdoc ISablierV2Lockup - function refundableAmountOf(uint256 streamId) + /// @inheritdoc ISablierV2LockupDynamic + function getTimestamps(uint256 streamId) external view override notNull(streamId) - returns (uint128 refundableAmount) - { - // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that - // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that - // canceled streams are not cancelable anymore. - if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { - refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); - } - // Otherwise, the result is implicitly zero. - } - - /// @inheritdoc ISablierV2Lockup - function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { - status = _statusOf(streamId); - } - - /// @inheritdoc ISablierV2LockupDynamic - function streamedAmountOf(uint256 streamId) - public - view - override(ISablierV2Lockup, ISablierV2LockupDynamic) - notNull(streamId) - returns (uint128 streamedAmount) + returns (LockupDynamic.Timestamps memory timestamps) { - streamedAmount = _streamedAmountOf(streamId); - } - - /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].wasCanceled; + timestamps = LockupDynamic.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); } /*////////////////////////////////////////////////////////////////////////// @@ -261,62 +135,74 @@ contract SablierV2LockupDynamic is //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierV2LockupDynamic - function createWithDeltas(LockupDynamic.CreateWithDeltas calldata params) + function createWithDurations(LockupDynamic.CreateWithDurations calldata params) external override noDelegateCall returns (uint256 streamId) { - // Checks: check the deltas and generate the canonical segments. - LockupDynamic.Segment[] memory segments = Helpers.checkDeltasAndCalculateMilestones(params.segments); + // Generate the canonical segments. + LockupDynamic.Segment[] memory segments = Helpers.calculateSegmentTimestamps(params.segments); // Checks, Effects and Interactions: create the stream. - streamId = _createWithMilestones( - LockupDynamic.CreateWithMilestones({ + streamId = _create( + LockupDynamic.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, asset: params.asset, - broker: params.broker, cancelable: params.cancelable, transferable: params.transferable, - recipient: params.recipient, - segments: segments, - sender: params.sender, startTime: uint40(block.timestamp), - totalAmount: params.totalAmount + segments: segments, + broker: params.broker }) ); } /// @inheritdoc ISablierV2LockupDynamic - function createWithMilestones(LockupDynamic.CreateWithMilestones calldata params) + function createWithTimestamps(LockupDynamic.CreateWithTimestamps calldata params) external override noDelegateCall returns (uint256 streamId) { // Checks, Effects and Interactions: create the stream. - streamId = _createWithMilestones(params); + streamId = _create(params); } /*////////////////////////////////////////////////////////////////////////// INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { + /// @inheritdoc SablierV2Lockup + /// @dev The distribution function is: + /// + /// $$ + /// f(x) = x^{exp} * csa + \Sigma(esa) + /// $$ + /// + /// Where: + /// + /// - $x$ is the elapsed time divided by the total duration of the current segment. + /// - $exp$ is the current segment exponent. + /// - $csa$ is the current segment amount. + /// - $\Sigma(esa)$ is the sum of all vested segments' amounts. + function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { // If the start time is in the future, return zero. - uint40 currentTime = uint40(block.timestamp); - if (_streams[streamId].startTime >= currentTime) { + uint40 blockTimestamp = uint40(block.timestamp); + if (_streams[streamId].startTime >= blockTimestamp) { return 0; } // If the end time is not in the future, return the deposited amount. uint40 endTime = _streams[streamId].endTime; - if (endTime <= currentTime) { + if (endTime <= blockTimestamp) { return _streams[streamId].amounts.deposited; } - if (_streams[streamId].segments.length > 1) { - // If there is more than one segment, it may be necessary to iterate over all of them. + if (_segments[streamId].length > 1) { + // If there is more than one segment, it may be required to iterate over all of them. return _calculateStreamedAmountForMultipleSegments(streamId); } else { // Otherwise, there is only one segment, and the calculation is simpler. @@ -334,47 +220,49 @@ contract SablierV2LockupDynamic is /// bounds" error. function _calculateStreamedAmountForMultipleSegments(uint256 streamId) internal view returns (uint128) { unchecked { - uint40 currentTime = uint40(block.timestamp); - LockupDynamic.Stream memory stream = _streams[streamId]; + uint40 blockTimestamp = uint40(block.timestamp); + Lockup.Stream memory stream = _streams[streamId]; + LockupDynamic.Segment[] memory segments = _segments[streamId]; - // Sum the amounts in all segments that precede the current time. + // Sum the amounts in all segments that precede the block timestamp. uint128 previousSegmentAmounts; - uint40 currentSegmentMilestone = stream.segments[0].milestone; + uint40 currentSegmentTimestamp = segments[0].timestamp; uint256 index = 0; - while (currentSegmentMilestone < currentTime) { - previousSegmentAmounts += stream.segments[index].amount; + while (currentSegmentTimestamp < blockTimestamp) { + previousSegmentAmounts += segments[index].amount; index += 1; - currentSegmentMilestone = stream.segments[index].milestone; + currentSegmentTimestamp = segments[index].timestamp; } // After exiting the loop, the current segment is at `index`. - SD59x18 currentSegmentAmount = stream.segments[index].amount.intoSD59x18(); - SD59x18 currentSegmentExponent = stream.segments[index].exponent.intoSD59x18(); - currentSegmentMilestone = stream.segments[index].milestone; - - uint40 previousMilestone; - if (index > 0) { - // When the current segment's index is greater than or equal to 1, it implies that the segment is not - // the first. In this case, use the previous segment's milestone. - previousMilestone = stream.segments[index - 1].milestone; + SD59x18 currentSegmentAmount = segments[index].amount.intoSD59x18(); + SD59x18 currentSegmentExponent = segments[index].exponent.intoSD59x18(); + currentSegmentTimestamp = segments[index].timestamp; + + uint40 previousTimestamp; + if (index == 0) { + // When the current segment's index is equal to 0, the current segment is the first, so use the start + // time as the previous timestamp. + previousTimestamp = stream.startTime; } else { - // Otherwise, the current segment is the first, so use the start time as the previous milestone. - previousMilestone = stream.startTime; + // Otherwise, when the current segment's index is greater than zero, it means that the segment is not + // the first. In this case, use the previous segment's timestamp. + previousTimestamp = segments[index - 1].timestamp; } - // Calculate how much time has passed since the segment started, and the total time of the segment. - SD59x18 elapsedSegmentTime = (currentTime - previousMilestone).intoSD59x18(); - SD59x18 totalSegmentTime = (currentSegmentMilestone - previousMilestone).intoSD59x18(); + // Calculate how much time has passed since the segment started, and the total duration of the segment. + SD59x18 elapsedTime = (blockTimestamp - previousTimestamp).intoSD59x18(); + SD59x18 segmentDuration = (currentSegmentTimestamp - previousTimestamp).intoSD59x18(); - // Divide the elapsed segment time by the total duration of the segment. - SD59x18 elapsedSegmentTimePercentage = elapsedSegmentTime.div(totalSegmentTime); + // Divide the elapsed time by the total duration of the segment. + SD59x18 elapsedTimePercentage = elapsedTime.div(segmentDuration); // Calculate the streamed amount using the special formula. - SD59x18 multiplier = elapsedSegmentTimePercentage.pow(currentSegmentExponent); + SD59x18 multiplier = elapsedTimePercentage.pow(currentSegmentExponent); SD59x18 segmentStreamedAmount = multiplier.mul(currentSegmentAmount); // Although the segment streamed amount should never exceed the total segment amount, this condition is - // checked without asserting to avoid locking funds in case of a bug. If this situation occurs, the + // checked without asserting to avoid locking assets in case of a bug. If this situation occurs, the // amount streamed in the segment is considered zero (except for past withdrawals), and the segment is // effectively voided. if (segmentStreamedAmount.gt(currentSegmentAmount)) { @@ -389,19 +277,19 @@ contract SablierV2LockupDynamic is } } - /// @dev Calculates the streamed amount for a a stream with one segment. Normalization to 18 decimals is not + /// @dev Calculates the streamed amount for a stream with one segment. Normalization to 18 decimals is not /// needed because there is no mix of amounts with different decimals. function _calculateStreamedAmountForOneSegment(uint256 streamId) internal view returns (uint128) { unchecked { // Calculate how much time has passed since the stream started, and the stream's total duration. SD59x18 elapsedTime = (uint40(block.timestamp) - _streams[streamId].startTime).intoSD59x18(); - SD59x18 totalTime = (_streams[streamId].endTime - _streams[streamId].startTime).intoSD59x18(); + SD59x18 totalDuration = (_streams[streamId].endTime - _streams[streamId].startTime).intoSD59x18(); // Divide the elapsed time by the stream's total duration. - SD59x18 elapsedTimePercentage = elapsedTime.div(totalTime); + SD59x18 elapsedTimePercentage = elapsedTime.div(totalDuration); // Cast the stream parameters to SD59x18. - SD59x18 exponent = _streams[streamId].segments[0].exponent.intoSD59x18(); + SD59x18 exponent = _segments[streamId][0].exponent.intoSD59x18(); SD59x18 depositedAmount = _streams[streamId].amounts.deposited.intoSD59x18(); // Calculate the streamed amount using the special formula. @@ -409,7 +297,7 @@ contract SablierV2LockupDynamic is SD59x18 streamedAmount = multiplier.mul(depositedAmount); // Although the streamed amount should never exceed the deposited amount, this condition is checked - // without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn + // without asserting to avoid locking assets in case of a bug. If this situation occurs, the withdrawn // amount is considered to be the streamed amount, and the stream is effectively frozen. if (streamedAmount.gt(depositedAmount)) { return _streams[streamId].amounts.withdrawn; @@ -420,176 +308,55 @@ contract SablierV2LockupDynamic is } } - /// @inheritdoc SablierV2Lockup - function _isCallerStreamSender(uint256 streamId) internal view override returns (bool) { - return msg.sender == _streams[streamId].sender; - } - - /// @inheritdoc SablierV2Lockup - function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) { - if (_streams[streamId].isDepleted) { - return Lockup.Status.DEPLETED; - } else if (_streams[streamId].wasCanceled) { - return Lockup.Status.CANCELED; - } - - if (block.timestamp < _streams[streamId].startTime) { - return Lockup.Status.PENDING; - } - - if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { - return Lockup.Status.STREAMING; - } else { - return Lockup.Status.SETTLED; - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - if (_streams[streamId].isDepleted) { - return amounts.withdrawn; - } else if (_streams[streamId].wasCanceled) { - return amounts.deposited - amounts.refunded; - } - - return _calculateStreamedAmount(streamId); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) { - return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; - } - /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 streamId) internal override { - // Calculate the streamed amount. - uint128 streamedAmount = _calculateStreamedAmount(streamId); - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Checks: the stream is not settled. - if (streamedAmount >= amounts.deposited) { - revert Errors.SablierV2Lockup_StreamSettled(streamId); - } - - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Calculate the sender's and the recipient's amount. - uint128 senderAmount = amounts.deposited - streamedAmount; - uint128 recipientAmount = streamedAmount - amounts.withdrawn; - - // Effects: mark the stream as canceled. - _streams[streamId].wasCanceled = true; - - // Effects: make the stream not cancelable anymore, because a stream can only be canceled once. - _streams[streamId].isCancelable = false; - - // Effects: If there are no assets left for the recipient to withdraw, mark the stream as depleted. - if (recipientAmount == 0) { - _streams[streamId].isDepleted = true; - } - - // Effects: set the refunded amount. - _streams[streamId].amounts.refunded = senderAmount; - - // Retrieve the sender and the recipient from storage. - address sender = _streams[streamId].sender; - address recipient = _ownerOf(streamId); - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: refund the sender. - asset.safeTransfer({ to: sender, value: senderAmount }); - - // Log the cancellation. - emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); - - // Emits an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interactions: if the recipient is a contract, try to invoke the cancel hook on the recipient without - // reverting if the hook is not implemented, and without bubbling up any potential revert. - if (recipient.code.length > 0) { - try ISablierV2LockupRecipient(recipient).onStreamCanceled({ - streamId: streamId, - sender: sender, - senderAmount: senderAmount, - recipientAmount: recipientAmount - }) { } catch { } - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _createWithMilestones(LockupDynamic.CreateWithMilestones memory params) - internal - returns (uint256 streamId) - { - // Safe Interactions: query the protocol fee. This is safe because it's a known Sablier contract that does - // not call other unknown contracts. - UD60x18 protocolFee = comptroller.protocolFees(params.asset); - - // Checks: check the fees and calculate the fee amounts. + function _create(LockupDynamic.CreateWithTimestamps memory params) internal returns (uint256 streamId) { + // Check: verify the broker fee and calculate the amounts. Lockup.CreateAmounts memory createAmounts = - Helpers.checkAndCalculateFees(params.totalAmount, protocolFee, params.broker.fee, MAX_FEE); + Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE); - // Checks: validate the user-provided parameters. - Helpers.checkCreateWithMilestones(createAmounts.deposit, params.segments, MAX_SEGMENT_COUNT, params.startTime); + // Check: validate the user-provided parameters. + Helpers.checkCreateLockupDynamic(createAmounts.deposit, params.segments, MAX_SEGMENT_COUNT, params.startTime); - // Load the stream id in a variable. + // Load the stream ID in a variable. streamId = nextStreamId; - // Effects: create the stream. - LockupDynamic.Stream storage stream = _streams[streamId]; + // Effect: create the stream. + Lockup.Stream storage stream = _streams[streamId]; stream.amounts.deposited = createAmounts.deposit; stream.asset = params.asset; stream.isCancelable = params.cancelable; - stream.isTransferable = params.transferable; stream.isStream = true; + stream.isTransferable = params.transferable; stream.sender = params.sender; + stream.startTime = params.startTime; unchecked { // The segment count cannot be zero at this point. uint256 segmentCount = params.segments.length; - stream.endTime = params.segments[segmentCount - 1].milestone; - stream.startTime = params.startTime; + stream.endTime = params.segments[segmentCount - 1].timestamp; - // Effects: store the segments. Since Solidity lacks a syntax for copying arrays directly from + // Effect: store the segments. Since Solidity lacks a syntax for copying arrays of structs directly from // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. for (uint256 i = 0; i < segmentCount; ++i) { - stream.segments.push(params.segments[i]); + _segments[streamId].push(params.segments[i]); } - // Effects: bump the next stream id and record the protocol fee. + // Effect: bump the next stream ID. // Using unchecked arithmetic because these calculations cannot realistically overflow, ever. nextStreamId = streamId + 1; - protocolRevenues[params.asset] = protocolRevenues[params.asset] + createAmounts.protocolFee; } - // Effects: mint the NFT to the recipient. + // Effect: mint the NFT to the recipient. _mint({ to: params.recipient, tokenId: streamId }); - // Interactions: transfer the deposit and the protocol fee. - // Using unchecked arithmetic because the deposit and the protocol fee are bounded by the total amount. - unchecked { - params.asset.safeTransferFrom({ - from: msg.sender, - to: address(this), - value: createAmounts.deposit + createAmounts.protocolFee - }); - } + // Interaction: transfer the deposit amount. + params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit }); - // Interactions: pay the broker fee, if not zero. + // Interaction: pay the broker fee, if not zero. if (createAmounts.brokerFee > 0) { params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee }); } @@ -605,47 +372,8 @@ contract SablierV2LockupDynamic is cancelable: params.cancelable, transferable: params.transferable, segments: params.segments, - range: LockupDynamic.Range({ start: stream.startTime, end: stream.endTime }), + timestamps: LockupDynamic.Timestamps({ start: stream.startTime, end: stream.endTime }), broker: params.broker.account }); } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal override { - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Effects: renounce the stream by making it not cancelable. - _streams[streamId].isCancelable = false; - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal override { - // Effects: update the withdrawn amount. - _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the - // withdrawn amount, the stream will still be marked as depleted. - if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { - // Effects: mark the stream as depleted. - _streams[streamId].isDepleted = true; - - // Effects: make the stream not cancelable anymore, because a depleted stream cannot be canceled. - _streams[streamId].isCancelable = false; - } - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: perform the ERC-20 transfer. - asset.safeTransfer({ to: to, value: amount }); - - // Log the withdrawal. - emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); - } } diff --git a/src/SablierV2LockupLinear.sol b/src/SablierV2LockupLinear.sol index 1f0c4da75..d8380cc93 100644 --- a/src/SablierV2LockupLinear.sol +++ b/src/SablierV2LockupLinear.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -7,12 +7,9 @@ import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; -import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol"; +import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; import { ISablierV2LockupLinear } from "./interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol"; -import { ISablierV2LockupRecipient } from "./interfaces/hooks/ISablierV2LockupRecipient.sol"; -import { Errors } from "./libraries/Errors.sol"; import { Helpers } from "./libraries/Helpers.sol"; import { Lockup, LockupLinear } from "./types/DataTypes.sol"; @@ -43,11 +40,11 @@ contract SablierV2LockupLinear is using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// - PRIVATE STORAGE + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ - /// @dev Sablier V2 Lockup Linear streams mapped by unsigned integers. - mapping(uint256 id => LockupLinear.Stream stream) private _streams; + /// @dev Cliff times mapped by stream IDs. This complements the `_streams` mapping in {SablierV2Lockup}. + mapping(uint256 id => uint40 cliff) internal _cliffs; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -55,15 +52,13 @@ contract SablierV2LockupLinear is /// @dev Emits a {TransferAdmin} event. /// @param initialAdmin The address of the initial contract admin. - /// @param initialComptroller The address of the initial comptroller. /// @param initialNFTDescriptor The address of the initial NFT descriptor. constructor( address initialAdmin, - ISablierV2Comptroller initialComptroller, ISablierV2NFTDescriptor initialNFTDescriptor ) ERC721("Sablier V2 Lockup Linear NFT", "SAB-V2-LOCKUP-LIN") - SablierV2Lockup(initialAdmin, initialComptroller, initialNFTDescriptor) + SablierV2Lockup(initialAdmin, initialNFTDescriptor) { nextStreamId = 1; } @@ -72,66 +67,9 @@ contract SablierV2LockupLinear is USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Lockup - function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { - asset = _streams[streamId].asset; - } - /// @inheritdoc ISablierV2LockupLinear function getCliffTime(uint256 streamId) external view override notNull(streamId) returns (uint40 cliffTime) { - cliffTime = _streams[streamId].cliffTime; - } - - /// @inheritdoc ISablierV2Lockup - function getDepositedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 depositedAmount) - { - depositedAmount = _streams[streamId].amounts.deposited; - } - - /// @inheritdoc ISablierV2Lockup - function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { - endTime = _streams[streamId].endTime; - } - - /// @inheritdoc ISablierV2LockupLinear - function getRange(uint256 streamId) - external - view - override - notNull(streamId) - returns (LockupLinear.Range memory range) - { - range = LockupLinear.Range({ - start: _streams[streamId].startTime, - cliff: _streams[streamId].cliffTime, - end: _streams[streamId].endTime - }); - } - - /// @inheritdoc ISablierV2Lockup - function getRefundedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundedAmount) - { - refundedAmount = _streams[streamId].amounts.refunded; - } - - /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) external view override notNull(streamId) returns (address sender) { - sender = _streams[streamId].sender; - } - - /// @inheritdoc ISablierV2Lockup - function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { - startTime = _streams[streamId].startTime; + cliffTime = _cliffs[streamId]; } /// @inheritdoc ISablierV2LockupLinear @@ -140,103 +78,45 @@ contract SablierV2LockupLinear is view override notNull(streamId) - returns (LockupLinear.Stream memory stream) + returns (LockupLinear.StreamLL memory stream) { - stream = _streams[streamId]; + // Retrieve the Lockup stream from storage. + Lockup.Stream memory lockupStream = _streams[streamId]; // Settled streams cannot be canceled. if (_statusOf(streamId) == Lockup.Status.SETTLED) { - stream.isCancelable = false; + lockupStream.isCancelable = false; } - } - /// @inheritdoc ISablierV2Lockup - function getWithdrawnAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawnAmount) - { - withdrawnAmount = _streams[streamId].amounts.withdrawn; - } - - /// @inheritdoc ISablierV2Lockup - function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - if (_statusOf(streamId) != Lockup.Status.SETTLED) { - result = _streams[streamId].isCancelable; - } - } - - /// @inheritdoc SablierV2Lockup - function isTransferable(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isTransferable; - } - - /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isDepleted; - } - - /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view override(ISablierV2Lockup, SablierV2Lockup) returns (bool result) { - result = _streams[streamId].isStream; + stream = LockupLinear.StreamLL({ + amounts: lockupStream.amounts, + asset: lockupStream.asset, + cliffTime: _cliffs[streamId], + endTime: lockupStream.endTime, + isCancelable: lockupStream.isCancelable, + isTransferable: lockupStream.isTransferable, + isDepleted: lockupStream.isDepleted, + isStream: lockupStream.isStream, + recipient: _ownerOf(streamId), + sender: lockupStream.sender, + startTime: lockupStream.startTime, + wasCanceled: lockupStream.wasCanceled + }); } - /// @inheritdoc ISablierV2Lockup - function refundableAmountOf(uint256 streamId) + /// @inheritdoc ISablierV2LockupLinear + function getTimestamps(uint256 streamId) external view override notNull(streamId) - returns (uint128 refundableAmount) - { - // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that - // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that - // canceled streams are not cancelable anymore. - if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { - refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); - } - // Otherwise, the result is implicitly zero. - } - - /// @inheritdoc ISablierV2Lockup - function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { - status = _statusOf(streamId); - } - - /// @inheritdoc ISablierV2LockupLinear - function streamedAmountOf(uint256 streamId) - public - view - override(ISablierV2Lockup, ISablierV2LockupLinear) - notNull(streamId) - returns (uint128 streamedAmount) + returns (LockupLinear.Timestamps memory timestamps) { - streamedAmount = _streamedAmountOf(streamId); - } - - /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].wasCanceled; + timestamps = LockupLinear.Timestamps({ + start: _streams[streamId].startTime, + cliff: _cliffs[streamId], + end: _streams[streamId].endTime + }); } /*////////////////////////////////////////////////////////////////////////// @@ -251,58 +131,74 @@ contract SablierV2LockupLinear is returns (uint256 streamId) { // Set the current block timestamp as the stream's start time. - LockupLinear.Range memory range; - range.start = uint40(block.timestamp); + LockupLinear.Timestamps memory timestamps; + timestamps.start = uint40(block.timestamp); - // Calculate the cliff time and the end time. It is safe to use unchecked arithmetic because - // {_createWithRange} will nonetheless check that the end time is greater than the cliff time, - // and also that the cliff time is greater than or equal to the start time. + // Calculate the cliff time and the end time. It is safe to use unchecked arithmetic because {_create} will + // nonetheless check that the end time is greater than the cliff time, and also that the cliff time, if set, + // is greater than or equal to the start time. unchecked { - range.cliff = range.start + params.durations.cliff; - range.end = range.start + params.durations.total; + if (params.durations.cliff > 0) { + timestamps.cliff = timestamps.start + params.durations.cliff; + } + timestamps.end = timestamps.start + params.durations.total; } + // Checks, Effects and Interactions: create the stream. - streamId = _createWithRange( - LockupLinear.CreateWithRange({ + streamId = _create( + LockupLinear.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, asset: params.asset, - broker: params.broker, cancelable: params.cancelable, transferable: params.transferable, - range: range, - recipient: params.recipient, - sender: params.sender, - totalAmount: params.totalAmount + timestamps: timestamps, + broker: params.broker }) ); } /// @inheritdoc ISablierV2LockupLinear - function createWithRange(LockupLinear.CreateWithRange calldata params) + function createWithTimestamps(LockupLinear.CreateWithTimestamps calldata params) external override noDelegateCall returns (uint256 streamId) { // Checks, Effects and Interactions: create the stream. - streamId = _createWithRange(params); + streamId = _create(params); } /*////////////////////////////////////////////////////////////////////////// INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { - // If the cliff time is in the future, return zero. - uint256 cliffTime = uint256(_streams[streamId].cliffTime); - uint256 currentTime = block.timestamp; - if (cliffTime > currentTime) { + /// @inheritdoc SablierV2Lockup + /// @dev The distribution function is: + /// + /// $$ + /// f(x) = x * d + c + /// $$ + /// + /// Where: + /// + /// - $x$ is the elapsed time divided by the stream's total duration. + /// - $d$ is the deposited amount. + /// - $c$ is the cliff amount. + function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { + uint256 cliffTime = uint256(_cliffs[streamId]); + uint256 startTime = uint256(_streams[streamId].startTime); + uint256 blockTimestamp = block.timestamp; + + // If the cliff time or the start time is in the future, return zero. + if (cliffTime > blockTimestamp || startTime >= blockTimestamp) { return 0; } // If the end time is not in the future, return the deposited amount. uint256 endTime = uint256(_streams[streamId].endTime); - if (currentTime >= endTime) { + if (blockTimestamp >= endTime) { return _streams[streamId].amounts.deposited; } @@ -310,12 +206,11 @@ contract SablierV2LockupLinear is // because there is no mix of amounts with different decimals. unchecked { // Calculate how much time has passed since the stream started, and the stream's total duration. - uint256 startTime = uint256(_streams[streamId].startTime); - UD60x18 elapsedTime = ud(currentTime - startTime); - UD60x18 totalTime = ud(endTime - startTime); + UD60x18 elapsedTime = ud(blockTimestamp - startTime); + UD60x18 totalDuration = ud(endTime - startTime); // Divide the elapsed time by the stream's total duration. - UD60x18 elapsedTimePercentage = elapsedTime.div(totalTime); + UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration); // Cast the deposited amount to UD60x18. UD60x18 depositedAmount = ud(_streams[streamId].amounts.deposited); @@ -324,7 +219,7 @@ contract SablierV2LockupLinear is UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmount); // Although the streamed amount should never exceed the deposited amount, this condition is checked - // without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn + // without asserting to avoid locking assets in case of a bug. If this situation occurs, the withdrawn // amount is considered to be the streamed amount, and the stream is effectively frozen. if (streamedAmount.gt(depositedAmount)) { return _streams[streamId].amounts.withdrawn; @@ -335,168 +230,54 @@ contract SablierV2LockupLinear is } } - /// @inheritdoc SablierV2Lockup - function _isCallerStreamSender(uint256 streamId) internal view override returns (bool) { - return msg.sender == _streams[streamId].sender; - } - - /// @inheritdoc SablierV2Lockup - function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) { - if (_streams[streamId].isDepleted) { - return Lockup.Status.DEPLETED; - } else if (_streams[streamId].wasCanceled) { - return Lockup.Status.CANCELED; - } - - if (block.timestamp < _streams[streamId].startTime) { - return Lockup.Status.PENDING; - } - - if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { - return Lockup.Status.STREAMING; - } else { - return Lockup.Status.SETTLED; - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - if (_streams[streamId].isDepleted) { - return amounts.withdrawn; - } else if (_streams[streamId].wasCanceled) { - return amounts.deposited - amounts.refunded; - } - - return _calculateStreamedAmount(streamId); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) { - return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; - } - /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 streamId) internal override { - // Calculate the streamed amount. - uint128 streamedAmount = _calculateStreamedAmount(streamId); - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Checks: the stream is not settled. - if (streamedAmount >= amounts.deposited) { - revert Errors.SablierV2Lockup_StreamSettled(streamId); - } - - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Calculate the sender's and the recipient's amount. - uint128 senderAmount = amounts.deposited - streamedAmount; - uint128 recipientAmount = streamedAmount - amounts.withdrawn; - - // Effects: mark the stream as canceled. - _streams[streamId].wasCanceled = true; - - // Effects: make the stream not cancelable anymore, because a stream can only be canceled once. - _streams[streamId].isCancelable = false; - - // Effects: If there are no assets left for the recipient to withdraw, mark the stream as depleted. - if (recipientAmount == 0) { - _streams[streamId].isDepleted = true; - } - - // Effects: set the refunded amount. - _streams[streamId].amounts.refunded = senderAmount; - - // Retrieve the sender and the recipient from storage. - address sender = _streams[streamId].sender; - address recipient = _ownerOf(streamId); - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: refund the sender. - asset.safeTransfer({ to: sender, value: senderAmount }); - - // Log the cancellation. - emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); - - // Emits an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interactions: if the recipient is a contract, try to invoke the cancel hook on the recipient without - // reverting if the hook is not implemented, and without bubbling up any potential revert. - if (recipient.code.length > 0) { - try ISablierV2LockupRecipient(recipient).onStreamCanceled({ - streamId: streamId, - sender: sender, - senderAmount: senderAmount, - recipientAmount: recipientAmount - }) { } catch { } - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _createWithRange(LockupLinear.CreateWithRange memory params) internal returns (uint256 streamId) { - // Safe Interactions: query the protocol fee. This is safe because it's a known Sablier contract that does - // not call other unknown contracts. - UD60x18 protocolFee = comptroller.protocolFees(params.asset); - - // Checks: check the fees and calculate the fee amounts. + function _create(LockupLinear.CreateWithTimestamps memory params) internal returns (uint256 streamId) { + // Check: verify the broker fee and calculate the amounts. Lockup.CreateAmounts memory createAmounts = - Helpers.checkAndCalculateFees(params.totalAmount, protocolFee, params.broker.fee, MAX_FEE); + Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE); - // Checks: validate the user-provided parameters. - Helpers.checkCreateWithRange(createAmounts.deposit, params.range); + // Check: validate the user-provided parameters. + Helpers.checkCreateLockupLinear(createAmounts.deposit, params.timestamps); - // Load the stream id. + // Load the stream ID. streamId = nextStreamId; - // Effects: create the stream. - _streams[streamId] = LockupLinear.Stream({ + // Effect: create the stream. + _streams[streamId] = Lockup.Stream({ amounts: Lockup.Amounts({ deposited: createAmounts.deposit, refunded: 0, withdrawn: 0 }), asset: params.asset, - cliffTime: params.range.cliff, - endTime: params.range.end, + endTime: params.timestamps.end, isCancelable: params.cancelable, - isTransferable: params.transferable, isDepleted: false, isStream: true, + isTransferable: params.transferable, sender: params.sender, - startTime: params.range.start, + startTime: params.timestamps.start, wasCanceled: false }); - // Effects: bump the next stream id and record the protocol fee. + // Effect: set the cliff time if it is greater than zero. + if (params.timestamps.cliff > 0) { + _cliffs[streamId] = params.timestamps.cliff; + } + + // Effect: bump the next stream ID. // Using unchecked arithmetic because these calculations cannot realistically overflow, ever. unchecked { nextStreamId = streamId + 1; - protocolRevenues[params.asset] = protocolRevenues[params.asset] + createAmounts.protocolFee; } - // Effects: mint the NFT to the recipient. + // Effect: mint the NFT to the recipient. _mint({ to: params.recipient, tokenId: streamId }); - // Interactions: transfer the deposit and the protocol fee. - // Using unchecked arithmetic because the deposit and the protocol fee are bounded by the total amount. - unchecked { - params.asset.safeTransferFrom({ - from: msg.sender, - to: address(this), - value: createAmounts.deposit + createAmounts.protocolFee - }); - } + // Interaction: transfer the deposit amount. + params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit }); - // Interactions: pay the broker fee, if not zero. + // Interaction: pay the broker fee, if not zero. if (createAmounts.brokerFee > 0) { params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee }); } @@ -511,47 +292,8 @@ contract SablierV2LockupLinear is asset: params.asset, cancelable: params.cancelable, transferable: params.transferable, - range: params.range, + timestamps: params.timestamps, broker: params.broker.account }); } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal override { - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Effects: renounce the stream by making it not cancelable. - _streams[streamId].isCancelable = false; - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal override { - // Effects: update the withdrawn amount. - _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the - // withdrawn amount, the stream will still be marked as depleted. - if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { - // Effects: mark the stream as depleted. - _streams[streamId].isDepleted = true; - - // Effects: make the stream not cancelable anymore, because a depleted stream cannot be canceled. - _streams[streamId].isCancelable = false; - } - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: perform the ERC-20 transfer. - asset.safeTransfer({ to: to, value: amount }); - - // Log the withdrawal. - emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); - } } diff --git a/src/SablierV2LockupTranched.sol b/src/SablierV2LockupTranched.sol new file mode 100644 index 000000000..aaf0105a1 --- /dev/null +++ b/src/SablierV2LockupTranched.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; +import { ISablierV2LockupTranched } from "./interfaces/ISablierV2LockupTranched.sol"; +import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol"; +import { Helpers } from "./libraries/Helpers.sol"; +import { Lockup, LockupTranched } from "./types/DataTypes.sol"; + +/* + +███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██████╗ +██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██║ ██║╚════██╗ +███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██║ ██║ █████╔╝ +╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚██╗ ██╔╝██╔═══╝ +███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ╚████╔╝ ███████╗ +╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝ + +██╗ ██████╗ ██████╗██╗ ██╗██╗ ██╗██████╗ ████████╗██████╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗███████╗██████╗ +██║ ██╔═══██╗██╔════╝██║ ██╔╝██║ ██║██╔══██╗ ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ ██║██╔════╝██╔══██╗ +██║ ██║ ██║██║ █████╔╝ ██║ ██║██████╔╝ ██║ ██████╔╝███████║██╔██╗ ██║██║ ███████║█████╗ ██║ ██║ +██║ ██║ ██║██║ ██╔═██╗ ██║ ██║██╔═══╝ ██║ ██╔══██╗██╔══██║██║╚██╗██║██║ ██╔══██║██╔══╝ ██║ ██║ +███████╗╚██████╔╝╚██████╗██║ ██╗╚██████╔╝██║ ██║ ██║ ██║██║ ██║██║ ╚████║╚██████╗██║ ██║███████╗██████╔╝ +╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ + +*/ + +/// @title SablierV2LockupTranched +/// @notice See the documentation in {ISablierV2LockupTranched}. +contract SablierV2LockupTranched is + ISablierV2LockupTranched, // 5 inherited components + SablierV2Lockup // 14 inherited components +{ + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2LockupTranched + uint256 public immutable override MAX_TRANCHE_COUNT; + + /// @dev Stream tranches mapped by stream IDs. This complements the `_streams` mapping in {SablierV2Lockup}. + mapping(uint256 id => LockupTranched.Tranche[] tranches) internal _tranches; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Emits a {TransferAdmin} event. + /// @param initialAdmin The address of the initial contract admin. + /// @param initialNFTDescriptor The address of the NFT descriptor contract. + /// @param maxTrancheCount The maximum number of tranches allowed in a stream. + constructor( + address initialAdmin, + ISablierV2NFTDescriptor initialNFTDescriptor, + uint256 maxTrancheCount + ) + ERC721("Sablier V2 Lockup Tranched NFT", "SAB-V2-LOCKUP-TRA") + SablierV2Lockup(initialAdmin, initialNFTDescriptor) + { + MAX_TRANCHE_COUNT = maxTrancheCount; + nextStreamId = 1; + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2LockupTranched + function getStream(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupTranched.StreamLT memory stream) + { + // Retrieve the Lockup stream from storage. + Lockup.Stream memory lockupStream = _streams[streamId]; + + // Settled streams cannot be canceled. + if (_statusOf(streamId) == Lockup.Status.SETTLED) { + lockupStream.isCancelable = false; + } + + stream = LockupTranched.StreamLT({ + amounts: lockupStream.amounts, + asset: lockupStream.asset, + endTime: lockupStream.endTime, + isCancelable: lockupStream.isCancelable, + isDepleted: lockupStream.isDepleted, + isStream: lockupStream.isStream, + isTransferable: lockupStream.isTransferable, + recipient: _ownerOf(streamId), + sender: lockupStream.sender, + startTime: lockupStream.startTime, + tranches: _tranches[streamId], + wasCanceled: lockupStream.wasCanceled + }); + } + + /// @inheritdoc ISablierV2LockupTranched + function getTimestamps(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupTranched.Timestamps memory timestamps) + { + timestamps = LockupTranched.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); + } + + /// @inheritdoc ISablierV2LockupTranched + function getTranches(uint256 streamId) + external + view + override + notNull(streamId) + returns (LockupTranched.Tranche[] memory tranches) + { + tranches = _tranches[streamId]; + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ISablierV2LockupTranched + function createWithDurations(LockupTranched.CreateWithDurations calldata params) + external + override + noDelegateCall + returns (uint256 streamId) + { + // Generate the canonical tranches. + LockupTranched.Tranche[] memory tranches = Helpers.calculateTrancheTimestamps(params.tranches); + + // Checks, Effects and Interactions: create the stream. + streamId = _create( + LockupTranched.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, + asset: params.asset, + cancelable: params.cancelable, + transferable: params.transferable, + startTime: uint40(block.timestamp), + tranches: tranches, + broker: params.broker + }) + ); + } + + /// @inheritdoc ISablierV2LockupTranched + function createWithTimestamps(LockupTranched.CreateWithTimestamps calldata params) + external + override + noDelegateCall + returns (uint256 streamId) + { + // Checks, Effects and Interactions: create the stream. + streamId = _create(params); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc SablierV2Lockup + /// @dev The distribution function is: + /// + /// $$ + /// f(x) = \Sigma(eta) + /// $$ + /// + /// Where: + /// + /// - $\Sigma(eta)$ is the sum of all vested tranches' amounts. + function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { + uint40 blockTimestamp = uint40(block.timestamp); + LockupTranched.Tranche[] memory tranches = _tranches[streamId]; + + // 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 (_streams[streamId].endTime <= blockTimestamp) { + return _streams[streamId].amounts.deposited; + } + + // Sum the amounts in all tranches that have already been vested. + // Using unchecked arithmetic is safe because the sum of the tranche amounts is equal to the total amount + // at this point. + uint128 streamedAmount = tranches[0].amount; + for (uint256 i = 1; i < tranches.length; ++i) { + // The loop breaks at the first tranche with a timestamp in the future. A tranche is considered vested if + // its timestamp is less than or equal to the block timestamp. + if (tranches[i].timestamp > blockTimestamp) { + break; + } + unchecked { + streamedAmount += tranches[i].amount; + } + } + + return streamedAmount; + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev See the documentation for the user-facing functions that call this internal function. + function _create(LockupTranched.CreateWithTimestamps memory params) internal returns (uint256 streamId) { + // Check: verify the broker fee and calculate the amounts. + Lockup.CreateAmounts memory createAmounts = + Helpers.checkAndCalculateBrokerFee(params.totalAmount, params.broker.fee, MAX_BROKER_FEE); + + // Check: validate the user-provided parameters. + Helpers.checkCreateLockupTranched(createAmounts.deposit, params.tranches, MAX_TRANCHE_COUNT, params.startTime); + + // Load the stream ID in a variable. + streamId = nextStreamId; + + // Effect: create the stream. + Lockup.Stream storage stream = _streams[streamId]; + stream.amounts.deposited = createAmounts.deposit; + stream.asset = params.asset; + stream.isCancelable = params.cancelable; + stream.isStream = true; + stream.isTransferable = params.transferable; + stream.sender = params.sender; + stream.startTime = params.startTime; + + unchecked { + // The tranche count cannot be zero at this point. + uint256 trancheCount = params.tranches.length; + stream.endTime = params.tranches[trancheCount - 1].timestamp; + + // Effect: store the tranches. Since Solidity lacks a syntax for copying arrays of structs directly from + // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. + for (uint256 i = 0; i < trancheCount; ++i) { + _tranches[streamId].push(params.tranches[i]); + } + + // Effect: bump the next stream ID. + // Using unchecked arithmetic because these calculations cannot realistically overflow, ever. + nextStreamId = streamId + 1; + } + + // Effect: mint the NFT to the recipient. + _mint({ to: params.recipient, tokenId: streamId }); + + // Interaction: transfer the deposit amount. + params.asset.safeTransferFrom({ from: msg.sender, to: address(this), value: createAmounts.deposit }); + + // Interaction: pay the broker fee, if not zero. + if (createAmounts.brokerFee > 0) { + params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee }); + } + + // Log the newly created stream. + emit ISablierV2LockupTranched.CreateLockupTranchedStream({ + streamId: streamId, + funder: msg.sender, + sender: params.sender, + recipient: params.recipient, + amounts: createAmounts, + asset: params.asset, + cancelable: params.cancelable, + transferable: params.transferable, + tranches: params.tranches, + timestamps: LockupTranched.Timestamps({ start: stream.startTime, end: stream.endTime }), + broker: params.broker.account + }); + } +} diff --git a/src/SablierV2NFTDescriptor.sol b/src/SablierV2NFTDescriptor.sol index 741bb51ce..171dc4878 100644 --- a/src/SablierV2NFTDescriptor.sol +++ b/src/SablierV2NFTDescriptor.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable max-line-length,quotes -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; @@ -15,6 +15,24 @@ import { Errors } from "./libraries/Errors.sol"; import { NFTSVG } from "./libraries/NFTSVG.sol"; import { SVGElements } from "./libraries/SVGElements.sol"; +/* + +███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██████╗ +██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██║ ██║╚════██╗ +███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██║ ██║ █████╔╝ +╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚██╗ ██╔╝██╔═══╝ +███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ╚████╔╝ ███████╗ +╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝ + +███╗ ██╗███████╗████████╗ ██████╗ ███████╗███████╗ ██████╗██████╗ ██╗██████╗ ████████╗ ██████╗ ██████╗ +████╗ ██║██╔════╝╚══██╔══╝ ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ +██╔██╗ ██║█████╗ ██║ ██║ ██║█████╗ ███████╗██║ ██████╔╝██║██████╔╝ ██║ ██║ ██║██████╔╝ +██║╚██╗██║██╔══╝ ██║ ██║ ██║██╔══╝ ╚════██║██║ ██╔══██╗██║██╔═══╝ ██║ ██║ ██║██╔══██╗ +██║ ╚████║██║ ██║ ██████╔╝███████╗███████║╚██████╗██║ ██║██║██║ ██║ ╚██████╔╝██║ ██║ +╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ + +*/ + /// @title SablierV2NFTDescriptor /// @notice See the documentation in {ISablierV2NFTDescriptor}. contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { @@ -31,13 +49,16 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { address asset; string assetSymbol; uint128 depositedAmount; + bool isTransferable; string json; + bytes returnData; ISablierV2Lockup sablier; - string sablierAddress; + string sablierModel; + string sablierStringified; string status; string svg; uint256 streamedPercentage; - string streamingModel; + bool success; } /// @inheritdoc ISablierV2NFTDescriptor @@ -46,7 +67,8 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { // Load the contracts. vars.sablier = ISablierV2Lockup(address(sablier)); - vars.sablierAddress = address(sablier).toHexString(); + vars.sablierModel = mapSymbol(sablier); + vars.sablierStringified = address(sablier).toHexString(); vars.asset = address(vars.sablier.getAsset(streamId)); vars.assetSymbol = safeAssetSymbol(vars.asset); vars.depositedAmount = vars.sablier.getDepositedAmount(streamId); @@ -57,7 +79,6 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { streamedAmount: vars.sablier.streamedAmountOf(streamId), depositedAmount: vars.depositedAmount }); - vars.streamingModel = mapSymbol(sablier); // Generate the SVG. vars.svg = NFTSVG.generateSVG( @@ -70,14 +91,21 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { startTime: vars.sablier.getStartTime(streamId), endTime: vars.sablier.getEndTime(streamId) }), - sablierAddress: vars.sablierAddress, + sablierAddress: vars.sablierStringified, progress: stringifyPercentage(vars.streamedPercentage), progressNumerical: vars.streamedPercentage, status: vars.status, - streamingModel: vars.streamingModel + sablierModel: vars.sablierModel }) ); + // Performs a low-level call to handle older deployments that miss the `isTransferable` function. + (vars.success, vars.returnData) = + address(vars.sablier).staticcall(abi.encodeCall(ISablierV2Lockup.isTransferable, (streamId))); + + // When the call has failed, the stream NFT is assumed to be transferable. + vars.isTransferable = vars.success ? abi.decode(vars.returnData, (bool)) : true; + // Generate the JSON metadata. vars.json = string.concat( '{"attributes":', @@ -88,14 +116,15 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { }), ',"description":"', generateDescription({ - streamingModel: vars.streamingModel, + sablierModel: vars.sablierModel, assetSymbol: vars.assetSymbol, + sablierStringified: vars.sablierStringified, + assetAddress: vars.asset.toHexString(), streamId: streamId.toString(), - sablierAddress: vars.sablierAddress, - assetAddress: vars.asset.toHexString() + isTransferable: vars.isTransferable }), '","external_url":"https://sablier.com","name":"', - generateName({ streamingModel: vars.streamingModel, streamId: streamId.toString() }), + generateName({ sablierModel: vars.sablierModel, streamId: streamId.toString() }), '","image":"data:image/svg+xml;base64,', Base64.encode(bytes(vars.svg)), '"}' @@ -193,7 +222,7 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { /// @notice Generates a pseudo-random HSL color by hashing together the `chainid`, the `sablier` address, /// and the `streamId`. This will be used as the accent color for the SVG. function generateAccentColor(address sablier, uint256 streamId) internal view returns (string memory) { - // The chain id is part of the hash so that the generated color is different across chains. + // The chain ID is part of the hash so that the generated color is different across chains. uint256 chainId = block.chainid; // Hash the parameters to generate a pseudo-random bit field, which will be used as entropy. @@ -248,43 +277,73 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { /// @notice Generates a string with the NFT's JSON metadata description, which provides a high-level overview. function generateDescription( - string memory streamingModel, + string memory sablierModel, string memory assetSymbol, + string memory sablierStringified, + string memory assetAddress, string memory streamId, - string memory sablierAddress, - string memory assetAddress + bool isTransferable ) internal pure returns (string memory) { + // Depending on the transferability of the NFT, declare the relevant information. + string memory info = isTransferable + ? + unicode"⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient." + : unicode"❕INFO: This NFT is non-transferable. It cannot be sold or transferred to another account."; + return string.concat( "This NFT represents a payment stream in a Sablier V2 ", - streamingModel, + sablierModel, " contract. The owner of this NFT can withdraw the streamed assets, which are denominated in ", assetSymbol, ".\\n\\n- Stream ID: ", streamId, "\\n- ", - streamingModel, + sablierModel, " Address: ", - sablierAddress, + sablierStringified, "\\n- ", assetSymbol, " Address: ", assetAddress, "\\n\\n", - unicode"⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient." + info ); } /// @notice Generates a string with the NFT's JSON metadata name, which is unique for each stream. /// @dev The `streamId` is equivalent to the ERC-721 `tokenId`. - function generateName(string memory streamingModel, string memory streamId) internal pure returns (string memory) { - return string.concat("Sablier V2 ", streamingModel, " #", streamId); + function generateName(string memory sablierModel, string memory streamId) internal pure returns (string memory) { + return string.concat("Sablier V2 ", sablierModel, " #", streamId); } - /// @notice Maps ERC-721 symbols to human-readable streaming models. + /// @notice Checks whether the provided string contains only alphanumeric characters, spaces, and dashes. + /// @dev Note that this returns true for empty strings. + function isAllowedCharacter(string memory str) internal pure returns (bool) { + // Convert the string to bytes to iterate over its characters. + bytes memory b = bytes(str); + + uint256 length = b.length; + for (uint256 i = 0; i < length; ++i) { + bytes1 char = b[i]; + + // Check if it's a space, dash, or an alphanumeric character. + bool isSpace = char == 0x20; // space + bool isDash = char == 0x2D; // dash + bool isDigit = char >= 0x30 && char <= 0x39; // 0-9 + bool isUppercaseLetter = char >= 0x41 && char <= 0x5A; // A-Z + bool isLowercaseLetter = char >= 0x61 && char <= 0x7A; // a-z + if (!(isSpace || isDash || isDigit || isUppercaseLetter || isLowercaseLetter)) { + return false; + } + } + return true; + } + + /// @notice Maps ERC-721 symbols to human-readable model names. /// @dev Reverts if the symbol is unknown. function mapSymbol(IERC721Metadata sablier) internal view returns (string memory) { string memory symbol = sablier.symbol(); @@ -292,6 +351,8 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { return "Lockup Linear"; } else if (symbol.equal("SAB-V2-LOCKUP-DYN")) { return "Lockup Dynamic"; + } else if (symbol.equal("SAB-V2-LOCKUP-TRA")) { + return "Lockup Tranched"; } else { revert Errors.SablierV2NFTDescriptor_UnknownNFT(sablier, symbol); } @@ -321,11 +382,14 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor { string memory symbol = abi.decode(returnData, (string)); - // The length check is a precautionary measure to help mitigate potential security threats from malicious assets - // injecting scripts in the symbol string. + // Check if the symbol is too long or contains disallowed characters. This measure helps mitigate potential + // security threats from malicious assets injecting scripts in the symbol string. if (bytes(symbol).length > 30) { return "Long Symbol"; } else { + if (!isAllowedCharacter(symbol)) { + return "Unsupported Symbol"; + } return symbol; } } diff --git a/src/abstracts/Adminable.sol b/src/abstracts/Adminable.sol index 6cbd952c0..add9799a0 100644 --- a/src/abstracts/Adminable.sol +++ b/src/abstracts/Adminable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IAdminable } from "../interfaces/IAdminable.sol"; import { Errors } from "../libraries/Errors.sol"; @@ -8,7 +8,7 @@ import { Errors } from "../libraries/Errors.sol"; /// @notice See the documentation in {IAdminable}. abstract contract Adminable is IAdminable { /*////////////////////////////////////////////////////////////////////////// - STORAGE + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IAdminable @@ -32,7 +32,7 @@ abstract contract Adminable is IAdminable { /// @inheritdoc IAdminable function transferAdmin(address newAdmin) public virtual override onlyAdmin { - // Effects: update the admin. + // Effect: update the admin. admin = newAdmin; // Log the transfer of the admin. diff --git a/src/abstracts/NoDelegateCall.sol b/src/abstracts/NoDelegateCall.sol index c01be8734..615297067 100644 --- a/src/abstracts/NoDelegateCall.sol +++ b/src/abstracts/NoDelegateCall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { Errors } from "../libraries/Errors.sol"; diff --git a/src/abstracts/SablierV2Base.sol b/src/abstracts/SablierV2Base.sol deleted file mode 100644 index 356777840..000000000 --- a/src/abstracts/SablierV2Base.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { IAdminable } from "../interfaces/IAdminable.sol"; -import { ISablierV2Base } from "../interfaces/ISablierV2Base.sol"; -import { ISablierV2Comptroller } from "../interfaces/ISablierV2Comptroller.sol"; -import { Errors } from "../libraries/Errors.sol"; -import { Adminable } from "./Adminable.sol"; -import { NoDelegateCall } from "./NoDelegateCall.sol"; - -/// @title SablierV2Base -/// @notice See the documentation in {ISablierV2Base}. -abstract contract SablierV2Base is - NoDelegateCall, // 0 inherited components - ISablierV2Base, // 1 inherited component - Adminable // 1 inherited component -{ - using SafeERC20 for IERC20; - - /*////////////////////////////////////////////////////////////////////////// - PUBLIC CONSTANTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2Base - UD60x18 public constant override MAX_FEE = UD60x18.wrap(0.1e18); - - /*////////////////////////////////////////////////////////////////////////// - PUBLIC STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2Base - ISablierV2Comptroller public override comptroller; - - /// @inheritdoc ISablierV2Base - mapping(IERC20 asset => uint128 revenues) public override protocolRevenues; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Emits a {TransferAdmin} event. - /// @param initialAdmin The address of the initial contract admin. - /// @param initialComptroller The address of the initial comptroller. - constructor(address initialAdmin, ISablierV2Comptroller initialComptroller) { - admin = initialAdmin; - comptroller = initialComptroller; - emit IAdminable.TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin }); - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ISablierV2Base - function claimProtocolRevenues(IERC20 asset) external override onlyAdmin { - // Checks: the protocol revenues are not zero. - uint128 revenues = protocolRevenues[asset]; - if (revenues == 0) { - revert Errors.SablierV2Base_NoProtocolRevenues(asset); - } - - // Effects: set the protocol revenues to zero. - protocolRevenues[asset] = 0; - - // Interactions: perform the ERC-20 transfer to pay the protocol revenues. - asset.safeTransfer({ to: msg.sender, value: revenues }); - - // Log the claim of the protocol revenues. - emit ISablierV2Base.ClaimProtocolRevenues({ admin: msg.sender, asset: asset, protocolRevenues: revenues }); - } - - /// @inheritdoc ISablierV2Base - function setComptroller(ISablierV2Comptroller newComptroller) external override onlyAdmin { - // Effects: set the new comptroller. - ISablierV2Comptroller oldComptroller = comptroller; - comptroller = newComptroller; - - // Log the change of the comptroller. - emit ISablierV2Base.SetComptroller({ - admin: msg.sender, - oldComptroller: oldComptroller, - newComptroller: newComptroller - }); - } -} diff --git a/src/abstracts/SablierV2FlashLoan.sol b/src/abstracts/SablierV2FlashLoan.sol deleted file mode 100644 index 2b2002f74..000000000 --- a/src/abstracts/SablierV2FlashLoan.sol +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; - -import { IERC3156FlashBorrower } from "../interfaces/erc3156/IERC3156FlashBorrower.sol"; -import { IERC3156FlashLender } from "../interfaces/erc3156/IERC3156FlashLender.sol"; -import { Errors } from "../libraries/Errors.sol"; -import { SablierV2Base } from "./SablierV2Base.sol"; - -/// @title SablierV2FlashLoan -/// @notice This contract implements the ERC-3156 standard to enable flash loans. -/// @dev See https://eips.ethereum.org/EIPS/eip-3156. -abstract contract SablierV2FlashLoan is - IERC3156FlashLender, // 0 inherited components - SablierV2Base // 4 inherited components -{ - using SafeERC20 for IERC20; - - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a flash loan is executed. - /// @param initiator The address of the flash loan initiator. - /// @param receiver The address of the flash borrower. - /// @param asset The address of the ERC-20 asset that has been flash loaned. - /// @param amount The amount of `asset` flash loaned. - /// @param feeAmount The fee amount of `asset` charged by the protocol. - /// @param data The data passed to the flash borrower. - event FlashLoan( - address indexed initiator, - IERC3156FlashBorrower indexed receiver, - IERC20 indexed asset, - uint256 amount, - uint256 feeAmount, - bytes data - ); - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL CONSTANTS - //////////////////////////////////////////////////////////////////////////*/ - - bytes32 internal constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The amount of fees to charge for a hypothetical flash loan amount. - /// - /// @dev You might notice a bit of a terminology clash here, since the ERC-3156 standard refers to the "flash fee" - /// as an amount, whereas the flash fee retrieved from the comptroller is a percentage. Throughout the code base, - /// the "amount" suffix is typically appended to variables that represent amounts, but, in this context, the name - /// must be kept unchanged to comply with the ERC. - /// - /// Requirements: - /// - The ERC-20 asset must be flash loanable. - /// - /// @param asset The ERC-20 asset to flash loan. - /// @param amount The amount of `asset` flash loaned. - /// @return fee The amount of `asset` to charge for the loan on top of the returned principal. - function flashFee(address asset, uint256 amount) public view override returns (uint256 fee) { - // Checks: the ERC-20 asset is flash loanable. - if (!comptroller.isFlashAsset(IERC20(asset))) { - revert Errors.SablierV2FlashLoan_AssetNotFlashLoanable(IERC20(asset)); - } - - // Calculate the flash fee. - fee = ud(amount).mul(comptroller.flashFee()).intoUint256(); - } - - /// @notice The amount of ERC-20 assets available for flash loan. - /// @dev If the ERC-20 asset is not flash loanable, this function returns zero. - /// @param asset The address of the ERC-20 asset to query. - /// @return amount The amount of `asset` that can be flash loaned. - function maxFlashLoan(address asset) external view override returns (uint256 amount) { - // The default value is zero, so it doesn't have to be explicitly set. - if (comptroller.isFlashAsset(IERC20(asset))) { - amount = IERC20(asset).balanceOf(address(this)); - } - } - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Allows smart contracts to access the entire liquidity of the Sablier V2 contract within one - /// transaction as long as the principal plus a flash fee is returned. - /// - /// @dev Emits a {FlashLoan} event. - /// - /// Requirements: - /// - Must not be delegate called. - /// - Refer to the requirements in {flashFee}. - /// - `amount` must be less than 2^128. - /// - `fee` must be less than 2^128. - /// - `amount` must not exceed the liquidity available for `asset`. - /// - `msg.sender` must allow this contract to spend at least `amount + fee` assets. - /// - `receiver` implementation of {IERC3156FlashBorrower.onFlashLoan} must return `CALLBACK_SUCCESS`. - /// - /// @param receiver The receiver of the flash loaned assets, and the receiver of the callback. - /// @param asset The address of the ERC-20 asset to use for flash borrowing. - /// @param amount The amount of `asset` to flash loan. - /// @param data Arbitrary data structure, intended to contain user-defined parameters. - /// @return success `true` on success. - function flashLoan( - IERC3156FlashBorrower receiver, - address asset, - uint256 amount, - bytes calldata data - ) - external - override - noDelegateCall - returns (bool success) - { - // Checks: the amount is less than 2^128. This prevents the below calculations from overflowing. - if (amount > type(uint128).max) { - revert Errors.SablierV2FlashLoan_AmountTooHigh(amount); - } - - // Calculate the flash fee. This also checks that the ERC-20 asset is flash loanable. - uint256 fee = flashFee(asset, amount); - - // Checks: the calculated fee is less than 2^128. This check can fail only when the comptroller flash fee - // is set to an abnormally high value. - if (fee > type(uint128).max) { - revert Errors.SablierV2FlashLoan_CalculatedFeeTooHigh(fee); - } - - // Interactions: perform the ERC-20 transfer to flash loan the assets to the borrower. - IERC20(asset).safeTransfer({ to: address(receiver), value: amount }); - - // Interactions: perform the borrower callback. - bytes32 response = - receiver.onFlashLoan({ initiator: msg.sender, asset: asset, amount: amount, fee: fee, data: data }); - - // Checks: the response matches the expected callback success hash. - if (response != CALLBACK_SUCCESS) { - revert Errors.SablierV2FlashLoan_FlashBorrowFail(); - } - - uint256 returnAmount; - - // Using unchecked arithmetic because the checks above prevent these calculations from overflowing. - unchecked { - // Effects: record the flash fee amount in the protocol revenues. The casting to uint128 is safe due - // to the check at the start of the function. - protocolRevenues[IERC20(asset)] = protocolRevenues[IERC20(asset)] + uint128(fee); - - // Calculate the amount that the borrower must return. - returnAmount = amount + fee; - } - - // Interactions: perform the ERC-20 transfer to get the principal back plus the fee. - IERC20(asset).safeTransferFrom({ from: address(receiver), to: address(this), value: returnAmount }); - - // Log the flash loan. - emit FlashLoan({ - initiator: msg.sender, - receiver: receiver, - asset: IERC20(asset), - amount: amount, - feeAmount: fee, - data: data - }); - - // Set the success flag. - success = true; - } -} diff --git a/src/abstracts/SablierV2Lockup.sol b/src/abstracts/SablierV2Lockup.sol index a3a323f3e..95166bd86 100644 --- a/src/abstracts/SablierV2Lockup.sol +++ b/src/abstracts/SablierV2Lockup.sol @@ -1,55 +1,61 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; -import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { ISablierV2Comptroller } from "../interfaces/ISablierV2Comptroller.sol"; +import { ISablierLockupRecipient } from "../interfaces/ISablierLockupRecipient.sol"; import { ISablierV2Lockup } from "../interfaces/ISablierV2Lockup.sol"; import { ISablierV2NFTDescriptor } from "../interfaces/ISablierV2NFTDescriptor.sol"; -import { ISablierV2LockupRecipient } from "../interfaces/hooks/ISablierV2LockupRecipient.sol"; import { Errors } from "../libraries/Errors.sol"; import { Lockup } from "../types/DataTypes.sol"; -import { SablierV2Base } from "./SablierV2Base.sol"; +import { Adminable } from "./Adminable.sol"; +import { NoDelegateCall } from "./NoDelegateCall.sol"; /// @title SablierV2Lockup /// @notice See the documentation in {ISablierV2Lockup}. abstract contract SablierV2Lockup is - IERC4906, // 2 inherited components - SablierV2Base, // 4 inherited components - ISablierV2Lockup, // 4 inherited components + NoDelegateCall, // 0 inherited components + Adminable, // 1 inherited components + ISablierV2Lockup, // 7 inherited components ERC721 // 6 inherited components { + using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////////////////// - USER-FACING STORAGE + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ISablierV2Lockup + UD60x18 public constant override MAX_BROKER_FEE = UD60x18.wrap(0.1e18); + /// @inheritdoc ISablierV2Lockup uint256 public override nextStreamId; - /*////////////////////////////////////////////////////////////////////////// - INTERNAL STORAGE - //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ISablierV2Lockup + ISablierV2NFTDescriptor public override nftDescriptor; + + /// @dev Mapping of contracts allowed to hook to Sablier when a stream is canceled or when assets are withdrawn. + mapping(address recipient => bool allowed) internal _allowedToHook; - /// @dev Contract that generates the non-fungible token URI. - ISablierV2NFTDescriptor internal _nftDescriptor; + /// @dev Sablier V2 Lockup streams mapped by unsigned integers. + mapping(uint256 id => Lockup.Stream stream) internal _streams; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ + /// @dev Emits a {TransferAdmin} event. /// @param initialAdmin The address of the initial contract admin. - /// @param initialComptroller The address of the initial comptroller. /// @param initialNFTDescriptor The address of the initial NFT descriptor. - constructor( - address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor initialNFTDescriptor - ) - SablierV2Base(initialAdmin, initialComptroller) - { - _nftDescriptor = initialNFTDescriptor; + constructor(address initialAdmin, ISablierV2NFTDescriptor initialNFTDescriptor) { + admin = initialAdmin; + nftDescriptor = initialNFTDescriptor; + emit TransferAdmin({ oldAdmin: address(0), newAdmin: initialAdmin }); } /*////////////////////////////////////////////////////////////////////////// @@ -58,29 +64,85 @@ abstract contract SablierV2Lockup is /// @dev Checks that `streamId` does not reference a null stream. modifier notNull(uint256 streamId) { - if (!isStream(streamId)) { + if (!_streams[streamId].isStream) { revert Errors.SablierV2Lockup_Null(streamId); } _; } - /// @dev Emits an ERC-4906 event to trigger an update of the NFT metadata. - modifier updateMetadata(uint256 streamId) { - _; - emit MetadataUpdate({ _tokenId: streamId }); - } - /*////////////////////////////////////////////////////////////////////////// USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ISablierV2Lockup + function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { + asset = _streams[streamId].asset; + } + + /// @inheritdoc ISablierV2Lockup + function getDepositedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 depositedAmount) + { + depositedAmount = _streams[streamId].amounts.deposited; + } + + /// @inheritdoc ISablierV2Lockup + function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { + endTime = _streams[streamId].endTime; + } + /// @inheritdoc ISablierV2Lockup function getRecipient(uint256 streamId) external view override returns (address recipient) { - // Checks: the stream NFT exists. - _requireMinted({ tokenId: streamId }); + // Check the stream NFT exists and return the owner, which is the stream's recipient. + recipient = _requireOwned({ tokenId: streamId }); + } + + /// @inheritdoc ISablierV2Lockup + function getRefundedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundedAmount) + { + refundedAmount = _streams[streamId].amounts.refunded; + } + + /// @inheritdoc ISablierV2Lockup + function getSender(uint256 streamId) external view override notNull(streamId) returns (address sender) { + sender = _streams[streamId].sender; + } + + /// @inheritdoc ISablierV2Lockup + function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { + startTime = _streams[streamId].startTime; + } + + /// @inheritdoc ISablierV2Lockup + function getWithdrawnAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 withdrawnAmount) + { + withdrawnAmount = _streams[streamId].amounts.withdrawn; + } - // The NFT owner is the stream's recipient. - recipient = _ownerOf(streamId); + /// @inheritdoc ISablierV2Lockup + function isAllowedToHook(address recipient) external view returns (bool result) { + result = _allowedToHook[recipient]; + } + + /// @inheritdoc ISablierV2Lockup + function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + if (_statusOf(streamId) != Lockup.Status.SETTLED) { + result = _streams[streamId].isCancelable; + } } /// @inheritdoc ISablierV2Lockup @@ -90,10 +152,19 @@ abstract contract SablierV2Lockup is } /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) public view virtual override returns (bool result); + function isDepleted(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isDepleted; + } /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view virtual override returns (bool result); + function isStream(uint256 streamId) external view override returns (bool result) { + result = _streams[streamId].isStream; + } + + /// @inheritdoc ISablierV2Lockup + function isTransferable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isTransferable; + } /// @inheritdoc ISablierV2Lockup function isWarm(uint256 streamId) external view override notNull(streamId) returns (bool result) { @@ -101,17 +172,58 @@ abstract contract SablierV2Lockup is result = status == Lockup.Status.PENDING || status == Lockup.Status.STREAMING; } + /// @inheritdoc ISablierV2Lockup + function refundableAmountOf(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundableAmount) + { + // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that + // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that + // canceled streams are not cancelable anymore. + if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { + refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); + } + // Otherwise, the result is implicitly zero. + } + + /// @inheritdoc ISablierV2Lockup + function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { + status = _statusOf(streamId); + } + + /// @inheritdoc ISablierV2Lockup + function streamedAmountOf(uint256 streamId) + public + view + override + notNull(streamId) + returns (uint128 streamedAmount) + { + streamedAmount = _streamedAmountOf(streamId); + } + + /// @inheritdoc ERC721 + function supportsInterface(bytes4 interfaceId) public view override(IERC165, ERC721) returns (bool) { + // 0x49064906 is the ERC-165 interface ID required by ERC-4906 + return interfaceId == 0x49064906 || super.supportsInterface(interfaceId); + } + /// @inheritdoc ERC721 function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) { - // Checks: the stream NFT exists. - _requireMinted({ tokenId: streamId }); + // Check: the stream NFT exists. + _requireOwned({ tokenId: streamId }); // Generate the URI describing the stream NFT. - uri = _nftDescriptor.tokenURI({ sablier: this, streamId: streamId }); + uri = nftDescriptor.tokenURI({ sablier: this, streamId: streamId }); } /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) public view virtual override returns (bool result); + function wasCanceled(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].wasCanceled; + } /// @inheritdoc ISablierV2Lockup function withdrawableAmountOf(uint256 streamId) @@ -124,41 +236,58 @@ abstract contract SablierV2Lockup is withdrawableAmount = _withdrawableAmountOf(streamId); } - /// @inheritdoc ISablierV2Lockup - function isTransferable(uint256 streamId) public view virtual returns (bool); - /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierV2Lockup - function burn(uint256 streamId) external override noDelegateCall { - // Checks: only depleted streams can be burned. This also checks that the stream is not null. - if (!isDepleted(streamId)) { + function allowToHook(address recipient) external override onlyAdmin { + // Check: non-zero code size. + if (recipient.code.length == 0) { + revert Errors.SablierV2Lockup_AllowToHookZeroCodeSize(recipient); + } + + // Check: recipients implements the ERC-165 interface ID required by {ISablierLockupRecipient}. + bytes4 interfaceId = type(ISablierLockupRecipient).interfaceId; + if (!ISablierLockupRecipient(recipient).supportsInterface(interfaceId)) { + revert Errors.SablierV2Lockup_AllowToHookUnsupportedInterface(recipient); + } + + // Effect: put the recipient on the allowlist. + _allowedToHook[recipient] = true; + + // Log the allowlist addition. + emit ISablierV2Lockup.AllowToHook({ admin: msg.sender, recipient: recipient }); + } + + /// @inheritdoc ISablierV2Lockup + function burn(uint256 streamId) external override noDelegateCall notNull(streamId) { + // Check: only depleted streams can be burned. + if (!_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamNotDepleted(streamId); } - // Checks: + // Check: // 1. NFT exists (see {IERC721.getApproved}). // 2. `msg.sender` is either the owner of the NFT or an approved third party. if (!_isCallerStreamRecipientOrApproved(streamId)) { revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender); } - // Effects: burn the NFT. + // Effect: burn the NFT. _burn({ tokenId: streamId }); } /// @inheritdoc ISablierV2Lockup - function cancel(uint256 streamId) public override noDelegateCall { - // Checks: the stream is neither depleted nor canceled. This also checks that the stream is not null. - if (isDepleted(streamId)) { + function cancel(uint256 streamId) public override noDelegateCall notNull(streamId) { + // Check: the stream is neither depleted nor canceled. + if (_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamDepleted(streamId); - } else if (wasCanceled(streamId)) { + } else if (_streams[streamId].wasCanceled) { revert Errors.SablierV2Lockup_StreamCanceled(streamId); } - // Checks: `msg.sender` is the stream's sender. + // Check: `msg.sender` is the stream's sender. if (!_isCallerStreamSender(streamId)) { revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender); } @@ -169,22 +298,17 @@ abstract contract SablierV2Lockup is /// @inheritdoc ISablierV2Lockup function cancelMultiple(uint256[] calldata streamIds) external override noDelegateCall { - // Iterate over the provided array of stream ids and cancel each stream. + // Iterate over the provided array of stream IDs and cancel each stream. uint256 count = streamIds.length; - for (uint256 i = 0; i < count;) { + for (uint256 i = 0; i < count; ++i) { // Effects and Interactions: cancel the stream. cancel(streamIds[i]); - - // Increment the loop iterator. - unchecked { - i += 1; - } } } /// @inheritdoc ISablierV2Lockup - function renounce(uint256 streamId) external override noDelegateCall notNull(streamId) updateMetadata(streamId) { - // Checks: the stream is not cold. + function renounce(uint256 streamId) external override noDelegateCall notNull(streamId) { + // Check: the stream is not cold. Lockup.Status status = _statusOf(streamId); if (status == Lockup.Status.DEPLETED) { revert Errors.SablierV2Lockup_StreamDepleted(streamId); @@ -194,7 +318,7 @@ abstract contract SablierV2Lockup is revert Errors.SablierV2Lockup_StreamSettled(streamId); } - // Checks: `msg.sender` is the stream's sender. + // Check: `msg.sender` is the stream's sender. if (!_isCallerStreamSender(streamId)) { revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender); } @@ -205,19 +329,15 @@ abstract contract SablierV2Lockup is // Log the renouncement. emit ISablierV2Lockup.RenounceLockupStream(streamId); - // Interactions: if the recipient is a contract, try to invoke the renounce hook on the recipient without - // reverting if the hook is not implemented, and also without bubbling up any potential revert. - address recipient = _ownerOf(streamId); - if (recipient.code.length > 0) { - try ISablierV2LockupRecipient(recipient).onStreamRenounced(streamId) { } catch { } - } + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit MetadataUpdate({ _tokenId: streamId }); } /// @inheritdoc ISablierV2Lockup function setNFTDescriptor(ISablierV2NFTDescriptor newNFTDescriptor) external override onlyAdmin { - // Effects: set the NFT descriptor. - ISablierV2NFTDescriptor oldNftDescriptor = _nftDescriptor; - _nftDescriptor = newNFTDescriptor; + // Effect: set the NFT descriptor. + ISablierV2NFTDescriptor oldNftDescriptor = nftDescriptor; + nftDescriptor = newNFTDescriptor; // Log the change of the NFT descriptor. emit ISablierV2Lockup.SetNFTDescriptor({ @@ -231,47 +351,32 @@ abstract contract SablierV2Lockup is } /// @inheritdoc ISablierV2Lockup - function withdraw( - uint256 streamId, - address to, - uint128 amount - ) - public - override - noDelegateCall - updateMetadata(streamId) - { - // Checks: the stream is not depleted. This also checks that the stream is not null. - if (isDepleted(streamId)) { + function withdraw(uint256 streamId, address to, uint128 amount) public override noDelegateCall notNull(streamId) { + // Check: the stream is not depleted. + if (_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamDepleted(streamId); } - bool isCallerStreamSender = _isCallerStreamSender(streamId); + // Check: the withdrawal address is not zero. + if (to == address(0)) { + revert Errors.SablierV2Lockup_WithdrawToZeroAddress(streamId); + } - // Checks: `msg.sender` is the stream's sender, the stream's recipient, or an approved third party. - if (!isCallerStreamSender && !_isCallerStreamRecipientOrApproved(streamId)) { - revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender); + // Check: the withdraw amount is not zero. + if (amount == 0) { + revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId); } // Retrieve the recipient from storage. address recipient = _ownerOf(streamId); - // Checks: if `msg.sender` is the stream's sender, the withdrawal address must be the recipient. - if (isCallerStreamSender && to != recipient) { - revert Errors.SablierV2Lockup_InvalidSenderWithdrawal(streamId, msg.sender, to); + // Check: if `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address + // must be the recipient. + if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId)) { + revert Errors.SablierV2Lockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to); } - // Checks: the withdrawal address is not zero. - if (to == address(0)) { - revert Errors.SablierV2Lockup_WithdrawToZeroAddress(); - } - - // Checks: the withdraw amount is not zero. - if (amount == 0) { - revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId); - } - - // Checks: the withdraw amount is not greater than the withdrawable amount. + // Check: the withdraw amount is not greater than the withdrawable amount. uint128 withdrawableAmount = _withdrawableAmountOf(streamId); if (amount > withdrawableAmount) { revert Errors.SablierV2Lockup_Overdraw(streamId, amount, withdrawableAmount); @@ -280,22 +385,29 @@ abstract contract SablierV2Lockup is // Effects and Interactions: make the withdrawal. _withdraw(streamId, to, amount); - // Interactions: if `msg.sender` is not the recipient and the recipient is a contract, try to invoke the - // withdraw hook on it without reverting if the hook is not implemented, and also without bubbling up - // any potential revert. - if (msg.sender != recipient && recipient.code.length > 0) { - try ISablierV2LockupRecipient(recipient).onStreamWithdrawn({ + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit MetadataUpdate({ _tokenId: streamId }); + + // Interaction: if `msg.sender` is not the recipient and the recipient is on the allowlist, run the hook. + if (msg.sender != recipient && _allowedToHook[recipient]) { + bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupWithdraw({ streamId: streamId, caller: msg.sender, to: to, amount: amount - }) { } catch { } + }); + + // Check: the recipient's hook returned the correct selector. + if (selector != ISablierLockupRecipient.onSablierLockupWithdraw.selector) { + revert Errors.SablierV2Lockup_InvalidHookSelector(recipient); + } } } /// @inheritdoc ISablierV2Lockup - function withdrawMax(uint256 streamId, address to) external override { - withdraw({ streamId: streamId, to: to, amount: _withdrawableAmountOf(streamId) }); + function withdrawMax(uint256 streamId, address to) external override returns (uint128 withdrawnAmount) { + withdrawnAmount = _withdrawableAmountOf(streamId); + withdraw({ streamId: streamId, to: to, amount: withdrawnAmount }); } /// @inheritdoc ISablierV2Lockup @@ -307,17 +419,18 @@ abstract contract SablierV2Lockup is override noDelegateCall notNull(streamId) + returns (uint128 withdrawnAmount) { - // Checks: the caller is the current recipient. This also checks that the NFT was not burned. + // Check: the caller is the current recipient. This also checks that the NFT was not burned. address currentRecipient = _ownerOf(streamId); if (msg.sender != currentRecipient) { revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender); } // Skip the withdrawal if the withdrawable amount is zero. - uint128 withdrawableAmount = _withdrawableAmountOf(streamId); - if (withdrawableAmount > 0) { - withdraw({ streamId: streamId, to: currentRecipient, amount: withdrawableAmount }); + withdrawnAmount = _withdrawableAmountOf(streamId); + if (withdrawnAmount > 0) { + withdraw({ streamId: streamId, to: currentRecipient, amount: withdrawnAmount }); } // Checks and Effects: transfer the NFT. @@ -327,29 +440,23 @@ abstract contract SablierV2Lockup is /// @inheritdoc ISablierV2Lockup function withdrawMultiple( uint256[] calldata streamIds, - address to, uint128[] calldata amounts ) external override noDelegateCall { - // Checks: there is an equal number of `streamIds` and `amounts`. + // Check: there is an equal number of `streamIds` and `amounts`. uint256 streamIdsCount = streamIds.length; uint256 amountsCount = amounts.length; if (streamIdsCount != amountsCount) { revert Errors.SablierV2Lockup_WithdrawArrayCountsNotEqual(streamIdsCount, amountsCount); } - // Iterate over the provided array of stream ids and withdraw from each stream. - for (uint256 i = 0; i < streamIdsCount;) { + // Iterate over the provided array of stream IDs, and withdraw from each stream to the recipient. + for (uint256 i = 0; i < streamIdsCount; ++i) { // Checks, Effects and Interactions: check the parameters and make the withdrawal. - withdraw(streamIds[i], to, amounts[i]); - - // Increment the loop iterator. - unchecked { - i += 1; - } + withdraw({ streamId: streamIds[i], to: _ownerOf(streamIds[i]), amount: amounts[i] }); } } @@ -357,41 +464,12 @@ abstract contract SablierV2Lockup is INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Overrides the internal ERC-721 transfer function to emit an ERC-4906 event upon transfer. The goal is to - /// refresh the NFT metadata on external platforms. - /// @dev This event is also emitted when the NFT is minted or burned. - function _afterTokenTransfer( - address, /* from */ - address, /* to */ - uint256 streamId, - uint256 /* batchSize */ - ) - internal - override - updateMetadata(streamId) - { } - - /// @notice Overrides the internal ERC-721 transfer function to check that the stream is transferable. - /// @dev There are two cases when the transferable flag is ignored: - /// - If `from` is 0, then the transfer is a mint and is allowed. - /// - If `to` is 0, then the transfer is a burn and is also allowed. - function _beforeTokenTransfer( - address from, - address to, - uint256 streamId, - uint256 /* batchSize */ - ) - internal - view - override - { - if (!isTransferable(streamId) && to != address(0) && from != address(0)) { - revert Errors.SablierV2Lockup_NotTransferable(streamId); - } - } + /// @notice Calculates the streamed amount of the stream without looking up the stream's status. + /// @dev This function is implemented by child contracts, so the logic varies depending on the model. + function _calculateStreamedAmount(uint256 streamId) internal view virtual returns (uint128); /// @notice Checks whether `msg.sender` is the stream's recipient or an approved third party. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function _isCallerStreamRecipientOrApproved(uint256 streamId) internal view returns (bool) { address recipient = _ownerOf(streamId); return msg.sender == recipient || isApprovedForAll({ owner: recipient, operator: msg.sender }) @@ -399,25 +477,184 @@ abstract contract SablierV2Lockup is } /// @notice Checks whether `msg.sender` is the stream's sender. - /// @param streamId The stream id for the query. - function _isCallerStreamSender(uint256 streamId) internal view virtual returns (bool); + /// @param streamId The stream ID for the query. + function _isCallerStreamSender(uint256 streamId) internal view returns (bool) { + return msg.sender == _streams[streamId].sender; + } /// @dev Retrieves the stream's status without performing a null check. - function _statusOf(uint256 streamId) internal view virtual returns (Lockup.Status); + function _statusOf(uint256 streamId) internal view returns (Lockup.Status) { + if (_streams[streamId].isDepleted) { + return Lockup.Status.DEPLETED; + } else if (_streams[streamId].wasCanceled) { + return Lockup.Status.CANCELED; + } + + if (block.timestamp < _streams[streamId].startTime) { + return Lockup.Status.PENDING; + } + + if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { + return Lockup.Status.STREAMING; + } else { + return Lockup.Status.SETTLED; + } + } + + /// @dev See the documentation for the user-facing functions that call this internal function. + function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + if (_streams[streamId].isDepleted) { + return amounts.withdrawn; + } else if (_streams[streamId].wasCanceled) { + return amounts.deposited - amounts.refunded; + } + + return _calculateStreamedAmount(streamId); + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view virtual returns (uint128); + function _withdrawableAmountOf(uint256 streamId) internal view returns (uint128) { + return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; + } /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 tokenId) internal virtual; + function _cancel(uint256 streamId) internal { + // Calculate the streamed amount. + uint128 streamedAmount = _calculateStreamedAmount(streamId); + + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + // Check: the stream is not settled. + if (streamedAmount >= amounts.deposited) { + revert Errors.SablierV2Lockup_StreamSettled(streamId); + } + + // Check: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); + } + + // Calculate the sender's amount. + uint128 senderAmount; + unchecked { + senderAmount = amounts.deposited - streamedAmount; + } + + // Calculate the recipient's amount. + uint128 recipientAmount = streamedAmount - amounts.withdrawn; + + // Effect: mark the stream as canceled. + _streams[streamId].wasCanceled = true; + + // Effect: make the stream not cancelable anymore, because a stream can only be canceled once. + _streams[streamId].isCancelable = false; + + // Effect: if there are no assets left for the recipient to withdraw, mark the stream as depleted. + if (recipientAmount == 0) { + _streams[streamId].isDepleted = true; + } + + // Effect: set the refunded amount. + _streams[streamId].amounts.refunded = senderAmount; + + // Retrieve the sender and the recipient from storage. + address sender = _streams[streamId].sender; + address recipient = _ownerOf(streamId); + + // Retrieve the ERC-20 asset from storage. + IERC20 asset = _streams[streamId].asset; + + // Interaction: refund the sender. + asset.safeTransfer({ to: sender, value: senderAmount }); + + // Log the cancellation. + emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); + + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit MetadataUpdate({ _tokenId: streamId }); + + // Interaction: if the recipient is on the allowlist, run the hook. + if (_allowedToHook[recipient]) { + bytes4 selector = ISablierLockupRecipient(recipient).onSablierLockupCancel({ + streamId: streamId, + sender: sender, + senderAmount: senderAmount, + recipientAmount: recipientAmount + }); + + // Check: the recipient's hook returned the correct selector. + if (selector != ISablierLockupRecipient.onSablierLockupCancel.selector) { + revert Errors.SablierV2Lockup_InvalidHookSelector(recipient); + } + } + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal virtual; + function _renounce(uint256 streamId) internal { + // Check: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); + } + + // Effect: renounce the stream by making it not cancelable. + _streams[streamId].isCancelable = false; + } + + /// @notice Overrides the {ERC-721._update} function to check that the stream is transferable, and emits an + /// ERC-4906 event. + /// @dev There are two cases when the transferable flag is ignored: + /// - If the current owner is 0, then the update is a mint and is allowed. + /// - If `to` is 0, then the update is a burn and is also allowed. + /// @param to The address of the new recipient of the stream. + /// @param streamId ID of the stream to update. + /// @param auth Optional parameter. If the value is not zero, the overridden implementation will check that + /// `auth` is either the recipient of the stream, or an approved third party. + /// @return The original recipient of the `streamId` before the update. + function _update(address to, uint256 streamId, address auth) internal override returns (address) { + address from = _ownerOf(streamId); + + if (from != address(0) && to != address(0) && !_streams[streamId].isTransferable) { + revert Errors.SablierV2Lockup_NotTransferable(streamId); + } + + // Emit an ERC-4906 event to trigger an update of the NFT metadata. + emit MetadataUpdate({ _tokenId: streamId }); + + return super._update(to, streamId, auth); + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal virtual; + function _withdraw(uint256 streamId, address to, uint128 amount) internal { + // Effect: update the withdrawn amount. + _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; + + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the + // withdrawn amount, the stream will still be marked as depleted. + if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { + // Effect: mark the stream as depleted. + _streams[streamId].isDepleted = true; + + // Effect: make the stream not cancelable anymore, because a depleted stream cannot be canceled. + _streams[streamId].isCancelable = false; + } + + // Retrieve the ERC-20 asset from storage. + IERC20 asset = _streams[streamId].asset; + + // Interaction: perform the ERC-20 transfer. + asset.safeTransfer({ to: to, value: amount }); + + // Log the withdrawal. + emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); + } } diff --git a/src/interfaces/IAdminable.sol b/src/interfaces/IAdminable.sol index e5ee09561..62a13d1a7 100644 --- a/src/interfaces/IAdminable.sol +++ b/src/interfaces/IAdminable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; /// @title IAdminable /// @notice Contract module that provides a basic access control mechanism, with an admin that can be diff --git a/src/interfaces/ISablierLockupRecipient.sol b/src/interfaces/ISablierLockupRecipient.sol new file mode 100644 index 000000000..cf6c80cb3 --- /dev/null +++ b/src/interfaces/ISablierLockupRecipient.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/// @title ISablierLockupRecipient +/// @notice Interface for recipient contracts capable of reacting to cancellations and withdrawals. For this to be able +/// to hook into Sablier, it must fully implement this interface and it must have been allowlisted by the Lockup +/// contract's admin. +/// @dev See {IERC165-supportsInterface}. +/// The implementation MUST implement the {IERC165-supportsInterface} method, which MUST return `true` when called with +/// `0xf8ee98d3`, i.e. `type(ISablierLockupRecipient).interfaceId`. +interface ISablierLockupRecipient is IERC165 { + /// @notice Responds to cancellations. + /// + /// @dev Notes: + /// - The function MUST return the selector `ISablierLockupRecipient.onSablierLockupCancel.selector`. + /// - If this function reverts, the execution in the Lockup contract will revert as well. + /// + /// @param streamId The ID of the canceled stream. + /// @param sender The stream's sender, who canceled the stream. + /// @param senderAmount The amount of assets refunded to the stream's sender, denoted in units of the asset's + /// decimals. + /// @param recipientAmount The amount of assets left for the stream's recipient to withdraw, denoted in units of + /// the asset's decimals. + /// + /// @return selector The selector of this function needed to validate the hook. + function onSablierLockupCancel( + uint256 streamId, + address sender, + uint128 senderAmount, + uint128 recipientAmount + ) + external + returns (bytes4 selector); + + /// @notice Responds to withdrawals triggered by any address except the contract implementing this interface. + /// + /// @dev Notes: + /// - The function MUST return the selector `ISablierLockupRecipient.onSablierLockupWithdraw.selector`. + /// - If this function reverts, the execution in the Lockup contract will revert as well. + /// + /// @param streamId The ID of the stream being withdrawn from. + /// @param caller The original `msg.sender` address that triggered the withdrawal. + /// @param to The address receiving the withdrawn assets. + /// @param amount The amount of assets withdrawn, denoted in units of the asset's decimals. + /// + /// @return selector The selector of this function needed to validate the hook. + function onSablierLockupWithdraw( + uint256 streamId, + address caller, + address to, + uint128 amount + ) + external + returns (bytes4 selector); +} diff --git a/src/interfaces/ISablierV2Base.sol b/src/interfaces/ISablierV2Base.sol deleted file mode 100644 index ec7bcdbaf..000000000 --- a/src/interfaces/ISablierV2Base.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { IAdminable } from "./IAdminable.sol"; -import { ISablierV2Comptroller } from "./ISablierV2Comptroller.sol"; - -/// @title ISablierV2Base -/// @notice Base logic for all Sablier V2 streaming contracts. -interface ISablierV2Base is IAdminable { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the admin claims all protocol revenues accrued for a particular ERC-20 asset. - /// @param admin The address of the contract admin. - /// @param asset The contract address of the ERC-20 asset the protocol revenues have been claimed for. - /// @param protocolRevenues The amount of protocol revenues claimed, denoted in units of the asset's decimals. - event ClaimProtocolRevenues(address indexed admin, IERC20 indexed asset, uint128 protocolRevenues); - - /// @notice Emitted when the admin sets a new comptroller contract. - /// @param admin The address of the contract admin. - /// @param oldComptroller The address of the old comptroller contract. - /// @param newComptroller The address of the new comptroller contract. - event SetComptroller( - address indexed admin, ISablierV2Comptroller oldComptroller, ISablierV2Comptroller newComptroller - ); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Retrieves the maximum fee that can be charged by the protocol or a broker, denoted as a fixed-point - /// number where 1e18 is 100%. - /// @dev This value is hard coded as a constant. - function MAX_FEE() external view returns (UD60x18); - - /// @notice Retrieves the address of the comptroller contract, responsible for the Sablier V2 protocol - /// configuration. - function comptroller() external view returns (ISablierV2Comptroller); - - /// @notice Retrieves the protocol revenues accrued for the provided ERC-20 asset, in units of the asset's - /// decimals. - /// @param asset The contract address of the ERC-20 asset to query. - function protocolRevenues(IERC20 asset) external view returns (uint128 revenues); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Claims all accumulated protocol revenues for the provided ERC-20 asset. - /// - /// @dev Emits a {ClaimProtocolRevenues} event. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param asset The contract address of the ERC-20 asset for which to claim protocol revenues. - function claimProtocolRevenues(IERC20 asset) external; - - /// @notice Assigns a new comptroller contract responsible for the protocol configuration. - /// - /// @dev Emits a {SetComptroller} event. - /// - /// Notes: - /// - Does not revert if the comptroller is the same. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param newComptroller The address of the new comptroller contract. - function setComptroller(ISablierV2Comptroller newComptroller) external; -} diff --git a/src/interfaces/ISablierV2Comptroller.sol b/src/interfaces/ISablierV2Comptroller.sol deleted file mode 100644 index a6a471530..000000000 --- a/src/interfaces/ISablierV2Comptroller.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { IAdminable } from "./IAdminable.sol"; - -/// @title ISablierV2Controller -/// @notice This contract is in charge of the Sablier V2 protocol configuration, handling such values as the -/// protocol fees. -interface ISablierV2Comptroller is IAdminable { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the admin sets a new flash fee. - /// @param admin The address of the contract admin. - /// @param oldFlashFee The old flash fee, denoted as a fixed-point number. - /// @param newFlashFee The new flash fee, denoted as a fixed-point number. - event SetFlashFee(address indexed admin, UD60x18 oldFlashFee, UD60x18 newFlashFee); - - /// @notice Emitted when the admin sets a new protocol fee for the provided ERC-20 asset. - /// @param admin The address of the contract admin. - /// @param asset The contract address of the ERC-20 asset the new protocol fee has been set for. - /// @param oldProtocolFee The old protocol fee, denoted as a fixed-point number. - /// @param newProtocolFee The new protocol fee, denoted as a fixed-point number. - event SetProtocolFee(address indexed admin, IERC20 indexed asset, UD60x18 oldProtocolFee, UD60x18 newProtocolFee); - - /// @notice Emitted when the admin enables or disables an ERC-20 asset for flash loaning. - /// @param admin The address of the contract admin. - /// @param asset The contract address of the ERC-20 asset to toggle. - /// @param newFlag Whether the ERC-20 asset can be flash loaned. - event ToggleFlashAsset(address indexed admin, IERC20 indexed asset, bool newFlag); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Retrieves the global flash fee, denoted as a fixed-point number where 1e18 is 100%. - /// - /// @dev Notes: - /// - This fee represents a percentage, not an amount. Do not confuse it with {IERC3156FlashLender.flashFee}, - /// which calculates the fee amount for a specified flash loan amount. - /// - Unlike the protocol fee, this is a global fee applied to all flash loans, not a per-asset fee. - function flashFee() external view returns (UD60x18 fee); - - /// @notice Retrieves a flag indicating whether the provided ERC-20 asset can be flash loaned. - /// @param token The contract address of the ERC-20 asset to check. - function isFlashAsset(IERC20 token) external view returns (bool result); - - /// @notice Retrieves the protocol fee for all streams created with the provided ERC-20 asset. - /// @param asset The contract address of the ERC-20 asset to query. - /// @return fee The protocol fee denoted as a fixed-point number where 1e18 is 100%. - function protocolFees(IERC20 asset) external view returns (UD60x18 fee); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Updates the flash fee charged on all flash loans made with any ERC-20 asset. - /// - /// @dev Emits a {SetFlashFee} event. - /// - /// Notes: - /// - Does not revert if the fee is the same. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param newFlashFee The new flash fee to set, denoted as a fixed-point number where 1e18 is 100%. - function setFlashFee(UD60x18 newFlashFee) external; - - /// @notice Sets a new protocol fee that will be charged on all streams created with the provided ERC-20 asset. - /// - /// @dev Emits a {SetProtocolFee} event. - /// - /// Notes: - /// - The fee is not denoted in units of the asset's decimals; it is a fixed-point number. Refer to the - /// PRBMath documentation for more detail on the logic of UD60x18. - /// - Does not revert if the fee is the same. - /// - /// Requirements: - /// - `msg.sender` must be the contract admin. - /// - /// @param asset The contract address of the ERC-20 asset to update the fee for. - /// @param newProtocolFee The new protocol fee, denoted as a fixed-point number where 1e18 is 100%. - function setProtocolFee(IERC20 asset, UD60x18 newProtocolFee) external; - - /// @notice Toggles the flash loanability of an ERC-20 asset. - /// - /// @dev Emits a {ToggleFlashAsset} event. - /// - /// Requirements: - /// - `msg.sender` must be the admin. - /// - /// @param asset The address of the ERC-20 asset to toggle. - function toggleFlashAsset(IERC20 asset) external; -} diff --git a/src/interfaces/ISablierV2Lockup.sol b/src/interfaces/ISablierV2Lockup.sol index 15ab4f511..72c6fcf48 100644 --- a/src/interfaces/ISablierV2Lockup.sol +++ b/src/interfaces/ISablierV2Lockup.sol @@ -1,28 +1,36 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; +import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Lockup } from "../types/DataTypes.sol"; -import { ISablierV2Base } from "./ISablierV2Base.sol"; +import { IAdminable } from "./IAdminable.sol"; import { ISablierV2NFTDescriptor } from "./ISablierV2NFTDescriptor.sol"; /// @title ISablierV2Lockup -/// @notice Common logic between all Sablier V2 Lockup streaming contracts. +/// @notice Common logic between all Sablier V2 Lockup contracts. interface ISablierV2Lockup is - ISablierV2Base, // 1 inherited component + IAdminable, // 0 inherited components + IERC4906, // 2 inherited components IERC721Metadata // 2 inherited components { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Emitted when the admin allows a new recipient contract to hook to Sablier. + /// @param admin The address of the current contract admin. + /// @param recipient The address of the recipient contract put on the allowlist. + event AllowToHook(address indexed admin, address recipient); + /// @notice Emitted when a stream is canceled. - /// @param streamId The id of the stream. + /// @param streamId The ID of the stream. /// @param sender The address of the stream's sender. /// @param recipient The address of the stream's recipient. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param senderAmount The amount of assets refunded to the stream's sender, denoted in units of the asset's /// decimals. /// @param recipientAmount The amount of assets left for the stream's recipient to withdraw, denoted in units of the @@ -37,7 +45,7 @@ interface ISablierV2Lockup is ); /// @notice Emitted when a sender gives up the right to cancel a stream. - /// @param streamId The id of the stream. + /// @param streamId The ID of the stream. event RenounceLockupStream(uint256 indexed streamId); /// @notice Emitted when the admin sets a new NFT descriptor contract. @@ -49,9 +57,9 @@ interface ISablierV2Lockup is ); /// @notice Emitted when assets are withdrawn from a stream. - /// @param streamId The id of the stream. + /// @param streamId The ID of the stream. /// @param to The address that has received the withdrawn assets. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param amount The amount of assets withdrawn, denoted in units of the asset's decimals. event WithdrawFromLockupStream(uint256 indexed streamId, address indexed to, IERC20 indexed asset, uint128 amount); @@ -59,111 +67,148 @@ interface ISablierV2Lockup is CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Retrieves the address of the ERC-20 asset used for streaming. + /// @notice Retrieves the address of the ERC-20 asset to be distributed. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getAsset(uint256 streamId) external view returns (IERC20 asset); /// @notice Retrieves the amount deposited in the stream, denoted in units of the asset's decimals. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getDepositedAmount(uint256 streamId) external view returns (uint128 depositedAmount); /// @notice Retrieves the stream's end time, which is a Unix timestamp. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getEndTime(uint256 streamId) external view returns (uint40 endTime); /// @notice Retrieves the stream's recipient. /// @dev Reverts if the NFT has been burned. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getRecipient(uint256 streamId) external view returns (address recipient); /// @notice Retrieves the amount refunded to the sender after a cancellation, denoted in units of the asset's /// decimals. This amount is always zero unless the stream was canceled. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getRefundedAmount(uint256 streamId) external view returns (uint128 refundedAmount); /// @notice Retrieves the stream's sender. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getSender(uint256 streamId) external view returns (address sender); /// @notice Retrieves the stream's start time, which is a Unix timestamp. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getStartTime(uint256 streamId) external view returns (uint40 startTime); /// @notice Retrieves the amount withdrawn from the stream, denoted in units of the asset's decimals. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getWithdrawnAmount(uint256 streamId) external view returns (uint128 withdrawnAmount); + /// @notice Retrieves a flag indicating whether the provided address is a contract allowed to hook to Sablier + /// when a stream is canceled or when assets are withdrawn. + /// @dev See {ISablierLockupRecipient} for more information. + function isAllowedToHook(address recipient) external view returns (bool result); + /// @notice Retrieves a flag indicating whether the stream can be canceled. When the stream is cold, this /// flag is always `false`. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isCancelable(uint256 streamId) external view returns (bool result); /// @notice Retrieves a flag indicating whether the stream is cold, i.e. settled, canceled, or depleted. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isCold(uint256 streamId) external view returns (bool result); /// @notice Retrieves a flag indicating whether the stream is depleted. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isDepleted(uint256 streamId) external view returns (bool result); /// @notice Retrieves a flag indicating whether the stream exists. /// @dev Does not revert if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isStream(uint256 streamId) external view returns (bool result); /// @notice Retrieves a flag indicating whether the stream NFT can be transferred. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isTransferable(uint256 streamId) external view returns (bool result); /// @notice Retrieves a flag indicating whether the stream is warm, i.e. either pending or streaming. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function isWarm(uint256 streamId) external view returns (bool result); - /// @notice Counter for stream ids, used in the create functions. + /// @notice Retrieves the maximum broker fee that can be charged by the broker, denoted as a fixed-point + /// number where 1e18 is 100%. + /// @dev This value is hard coded as a constant. + function MAX_BROKER_FEE() external view returns (UD60x18); + + /// @notice Counter for stream IDs, used in the create functions. function nextStreamId() external view returns (uint256); + /// @notice Contract that generates the non-fungible token URI. + function nftDescriptor() external view returns (ISablierV2NFTDescriptor); + /// @notice Calculates the amount that the sender would be refunded if the stream were canceled, denoted in units /// of the asset's decimals. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function refundableAmountOf(uint256 streamId) external view returns (uint128 refundableAmount); /// @notice Retrieves the stream's status. - /// @param streamId The stream id for the query. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. function statusOf(uint256 streamId) external view returns (Lockup.Status status); /// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// + /// Notes: + /// - Upon cancellation of the stream, the amount streamed is calculated as the difference between the deposited + /// amount and the refunded amount. Ultimately, when the stream becomes depleted, the streamed amount is equivalent + /// to the total amount withdrawn. + /// + /// @param streamId The stream ID for the query. function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount); /// @notice Retrieves a flag indicating whether the stream was canceled. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function wasCanceled(uint256 streamId) external view returns (bool result); /// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in units of the asset's /// decimals. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Allows a recipient contract to hook to Sablier when a stream is canceled or when assets are withdrawn. + /// Useful for implementing contracts that hold streams on behalf of users, such as vaults or staking contracts. + /// + /// @dev Emits an {AllowToHook} event. + /// + /// Notes: + /// - Does not revert if the contract is already on the allowlist. + /// - This is an irreversible operation. The contract cannot be removed from the allowlist. + /// + /// Requirements: + /// - `msg.sender` must be the contract admin. + /// - `recipient` must have a non-zero code size. + /// - `recipient` must implement {ISablierLockupRecipient}. + /// + /// @param recipient The address of the contract to allow for hooks. + function allowToHook(address recipient) external; + /// @notice Burns the NFT associated with the stream. /// /// @dev Emits a {Transfer} event. @@ -174,7 +219,7 @@ interface ISablierV2Lockup is /// - The NFT must exist. /// - `msg.sender` must be either the NFT owner or an approved third party. /// - /// @param streamId The id of the stream NFT to burn. + /// @param streamId The ID of the stream NFT to burn. function burn(uint256 streamId) external; /// @notice Cancels the stream and refunds any remaining assets to the sender. @@ -191,7 +236,7 @@ interface ISablierV2Lockup is /// - The stream must be warm and cancelable. /// - `msg.sender` must be the stream's sender. /// - /// @param streamId The id of the stream to cancel. + /// @param streamId The ID of the stream to cancel. function cancel(uint256 streamId) external; /// @notice Cancels multiple streams and refunds any remaining assets to the sender. @@ -204,7 +249,7 @@ interface ISablierV2Lockup is /// Requirements: /// - All requirements from {cancel} must be met for each stream. /// - /// @param streamIds The ids of the streams to cancel. + /// @param streamIds The IDs of the streams to cancel. function cancelMultiple(uint256[] calldata streamIds) external; /// @notice Removes the right of the stream's sender to cancel the stream. @@ -213,7 +258,6 @@ interface ISablierV2Lockup is /// /// Notes: /// - This is an irreversible operation. - /// - This function attempts to invoke a hook on the stream's recipient, provided that the recipient is a contract. /// /// Requirements: /// - Must not be delegate called. @@ -221,7 +265,7 @@ interface ISablierV2Lockup is /// - `msg.sender` must be the stream's sender. /// - The stream must be cancelable. /// - /// @param streamId The id of the stream to renounce. + /// @param streamId The ID of the stream to renounce. function renounce(uint256 streamId) external; /// @notice Sets a new NFT descriptor contract, which produces the URI describing the Sablier stream NFTs. @@ -242,18 +286,16 @@ interface ISablierV2Lockup is /// @dev Emits a {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} event. /// /// Notes: - /// - This function attempts to invoke a hook on the stream's recipient, provided that the recipient is a contract - /// and `msg.sender` is either the sender or an approved operator. + /// - This function attempts to call a hook on the recipient of the stream, unless `msg.sender` is the recipient. /// /// Requirements: /// - Must not be delegate called. /// - `streamId` must not reference a null or depleted stream. - /// - `msg.sender` must be the stream's sender, the stream's recipient or an approved third party. - /// - `to` must be the recipient if `msg.sender` is the stream's sender. /// - `to` must not be the zero address. /// - `amount` must be greater than zero and must not exceed the withdrawable amount. + /// - `to` must be the recipient if `msg.sender` is not the stream's recipient or an approved third party. /// - /// @param streamId The id of the stream to withdraw from. + /// @param streamId The ID of the stream to withdraw from. /// @param to The address receiving the withdrawn assets. /// @param amount The amount to withdraw, denoted in units of the asset's decimals. function withdraw(uint256 streamId, address to, uint128 amount) external; @@ -268,9 +310,10 @@ interface ISablierV2Lockup is /// Requirements: /// - Refer to the requirements in {withdraw}. /// - /// @param streamId The id of the stream to withdraw from. + /// @param streamId The ID of the stream to withdraw from. /// @param to The address receiving the withdrawn assets. - function withdrawMax(uint256 streamId, address to) external; + /// @return withdrawnAmount The amount withdrawn, denoted in units of the asset's decimals. + function withdrawMax(uint256 streamId, address to) external returns (uint128 withdrawnAmount); /// @notice Withdraws the maximum withdrawable amount from the stream to the current recipient, and transfers the /// NFT to `newRecipient`. @@ -286,11 +329,17 @@ interface ISablierV2Lockup is /// - Refer to the requirements in {withdraw}. /// - Refer to the requirements in {IERC721.transferFrom}. /// - /// @param streamId The id of the stream NFT to transfer. + /// @param streamId The ID of the stream NFT to transfer. /// @param newRecipient The address of the new owner of the stream NFT. - function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external; + /// @return withdrawnAmount The amount withdrawn, denoted in units of the asset's decimals. + function withdrawMaxAndTransfer( + uint256 streamId, + address newRecipient + ) + external + returns (uint128 withdrawnAmount); - /// @notice Withdraws assets from streams to the provided address `to`. + /// @notice Withdraws assets from streams to the recipient of each stream. /// /// @dev Emits multiple {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} events. /// @@ -298,11 +347,12 @@ interface ISablierV2Lockup is /// - This function attempts to call a hook on the recipient of each stream, unless `msg.sender` is the recipient. /// /// Requirements: - /// - All requirements from {withdraw} must be met for each stream. + /// - Must not be delegate called. /// - There must be an equal number of `streamIds` and `amounts`. + /// - Each stream ID in the array must not reference a null or depleted stream. + /// - Each amount in the array must be greater than zero and must not exceed the withdrawable amount. /// - /// @param streamIds The ids of the streams to withdraw from. - /// @param to The address receiving the withdrawn assets. + /// @param streamIds The IDs of the streams to withdraw from. /// @param amounts The amounts to withdraw, denoted in units of the asset's decimals. - function withdrawMultiple(uint256[] calldata streamIds, address to, uint128[] calldata amounts) external; + function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external; } diff --git a/src/interfaces/ISablierV2LockupDynamic.sol b/src/interfaces/ISablierV2LockupDynamic.sol index d117d9c8a..df77c8f84 100644 --- a/src/interfaces/ISablierV2LockupDynamic.sol +++ b/src/interfaces/ISablierV2LockupDynamic.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -7,24 +7,24 @@ import { Lockup, LockupDynamic } from "../types/DataTypes.sol"; import { ISablierV2Lockup } from "./ISablierV2Lockup.sol"; /// @title ISablierV2LockupDynamic -/// @notice Creates and manages Lockup streams with dynamic streaming functions. +/// @notice Creates and manages Lockup streams with a dynamic distribution function. interface ISablierV2LockupDynamic is ISablierV2Lockup { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when a stream is created. - /// @param streamId The id of the newly created stream. + /// @param streamId The ID of the newly created stream. /// @param funder The address which has funded the stream. - /// @param sender The address from which to stream the assets, who will have the ability to cancel the stream. + /// @param sender The address distributing the assets, which will have the ability to cancel the stream. /// @param recipient The address toward which to stream the assets. - /// @param amounts Struct containing (i) the deposit amount, (ii) the protocol fee amount, and (iii) the - /// broker fee amount, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param amounts Struct containing (i) the deposit amount, and (ii) the broker fee amount, both denoted + /// in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Boolean indicating whether the stream will be cancelable or not. /// @param transferable Boolean indicating whether the stream NFT is transferable or not. - /// @param segments The segments the protocol uses to compose the custom streaming curve. - /// @param range Struct containing (i) the stream's start time and (ii) end time, both as Unix timestamps. + /// @param segments The segments the protocol uses to compose the dynamic distribution function. + /// @param timestamps Struct containing (i) the stream's start time and (ii) end time, both as Unix timestamps. /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. event CreateLockupDynamicStream( uint256 streamId, @@ -36,7 +36,7 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup { bool cancelable, bool transferable, LockupDynamic.Segment[] segments, - LockupDynamic.Range range, + LockupDynamic.Timestamps timestamps, address broker ); @@ -44,90 +44,70 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup { CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice The maximum number of segments allowed in a stream. - /// @dev This is initialized at construction time and cannot be changed later. - function MAX_SEGMENT_COUNT() external view returns (uint256); - - /// @notice Retrieves the stream's range, which is a struct containing (i) the stream's start time and (ii) end - /// time, both as Unix timestamps. + /// @notice Retrieves the segments used to compose the dynamic distribution function. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function getRange(uint256 streamId) external view returns (LockupDynamic.Range memory range); - - /// @notice Retrieves the segments the protocol uses to compose the custom streaming curve. - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments); - /// @notice Retrieves the stream entity. + /// @notice Retrieves the full stream details. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function getStream(uint256 streamId) external view returns (LockupDynamic.Stream memory stream); + /// @param streamId The stream ID for the query. + /// @return stream See the documentation in {DataTypes}. + function getStream(uint256 streamId) external view returns (LockupDynamic.StreamLD memory stream); - /// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals. - /// - /// When the stream is warm, the streaming function is: - /// - /// $$ - /// f(x) = x^{exp} * csa + \Sigma(esa) - /// $$ - /// - /// Where: - /// - /// - $x$ is the elapsed time divided by the total time in the current segment. - /// - $exp$ is the current segment exponent. - /// - $csa$ is the current segment amount. - /// - $\Sigma(esa)$ is the sum of all elapsed segments' amounts. - /// - /// Upon cancellation of the stream, the amount streamed is calculated as the difference between the deposited - /// amount and the refunded amount. Ultimately, when the stream becomes depleted, the streamed amount is equivalent - /// to the total amount withdrawn. - /// + /// @notice Retrieves the stream's start and end timestamps. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount); + /// @param streamId The stream ID for the query. + /// @return timestamps See the documentation in {DataTypes}. + function getTimestamps(uint256 streamId) external view returns (LockupDynamic.Timestamps memory timestamps); + + /// @notice The maximum number of segments allowed in a stream. + /// @dev This is initialized at construction time and cannot be changed later. + function MAX_SEGMENT_COUNT() external view returns (uint256); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of - /// `block.timestamp` and all specified time deltas. The segment milestones are derived from these - /// deltas. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// `block.timestamp` and all specified time durations. The segment timestamps are derived from these + /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. /// /// @dev Emits a {Transfer} and {CreateLockupDynamicStream} event. /// /// Requirements: - /// - All requirements in {createWithMilestones} must be met for the calculated parameters. + /// - All requirements in {createWithTimestamps} must be met for the calculated parameters. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @return streamId The id of the newly created stream. - function createWithDeltas(LockupDynamic.CreateWithDeltas calldata params) external returns (uint256 streamId); + /// @return streamId The ID of the newly created stream. + function createWithDurations(LockupDynamic.CreateWithDurations calldata params) + external + returns (uint256 streamId); - /// @notice Creates a stream with the provided segment milestones, implying the end time from the last milestone. + /// @notice Creates a stream with the provided segment timestamps, implying the end time from the last timestamp. /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. /// /// @dev Emits a {Transfer} and {CreateLockupDynamicStream} event. /// /// Notes: - /// - As long as the segment milestones are arranged in ascending order, it is not an error for some + /// - As long as the segment timestamps are arranged in ascending order, it is not an error for some /// of them to be in the past. /// /// Requirements: /// - Must not be delegate called. /// - `params.totalAmount` must be greater than zero. - /// - If set, `params.broker.fee` must not be greater than `MAX_FEE`. + /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. + /// - `params.startTime` must be greater than zero and less than the first segment's timestamp. /// - `params.segments` must have at least one segment, but not more than `MAX_SEGMENT_COUNT`. - /// - `params.startTime` must be less than the first segment's milestone. - /// - The segment milestones must be arranged in ascending order. - /// - The last segment milestone (i.e. the stream's end time) must be in the future. + /// - The segment timestamps must be arranged in ascending order. + /// - The last segment timestamp (i.e. the stream's end time) must be in the future. /// - The sum of the segment amounts must equal the deposit amount. /// - `params.recipient` must not be the zero address. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @return streamId The id of the newly created stream. - function createWithMilestones(LockupDynamic.CreateWithMilestones calldata params) + /// @return streamId The ID of the newly created stream. + function createWithTimestamps(LockupDynamic.CreateWithTimestamps calldata params) external returns (uint256 streamId); } diff --git a/src/interfaces/ISablierV2LockupLinear.sol b/src/interfaces/ISablierV2LockupLinear.sol index be1edebd3..425514c09 100644 --- a/src/interfaces/ISablierV2LockupLinear.sol +++ b/src/interfaces/ISablierV2LockupLinear.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -7,24 +7,24 @@ import { Lockup, LockupLinear } from "../types/DataTypes.sol"; import { ISablierV2Lockup } from "./ISablierV2Lockup.sol"; /// @title ISablierV2LockupLinear -/// @notice Creates and manages Lockup streams with linear streaming functions. +/// @notice Creates and manages Lockup streams with a linear distribution function. interface ISablierV2LockupLinear is ISablierV2Lockup { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when a stream is created. - /// @param streamId The id of the newly created stream. + /// @param streamId The ID of the newly created stream. /// @param funder The address which funded the stream. - /// @param sender The address streaming the assets, with the ability to cancel the stream. + /// @param sender The address distributing the assets, which will have the ability to cancel the stream. /// @param recipient The address receiving the assets. - /// @param amounts Struct containing (i) the deposit amount, (ii) the protocol fee amount, and (iii) the - /// broker fee amount, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param amounts Struct containing (i) the deposit amount, and (ii) the broker fee amount, both denoted + /// in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Boolean indicating whether the stream will be cancelable or not. /// @param transferable Boolean indicating whether the stream NFT is transferable or not. - /// @param range Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as Unix - /// timestamps. + /// @param timestamps Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as + /// Unix timestamps. /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. event CreateLockupLinearStream( uint256 streamId, @@ -35,7 +35,7 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { IERC20 indexed asset, bool cancelable, bool transferable, - LockupLinear.Range range, + LockupLinear.Timestamps timestamps, address broker ); @@ -43,43 +43,23 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. + /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. A value of zero means there + /// is no cliff. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. + /// @param streamId The stream ID for the query. function getCliffTime(uint256 streamId) external view returns (uint40 cliffTime); - /// @notice Retrieves the stream's range, which is a struct containing (i) the stream's start time, (ii) cliff - /// time, and (iii) end time, all as Unix timestamps. + /// @notice Retrieves the full stream details. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function getRange(uint256 streamId) external view returns (LockupLinear.Range memory range); + /// @param streamId The stream ID for the query. + /// @return stream See the documentation in {DataTypes}. + function getStream(uint256 streamId) external view returns (LockupLinear.StreamLL memory stream); - /// @notice Retrieves the stream entity. + /// @notice Retrieves the stream's start, cliff and end timestamps. /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function getStream(uint256 streamId) external view returns (LockupLinear.Stream memory stream); - - /// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals. - /// - /// When the stream is warm, the streaming function is: - /// - /// $$ - /// f(x) = x * d + c - /// $$ - /// - /// Where: - /// - /// - $x$ is the elapsed time divided by the stream's total duration. - /// - $d$ is the deposited amount. - /// - $c$ is the cliff amount. - /// - /// Upon cancellation of the stream, the amount streamed is calculated as the difference between the deposited - /// amount and the refunded amount. Ultimately, when the stream becomes depleted, the streamed amount is equivalent - /// to the total amount withdrawn. - /// - /// @dev Reverts if `streamId` references a null stream. - /// @param streamId The stream id for the query. - function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount); + /// @param streamId The stream ID for the query. + /// @return timestamps See the documentation in {DataTypes}. + function getTimestamps(uint256 streamId) external view returns (LockupLinear.Timestamps memory timestamps); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS @@ -92,33 +72,37 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { /// @dev Emits a {Transfer} and {CreateLockupLinearStream} event. /// /// Requirements: - /// - All requirements in {createWithRange} must be met for the calculated parameters. + /// - All requirements in {createWithTimestamps} must be met for the calculated parameters. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @return streamId The id of the newly created stream. + /// @return streamId The ID of the newly created stream. function createWithDurations(LockupLinear.CreateWithDurations calldata params) external returns (uint256 streamId); - /// @notice Creates a stream with the provided start time and end time as the range. The stream is - /// funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// @notice Creates a stream with the provided start time and end time. The stream is funded by `msg.sender` and is + /// wrapped in an ERC-721 NFT. /// /// @dev Emits a {Transfer} and {CreateLockupLinearStream} event. /// /// Notes: + /// - A cliff time of zero means there is no cliff. /// - As long as the times are ordered, it is not an error for the start or the cliff time to be in the past. /// /// Requirements: /// - Must not be delegate called. /// - `params.totalAmount` must be greater than zero. - /// - If set, `params.broker.fee` must not be greater than `MAX_FEE`. - /// - `params.range.start` must be less than or equal to `params.range.cliff`. - /// - `params.range.cliff` must be less than `params.range.end`. - /// - `params.range.end` must be in the future. + /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. + /// - `params.timestamps.start` must be greater than zero and less than `params.timestamps.end`. + /// - If set, `params.timestamps.cliff` must be greater than `params.timestamps.start` and less than + /// `params.timestamps.end`. + /// - `params.timestamps.end` must be in the future. /// - `params.recipient` must not be the zero address. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets. /// /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. - /// @return streamId The id of the newly created stream. - function createWithRange(LockupLinear.CreateWithRange calldata params) external returns (uint256 streamId); + /// @return streamId The ID of the newly created stream. + function createWithTimestamps(LockupLinear.CreateWithTimestamps calldata params) + external + returns (uint256 streamId); } diff --git a/src/interfaces/ISablierV2LockupTranched.sol b/src/interfaces/ISablierV2LockupTranched.sol new file mode 100644 index 000000000..836925ccc --- /dev/null +++ b/src/interfaces/ISablierV2LockupTranched.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Lockup, LockupTranched } from "../types/DataTypes.sol"; +import { ISablierV2Lockup } from "./ISablierV2Lockup.sol"; + +/// @title ISablierV2LockupTranched +/// @notice Creates and manages Lockup streams with a tranched distribution function. +interface ISablierV2LockupTranched is ISablierV2Lockup { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a stream is created. + /// @param streamId The ID of the newly created stream. + /// @param funder The address which has funded the stream. + /// @param sender The address distributing the assets, which will have the ability to cancel the stream. + /// @param recipient The address toward which to stream the assets. + /// @param amounts Struct containing (i) the deposit amount, and (ii) the broker fee amount, both denoted + /// in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param cancelable Boolean indicating whether the stream will be cancelable or not. + /// @param transferable Boolean indicating whether the stream NFT is transferable or not. + /// @param tranches The tranches the protocol uses to compose the tranched distribution function. + /// @param timestamps Struct containing (i) the stream's start time and (ii) end time, both as Unix timestamps. + /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. + event CreateLockupTranchedStream( + uint256 streamId, + address funder, + address indexed sender, + address indexed recipient, + Lockup.CreateAmounts amounts, + IERC20 indexed asset, + bool cancelable, + bool transferable, + LockupTranched.Tranche[] tranches, + LockupTranched.Timestamps timestamps, + address broker + ); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Retrieves the full stream details. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + /// @return stream See the documentation in {DataTypes}. + function getStream(uint256 streamId) external view returns (LockupTranched.StreamLT memory stream); + + /// @notice Retrieves the stream's start and end timestamps. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + /// @return timestamps See the documentation in {DataTypes}. + function getTimestamps(uint256 streamId) external view returns (LockupTranched.Timestamps memory timestamps); + + /// @notice Retrieves the tranches used to compose the tranched distribution function. + /// @dev Reverts if `streamId` references a null stream. + /// @param streamId The stream ID for the query. + function getTranches(uint256 streamId) external view returns (LockupTranched.Tranche[] memory tranches); + + /// @notice The maximum number of tranches allowed in a stream. + /// @dev This is initialized at construction time and cannot be changed later. + function MAX_TRANCHE_COUNT() external view returns (uint256); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a stream by setting the start time to `block.timestamp`, and the end time to the sum of + /// `block.timestamp` and all specified time durations. The tranche timestamps are derived from these + /// durations. The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer} and {CreateLockupTrancheStream} event. + /// + /// Requirements: + /// - All requirements in {createWithTimestamps} must be met for the calculated parameters. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. + /// @return streamId The ID of the newly created stream. + function createWithDurations(LockupTranched.CreateWithDurations calldata params) + external + returns (uint256 streamId); + + /// @notice Creates a stream with the provided tranche timestamps, implying the end time from the last timestamp. + /// The stream is funded by `msg.sender` and is wrapped in an ERC-721 NFT. + /// + /// @dev Emits a {Transfer} and {CreateLockupTrancheStream} event. + /// + /// Notes: + /// - As long as the tranche timestamps are arranged in ascending order, it is not an error for some + /// of them to be in the past. + /// + /// Requirements: + /// - Must not be delegate called. + /// - `params.totalAmount` must be greater than zero. + /// - If set, `params.broker.fee` must not be greater than `MAX_BROKER_FEE`. + /// - `params.startTime` must be greater than zero and less than the first tranche's timestamp. + /// - `params.tranches` must have at least one tranche, but not more than `MAX_TRANCHE_COUNT`. + /// - The tranche timestamps must be arranged in ascending order. + /// - The last tranche timestamp (i.e. the stream's end time) must be in the future. + /// - The sum of the tranche amounts must equal the deposit amount. + /// - `params.recipient` must not be the zero address. + /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets. + /// + /// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}. + /// @return streamId The ID of the newly created stream. + function createWithTimestamps(LockupTranched.CreateWithTimestamps calldata params) + external + returns (uint256 streamId); +} diff --git a/src/interfaces/ISablierV2NFTDescriptor.sol b/src/interfaces/ISablierV2NFTDescriptor.sol index b3bb98864..f0d134cb8 100644 --- a/src/interfaces/ISablierV2NFTDescriptor.sol +++ b/src/interfaces/ISablierV2NFTDescriptor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; @@ -10,7 +10,7 @@ interface ISablierV2NFTDescriptor { /// @notice Produces the URI describing a particular stream NFT. /// @dev This is a data URI with the JSON contents directly inlined. /// @param sablier The address of the Sablier contract the stream was created in. - /// @param streamId The id of the stream for which to produce a description. + /// @param streamId The ID of the stream for which to produce a description. /// @return uri The URI of the ERC721-compliant metadata. function tokenURI(IERC721Metadata sablier, uint256 streamId) external view returns (string memory uri); } diff --git a/src/interfaces/erc3156/IERC3156FlashBorrower.sol b/src/interfaces/erc3156/IERC3156FlashBorrower.sol deleted file mode 100644 index d065ba3c3..000000000 --- a/src/interfaces/erc3156/IERC3156FlashBorrower.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -/// @title IERC3156FlashBorrower -/// @notice Interface for ERC-3156 flash borrowers. -/// @dev See https://eips.ethereum.org/EIPS/eip-3156. -interface IERC3156FlashBorrower { - function onFlashLoan( - address initiator, - address asset, - uint256 amount, - uint256 fee, - bytes calldata data - ) - external - returns (bytes32); -} diff --git a/src/interfaces/erc3156/IERC3156FlashLender.sol b/src/interfaces/erc3156/IERC3156FlashLender.sol deleted file mode 100644 index bfecc2757..000000000 --- a/src/interfaces/erc3156/IERC3156FlashLender.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC3156FlashBorrower } from "./IERC3156FlashBorrower.sol"; - -/// @title IERC3156FlashLender -/// @notice Interface for ERC-3156 flash lenders. -/// @dev See https://eips.ethereum.org/EIPS/eip-3156. -interface IERC3156FlashLender { - function maxFlashLoan(address asset) external view returns (uint256); - - function flashFee(address asset, uint256 amount) external view returns (uint256); - - function flashLoan( - IERC3156FlashBorrower receiver, - address asset, - uint256 amount, - bytes calldata data - ) - external - returns (bool); -} diff --git a/src/interfaces/hooks/ISablierV2LockupRecipient.sol b/src/interfaces/hooks/ISablierV2LockupRecipient.sol deleted file mode 100644 index 2e5e3c04b..000000000 --- a/src/interfaces/hooks/ISablierV2LockupRecipient.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -/// @title ISablierV2LockupRecipient -/// @notice Interface for recipient contracts capable of reacting to cancellations, renouncements, and withdrawals. -/// @dev Implementation of this interface is optional. If a recipient contract doesn't implement this -/// interface or implements it partially, function execution will not revert. -interface ISablierV2LockupRecipient { - /// @notice Responds to sender-triggered cancellations. - /// - /// @dev Notes: - /// - This function may revert, but the Sablier contract will ignore the revert. - /// - /// @param streamId The id of the canceled stream. - /// @param sender The stream's sender, who canceled the stream. - /// @param senderAmount The amount of assets refunded to the stream's sender, denoted in units of the asset's - /// decimals. - /// @param recipientAmount The amount of assets left for the stream's recipient to withdraw, denoted in units of - /// the asset's decimals. - function onStreamCanceled( - uint256 streamId, - address sender, - uint128 senderAmount, - uint128 recipientAmount - ) - external; - - /// @notice Responds to renouncements. - /// - /// @dev Notes: - /// - This function may revert, but the Sablier contract will ignore the revert. - /// - /// @param streamId The id of the renounced stream. - function onStreamRenounced(uint256 streamId) external; - - /// @notice Responds to withdrawals triggered by either the stream's sender or an approved third party. - /// - /// @dev Notes: - /// - This function may revert, but the Sablier contract will ignore the revert. - /// - /// @param streamId The id of the stream being withdrawn from. - /// @param caller The original `msg.sender` address that triggered the withdrawal. - /// @param to The address receiving the withdrawn assets. - /// @param amount The amount of assets withdrawn, denoted in units of the asset's decimals. - function onStreamWithdrawn(uint256 streamId, address caller, address to, uint128 amount) external; -} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 147cf9d1a..03e6c8e05 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; @@ -19,55 +18,38 @@ library Errors { error DelegateCall(); /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-BASE - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when trying to claim protocol revenues for an asset with no accrued revenues. - error SablierV2Base_NoProtocolRevenues(IERC20 asset); - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-FLASH-LOAN + SABLIER-V2-LOCKUP //////////////////////////////////////////////////////////////////////////*/ - /// @notice Thrown when trying to flash loan an unsupported asset. - error SablierV2FlashLoan_AssetNotFlashLoanable(IERC20 asset); - - /// @notice Thrown when trying to flash loan an amount greater than or equal to 2^128. - error SablierV2FlashLoan_AmountTooHigh(uint256 amount); - - /// @notice Thrown when the calculated fee during a flash loan is greater than or equal to 2^128. - error SablierV2FlashLoan_CalculatedFeeTooHigh(uint256 amount); + /// @notice Thrown when trying to allow to hook a contract that doesn't implement the interface correctly. + error SablierV2Lockup_AllowToHookUnsupportedInterface(address recipient); - /// @notice Thrown when the callback to the flash borrower fails. - error SablierV2FlashLoan_FlashBorrowFail(); - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-LOCKUP - //////////////////////////////////////////////////////////////////////////*/ + /// @notice Thrown when trying to allow to hook an address with no code. + error SablierV2Lockup_AllowToHookZeroCodeSize(address recipient); /// @notice Thrown when the broker fee exceeds the maximum allowed fee. - error SablierV2Lockup_BrokerFeeTooHigh(UD60x18 brokerFee, UD60x18 maxFee); + error SablierV2Lockup_BrokerFeeTooHigh(UD60x18 brokerFee, UD60x18 maxBrokerFee); /// @notice Thrown when trying to create a stream with a zero deposit amount. error SablierV2Lockup_DepositAmountZero(); /// @notice Thrown when trying to create a stream with an end time not in the future. - error SablierV2Lockup_EndTimeNotInTheFuture(uint40 currentTime, uint40 endTime); + error SablierV2Lockup_EndTimeNotInTheFuture(uint40 blockTimestamp, uint40 endTime); - /// @notice Thrown when the stream's sender tries to withdraw to an address other than the recipient's. - error SablierV2Lockup_InvalidSenderWithdrawal(uint256 streamId, address sender, address to); + /// @notice Thrown when the hook does not return the correct selector. + error SablierV2Lockup_InvalidHookSelector(address recipient); /// @notice Thrown when trying to transfer Stream NFT when transferability is disabled. error SablierV2Lockup_NotTransferable(uint256 tokenId); - /// @notice Thrown when the id references a null stream. + /// @notice Thrown when the ID references a null stream. error SablierV2Lockup_Null(uint256 streamId); /// @notice Thrown when trying to withdraw an amount greater than the withdrawable amount. error SablierV2Lockup_Overdraw(uint256 streamId, uint128 amount, uint128 withdrawableAmount); - /// @notice Thrown when the protocol fee exceeds the maximum allowed fee. - error SablierV2Lockup_ProtocolFeeTooHigh(UD60x18 protocolFee, UD60x18 maxFee); + /// @notice Thrown when trying to create a stream with a zero start time. + error SablierV2Lockup_StartTimeZero(); /// @notice Thrown when trying to cancel or renounce a canceled stream. error SablierV2Lockup_StreamCanceled(uint256 streamId); @@ -87,15 +69,18 @@ library Errors { /// @notice Thrown when `msg.sender` lacks authorization to perform an action. error SablierV2Lockup_Unauthorized(uint256 streamId, address caller); + /// @notice Thrown when trying to withdraw to an address other than the recipient's. + error SablierV2Lockup_WithdrawalAddressNotRecipient(uint256 streamId, address caller, address to); + /// @notice Thrown when trying to withdraw zero assets from a stream. error SablierV2Lockup_WithdrawAmountZero(uint256 streamId); - /// @notice Thrown when trying to withdraw from multiple streams and the number of stream ids does + /// @notice Thrown when trying to withdraw from multiple streams and the number of stream IDs does /// not match the number of withdraw amounts. error SablierV2Lockup_WithdrawArrayCountsNotEqual(uint256 streamIdsCount, uint256 amountsCount); /// @notice Thrown when trying to withdraw to the zero address. - error SablierV2Lockup_WithdrawToZeroAddress(); + error SablierV2Lockup_WithdrawToZeroAddress(uint256 streamId); /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP-DYNAMIC @@ -113,15 +98,15 @@ library Errors { /// @notice Thrown when trying to create a stream with no segments. error SablierV2LockupDynamic_SegmentCountZero(); - /// @notice Thrown when trying to create a stream with unordered segment milestones. - error SablierV2LockupDynamic_SegmentMilestonesNotOrdered( - uint256 index, uint40 previousMilestone, uint40 currentMilestone + /// @notice Thrown when trying to create a stream with unordered segment timestamps. + error SablierV2LockupDynamic_SegmentTimestampsNotOrdered( + uint256 index, uint40 previousTimestamp, uint40 currentTimestamp ); /// @notice Thrown when trying to create a stream with a start time not strictly less than the first - /// segment milestone. - error SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone( - uint40 startTime, uint40 firstSegmentMilestone + /// segment timestamp. + error SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp( + uint40 startTime, uint40 firstSegmentTimestamp ); /*////////////////////////////////////////////////////////////////////////// @@ -131,8 +116,12 @@ library Errors { /// @notice Thrown when trying to create a stream with a cliff time not strictly less than the end time. error SablierV2LockupLinear_CliffTimeNotLessThanEndTime(uint40 cliffTime, uint40 endTime); - /// @notice Thrown when trying to create a stream with a start time greater than the cliff time. - error SablierV2LockupLinear_StartTimeGreaterThanCliffTime(uint40 startTime, uint40 cliffTime); + /// @notice Thrown when trying to create a stream with a start time not strictly less than the cliff time, when the + /// cliff time does not have a zero value. + error SablierV2LockupLinear_StartTimeNotLessThanCliffTime(uint40 startTime, uint40 cliffTime); + + /// @notice Thrown when trying to create a stream with a start time not strictly less than the end time. + error SablierV2LockupLinear_StartTimeNotLessThanEndTime(uint40 startTime, uint40 endTime); /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-NFT-DESCRIPTOR @@ -140,4 +129,31 @@ library Errors { /// @notice Thrown when trying to generate the token URI for an unknown ERC-721 NFT contract. error SablierV2NFTDescriptor_UnknownNFT(IERC721Metadata nft, string symbol); + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-TRANCHE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when trying to create a stream with a deposit amount not equal to the sum of the + /// tranche amounts. + error SablierV2LockupTranched_DepositAmountNotEqualToTrancheAmountsSum( + uint128 depositAmount, uint128 trancheAmountsSum + ); + + /// @notice Thrown when trying to create a stream with a start time not strictly less than the first + /// tranche timestamp. + error SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp( + uint40 startTime, uint40 firstTrancheTimestamp + ); + + /// @notice Thrown when trying to create a stream with more tranches than the maximum allowed. + error SablierV2LockupTranched_TrancheCountTooHigh(uint256 count); + + /// @notice Thrown when trying to create a stream with no tranches. + error SablierV2LockupTranched_TrancheCountZero(); + + /// @notice Thrown when trying to create a stream with unordered tranche timestamps. + error SablierV2LockupTranched_TrancheTimestampsNotOrdered( + uint256 index, uint40 previousTimestamp, uint40 currentTimestamp + ); } diff --git a/src/libraries/Helpers.sol b/src/libraries/Helpers.sol index a0c195fb5..d4bd40b48 100644 --- a/src/libraries/Helpers.sol +++ b/src/libraries/Helpers.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic, LockupLinear } from "../types/DataTypes.sol"; +import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../types/DataTypes.sol"; import { Errors } from "./Errors.sol"; /// @title Helpers @@ -13,50 +13,102 @@ library Helpers { INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Checks that neither fee is greater than `maxFee`, and then calculates the protocol fee amount, the - /// broker fee amount, and the deposit amount from the total amount. - function checkAndCalculateFees( + /// @dev Calculate the timestamps and return the segments. + function calculateSegmentTimestamps(LockupDynamic.SegmentWithDuration[] memory segments) + internal + view + returns (LockupDynamic.Segment[] memory segmentsWithTimestamps) + { + uint256 segmentCount = segments.length; + segmentsWithTimestamps = new LockupDynamic.Segment[](segmentCount); + + // Make the block timestamp the stream's start time. + uint40 startTime = uint40(block.timestamp); + + // It is safe to use unchecked arithmetic because {SablierV2LockupDynamic-_create} will nonetheless check the + // correctness of the calculated segment timestamps. + unchecked { + // The first segment is precomputed because it is needed in the for loop below. + segmentsWithTimestamps[0] = LockupDynamic.Segment({ + amount: segments[0].amount, + exponent: segments[0].exponent, + timestamp: startTime + segments[0].duration + }); + + // Copy the segment amounts and exponents, and calculate the segment timestamps. + for (uint256 i = 1; i < segmentCount; ++i) { + segmentsWithTimestamps[i] = LockupDynamic.Segment({ + amount: segments[i].amount, + exponent: segments[i].exponent, + timestamp: segmentsWithTimestamps[i - 1].timestamp + segments[i].duration + }); + } + } + } + + /// @dev Calculate the timestamps and return the tranches. + function calculateTrancheTimestamps(LockupTranched.TrancheWithDuration[] memory tranches) + internal + view + returns (LockupTranched.Tranche[] memory tranchesWithTimestamps) + { + uint256 trancheCount = tranches.length; + tranchesWithTimestamps = new LockupTranched.Tranche[](trancheCount); + + // Make the block timestamp the stream's start time. + uint40 startTime = uint40(block.timestamp); + + // It is safe to use unchecked arithmetic because {SablierV2LockupTranched-_create} will nonetheless check the + // correctness of the calculated tranche timestamps. + unchecked { + // The first tranche is precomputed because it is needed in the for loop below. + tranchesWithTimestamps[0] = + LockupTranched.Tranche({ amount: tranches[0].amount, timestamp: startTime + tranches[0].duration }); + + // Copy the tranche amounts and calculate the tranche timestamps. + for (uint256 i = 1; i < trancheCount; ++i) { + tranchesWithTimestamps[i] = LockupTranched.Tranche({ + amount: tranches[i].amount, + timestamp: tranchesWithTimestamps[i - 1].timestamp + tranches[i].duration + }); + } + } + } + + /// @dev Checks the broker fee is not greater than `maxBrokerFee`, and then calculates the broker fee amount and the + /// deposit amount from the total amount. + function checkAndCalculateBrokerFee( uint128 totalAmount, - UD60x18 protocolFee, UD60x18 brokerFee, - UD60x18 maxFee + UD60x18 maxBrokerFee ) internal pure returns (Lockup.CreateAmounts memory amounts) { - // When the total amount is zero, the fees are also zero. + // When the total amount is zero, the broker fee is also zero. if (totalAmount == 0) { - return Lockup.CreateAmounts(0, 0, 0); + return Lockup.CreateAmounts(0, 0); } - // Checks: the protocol fee is not greater than `maxFee`. - if (protocolFee.gt(maxFee)) { - revert Errors.SablierV2Lockup_ProtocolFeeTooHigh(protocolFee, maxFee); - } - // Checks: the broker fee is not greater than `maxFee`. - if (brokerFee.gt(maxFee)) { - revert Errors.SablierV2Lockup_BrokerFeeTooHigh(brokerFee, maxFee); + // Check: the broker fee is not greater than `maxBrokerFee`. + if (brokerFee.gt(maxBrokerFee)) { + revert Errors.SablierV2Lockup_BrokerFeeTooHigh(brokerFee, maxBrokerFee); } - // Calculate the protocol fee amount. - // The cast to uint128 is safe because the maximum fee is hard coded. - amounts.protocolFee = uint128(ud(totalAmount).mul(protocolFee).intoUint256()); - // Calculate the broker fee amount. // The cast to uint128 is safe because the maximum fee is hard coded. amounts.brokerFee = uint128(ud(totalAmount).mul(brokerFee).intoUint256()); - // Assert that the total amount is strictly greater than the sum of the protocol fee amount and the - // broker fee amount. - assert(totalAmount > amounts.protocolFee + amounts.brokerFee); + // Assert that the total amount is strictly greater than the broker fee amount. + assert(totalAmount > amounts.brokerFee); - // Calculate the deposit amount (the amount to stream, net of fees). - amounts.deposit = totalAmount - amounts.protocolFee - amounts.brokerFee; + // Calculate the deposit amount (the amount to stream, net of the broker fee). + amounts.deposit = totalAmount - amounts.brokerFee; } - /// @dev Checks the parameters of the {SablierV2LockupDynamic-_createWithMilestones} function. - function checkCreateWithMilestones( + /// @dev Checks the parameters of the {SablierV2LockupDynamic-_create} function. + function checkCreateLockupDynamic( uint128 depositAmount, LockupDynamic.Segment[] memory segments, uint256 maxSegmentCount, @@ -65,81 +117,101 @@ library Helpers { internal view { - // Checks: the deposit amount is not zero. + // Check: the deposit amount is not zero. if (depositAmount == 0) { revert Errors.SablierV2Lockup_DepositAmountZero(); } - // Checks: the segment count is not zero. + // Check: the start time is not zero. + if (startTime == 0) { + revert Errors.SablierV2Lockup_StartTimeZero(); + } + + // Check: the segment count is not zero. uint256 segmentCount = segments.length; if (segmentCount == 0) { revert Errors.SablierV2LockupDynamic_SegmentCountZero(); } - // Checks: the segment count is not greater than the maximum allowed. + // Check: the segment count is not greater than the maximum allowed. if (segmentCount > maxSegmentCount) { revert Errors.SablierV2LockupDynamic_SegmentCountTooHigh(segmentCount); } - // Checks: requirements of segments variables. + // Check: requirements of segments. _checkSegments(segments, depositAmount, startTime); } - /// @dev Checks the parameters of the {SablierV2LockupLinear-_createWithRange} function. - function checkCreateWithRange(uint128 depositAmount, LockupLinear.Range memory range) internal view { - // Checks: the deposit amount is not zero. + /// @dev Checks the parameters of the {SablierV2LockupLinear-_create} function. + function checkCreateLockupLinear(uint128 depositAmount, LockupLinear.Timestamps memory timestamps) internal view { + // Check: the deposit amount is not zero. if (depositAmount == 0) { revert Errors.SablierV2Lockup_DepositAmountZero(); } - // Checks: the start time is less than or equal to the cliff time. - if (range.start > range.cliff) { - revert Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime(range.start, range.cliff); + // Check: the start time is not zero. + if (timestamps.start == 0) { + revert Errors.SablierV2Lockup_StartTimeZero(); } - // Checks: the cliff time is strictly less than the end time. - if (range.cliff >= range.end) { - revert Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime(range.cliff, range.end); + // Since a cliff time of zero means there is no cliff, the following checks are performed only if it's not zero. + if (timestamps.cliff > 0) { + // Check: the start time is strictly less than the cliff time. + if (timestamps.start >= timestamps.cliff) { + revert Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime(timestamps.start, timestamps.cliff); + } + + // Check: the cliff time is strictly less than the end time. + if (timestamps.cliff >= timestamps.end) { + revert Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime(timestamps.cliff, timestamps.end); + } } - // Checks: the end time is in the future. - uint40 currentTime = uint40(block.timestamp); - if (currentTime >= range.end) { - revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(currentTime, range.end); + // Check: the start time is strictly less than the end time. + if (timestamps.start >= timestamps.end) { + revert Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime(timestamps.start, timestamps.end); + } + + // Check: the end time is in the future. + uint40 blockTimestamp = uint40(block.timestamp); + if (blockTimestamp >= timestamps.end) { + revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, timestamps.end); } } - /// @dev Checks that the segment array counts match, and then adjusts the segments by calculating the milestones. - function checkDeltasAndCalculateMilestones(LockupDynamic.SegmentWithDelta[] memory segments) + /// @dev Checks the parameters of the {SablierV2LockupTranched-_create} function. + function checkCreateLockupTranched( + uint128 depositAmount, + LockupTranched.Tranche[] memory tranches, + uint256 maxTrancheCount, + uint40 startTime + ) internal view - returns (LockupDynamic.Segment[] memory segmentsWithMilestones) { - uint256 segmentCount = segments.length; - segmentsWithMilestones = new LockupDynamic.Segment[](segmentCount); + // Check: the deposit amount is not zero. + if (depositAmount == 0) { + revert Errors.SablierV2Lockup_DepositAmountZero(); + } - // Make the current time the stream's start time. - uint40 startTime = uint40(block.timestamp); + // Check: the start time is not zero. + if (startTime == 0) { + revert Errors.SablierV2Lockup_StartTimeZero(); + } - // It is safe to use unchecked arithmetic because {_createWithMilestone} will nonetheless check the soundness - // of the calculated segment milestones. - unchecked { - // Precompute the first segment because of the need to add the start time to the first segment delta. - segmentsWithMilestones[0] = LockupDynamic.Segment({ - amount: segments[0].amount, - exponent: segments[0].exponent, - milestone: startTime + segments[0].delta - }); + // Check: the tranche count is not zero. + uint256 trancheCount = tranches.length; + if (trancheCount == 0) { + revert Errors.SablierV2LockupTranched_TrancheCountZero(); + } - // Copy the segment amounts and exponents, and calculate the segment milestones. - for (uint256 i = 1; i < segmentCount; ++i) { - segmentsWithMilestones[i] = LockupDynamic.Segment({ - amount: segments[i].amount, - exponent: segments[i].exponent, - milestone: segmentsWithMilestones[i - 1].milestone + segments[i].delta - }); - } + // Check: the tranche count is not greater than the maximum allowed. + if (trancheCount > maxTrancheCount) { + revert Errors.SablierV2LockupTranched_TrancheCountTooHigh(trancheCount); } + + // Check: requirements of tranches. + _checkTranches(tranches, depositAmount, startTime); } /*////////////////////////////////////////////////////////////////////////// @@ -148,9 +220,9 @@ library Helpers { /// @dev Checks that: /// - /// 1. The first milestone is strictly greater than the start time. - /// 2. The milestones are ordered chronologically. - /// 3. There are no duplicate milestones. + /// 1. The first timestamp is strictly greater than the start time. + /// 2. The timestamps are ordered chronologically. + /// 3. There are no duplicate timestamps. /// 4. The deposit amount is equal to the sum of all segment amounts. function _checkSegments( LockupDynamic.Segment[] memory segments, @@ -160,56 +232,115 @@ library Helpers { private view { - // Checks: the start time is strictly less than the first segment milestone. - if (startTime >= segments[0].milestone) { - revert Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone( - startTime, segments[0].milestone + // Check: the start time is strictly less than the first segment timestamp. + if (startTime >= segments[0].timestamp) { + revert Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp( + startTime, segments[0].timestamp ); } // Pre-declare the variables needed in the for loop. uint128 segmentAmountsSum; - uint40 currentMilestone; - uint40 previousMilestone; + uint40 currentSegmentTimestamp; + uint40 previousSegmentTimestamp; // Iterate over the segments to: // // 1. Calculate the sum of all segment amounts. - // 2. Check that the milestones are ordered. + // 2. Check that the timestamps are ordered. uint256 count = segments.length; - for (uint256 index = 0; index < count;) { + for (uint256 index = 0; index < count; ++index) { // Add the current segment amount to the sum. segmentAmountsSum += segments[index].amount; - // Checks: the current milestone is strictly greater than the previous milestone. - currentMilestone = segments[index].milestone; - if (currentMilestone <= previousMilestone) { - revert Errors.SablierV2LockupDynamic_SegmentMilestonesNotOrdered( - index, previousMilestone, currentMilestone + // Check: the current timestamp is strictly greater than the previous timestamp. + currentSegmentTimestamp = segments[index].timestamp; + if (currentSegmentTimestamp <= previousSegmentTimestamp) { + revert Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered( + index, previousSegmentTimestamp, currentSegmentTimestamp ); } - // Make the current milestone the previous milestone of the next loop iteration. - previousMilestone = currentMilestone; - - // Increment the loop iterator. - unchecked { - index += 1; - } + // Make the current timestamp the previous timestamp of the next loop iteration. + previousSegmentTimestamp = currentSegmentTimestamp; } - // Checks: the last milestone is in the future. - // When the loop exits, the current milestone is the last milestone, i.e. the stream's end time. - uint40 currentTime = uint40(block.timestamp); - if (currentTime >= currentMilestone) { - revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(currentTime, currentMilestone); + // Check: the last timestamp is in the future. + // When the loop exits, the current segment's timestamp is the last segment's timestamp, i.e. the stream's end + // time. The variable is not renamed for gas efficiency purposes. + uint40 blockTimestamp = uint40(block.timestamp); + if (blockTimestamp >= currentSegmentTimestamp) { + revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentSegmentTimestamp); } - // Checks: the deposit amount is equal to the segment amounts sum. + // Check: the deposit amount is equal to the segment amounts sum. if (depositAmount != segmentAmountsSum) { revert Errors.SablierV2LockupDynamic_DepositAmountNotEqualToSegmentAmountsSum( depositAmount, segmentAmountsSum ); } } + + /// @dev Checks that: + /// + /// 1. The first timestamp is strictly greater than the start time. + /// 2. The timestamps are ordered chronologically. + /// 3. There are no duplicate timestamps. + /// 4. The deposit amount is equal to the sum of all tranche amounts. + function _checkTranches( + LockupTranched.Tranche[] memory tranches, + uint128 depositAmount, + uint40 startTime + ) + private + view + { + // Check: the start time is strictly less than the first tranche timestamp. + if (startTime >= tranches[0].timestamp) { + revert Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp( + startTime, tranches[0].timestamp + ); + } + + // Pre-declare the variables needed in the for loop. + uint128 trancheAmountsSum; + uint40 currentTrancheTimestamp; + uint40 previousTrancheTimestamp; + + // Iterate over the tranches to: + // + // 1. Calculate the sum of all tranche amounts. + // 2. Check that the timestamps are ordered. + uint256 count = tranches.length; + for (uint256 index = 0; index < count; ++index) { + // Add the current tranche amount to the sum. + trancheAmountsSum += tranches[index].amount; + + // Check: the current timestamp is strictly greater than the previous timestamp. + currentTrancheTimestamp = tranches[index].timestamp; + if (currentTrancheTimestamp <= previousTrancheTimestamp) { + revert Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered( + index, previousTrancheTimestamp, currentTrancheTimestamp + ); + } + + // Make the current timestamp the previous timestamp of the next loop iteration. + previousTrancheTimestamp = currentTrancheTimestamp; + } + + // Check: the last timestamp is in the future. + // When the loop exits, the current tranche's timestamp is the last tranche's timestamp, i.e. the stream's end + // time. The variable is not renamed for gas efficiency purposes. + uint40 blockTimestamp = uint40(block.timestamp); + if (blockTimestamp >= currentTrancheTimestamp) { + revert Errors.SablierV2Lockup_EndTimeNotInTheFuture(blockTimestamp, currentTrancheTimestamp); + } + + // Check: the deposit amount is equal to the tranche amounts sum. + if (depositAmount != trancheAmountsSum) { + revert Errors.SablierV2LockupTranched_DepositAmountNotEqualToTrancheAmountsSum( + depositAmount, trancheAmountsSum + ); + } + } } diff --git a/src/libraries/NFTSVG.sol b/src/libraries/NFTSVG.sol index 807b03e3b..0cfa05c0f 100644 --- a/src/libraries/NFTSVG.sol +++ b/src/libraries/NFTSVG.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable quotes -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; @@ -20,8 +20,8 @@ library NFTSVG { string progress; uint256 progressNumerical; string sablierAddress; + string sablierModel; string status; - string streamingModel; } struct SVGVars { @@ -89,7 +89,7 @@ library NFTSVG { '', SVGElements.BACKGROUND, generateDefs(params.accentColor, params.status, vars.cards), - generateFloatingText(params.sablierAddress, params.streamingModel, params.assetAddress, params.assetSymbol), + generateFloatingText(params.sablierAddress, params.sablierModel, params.assetAddress, params.assetSymbol), generateHrefs(vars.progressXPosition, vars.statusXPosition, vars.amountXPosition, vars.durationXPosition), "" ); @@ -119,7 +119,7 @@ library NFTSVG { function generateFloatingText( string memory sablierAddress, - string memory streamingModel, + string memory sablierModel, string memory assetAddress, string memory assetSymbol ) @@ -131,11 +131,11 @@ library NFTSVG { '', SVGElements.floatingText({ offset: "-100%", - text: string.concat(sablierAddress, unicode" • ", "Sablier V2 ", streamingModel) + text: string.concat(sablierAddress, unicode" • ", "Sablier V2 ", sablierModel) }), SVGElements.floatingText({ offset: "0%", - text: string.concat(sablierAddress, unicode" • ", "Sablier V2 ", streamingModel) + text: string.concat(sablierAddress, unicode" • ", "Sablier V2 ", sablierModel) }), SVGElements.floatingText({ offset: "-50%", text: string.concat(assetAddress, unicode" • ", assetSymbol) }), SVGElements.floatingText({ offset: "50%", text: string.concat(assetAddress, unicode" • ", assetSymbol) }), diff --git a/src/libraries/SVGElements.sol b/src/libraries/SVGElements.sol index 0c8461dbe..92f1bbdd0 100644 --- a/src/libraries/SVGElements.sol +++ b/src/libraries/SVGElements.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable max-line-length,quotes -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; @@ -239,12 +239,11 @@ library SVGElements { unchecked { uint256 charWidth = largeFont ? 16 : 13; uint256 semicolonIndex; - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ++i) { if (bytes(text)[i] == ";") { semicolonIndex = i; } width += charWidth; - i += 1; } // Account for escaped characters (such as ≥). diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index 48a899c75..c73b01b93 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UD2x18 } from "@prb/math/src/UD2x18.sol"; @@ -12,6 +12,7 @@ import { UD60x18 } from "@prb/math/src/UD60x18.sol"; // - Lockup // - LockupDynamic // - LockupLinear +// - LockupTranched // // You will notice that some structs contain "slot" annotations - they are used to indicate the // storage layout of the struct. It is more gas efficient to group small data types together so @@ -27,11 +28,11 @@ struct Broker { /// @notice Namespace for the structs used in both {SablierV2LockupLinear} and {SablierV2LockupDynamic}. library Lockup { - /// @notice Struct encapsulating the deposit, withdrawn, and refunded amounts, all denoted in units - /// of the asset's decimals. - /// @dev Because the deposited and the withdrawn amount are often read together, declaring them in - /// the same slot saves gas. - /// @param deposited The initial amount deposited in the stream, net of fees. + /// @notice Struct encapsulating the deposit, withdrawn, and refunded amounts, both denoted in units of the asset's + /// decimals. + /// @dev Because the deposited and the withdrawn amount are often read together, declaring them in the same slot + /// saves gas. + /// @param deposited The initial amount deposited in the stream, net of broker fee. /// @param withdrawn The cumulative amount withdrawn from the stream. /// @param refunded The amount refunded to the sender. Unless the stream was canceled, this is always zero. struct Amounts { @@ -42,155 +43,166 @@ library Lockup { uint128 refunded; } - /// @notice Struct encapsulating the deposit amount, the protocol fee amount, and the broker fee amount, - /// all denoted in units of the asset's decimals. + /// @notice Struct encapsulating the deposit amount and the broker fee amount, both denoted in units of the asset's + /// decimals. /// @param deposit The amount to deposit in the stream. - /// @param protocolFee The protocol fee amount. /// @param brokerFee The broker fee amount. struct CreateAmounts { uint128 deposit; - uint128 protocolFee; uint128 brokerFee; } /// @notice Enum representing the different statuses of a stream. - /// @custom:value PENDING Stream created but not started; assets are in a pending state. - /// @custom:value STREAMING Active stream where assets are currently being streamed. - /// @custom:value SETTLED All assets have been streamed; recipient is due to withdraw them. - /// @custom:value CANCELED Canceled stream; remaining assets await recipient's withdrawal. - /// @custom:value DEPLETED Depleted stream; all assets have been withdrawn and/or refunded. + /// @custom:value0 PENDING Stream created but not started; assets are in a pending state. + /// @custom:value1 STREAMING Active stream where assets are currently being streamed. + /// @custom:value2 SETTLED All assets have been streamed; recipient is due to withdraw them. + /// @custom:value3 CANCELED Canceled stream; remaining assets await recipient's withdrawal. + /// @custom:value4 DEPLETED Depleted stream; all assets have been withdrawn and/or refunded. enum Status { - PENDING, // value 0 - STREAMING, // value 1 - SETTLED, // value 2 - CANCELED, // value 3 - DEPLETED // value 4 + PENDING, + STREAMING, + SETTLED, + CANCELED, + DEPLETED + } + + /// @notice A common data structure to be stored in all {SablierV2Lockup} models. + /// @dev The fields are arranged like this to save gas via tight variable packing. + /// @param sender The address distributing the assets, with the ability to cancel the stream. + /// @param startTime The Unix timestamp indicating the stream's start. + /// @param endTime The Unix timestamp indicating the stream's end. + /// @param isCancelable Boolean indicating if the stream is cancelable. + /// @param wasCanceled Boolean indicating if the stream was canceled. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param isDepleted Boolean indicating if the stream is depleted. + /// @param isStream Boolean indicating if the struct entity exists. + /// @param isTransferable Boolean indicating if the stream NFT is transferable. + /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, both denoted in units of the + /// asset's decimals. + struct Stream { + // slot 0 + address sender; + uint40 startTime; + uint40 endTime; + bool isCancelable; + bool wasCanceled; + // slot 1 + IERC20 asset; + bool isDepleted; + bool isStream; + bool isTransferable; + // slot 2 and 3 + Lockup.Amounts amounts; } } /// @notice Namespace for the structs used in {SablierV2LockupDynamic}. library LockupDynamic { - /// @notice Struct encapsulating the parameters for the {SablierV2LockupDynamic.createWithDeltas} function. - /// @param sender The address streaming the assets, with the ability to cancel the stream. It doesn't have to be the - /// same as `msg.sender`. + /// @notice Struct encapsulating the parameters of the {SablierV2LockupDynamic.createWithDurations} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. /// @param recipient The address receiving the assets. - /// @param totalAmount The total amount of ERC-20 assets to be paid, including the stream deposit and any potential - /// fees, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Indicates if the stream is cancelable. /// @param transferable Indicates if the stream NFT is transferable. + /// @param segments Segments with durations used to compose the dynamic distribution function. Timestamps are + /// calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. /// @param broker Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. - /// @param segments Segments with deltas used to compose the custom streaming curve. Milestones are calculated by - /// starting from `block.timestamp` and adding each delta to the previous milestone. - struct CreateWithDeltas { + struct CreateWithDurations { address sender; - bool cancelable; - bool transferable; address recipient; uint128 totalAmount; IERC20 asset; + bool cancelable; + bool transferable; + SegmentWithDuration[] segments; Broker broker; - SegmentWithDelta[] segments; } - /// @notice Struct encapsulating the parameters for the {SablierV2LockupDynamic.createWithMilestones} - /// function. - /// @param sender The address streaming the assets, with the ability to cancel the stream. It doesn't have to be the - /// same as `msg.sender`. - /// @param startTime The Unix timestamp indicating the stream's start. + /// @notice Struct encapsulating the parameters of the {SablierV2LockupDynamic.createWithTimestamps} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. + /// @param recipient The address receiving the assets. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Indicates if the stream is cancelable. /// @param transferable Indicates if the stream NFT is transferable. - /// @param recipient The address receiving the assets. - /// @param totalAmount The total amount of ERC-20 assets to be paid, including the stream deposit and any potential - /// fees, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param startTime The Unix timestamp indicating the stream's start. + /// @param segments Segments used to compose the dynamic distribution function. /// @param broker Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. - /// @param segments Segments used to compose the custom streaming curve. - struct CreateWithMilestones { + struct CreateWithTimestamps { address sender; - uint40 startTime; - bool cancelable; - bool transferable; address recipient; uint128 totalAmount; IERC20 asset; - Broker broker; + bool cancelable; + bool transferable; + uint40 startTime; Segment[] segments; - } - - /// @notice Struct encapsulating the time range. - /// @param start The Unix timestamp indicating the stream's start. - /// @param end The Unix timestamp indicating the stream's end. - struct Range { - uint40 start; - uint40 end; + Broker broker; } /// @notice Segment struct used in the Lockup Dynamic stream. - /// @param amount The amount of assets to be streamed in this segment, denoted in units of the asset's decimals. - /// @param exponent The exponent of this segment, denoted as a fixed-point number. - /// @param milestone The Unix timestamp indicating this segment's end. + /// @param amount The amount of assets to be streamed in the segment, denoted in units of the asset's decimals. + /// @param exponent The exponent of the segment, denoted as a fixed-point number. + /// @param timestamp The Unix timestamp indicating the segment's end. struct Segment { // slot 0 uint128 amount; UD2x18 exponent; - uint40 milestone; + uint40 timestamp; } - /// @notice Segment struct used at runtime in {SablierV2LockupDynamic.createWithDeltas}. - /// @param amount The amount of assets to be streamed in this segment, denoted in units of the asset's decimals. - /// @param exponent The exponent of this segment, denoted as a fixed-point number. - /// @param delta The time difference in seconds between this segment and the previous one. - struct SegmentWithDelta { + /// @notice Segment struct used at runtime in {SablierV2LockupDynamic.createWithDurations}. + /// @param amount The amount of assets to be streamed in the segment, denoted in units of the asset's decimals. + /// @param exponent The exponent of the segment, denoted as a fixed-point number. + /// @param duration The time difference in seconds between the segment and the previous one. + struct SegmentWithDuration { uint128 amount; UD2x18 exponent; - uint40 delta; + uint40 duration; } - /// @notice Lockup Dynamic stream. - /// @dev The fields are arranged like this to save gas via tight variable packing. - /// @param sender The address streaming the assets, with the ability to cancel the stream. - /// @param startTime The Unix timestamp indicating the stream's start. - /// @param endTime The Unix timestamp indicating the stream's end. - /// @param isCancelable Boolean indicating if the stream is cancelable. - /// @param wasCanceled Boolean indicating if the stream was canceled. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param isDepleted Boolean indicating if the stream is depleted. - /// @param isStream Boolean indicating if the struct entity exists. - /// @param isTransferable Boolean indicating if the stream NFT is transferable. - /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the - /// asset's decimals. - /// @param segments Segments used to compose the custom streaming curve. - struct Stream { - // slot 0 + /// @notice Struct encapsulating the full details of a stream. + /// @dev Extends `Lockup.Stream` by including the recipient and the segments. + struct StreamLD { address sender; + address recipient; uint40 startTime; uint40 endTime; bool isCancelable; bool wasCanceled; - // slot 1 IERC20 asset; bool isDepleted; bool isStream; bool isTransferable; - // slot 2 and 3 Lockup.Amounts amounts; - // slots [4..n] Segment[] segments; } + + /// @notice Struct encapsulating the LockupDynamic timestamps. + /// @param start The Unix timestamp indicating the stream's start. + /// @param end The Unix timestamp indicating the stream's end. + struct Timestamps { + uint40 start; + uint40 end; + } } /// @notice Namespace for the structs used in {SablierV2LockupLinear}. library LockupLinear { - /// @notice Struct encapsulating the parameters for the {SablierV2LockupLinear.createWithDurations} function. - /// @param sender The address streaming the assets, with the ability to cancel the stream. It doesn't have to be the - /// same as `msg.sender`. + /// @notice Struct encapsulating the parameters of the {SablierV2LockupLinear.createWithDurations} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. /// @param recipient The address receiving the assets. - /// @param totalAmount The total amount of ERC-20 assets to be paid, including the stream deposit and any potential - /// fees, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Indicates if the stream is cancelable. /// @param transferable Indicates if the stream NFT is transferable. /// @param durations Struct containing (i) cliff period duration and (ii) total stream duration, both in seconds. @@ -207,27 +219,27 @@ library LockupLinear { Broker broker; } - /// @notice Struct encapsulating the parameters for the {SablierV2LockupLinear.createWithRange} function. - /// @param sender The address streaming the assets, with the ability to cancel the stream. It doesn't have to be the - /// same as `msg.sender`. + /// @notice Struct encapsulating the parameters of the {SablierV2LockupLinear.createWithTimestamps} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. /// @param recipient The address receiving the assets. - /// @param totalAmount The total amount of ERC-20 assets to be paid, including the stream deposit and any potential - /// fees, all denoted in units of the asset's decimals. - /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. /// @param cancelable Indicates if the stream is cancelable. /// @param transferable Indicates if the stream NFT is transferable. - /// @param range Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as Unix - /// timestamps. + /// @param timestamps Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as + /// Unix timestamps. /// @param broker Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. - struct CreateWithRange { + struct CreateWithTimestamps { address sender; address recipient; uint128 totalAmount; IERC20 asset; bool cancelable; bool transferable; - Range range; + Timestamps timestamps; Broker broker; } @@ -239,44 +251,124 @@ library LockupLinear { uint40 total; } - /// @notice Struct encapsulating the time range. + /// @notice Struct encapsulating the full details of a stream. + /// @dev Extends `Lockup.Stream` by including the recipient and the cliff time. + struct StreamLL { + address sender; + address recipient; + uint40 startTime; + bool isCancelable; + bool wasCanceled; + IERC20 asset; + uint40 endTime; + bool isDepleted; + bool isStream; + bool isTransferable; + Lockup.Amounts amounts; + uint40 cliffTime; + } + + /// @notice Struct encapsulating the LockupLinear timestamps. /// @param start The Unix timestamp for the stream's start. - /// @param cliff The Unix timestamp for the cliff period's end. + /// @param cliff The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. /// @param end The Unix timestamp for the stream's end. - struct Range { + struct Timestamps { uint40 start; uint40 cliff; uint40 end; } +} - /// @notice Lockup Linear stream. - /// @dev The fields are arranged like this to save gas via tight variable packing. - /// @param sender The address streaming the assets, with the ability to cancel the stream. +/// @notice Namespace for the structs used in {SablierV2LockupTranched}. +library LockupTranched { + /// @notice Struct encapsulating the parameters of the {SablierV2LockupTranched.createWithDurations} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. + /// @param recipient The address receiving the assets. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param cancelable Indicates if the stream is cancelable. + /// @param transferable Indicates if the stream NFT is transferable. + /// @param tranches Tranches with durations used to compose the tranched distribution function. Timestamps are + /// calculated by starting from `block.timestamp` and adding each duration to the previous timestamp. + /// @param broker Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the + /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. + struct CreateWithDurations { + address sender; + address recipient; + uint128 totalAmount; + IERC20 asset; + bool cancelable; + bool transferable; + TrancheWithDuration[] tranches; + Broker broker; + } + + /// @notice Struct encapsulating the parameters of the {SablierV2LockupTranched.createWithTimestamps} function. + /// @param sender The address distributing the assets, with the ability to cancel the stream. It doesn't have to be + /// the same as `msg.sender`. + /// @param recipient The address receiving the assets. + /// @param totalAmount The total amount of ERC-20 assets to be distributed, including the stream deposit and any + /// broker fee, both denoted in units of the asset's decimals. + /// @param asset The contract address of the ERC-20 asset to be distributed. + /// @param cancelable Indicates if the stream is cancelable. + /// @param transferable Indicates if the stream NFT is transferable. /// @param startTime The Unix timestamp indicating the stream's start. - /// @param cliffTime The Unix timestamp indicating the cliff period's end. - /// @param isCancelable Boolean indicating if the stream is cancelable. - /// @param wasCanceled Boolean indicating if the stream was canceled. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param endTime The Unix timestamp indicating the stream's end. - /// @param isDepleted Boolean indicating if the stream is depleted. - /// @param isStream Boolean indicating if the struct entity exists. - /// @param isTransferable Boolean indicating if the stream NFT is transferable. - /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the - /// asset's decimals. - struct Stream { - // slot 0 + /// @param tranches Tranches used to compose the tranched distribution function. + /// @param broker Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the + /// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point number. Both can be set to zero. + struct CreateWithTimestamps { + address sender; + address recipient; + uint128 totalAmount; + IERC20 asset; + bool cancelable; + bool transferable; + uint40 startTime; + Tranche[] tranches; + Broker broker; + } + + /// @notice Struct encapsulating the full details of a stream. + /// @dev Extends `Lockup.Stream` by including the recipient and the tranches. + struct StreamLT { address sender; + address recipient; uint40 startTime; - uint40 cliffTime; + uint40 endTime; bool isCancelable; bool wasCanceled; - // slot 1 IERC20 asset; - uint40 endTime; bool isDepleted; bool isStream; bool isTransferable; - // slot 2 and 3 Lockup.Amounts amounts; + Tranche[] tranches; + } + + /// @notice Struct encapsulating the LockupTranched timestamps. + /// @param start The Unix timestamp indicating the stream's start. + /// @param end The Unix timestamp indicating the stream's end. + struct Timestamps { + uint40 start; + uint40 end; + } + + /// @notice Tranche struct used in the Lockup Tranched stream. + /// @param amount The amount of assets to be unlocked in the tranche, denoted in units of the asset's decimals. + /// @param timestamp The Unix timestamp indicating the tranche's end. + struct Tranche { + // slot 0 + uint128 amount; + uint40 timestamp; + } + + /// @notice Tranche struct used at runtime in {SablierV2LockupTranched.createWithDurations}. + /// @param amount The amount of assets to be unlocked in the tranche, denoted in units of the asset's decimals. + /// @param duration The time difference in seconds between the tranche and the previous one. + struct TrancheWithDuration { + uint128 amount; + uint40 duration; } } diff --git a/test/Base.t.sol b/test/Base.t.sol index b735e2f5f..f11fc8ca3 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -1,22 +1,21 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2Comptroller } from "../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2LockupDynamic } from "../src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "../src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "../src/interfaces/ISablierV2LockupTranched.sol"; import { ISablierV2NFTDescriptor } from "../src/interfaces/ISablierV2NFTDescriptor.sol"; -import { SablierV2Comptroller } from "../src/SablierV2Comptroller.sol"; import { SablierV2LockupDynamic } from "../src/SablierV2LockupDynamic.sol"; import { SablierV2LockupLinear } from "../src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "../src/SablierV2LockupTranched.sol"; import { SablierV2NFTDescriptor } from "../src/SablierV2NFTDescriptor.sol"; +import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol"; import { ERC20MissingReturn } from "./mocks/erc20/ERC20MissingReturn.sol"; -import { GoodFlashLoanReceiver } from "./mocks/flash-loan/GoodFlashLoanReceiver.sol"; import { Noop } from "./mocks/Noop.sol"; -import { GoodRecipient } from "./mocks/hooks/GoodRecipient.sol"; +import { RecipientGood } from "./mocks/Hooks.sol"; import { Assertions } from "./utils/Assertions.sol"; import { Calculations } from "./utils/Calculations.sol"; import { Constants } from "./utils/Constants.sol"; @@ -38,13 +37,12 @@ abstract contract Base_Test is Assertions, Calculations, Constants, DeployOptimi TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - ISablierV2Comptroller internal comptroller; - ERC20 internal dai; + ERC20Mock internal dai; Defaults internal defaults; - GoodFlashLoanReceiver internal goodFlashLoanReceiver; - GoodRecipient internal goodRecipient; + RecipientGood internal recipientGood; ISablierV2LockupDynamic internal lockupDynamic; ISablierV2LockupLinear internal lockupLinear; + ISablierV2LockupTranched internal lockupTranched; ISablierV2NFTDescriptor internal nftDescriptor; Noop internal noop; ERC20MissingReturn internal usdt; @@ -55,80 +53,64 @@ abstract contract Base_Test is Assertions, Calculations, Constants, DeployOptimi function setUp() public virtual { // Deploy the base test contracts. - dai = new ERC20("Dai Stablecoin", "DAI"); - goodFlashLoanReceiver = new GoodFlashLoanReceiver(); - goodRecipient = new GoodRecipient(); + dai = new ERC20Mock("Dai Stablecoin", "DAI"); + recipientGood = new RecipientGood(); noop = new Noop(); usdt = new ERC20MissingReturn("Tether USD", "USDT", 6); // Label the base test contracts. vm.label({ account: address(dai), newLabel: "DAI" }); - vm.label({ account: address(goodFlashLoanReceiver), newLabel: "Good Flash Loan Receiver" }); - vm.label({ account: address(goodRecipient), newLabel: "Good Recipient" }); - vm.label({ account: address(nftDescriptor), newLabel: "NFT Descriptor" }); + vm.label({ account: address(recipientGood), newLabel: "Good Recipient" }); vm.label({ account: address(noop), newLabel: "Noop" }); vm.label({ account: address(usdt), newLabel: "USDT" }); - // Create users for testing. - users = Users({ - admin: createUser("Admin"), - alice: createUser("Alice"), - broker: createUser("Broker"), - eve: createUser("Eve"), - operator: createUser("Operator"), - recipient: createUser("Recipient"), - sender: createUser("Sender") - }); - // Deploy the defaults contract. defaults = new Defaults(); defaults.setAsset(dai); + + // Create the protocol admin. + users.admin = payable(makeAddr({ name: "Admin" })); + vm.startPrank({ msgSender: users.admin }); + + // Deploy the V2 Core contracts. + deployCoreConditionally(); + + // Create users for testing. + users.alice = createUser("Alice"); + users.broker = createUser("Broker"); + users.eve = createUser("Eve"); + users.operator = createUser("Operator"); + users.recipient = createUser("Recipient"); + users.sender = createUser("Sender"); + defaults.setUsers(users); - // Warp to May 1, 2023 at 00:00 GMT to provide a more realistic testing environment. - vm.warp({ timestamp: MAY_1_2023 }); + // Warp to May 1, 2024 at 00:00 GMT to provide a more realistic testing environment. + vm.warp({ newTimestamp: MAY_1_2024 }); } /*////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Approves all V2 Core contracts to spend assets from the Sender, Recipient, Alice and Eve. - function approveProtocol() internal { - changePrank({ msgSender: users.sender }); - dai.approve({ spender: address(lockupLinear), amount: MAX_UINT256 }); - dai.approve({ spender: address(lockupDynamic), amount: MAX_UINT256 }); + /// @dev Approves all V2 Core contracts to spend assets from the address passed. + function approveProtocol(address from) internal { + resetPrank({ msgSender: from }); + dai.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); + dai.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); + dai.approve({ spender: address(lockupTranched), value: MAX_UINT256 }); usdt.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); usdt.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); - - changePrank({ msgSender: users.recipient }); - dai.approve({ spender: address(lockupLinear), amount: MAX_UINT256 }); - dai.approve({ spender: address(lockupDynamic), amount: MAX_UINT256 }); - usdt.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); - usdt.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); - - changePrank({ msgSender: users.alice }); - dai.approve({ spender: address(lockupLinear), amount: MAX_UINT256 }); - dai.approve({ spender: address(lockupDynamic), amount: MAX_UINT256 }); - usdt.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); - usdt.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); - - changePrank({ msgSender: users.eve }); - dai.approve({ spender: address(lockupLinear), amount: MAX_UINT256 }); - dai.approve({ spender: address(lockupDynamic), amount: MAX_UINT256 }); - usdt.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); - usdt.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); - - // Finally, change the active prank back to the Admin. - changePrank({ msgSender: users.admin }); + usdt.approve({ spender: address(lockupTranched), value: MAX_UINT256 }); } - /// @dev Generates a user, labels its address, and funds it with test assets. + /// @dev Generates a user, labels its address, funds it with test assets, and approves the protocol contracts. function createUser(string memory name) internal returns (address payable) { address payable user = payable(makeAddr(name)); vm.deal({ account: user, newBalance: 100 ether }); deal({ token: address(dai), to: user, give: 1_000_000e18 }); deal({ token: address(usdt), to: user, give: 1_000_000e18 }); + approveProtocol({ from: user }); return user; } @@ -138,20 +120,19 @@ abstract contract Base_Test is Assertions, Calculations, Constants, DeployOptimi /// deployer's nonce, which would in turn lead to different addresses (recall that the addresses /// for contracts deployed via `CREATE` are based on the caller-and-nonce-hash). function deployCoreConditionally() internal { - if (!isTestOptimizedProfile()) { - comptroller = new SablierV2Comptroller(users.admin); + if (!isBenchmarkProfile() && !isTestOptimizedProfile()) { nftDescriptor = new SablierV2NFTDescriptor(); - lockupDynamic = - new SablierV2LockupDynamic(users.admin, comptroller, nftDescriptor, defaults.MAX_SEGMENT_COUNT()); - lockupLinear = new SablierV2LockupLinear(users.admin, comptroller, nftDescriptor); + lockupDynamic = new SablierV2LockupDynamic(users.admin, nftDescriptor, defaults.MAX_SEGMENT_COUNT()); + lockupLinear = new SablierV2LockupLinear(users.admin, nftDescriptor); + lockupTranched = new SablierV2LockupTranched(users.admin, nftDescriptor, defaults.MAX_TRANCHE_COUNT()); } else { - (comptroller, lockupDynamic, lockupLinear, nftDescriptor) = - deployOptimizedCore(users.admin, defaults.MAX_SEGMENT_COUNT()); + (lockupDynamic, lockupLinear, lockupTranched, nftDescriptor) = + deployOptimizedCore(users.admin, defaults.MAX_SEGMENT_COUNT(), defaults.MAX_TRANCHE_COUNT()); } - vm.label({ account: address(comptroller), newLabel: "Comptroller" }); vm.label({ account: address(lockupDynamic), newLabel: "LockupDynamic" }); vm.label({ account: address(lockupLinear), newLabel: "LockupLinear" }); + vm.label({ account: address(lockupTranched), newLabel: "LockupTranched" }); vm.label({ account: address(nftDescriptor), newLabel: "NFTDescriptor" }); } @@ -160,22 +141,22 @@ abstract contract Base_Test is Assertions, Calculations, Constants, DeployOptimi //////////////////////////////////////////////////////////////////////////*/ /// @dev Expects a call to {IERC20.transfer}. - function expectCallToTransfer(address to, uint256 amount) internal { - vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transfer, (to, amount)) }); + function expectCallToTransfer(address to, uint256 value) internal { + vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transfer, (to, value)) }); } /// @dev Expects a call to {IERC20.transfer}. - function expectCallToTransfer(IERC20 asset, address to, uint256 amount) internal { - vm.expectCall({ callee: address(asset), data: abi.encodeCall(IERC20.transfer, (to, amount)) }); + function expectCallToTransfer(IERC20 asset, address to, uint256 value) internal { + vm.expectCall({ callee: address(asset), data: abi.encodeCall(IERC20.transfer, (to, value)) }); } /// @dev Expects a call to {IERC20.transferFrom}. - function expectCallToTransferFrom(address from, address to, uint256 amount) internal { - vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transferFrom, (from, to, amount)) }); + function expectCallToTransferFrom(address from, address to, uint256 value) internal { + vm.expectCall({ callee: address(dai), data: abi.encodeCall(IERC20.transferFrom, (from, to, value)) }); } /// @dev Expects a call to {IERC20.transferFrom}. - function expectCallToTransferFrom(IERC20 asset, address from, address to, uint256 amount) internal { - vm.expectCall({ callee: address(asset), data: abi.encodeCall(IERC20.transferFrom, (from, to, amount)) }); + function expectCallToTransferFrom(IERC20 asset, address from, address to, uint256 value) internal { + vm.expectCall({ callee: address(asset), data: abi.encodeCall(IERC20.transferFrom, (from, to, value)) }); } } diff --git a/test/fork/Fork.t.sol b/test/fork/Fork.t.sol index 257f93aaf..770219d1d 100644 --- a/test/fork/Fork.t.sol +++ b/test/fork/Fork.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -9,16 +9,11 @@ import { Base_Test } from "../Base.t.sol"; /// @notice Common logic needed by all fork tests. abstract contract Fork_Test is Base_Test { /*////////////////////////////////////////////////////////////////////////// - CONSTANTS + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ IERC20 internal immutable ASSET; address internal immutable HOLDER; - - /*////////////////////////////////////////////////////////////////////////// - VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - uint256 internal initialHolderBalance; /*////////////////////////////////////////////////////////////////////////// @@ -36,19 +31,16 @@ abstract contract Fork_Test is Base_Test { function setUp() public virtual override { // Fork Ethereum Mainnet at a specific block number. - vm.createSelectFork({ blockNumber: 16_126_000, urlOrAlias: "mainnet" }); + vm.createSelectFork({ blockNumber: 19_000_000, urlOrAlias: "mainnet" }); // The base is set up after the fork is selected so that the base test contracts are deployed on the fork. Base_Test.setUp(); - // Deploy V2 Core. - deployCoreConditionally(); - // Label the contracts. labelContracts(); // Make the ASSET HOLDER the caller in this test suite. - vm.startPrank({ msgSender: HOLDER }); + resetPrank({ msgSender: HOLDER }); // Query the initial balance of the ASSET HOLDER. initialHolderBalance = ASSET.balanceOf(HOLDER); diff --git a/test/fork/LockupDynamic.t.sol b/test/fork/LockupDynamic.t.sol index 54c671cb4..67ccc5e4e 100644 --- a/test/fork/LockupDynamic.t.sol +++ b/test/fork/LockupDynamic.t.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Solarray } from "solarray/src/Solarray.sol"; import { Broker, Lockup, LockupDynamic } from "src/types/DataTypes.sol"; @@ -34,15 +33,13 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { //////////////////////////////////////////////////////////////////////////*/ struct Params { - Broker broker; - UD60x18 protocolFee; - address recipient; address sender; + address recipient; + uint128 withdrawAmount; uint40 startTime; uint40 warpTimestamp; LockupDynamic.Segment[] segments; - uint128 withdrawAmount; - bool transferable; + Broker broker; } struct Vars { @@ -61,20 +58,17 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { bool isCancelable; bool isDepleted; bool isSettled; - LockupDynamic.Range range; uint256 streamId; + LockupDynamic.Timestamps timestamps; // Create vars uint256 actualBrokerBalance; uint256 actualHolderBalance; uint256 actualNextStreamId; - uint256 actualProtocolRevenues; Lockup.CreateAmounts createAmounts; uint256 expectedBrokerBalance; uint256 expectedHolderBalance; - uint256 expectedProtocolRevenues; uint256 expectedNextStreamId; uint256 initialBrokerBalance; - uint256 initialProtocolRevenues; uint128 totalAmount; // Withdraw vars uint128 actualWithdrawnAmount; @@ -92,8 +86,7 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { /// /// - It should perform all expected ERC-20 transfers. /// - It should create the stream. - /// - It should bump the next stream id. - /// - It should record the protocol fee. + /// - It should bump the next stream ID. /// - It should mint the NFT. /// - It should emit a {CreateLockupDynamicStream} event. /// - It may make a withdrawal. @@ -109,45 +102,34 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { /// - Start time in the past /// - Start time in the present /// - Start time in the future - /// - Start time equal and not equal to the first segment milestone + /// - Start time equal and not equal to the first segment timestamp /// - Multiple values for the broker fee, including zero - /// - Multiple values for the protocol fee, including zero /// - Multiple values for the withdraw amount, including zero /// - The whole gamut of stream statuses function testForkFuzz_LockupDynamic_CreateWithdrawCancel(Params memory params) external { checkUsers(params.sender, params.recipient, params.broker.account, address(lockupDynamic)); vm.assume(params.segments.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - params.protocolFee = _bound(params.protocolFee, 0, MAX_FEE); - params.startTime = boundUint40(params.startTime, 0, defaults.START_TIME()); - params.transferable = true; + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.startTime = boundUint40(params.startTime, 1, defaults.START_TIME()); - // Fuzz the segment milestones. - fuzzSegmentMilestones(params.segments, params.startTime); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(params.segments, params.startTime); - // Fuzz the segment amounts and calculate the create amounts (total, deposit, protocol fee, and broker fee). + // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). Vars memory vars; (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts({ upperBound: uint128(initialHolderBalance), segments: params.segments, - protocolFee: params.protocolFee, brokerFee: params.broker.fee }); - // Set the fuzzed protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: ASSET, newProtocolFee: params.protocolFee }); - // Make the holder the caller. - changePrank(HOLDER); + resetPrank(HOLDER); /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create protocol revenues. - vars.initialProtocolRevenues = lockupDynamic.protocolRevenues(ASSET); - // Load the pre-create asset balances. vars.balances = getTokenBalances(address(ASSET), Solarray.addresses(address(lockupDynamic), params.broker.account)); @@ -155,8 +137,10 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { vars.initialBrokerBalance = vars.balances[1]; vars.streamId = lockupDynamic.nextStreamId(); - vars.range = - LockupDynamic.Range({ start: params.startTime, end: params.segments[params.segments.length - 1].milestone }); + vars.timestamps = LockupDynamic.Timestamps({ + start: params.startTime, + end: params.segments[params.segments.length - 1].timestamp + }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockupDynamic) }); @@ -170,41 +154,42 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { amounts: vars.createAmounts, asset: ASSET, cancelable: true, - transferable: params.transferable, + transferable: true, segments: params.segments, - range: vars.range, + timestamps: vars.timestamps, broker: params.broker.account }); // Create the stream. - lockupDynamic.createWithMilestones( - LockupDynamic.CreateWithMilestones({ + lockupDynamic.createWithTimestamps( + LockupDynamic.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: vars.totalAmount, asset: ASSET, - broker: params.broker, cancelable: true, - transferable: params.transferable, - recipient: params.recipient, - segments: params.segments, - sender: params.sender, + transferable: true, startTime: params.startTime, - totalAmount: vars.totalAmount + segments: params.segments, + broker: params.broker }) ); - // Check if the stream is settled. It is possible for a lockupDynamic stream to settle at the time of creation + // Check if the stream is settled. It is possible for a Lockup Dynamic stream to settle at the time of creation // because some segment amounts can be zero. vars.isSettled = lockupDynamic.refundableAmountOf(vars.streamId) == 0; vars.isCancelable = vars.isSettled ? false : true; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(vars.streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(vars.streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, ASSET, "asset"); - assertEq(actualStream.endTime, vars.range.end, "endTime"); + assertEq(actualStream.endTime, vars.timestamps.end, "endTime"); assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); assertEq(actualStream.isDepleted, false, "isDepleted"); - assertEq(actualStream.isTransferable, true, "isTransferable"); assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); assertEq(actualStream.segments, params.segments, "segments"); assertEq(actualStream.sender, params.sender, "sender"); assertEq(actualStream.startTime, params.startTime, "startTime"); @@ -221,16 +206,11 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { } assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. vars.actualNextStreamId = lockupDynamic.nextStreamId(); vars.expectedNextStreamId = vars.streamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - // Assert that the protocol fee has been recorded. - vars.actualProtocolRevenues = lockupDynamic.protocolRevenues(ASSET); - vars.expectedProtocolRevenues = vars.initialProtocolRevenues + vars.createAmounts.protocolFee; - assertEq(vars.actualProtocolRevenues, vars.expectedProtocolRevenues, "post-create protocolRevenues"); - // Assert that the NFT has been minted. vars.actualNFTOwner = lockupDynamic.ownerOf({ tokenId: vars.streamId }); vars.expectedNFTOwner = params.recipient; @@ -244,12 +224,11 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { vars.actualBrokerBalance = vars.balances[2]; // Assert that the contract's balance has been updated. - vars.expectedLockupDynamicBalance = - vars.initialLockupDynamicBalance + vars.createAmounts.deposit + vars.createAmounts.protocolFee; + vars.expectedLockupDynamicBalance = vars.initialLockupDynamicBalance + vars.createAmounts.deposit; assertEq( vars.actualLockupDynamicBalance, vars.expectedLockupDynamicBalance, - "post-create lockupDynamic contract balance" + "post-create LockupDynamic contract balance" ); // Assert that the holder's balance has been updated. @@ -265,8 +244,9 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { //////////////////////////////////////////////////////////////////////////*/ // Simulate the passage of time. - params.warpTimestamp = boundUint40(params.warpTimestamp, vars.range.start, vars.range.end + 100 seconds); - vm.warp({ timestamp: params.warpTimestamp }); + params.warpTimestamp = + boundUint40(params.warpTimestamp, vars.timestamps.start, vars.timestamps.end + 100 seconds); + vm.warp({ newTimestamp: params.warpTimestamp }); // Bound the withdraw amount. vars.withdrawableAmount = lockupDynamic.withdrawableAmountOf(vars.streamId); @@ -295,7 +275,7 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { emit MetadataUpdate({ _tokenId: vars.streamId }); // Make the withdrawal. - changePrank({ msgSender: params.recipient }); + resetPrank({ msgSender: params.recipient }); lockupDynamic.withdraw({ streamId: vars.streamId, to: params.recipient, amount: params.withdrawAmount }); // Assert that the stream's status is correct. @@ -358,7 +338,7 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { emit MetadataUpdate({ _tokenId: vars.streamId }); // Cancel the stream. - changePrank({ msgSender: params.sender }); + resetPrank({ msgSender: params.sender }); lockupDynamic.cancel(vars.streamId); // Assert that the stream's status is correct. diff --git a/test/fork/LockupLinear.t.sol b/test/fork/LockupLinear.t.sol index e0fa7c6a6..46fac970d 100644 --- a/test/fork/LockupLinear.t.sol +++ b/test/fork/LockupLinear.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { ud } from "@prb/math/src/UD60x18.sol"; import { Solarray } from "solarray/src/Solarray.sol"; import { Broker, Lockup, LockupLinear } from "src/types/DataTypes.sol"; @@ -34,15 +34,13 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { //////////////////////////////////////////////////////////////////////////*/ struct Params { - Broker broker; - UD60x18 protocolFee; - LockupLinear.Range range; - address recipient; address sender; + address recipient; uint128 totalAmount; - uint40 warpTimestamp; uint128 withdrawAmount; - bool transferable; + uint40 warpTimestamp; + LockupLinear.Timestamps timestamps; + Broker broker; } struct Vars { @@ -53,11 +51,14 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { uint256 actualRecipientBalance; Lockup.Status actualStatus; uint256[] balances; + uint40 blockTimestamp; + uint40 endTimeLowerBound; uint256 expectedLockupLinearBalance; uint256 expectedHolderBalance; address expectedNFTOwner; uint256 expectedRecipientBalance; Lockup.Status expectedStatus; + bool hasCliff; uint256 initialLockupLinearBalance; uint256 initialRecipientBalance; bool isDepleted; @@ -66,13 +67,10 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { // Create vars uint256 actualBrokerBalance; uint256 actualNextStreamId; - uint256 actualProtocolRevenues; Lockup.CreateAmounts createAmounts; uint256 expectedBrokerBalance; uint256 expectedNextStreamId; - uint256 expectedProtocolRevenues; uint256 initialBrokerBalance; - uint256 initialProtocolRevenues; // Withdraw vars uint128 actualWithdrawnAmount; uint128 expectedWithdrawnAmount; @@ -89,8 +87,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { /// /// - It should perform all expected ERC-20 transfers. /// - It should create the stream. - /// - It should bump the next stream id. - /// - It should record the protocol fee. + /// - It should bump the next stream ID. /// - It should mint the NFT. /// - It should emit a {MetadataUpdate} event /// - It should emit a {CreateLockupLinearStream} event. @@ -104,60 +101,54 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { /// /// - Multiple values for the sender, recipient, and broker /// - Multiple values for the total amount - /// - Multiple values for the cliff time and the end time - /// - Multiple values for the broker fee, including zero - /// - Multiple values for the protocol fee, including zero /// - Multiple values for the withdraw amount, including zero /// - Start time in the past /// - Start time in the present /// - Start time in the future - /// - Start time lower than and equal to cliff time + /// - Multiple values for the cliff time and the end time + /// - Cliff time zero and not zero + /// - Multiple values for the broker fee, including zero /// - The whole gamut of stream statuses function testForkFuzz_LockupLinear_CreateWithdrawCancel(Params memory params) external { checkUsers(params.sender, params.recipient, params.broker.account, address(lockupLinear)); // Bound the parameters. - uint40 currentTime = getBlockTimestamp(); - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - params.protocolFee = _bound(params.protocolFee, 0, MAX_FEE); - params.range.start = boundUint40(params.range.start, currentTime - 1000 seconds, currentTime + 10_000 seconds); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); - params.totalAmount = boundUint128(params.totalAmount, 1, uint128(initialHolderBalance)); - params.transferable = true; - - // Bound the end time so that it is always greater than both the current time and the cliff time (this is - // a requirement of the protocol). - params.range.end = boundUint40( - params.range.end, - (params.range.cliff <= currentTime ? currentTime : params.range.cliff) + 1, - MAX_UNIX_TIMESTAMP + Vars memory vars; + vars.blockTimestamp = getBlockTimestamp(); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.timestamps.start = boundUint40( + params.timestamps.start, vars.blockTimestamp - 1000 seconds, vars.blockTimestamp + 10_000 seconds ); + params.totalAmount = boundUint128(params.totalAmount, 1, uint128(initialHolderBalance)); - // Set the fuzzed protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: ASSET, newProtocolFee: params.protocolFee }); + // The cliff time must be either zero or greater than the start time. + vars.hasCliff = params.timestamps.cliff > 0; + if (vars.hasCliff) { + params.timestamps.cliff = boundUint40( + params.timestamps.cliff, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks + ); + } + // Bound the end time so that it is always greater than the block timestamp, the start time, and the cliff time. + vars.endTimeLowerBound = maxOfThree(params.timestamps.start, params.timestamps.cliff, vars.blockTimestamp); + params.timestamps.end = + boundUint40(params.timestamps.end, vars.endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); // Make the holder the caller. - changePrank(HOLDER); + resetPrank(HOLDER); /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/ - // Load the pre-create protocol revenues. - Vars memory vars; - vars.initialProtocolRevenues = lockupLinear.protocolRevenues(ASSET); - // Load the pre-create asset balances. vars.balances = getTokenBalances(address(ASSET), Solarray.addresses(address(lockupLinear), params.broker.account)); vars.initialLockupLinearBalance = vars.balances[0]; vars.initialBrokerBalance = vars.balances[1]; - // Calculate the fee amounts and the deposit amount. - vars.createAmounts.protocolFee = ud(params.totalAmount).mul(params.protocolFee).intoUint128(); + // Calculate the broker fee amount and the deposit amount. vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); - vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.protocolFee - vars.createAmounts.brokerFee; + vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; vars.streamId = lockupLinear.nextStreamId(); @@ -173,54 +164,51 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { amounts: vars.createAmounts, asset: ASSET, cancelable: true, - transferable: params.transferable, - range: params.range, + transferable: true, + timestamps: params.timestamps, broker: params.broker.account }); // Create the stream. - lockupLinear.createWithRange( - LockupLinear.CreateWithRange({ + lockupLinear.createWithTimestamps( + LockupLinear.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, asset: ASSET, - broker: params.broker, cancelable: true, - transferable: params.transferable, - range: params.range, - recipient: params.recipient, - sender: params.sender, - totalAmount: params.totalAmount + transferable: true, + timestamps: params.timestamps, + broker: params.broker }) ); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(vars.streamId); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(vars.streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, ASSET, "asset"); - assertEq(actualStream.cliffTime, params.range.cliff, "cliffTime"); - assertEq(actualStream.endTime, params.range.end, "endTime"); + assertEq(actualStream.cliffTime, params.timestamps.cliff, "cliffTime"); + assertEq(actualStream.endTime, params.timestamps.end, "endTime"); assertEq(actualStream.isCancelable, true, "isCancelable"); assertEq(actualStream.isDepleted, false, "isDepleted"); - assertEq(actualStream.isTransferable, true, "isTransferable"); assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); assertEq(actualStream.sender, params.sender, "sender"); - assertEq(actualStream.startTime, params.range.start, "startTime"); + assertEq(actualStream.startTime, params.timestamps.start, "startTime"); assertEq(actualStream.wasCanceled, false, "wasCanceled"); // Assert that the stream's status is correct. vars.actualStatus = lockupLinear.statusOf(vars.streamId); - vars.expectedStatus = params.range.start > currentTime ? Lockup.Status.PENDING : Lockup.Status.STREAMING; + vars.expectedStatus = + params.timestamps.start > vars.blockTimestamp ? Lockup.Status.PENDING : Lockup.Status.STREAMING; assertEq(vars.actualStatus, vars.expectedStatus, "post-create stream status"); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. vars.actualNextStreamId = lockupLinear.nextStreamId(); vars.expectedNextStreamId = vars.streamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); - // Assert that the protocol fee has been recorded. - vars.actualProtocolRevenues = lockupLinear.protocolRevenues(ASSET); - vars.expectedProtocolRevenues = vars.initialProtocolRevenues + vars.createAmounts.protocolFee; - assertEq(vars.actualProtocolRevenues, vars.expectedProtocolRevenues, "post-create protocolRevenues"); - // Assert that the NFT has been minted. vars.actualNFTOwner = lockupLinear.ownerOf({ tokenId: vars.streamId }); vars.expectedNFTOwner = params.recipient; @@ -234,8 +222,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { vars.actualBrokerBalance = vars.balances[2]; // Assert that the LockupLinear contract's balance has been updated. - vars.expectedLockupLinearBalance = - vars.initialLockupLinearBalance + vars.createAmounts.deposit + vars.createAmounts.protocolFee; + vars.expectedLockupLinearBalance = vars.initialLockupLinearBalance + vars.createAmounts.deposit; assertEq(vars.actualLockupLinearBalance, vars.expectedLockupLinearBalance, "post-create LockupLinear balance"); // Assert that the holder's balance has been updated. @@ -251,8 +238,12 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { //////////////////////////////////////////////////////////////////////////*/ // Simulate the passage of time. - params.warpTimestamp = boundUint40(params.warpTimestamp, params.range.cliff, params.range.end + 100 seconds); - vm.warp({ timestamp: params.warpTimestamp }); + params.warpTimestamp = boundUint40( + params.warpTimestamp, + vars.hasCliff ? params.timestamps.cliff : params.timestamps.start + 1 seconds, + params.timestamps.end + 100 seconds + ); + vm.warp({ newTimestamp: params.warpTimestamp }); // Bound the withdraw amount. vars.withdrawableAmount = lockupLinear.withdrawableAmountOf(vars.streamId); @@ -281,7 +272,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { emit MetadataUpdate({ _tokenId: vars.streamId }); // Make the withdrawal. - changePrank({ msgSender: params.recipient }); + resetPrank({ msgSender: params.recipient }); lockupLinear.withdraw({ streamId: vars.streamId, to: params.recipient, amount: params.withdrawAmount }); // Assert that the stream's status is correct. @@ -342,7 +333,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { emit MetadataUpdate({ _tokenId: vars.streamId }); // Cancel the stream. - changePrank({ msgSender: params.sender }); + resetPrank({ msgSender: params.sender }); lockupLinear.cancel(vars.streamId); // Assert that the stream's status is correct. diff --git a/test/fork/LockupTranched.t.sol b/test/fork/LockupTranched.t.sol new file mode 100644 index 000000000..e39f0423d --- /dev/null +++ b/test/fork/LockupTranched.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Solarray } from "solarray/src/Solarray.sol"; + +import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { Fork_Test } from "./Fork.t.sol"; + +abstract contract LockupTranched_Fork_Test is Fork_Test { + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor(IERC20 asset, address holder) Fork_Test(asset, holder) { } + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + Fork_Test.setUp(); + + // Approve {SablierV2LockupTranched} to transfer the holder's assets. + // We use a low-level call to ignore reverts because the asset can have the missing return value bug. + (bool success,) = address(ASSET).call(abi.encodeCall(IERC20.approve, (address(lockupTranched), MAX_UINT256))); + success; + } + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + struct Params { + address sender; + address recipient; + uint128 withdrawAmount; + uint40 startTime; + uint40 warpTimestamp; + LockupTranched.Tranche[] tranches; + Broker broker; + } + + struct Vars { + // Generic vars + address actualNFTOwner; + uint256 actualLockupTranchedBalance; + uint256 actualRecipientBalance; + Lockup.Status actualStatus; + uint256[] balances; + address expectedNFTOwner; + uint256 expectedLockupTranchedBalance; + uint256 expectedRecipientBalance; + Lockup.Status expectedStatus; + uint256 initialLockupTranchedBalance; + uint256 initialRecipientBalance; + bool isCancelable; + bool isDepleted; + bool isSettled; + uint256 streamId; + LockupTranched.Timestamps timestamps; + // Create vars + uint256 actualBrokerBalance; + uint256 actualHolderBalance; + uint256 actualNextStreamId; + Lockup.CreateAmounts createAmounts; + uint256 expectedBrokerBalance; + uint256 expectedHolderBalance; + uint256 expectedNextStreamId; + uint256 initialBrokerBalance; + uint128 totalAmount; + // Withdraw vars + uint128 actualWithdrawnAmount; + uint128 expectedWithdrawnAmount; + uint128 withdrawableAmount; + // Cancel vars + uint256 actualSenderBalance; + uint256 expectedSenderBalance; + uint256 initialSenderBalance; + uint128 recipientAmount; + uint128 senderAmount; + } + + /// @dev Checklist: + /// + /// - It should perform all expected ERC-20 transfers. + /// - It should create the stream. + /// - It should bump the next stream ID. + /// - It should mint the NFT. + /// - It should emit a {CreateLockupTranchedStream} event. + /// - It may make a withdrawal. + /// - It may update the withdrawn amounts. + /// - It may emit a {WithdrawFromLockupStream} event. + /// - It may cancel the stream + /// - It may emit a {CancelLockupStream} event + /// + /// Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - Multiple values for the funder, recipient, sender, and broker + /// - Multiple values for the total amount + /// - Start time in the past + /// - Start time in the present + /// - Start time in the future + /// - Start time equal and not equal to the first tranche timestamp + /// - Multiple values for the broker fee, including zero. + /// - Multiple values for the withdraw amount, including zero + /// - The whole gamut of stream statuses + function testForkFuzz_LockupTranched_CreateWithdrawCancel(Params memory params) external { + checkUsers(params.sender, params.recipient, params.broker.account, address(lockupTranched)); + vm.assume(params.tranches.length != 0); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.startTime = boundUint40(params.startTime, 1, defaults.START_TIME()); + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(params.tranches, params.startTime); + + // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). + Vars memory vars; + (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts({ + upperBound: uint128(initialHolderBalance), + tranches: params.tranches, + brokerFee: params.broker.fee + }); + + // Make the holder the caller. + resetPrank(HOLDER); + + /*////////////////////////////////////////////////////////////////////////// + CREATE + //////////////////////////////////////////////////////////////////////////*/ + + // Load the pre-create asset balances. + vars.balances = + getTokenBalances(address(ASSET), Solarray.addresses(address(lockupTranched), params.broker.account)); + vars.initialLockupTranchedBalance = vars.balances[0]; + vars.initialBrokerBalance = vars.balances[1]; + + vars.streamId = lockupTranched.nextStreamId(); + vars.timestamps = LockupTranched.Timestamps({ + start: params.startTime, + end: params.tranches[params.tranches.length - 1].timestamp + }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: vars.streamId }); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit CreateLockupTranchedStream({ + streamId: vars.streamId, + funder: HOLDER, + sender: params.sender, + recipient: params.recipient, + amounts: vars.createAmounts, + asset: ASSET, + cancelable: true, + transferable: true, + tranches: params.tranches, + timestamps: vars.timestamps, + broker: params.broker.account + }); + + // Create the stream. + lockupTranched.createWithTimestamps( + LockupTranched.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: vars.totalAmount, + asset: ASSET, + cancelable: true, + transferable: true, + startTime: params.startTime, + tranches: params.tranches, + broker: params.broker + }) + ); + + // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation + // because some tranche amounts can be zero. + vars.isSettled = lockupTranched.refundableAmountOf(vars.streamId) == 0; + vars.isCancelable = vars.isSettled ? false : true; + + // Assert that the stream has been created. + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(vars.streamId); + assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); + assertEq(actualStream.asset, ASSET, "asset"); + assertEq(actualStream.endTime, vars.timestamps.end, "endTime"); + assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); + assertEq(actualStream.isDepleted, false, "isDepleted"); + assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); + assertEq(actualStream.tranches, params.tranches, "tranches"); + assertEq(actualStream.sender, params.sender, "sender"); + assertEq(actualStream.startTime, params.startTime, "startTime"); + assertEq(actualStream.wasCanceled, false, "wasCanceled"); + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.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 = lockupTranched.nextStreamId(); + vars.expectedNextStreamId = vars.streamId + 1; + assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "post-create nextStreamId"); + + // Assert that the NFT has been minted. + vars.actualNFTOwner = lockupTranched.ownerOf({ tokenId: vars.streamId }); + vars.expectedNFTOwner = params.recipient; + assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-create NFT owner"); + + // Load the post-create asset balances. + vars.balances = + getTokenBalances(address(ASSET), Solarray.addresses(address(lockupTranched), HOLDER, params.broker.account)); + vars.actualLockupTranchedBalance = vars.balances[0]; + vars.actualHolderBalance = vars.balances[1]; + vars.actualBrokerBalance = vars.balances[2]; + + // Assert that the contract's balance has been updated. + vars.expectedLockupTranchedBalance = vars.initialLockupTranchedBalance + vars.createAmounts.deposit; + assertEq( + vars.actualLockupTranchedBalance, + vars.expectedLockupTranchedBalance, + "post-create LockupTranched contract balance" + ); + + // Assert that the holder's balance has been updated. + vars.expectedHolderBalance = initialHolderBalance - vars.totalAmount; + assertEq(vars.actualHolderBalance, vars.expectedHolderBalance, "post-create Holder balance"); + + // Assert that the broker's balance has been updated. + vars.expectedBrokerBalance = vars.initialBrokerBalance + vars.createAmounts.brokerFee; + assertEq(vars.actualBrokerBalance, vars.expectedBrokerBalance, "post-create Broker balance"); + + /*////////////////////////////////////////////////////////////////////////// + WITHDRAW + //////////////////////////////////////////////////////////////////////////*/ + + // 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 = lockupTranched.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.createAmounts.deposit; + vars.isSettled = lockupTranched.refundableAmountOf(vars.streamId) == 0; + + // Only run the withdraw tests if the withdraw amount is not zero. + if (params.withdrawAmount > 0) { + // Load the pre-withdraw asset balances. + vars.initialLockupTranchedBalance = vars.actualLockupTranchedBalance; + vars.initialRecipientBalance = ASSET.balanceOf(params.recipient); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit WithdrawFromLockupStream({ + streamId: vars.streamId, + to: params.recipient, + asset: ASSET, + amount: params.withdrawAmount + }); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: vars.streamId }); + + // Make the withdrawal. + resetPrank({ msgSender: params.recipient }); + lockupTranched.withdraw({ streamId: vars.streamId, to: params.recipient, amount: params.withdrawAmount }); + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.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 = lockupTranched.getWithdrawnAmount(vars.streamId); + vars.expectedWithdrawnAmount = params.withdrawAmount; + assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "post-withdraw withdrawnAmount"); + + // Load the post-withdraw asset balances. + vars.balances = + getTokenBalances(address(ASSET), Solarray.addresses(address(lockupTranched), params.recipient)); + vars.actualLockupTranchedBalance = vars.balances[0]; + vars.actualRecipientBalance = vars.balances[1]; + + // Assert that the contract's balance has been updated. + vars.expectedLockupTranchedBalance = vars.initialLockupTranchedBalance - uint256(params.withdrawAmount); + assertEq( + vars.actualLockupTranchedBalance, + vars.expectedLockupTranchedBalance, + "post-withdraw lockupTranched contract balance" + ); + + // 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"); + } + + /*////////////////////////////////////////////////////////////////////////// + CANCEL + //////////////////////////////////////////////////////////////////////////*/ + + // Only run the cancel tests if the stream is neither depleted nor settled. + if (!vars.isDepleted && !vars.isSettled) { + // Load the pre-cancel asset balances. + vars.balances = getTokenBalances( + address(ASSET), Solarray.addresses(address(lockupTranched), params.sender, params.recipient) + ); + vars.initialLockupTranchedBalance = vars.balances[0]; + vars.initialSenderBalance = vars.balances[1]; + vars.initialRecipientBalance = vars.balances[2]; + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + vars.senderAmount = lockupTranched.refundableAmountOf(vars.streamId); + vars.recipientAmount = lockupTranched.withdrawableAmountOf(vars.streamId); + emit CancelLockupStream( + vars.streamId, params.sender, params.recipient, ASSET, vars.senderAmount, vars.recipientAmount + ); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: vars.streamId }); + + // Cancel the stream. + resetPrank({ msgSender: params.sender }); + lockupTranched.cancel(vars.streamId); + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.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 asset balances. + vars.balances = getTokenBalances( + address(ASSET), Solarray.addresses(address(lockupTranched), params.sender, params.recipient) + ); + vars.actualLockupTranchedBalance = vars.balances[0]; + vars.actualSenderBalance = vars.balances[1]; + vars.actualRecipientBalance = vars.balances[2]; + + // Assert that the contract's balance has been updated. + vars.expectedLockupTranchedBalance = vars.initialLockupTranchedBalance - uint256(vars.senderAmount); + assertEq( + vars.actualLockupTranchedBalance, + vars.expectedLockupTranchedBalance, + "post-cancel lockupTranched 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 NFT has not been burned. + vars.actualNFTOwner = lockupTranched.ownerOf({ tokenId: vars.streamId }); + vars.expectedNFTOwner = params.recipient; + assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "post-cancel NFT owner"); + } +} diff --git a/test/fork/NFTDescriptor.t.sol b/test/fork/NFTDescriptor.t.sol new file mode 100644 index 000000000..89912d632 --- /dev/null +++ b/test/fork/NFTDescriptor.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.sol"; + +import { Fork_Test } from "./Fork.t.sol"; + +contract NFTDescriptor_Fork_Test is Fork_Test { + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + IERC20 internal constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + address internal constant DAI_HOLDER = 0x66F62574ab04989737228D18C3624f7FC1edAe14; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor() Fork_Test(DAI, DAI_HOLDER) { } + + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Loads the Lockup V2.0 contracts pre-deployed on Mainnet. + modifier loadDeployments_V2_0() { + lockupDynamic = ISablierV2LockupDynamic(0x39EFdC3dbB57B2388CcC4bb40aC4CB1226Bc9E44); + lockupLinear = ISablierV2LockupLinear(0xB10daee1FCF62243aE27776D7a92D39dC8740f95); + _; + } + + /// @dev Loads the Lockup V2.1 contracts pre-deployed on Mainnet. + modifier loadDeployments_V2_1() { + lockupDynamic = ISablierV2LockupDynamic(0x7CC7e125d83A581ff438608490Cc0f7bDff79127); + lockupLinear = ISablierV2LockupLinear(0xAFb979d9afAd1aD27C5eFf4E27226E3AB9e5dCC9); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + Fork_Test.setUp(); + } + + /*////////////////////////////////////////////////////////////////////////// + TEST FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev The following test checks whether the new NFT descriptor is compatible with Lockup Dynamic v2.0. + /// + /// Checklist: + /// - It should expect a call to {ISablierV2LockupDynamic.tokenURI}. + /// - The test would fail if the call to {ISablierV2LockupDynamic.tokenURI} reverts. + /// + /// Given enough fuzz runs, all the following scenarios will be fuzzed: + /// - Multiple values of streamId. + function testForkFuzz_TokenURI_LockupDynamic_V2_0(uint256 streamId) external loadDeployments_V2_0 { + streamId = _bound(streamId, 1, lockupDynamic.nextStreamId() - 1); + + // Set the new NFT descriptor for the previous version of Lockup Dynamic. + resetPrank({ msgSender: lockupDynamic.admin() }); + lockupDynamic.setNFTDescriptor(nftDescriptor); + + // Expects a successful call to the new NFT Descriptor. + vm.expectCall({ + callee: address(nftDescriptor), + data: abi.encodeCall(nftDescriptor.tokenURI, (lockupDynamic, streamId)), + count: 1 + }); + + // Generate the token URI using the new NFT Descriptor. + lockupDynamic.tokenURI(streamId); + } + + /// @dev The following test checks whether the new NFT descriptor is compatible with Lockup Dynamic v2.1. + /// + /// Checklist: + /// - It should expect a call to {ISablierV2LockupDynamic.tokenURI}. + /// - The test would fail if the call to {ISablierV2LockupDynamic.tokenURI} reverts. + /// + /// Given enough fuzz runs, all the following scenarios will be fuzzed: + /// - Multiple values of streamId. + function testForkFuzz_TokenURI_LockupDynamic_V2_1(uint256 streamId) external loadDeployments_V2_1 { + streamId = _bound(streamId, 1, lockupDynamic.nextStreamId() - 1); + + // Set the new NFT descriptor for the previous version of Lockup Dynamic. + resetPrank({ msgSender: lockupDynamic.admin() }); + lockupDynamic.setNFTDescriptor(nftDescriptor); + + // Expects a successful call to the new NFT Descriptor. + vm.expectCall({ + callee: address(nftDescriptor), + data: abi.encodeCall(nftDescriptor.tokenURI, (lockupDynamic, streamId)), + count: 1 + }); + + // Generate the token URI using the new NFT Descriptor. + lockupDynamic.tokenURI(streamId); + } + + /// @dev The following test checks whether the new NFT descriptor is compatible with Lockup Linear v2.0. + /// + /// Checklist: + /// - It should expect a call to {ISablierV2LockupLinear.tokenURI}. + /// - The test would fail if the call to {ISablierV2LockupLinear.tokenURI} reverts. + /// + /// Given enough fuzz runs, all the following scenarios will be fuzzed: + /// - Multiple values of streamId. + function testForkFuzz_TokenURI_LockupLinear_V2_0(uint256 streamId) external loadDeployments_V2_0 { + streamId = _bound(streamId, 1, lockupLinear.nextStreamId() - 1); + + // Set the new NFT descriptor for the previous version of Lockup Linear. + resetPrank({ msgSender: lockupLinear.admin() }); + lockupLinear.setNFTDescriptor(nftDescriptor); + + // Expects a successful call to the new NFT Descriptor. + vm.expectCall({ + callee: address(nftDescriptor), + data: abi.encodeCall(nftDescriptor.tokenURI, (lockupLinear, streamId)), + count: 1 + }); + + // Generate the token URI using the new NFT Descriptor. + lockupLinear.tokenURI(streamId); + } + + /// @dev The following test checks whether the new NFT descriptor is compatible with Lockup Linear v2.1. + /// + /// Checklist: + /// - It should expect a call to {ISablierV2LockupLinear.tokenURI}. + /// - The test would fail if the call to {ISablierV2LockupLinear.tokenURI} reverts. + /// + /// Given enough fuzz runs, all the following scenarios will be fuzzed: + /// - Multiple values of streamId. + function testForkFuzz_TokenURI_LockupLinear_V2_1(uint256 streamId) external loadDeployments_V2_1 { + streamId = _bound(streamId, 1, lockupLinear.nextStreamId() - 1); + + // Set the new NFT descriptor for the previous version of Lockup Linear. + resetPrank({ msgSender: lockupLinear.admin() }); + lockupLinear.setNFTDescriptor(nftDescriptor); + + // Expects a successful call to the new NFT Descriptor. + vm.expectCall({ + callee: address(nftDescriptor), + data: abi.encodeCall(nftDescriptor.tokenURI, (lockupLinear, streamId)), + count: 1 + }); + + // Generate the token URI using the new NFT Descriptor. + lockupLinear.tokenURI(streamId); + } +} diff --git a/test/fork/assets/DAI.t.sol b/test/fork/assets/DAI.t.sol index 3fdb579c3..6bf507732 100644 --- a/test/fork/assets/DAI.t.sol +++ b/test/fork/assets/DAI.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic_Fork_Test } from "../LockupDynamic.t.sol"; import { LockupLinear_Fork_Test } from "../LockupLinear.t.sol"; +import { LockupTranched_Fork_Test } from "../LockupTranched.t.sol"; /// @dev A typical 18-decimal ERC-20 asset with a normal total supply. IERC20 constant ASSET = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); @@ -13,3 +14,5 @@ address constant HOLDER = 0x66F62574ab04989737228D18C3624f7FC1edAe14; contract DAI_LockupDynamic_Fork_Test is LockupDynamic_Fork_Test(ASSET, HOLDER) { } contract DAI_LockupLinear_Fork_Test is LockupLinear_Fork_Test(ASSET, HOLDER) { } + +contract DAI_LockupTranched_Fork_Test is LockupTranched_Fork_Test(ASSET, HOLDER) { } diff --git a/test/fork/assets/EURS.t.sol b/test/fork/assets/EURS.t.sol index 969daa1bc..c27504e63 100644 --- a/test/fork/assets/EURS.t.sol +++ b/test/fork/assets/EURS.t.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic_Fork_Test } from "../LockupDynamic.t.sol"; import { LockupLinear_Fork_Test } from "../LockupLinear.t.sol"; +import { LockupTranched_Fork_Test } from "../LockupTranched.t.sol"; /// @dev An ERC-20 asset with 2 decimals. IERC20 constant ASSET = IERC20(0xdB25f211AB05b1c97D595516F45794528a807ad8); -address constant HOLDER = 0x9712c160925403A9458BfC6bBD7D8a1E694C984a; +address constant HOLDER = 0x1bee4F735062CD00841d6997964F187f5f5F5Ac9; contract EURS_LockupDynamic_Fork_Test is LockupDynamic_Fork_Test(ASSET, HOLDER) { } contract EURS_LockupLinear_Fork_Test is LockupLinear_Fork_Test(ASSET, HOLDER) { } + +contract EURS_LockupTranched_Fork_Test is LockupTranched_Fork_Test(ASSET, HOLDER) { } diff --git a/test/fork/assets/SHIB.t.sol b/test/fork/assets/SHIB.t.sol index e8d321a03..6d2f9dcbd 100644 --- a/test/fork/assets/SHIB.t.sol +++ b/test/fork/assets/SHIB.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic_Fork_Test } from "../LockupDynamic.t.sol"; import { LockupLinear_Fork_Test } from "../LockupLinear.t.sol"; +import { LockupTranched_Fork_Test } from "../LockupTranched.t.sol"; /// @dev An ERC-20 asset with a large total supply. IERC20 constant ASSET = IERC20(0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE); @@ -13,3 +14,5 @@ address constant HOLDER = 0x73AF3bcf944a6559933396c1577B257e2054D935; contract SHIB_LockupDynamic_Fork_Test is LockupDynamic_Fork_Test(ASSET, HOLDER) { } contract SHIB_LockupLinear_Fork_Test is LockupLinear_Fork_Test(ASSET, HOLDER) { } + +contract SHIB_LockupTranched_Fork_Test is LockupTranched_Fork_Test(ASSET, HOLDER) { } diff --git a/test/fork/assets/USDC.t.sol b/test/fork/assets/USDC.t.sol index ea79ae380..f253f779a 100644 --- a/test/fork/assets/USDC.t.sol +++ b/test/fork/assets/USDC.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic_Fork_Test } from "../LockupDynamic.t.sol"; import { LockupLinear_Fork_Test } from "../LockupLinear.t.sol"; +import { LockupTranched_Fork_Test } from "../LockupTranched.t.sol"; /// @dev An ERC-20 asset with 6 decimals. IERC20 constant ASSET = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); @@ -13,3 +14,5 @@ address constant HOLDER = 0x09528d637deb5857dc059dddE6316D465a8b3b69; contract USDC_LockupDynamic_Fork_Test is LockupDynamic_Fork_Test(ASSET, HOLDER) { } contract USDC_LockupLinear_Fork_Test is LockupLinear_Fork_Test(ASSET, HOLDER) { } + +contract USDC_LockupTranched_Fork_Test is LockupTranched_Fork_Test(ASSET, HOLDER) { } diff --git a/test/fork/assets/USDT.t.sol b/test/fork/assets/USDT.t.sol index 421144ca1..d2d803eec 100644 --- a/test/fork/assets/USDT.t.sol +++ b/test/fork/assets/USDT.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LockupDynamic_Fork_Test } from "../LockupDynamic.t.sol"; import { LockupLinear_Fork_Test } from "../LockupLinear.t.sol"; +import { LockupTranched_Fork_Test } from "../LockupTranched.t.sol"; /// @dev An ERC-20 asset that suffers from the missing return value bug. IERC20 constant ASSET = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); @@ -13,3 +14,5 @@ address constant HOLDER = 0xee5B5B923fFcE93A870B3104b7CA09c3db80047A; contract USDT_LockupDynamic_Fork_Test is LockupDynamic_Fork_Test(ASSET, HOLDER) { } contract USDT_LockupLinear_Fork_Test is LockupLinear_Fork_Test(ASSET, HOLDER) { } + +contract USDT_LockupTranched_Fork_Test is LockupTranched_Fork_Test(ASSET, HOLDER) { } diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 2e0c62ab9..316657fef 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -1,24 +1,29 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { Base_Test } from "../Base.t.sol"; -import { FaultyFlashLoanReceiver } from "../mocks/flash-loan/FaultyFlashLoanReceiver.sol"; -import { ReentrantFlashLoanReceiver } from "../mocks/flash-loan/ReentrantFlashLoanReceiver.sol"; -import { ReentrantRecipient } from "../mocks/hooks/ReentrantRecipient.sol"; -import { RevertingRecipient } from "../mocks/hooks/RevertingRecipient.sol"; +import { + RecipientInterfaceIDIncorrect, + RecipientInterfaceIDMissing, + RecipientInvalidSelector, + RecipientReentrant, + RecipientReverting +} from "../mocks/Hooks.sol"; /// @notice Common logic needed by all integration tests, both concrete and fuzz tests. + abstract contract Integration_Test is Base_Test { /*////////////////////////////////////////////////////////////////////////// TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - FaultyFlashLoanReceiver internal faultyFlashLoanReceiver = new FaultyFlashLoanReceiver(); - ReentrantFlashLoanReceiver internal reentrantFlashLoanReceiver = new ReentrantFlashLoanReceiver(); - ReentrantRecipient internal reentrantRecipient = new ReentrantRecipient(); - RevertingRecipient internal revertingRecipient = new RevertingRecipient(); + RecipientInterfaceIDIncorrect internal recipientInterfaceIDIncorrect = new RecipientInterfaceIDIncorrect(); + RecipientInterfaceIDMissing internal recipientInterfaceIDMissing = new RecipientInterfaceIDMissing(); + RecipientInvalidSelector internal recipientInvalidSelector = new RecipientInvalidSelector(); + RecipientReentrant internal recipientReentrant = new RecipientReentrant(); + RecipientReverting internal recipientReverting = new RecipientReverting(); /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION @@ -27,17 +32,11 @@ abstract contract Integration_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - // Deploy V2 Core. - deployCoreConditionally(); - // Label the contracts. labelContracts(); // Make the Admin the default caller in this test suite. - vm.startPrank({ msgSender: users.admin }); - - // Approve V2 Core to spend assets from the users. - approveProtocol(); + resetPrank({ msgSender: users.admin }); } /*////////////////////////////////////////////////////////////////////////// @@ -46,10 +45,11 @@ abstract contract Integration_Test is Base_Test { /// @dev Labels the most relevant contracts. function labelContracts() internal { - vm.label({ account: address(faultyFlashLoanReceiver), newLabel: "Faulty Flash Loan Receiver" }); - vm.label({ account: address(reentrantFlashLoanReceiver), newLabel: "Reentrant Flash Loan Receiver" }); - vm.label({ account: address(reentrantRecipient), newLabel: "Reentrant Lockup Recipient" }); - vm.label({ account: address(revertingRecipient), newLabel: "Reverting Lockup Recipient" }); + vm.label({ account: address(recipientInterfaceIDIncorrect), newLabel: "Recipient Interface ID Incorrect" }); + vm.label({ account: address(recipientInterfaceIDMissing), newLabel: "Recipient Interface ID Missing" }); + vm.label({ account: address(recipientInvalidSelector), newLabel: "Recipient Invalid Selector" }); + vm.label({ account: address(recipientReentrant), newLabel: "Recipient Reentrant" }); + vm.label({ account: address(recipientReverting), newLabel: "Recipient Reverting" }); } /*////////////////////////////////////////////////////////////////////////// @@ -57,7 +57,7 @@ abstract contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ /// @dev Expects a delegate call error. - function expectRevertDueToDelegateCall(bool success, bytes memory returnData) internal { + function expectRevertDueToDelegateCall(bool success, bytes memory returnData) internal pure { assertFalse(success, "delegatecall success"); assertEq(returnData, abi.encodeWithSelector(Errors.DelegateCall.selector), "delegatecall return data"); } diff --git a/test/integration/concrete/comptroller/protocol-fees/protocolFees.t.sol b/test/integration/concrete/comptroller/protocol-fees/protocolFees.t.sol deleted file mode 100644 index d7af61c96..000000000 --- a/test/integration/concrete/comptroller/protocol-fees/protocolFees.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract ProtocolFees_Integration_Concrete_Test is Integration_Test { - function setUp() public override { - Integration_Test.setUp(); - - // Make the Admin the caller in this test suite. - changePrank({ msgSender: users.admin }); - } - - function test_ProtocolFees_ProtocolFeeNotSet() external { - UD60x18 actualProtocolFee = comptroller.protocolFees(dai); - UD60x18 expectedProtocolFee = ZERO; - assertEq(actualProtocolFee, expectedProtocolFee, "protocolFees"); - } - - modifier givenProtocolFeeSet() { - comptroller.setProtocolFee({ asset: dai, newProtocolFee: defaults.PROTOCOL_FEE() }); - _; - } - - function test_ProtocolFees() external givenProtocolFeeSet { - UD60x18 actualProtocolFee = comptroller.protocolFees(dai); - UD60x18 expectedProtocolFee = defaults.PROTOCOL_FEE(); - assertEq(actualProtocolFee, expectedProtocolFee, "protocolFees"); - } -} diff --git a/test/integration/concrete/comptroller/protocol-fees/protocolFees.tree b/test/integration/concrete/comptroller/protocol-fees/protocolFees.tree deleted file mode 100644 index c2e81b8d8..000000000 --- a/test/integration/concrete/comptroller/protocol-fees/protocolFees.tree +++ /dev/null @@ -1,5 +0,0 @@ -protocolFees.t.sol -├── given the protocol fee has not been set -│ └── it should return zero -└── given the protocol fee has been set - └── it should return the correct protocol fee diff --git a/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.t.sol b/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.t.sol deleted file mode 100644 index 91944a606..000000000 --- a/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.t.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract SetProtocolFee_Integration_Concrete_Test is Integration_Test { - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: MAX_FEE }); - } - - /// @dev The Admin is the default caller in the comptroller tests. - modifier whenCallerAdmin() { - _; - } - - function test_SetProtocolFee_SameFee() external whenCallerAdmin { - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetProtocolFee({ admin: users.admin, asset: dai, oldProtocolFee: ZERO, newProtocolFee: ZERO }); - - // Set the same protocol fee. - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); - - // Assert that the protocol fee has not changed. - UD60x18 actualProtocolFee = comptroller.protocolFees(dai); - UD60x18 expectedProtocolFee = ZERO; - assertEq(actualProtocolFee, expectedProtocolFee, "protocolFee"); - } - - modifier whenNewFee() { - _; - } - - function test_SetProtocolFee() external whenCallerAdmin whenNewFee { - UD60x18 newProtocolFee = defaults.FLASH_FEE(); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetProtocolFee({ admin: users.admin, asset: dai, oldProtocolFee: ZERO, newProtocolFee: newProtocolFee }); - - // Set the new protocol fee. - comptroller.setProtocolFee({ asset: dai, newProtocolFee: newProtocolFee }); - - // Assert that the protocol fee has been updated. - UD60x18 actualProtocolFee = comptroller.protocolFees(dai); - UD60x18 expectedProtocolFee = newProtocolFee; - assertEq(actualProtocolFee, expectedProtocolFee, "protocolFee"); - } -} diff --git a/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.tree b/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.tree deleted file mode 100644 index 7bd610ee8..000000000 --- a/test/integration/concrete/comptroller/set-protocol-fee/setProtocolFee.tree +++ /dev/null @@ -1,11 +0,0 @@ -setProtocolFee.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── when the new protocol fee is the same as the current protocol fee - │ ├── it should re-set the protocol fee - │ └── it should emit a {SetProtocolFee} event - └── when the new protocol fee is not the same as the current protocol fee - ├── it should set the new protocol fee - └── it should emit a {SetProtocolFee} event - diff --git a/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.t.sol b/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.t.sol deleted file mode 100644 index 98ede2015..000000000 --- a/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.t.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract ToggleFlashAsset_Integration_Concrete_Test is Integration_Test { - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - comptroller.toggleFlashAsset(dai); - } - - /// @dev The admin is the default caller in the comptroller tests. - modifier whenCallerAdmin() { - _; - } - - function test_ToggleFlashAsset_FlagNotEnabled() external whenCallerAdmin { - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit ToggleFlashAsset({ admin: users.admin, asset: dai, newFlag: true }); - - // Toggle the flash asset. - comptroller.toggleFlashAsset(dai); - - // Assert that the flash asset has been toggled. - bool isFlashAsset = comptroller.isFlashAsset(dai); - assertTrue(isFlashAsset, "isFlashAsset"); - } - - modifier givenFlagEnabled() { - comptroller.toggleFlashAsset(dai); - _; - } - - function test_ToggleFlashAsset() external whenCallerAdmin givenFlagEnabled { - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit ToggleFlashAsset({ admin: users.admin, asset: dai, newFlag: false }); - - // Toggle the flash asset. - comptroller.toggleFlashAsset(dai); - - // Assert that the flash asset has been toggled. - bool isFlashAsset = comptroller.isFlashAsset(dai); - assertFalse(isFlashAsset, "isFlashAsset"); - } -} diff --git a/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.tree b/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.tree deleted file mode 100644 index e6184a843..000000000 --- a/test/integration/concrete/comptroller/toggle-flash-asset/toggleFlashAsset.tree +++ /dev/null @@ -1,10 +0,0 @@ -toggleFlashAsset.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── given the flag is not enabled - │ ├── it should toggle the flash asset - │ └── it should emit a {ToggleFlashAsset} event - └── given the flag is enabled - ├── it should toggle the flash asset - └── it should emit a {ToggleFlashAsset} event diff --git a/test/integration/concrete/flash-loan/flash-fee/flashFee.t.sol b/test/integration/concrete/flash-loan/flash-fee/flashFee.t.sol deleted file mode 100644 index f7eaf9f79..000000000 --- a/test/integration/concrete/flash-loan/flash-fee/flashFee.t.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { ud } from "@prb/math/src/UD60x18.sol"; - -import { Errors } from "src/libraries/Errors.sol"; - -import { FlashLoan_Integration_Shared_Test } from "../../../shared/flash-loan/FlashLoan.t.sol"; - -contract FlashFee_Integration_Concrete_Test is FlashLoan_Integration_Shared_Test { - function test_RevertGiven_AssetNotFlashLoanable() external { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_AssetNotFlashLoanable.selector, dai)); - flashLoan.flashFee({ asset: address(dai), amount: 0 }); - } - - modifier givenAssetFlashLoanable() { - comptroller.toggleFlashAsset(dai); - _; - } - - function test_FlashFee() external givenAssetFlashLoanable { - uint256 amount = 782.23e18; - uint256 actualFlashFee = flashLoan.flashFee({ asset: address(dai), amount: amount }); - uint256 expectedFlashFee = ud(amount).mul(defaults.FLASH_FEE()).intoUint256(); - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } -} diff --git a/test/integration/concrete/flash-loan/flash-fee/flashFee.tree b/test/integration/concrete/flash-loan/flash-fee/flashFee.tree deleted file mode 100644 index 106525e37..000000000 --- a/test/integration/concrete/flash-loan/flash-fee/flashFee.tree +++ /dev/null @@ -1,5 +0,0 @@ -flashFee.t.sol -├── given the asset is not flash loanable -│ └── it should revert -└── given the asset is flash loanable - └── it should return the correct flash fee diff --git a/test/integration/concrete/flash-loan/flash-loan/flashLoan.t.sol b/test/integration/concrete/flash-loan/flash-loan/flashLoan.t.sol deleted file mode 100644 index 6f67dcf6b..000000000 --- a/test/integration/concrete/flash-loan/flash-loan/flashLoan.t.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; - -import { ud } from "@prb/math/src/UD60x18.sol"; - -import { IERC3156FlashLender } from "src/interfaces/erc3156/IERC3156FlashLender.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { FlashLoanFunction_Integration_Shared_Test } from "../../../shared/flash-loan/flashLoanFunction.t.sol"; - -contract FlashLoanFunction_Integration_Concrete_Test is FlashLoanFunction_Integration_Shared_Test { - function setUp() public virtual override { - FlashLoanFunction_Integration_Shared_Test.setUp(); - } - - function test_RevertWhen_DelegateCalled() external { - bytes memory callData = - abi.encodeCall(IERC3156FlashLender.flashLoan, (goodFlashLoanReceiver, address(dai), 0, bytes(""))); - (bool success, bytes memory returnData) = address(flashLoan).delegatecall(callData); - expectRevertDueToDelegateCall(success, returnData); - } - - function test_RevertWhen_AmountTooHigh() external whenNotDelegateCalled { - uint256 amount = uint256(MAX_UINT128) + 1; - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_AmountTooHigh.selector, amount)); - flashLoan.flashLoan({ receiver: goodFlashLoanReceiver, asset: address(dai), amount: amount, data: bytes("") }); - } - - function test_RevertGiven_AssetNotFlashLoanable() external whenNotDelegateCalled whenAmountNotTooHigh { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_AssetNotFlashLoanable.selector, dai)); - flashLoan.flashLoan({ receiver: goodFlashLoanReceiver, asset: address(dai), amount: 0, data: bytes("") }); - } - - function test_RevertWhen_CalculatedFeeTooHigh() - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - { - // Set the comptroller flash fee so that the calculated fee ends up being greater than 2^128. - comptroller.setFlashFee({ newFlashFee: ud(1.1e18) }); - - uint256 fee = flashLoan.flashFee({ asset: address(dai), amount: MAX_UINT128 }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_CalculatedFeeTooHigh.selector, fee)); - flashLoan.flashLoan({ - receiver: goodFlashLoanReceiver, - asset: address(dai), - amount: MAX_UINT128, - data: bytes("") - }); - } - - function test_RevertWhen_BorrowFailed() - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - whenCalculatedFeeNotTooHigh - { - deal({ token: address(dai), to: address(flashLoan), give: LIQUIDITY_AMOUNT }); - vm.expectRevert(Errors.SablierV2FlashLoan_FlashBorrowFail.selector); - flashLoan.flashLoan({ - receiver: faultyFlashLoanReceiver, - asset: address(dai), - amount: LIQUIDITY_AMOUNT, - data: bytes("") - }); - } - - function test_RevertWhen_Reentrancy() - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - whenCalculatedFeeNotTooHigh - whenBorrowDoesNotFail - { - uint256 amount = 100e18; - deal({ token: address(dai), to: address(flashLoan), give: amount * 2 }); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - flashLoan.flashLoan({ - receiver: reentrantFlashLoanReceiver, - asset: address(dai), - amount: LIQUIDITY_AMOUNT / 4, - data: bytes("") - }); - } - - function test_FlashLoan() - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - whenCalculatedFeeNotTooHigh - whenBorrowDoesNotFail - whenNoReentrancy - { - // Mint the liquidity amount to the contract. - deal({ token: address(dai), to: address(flashLoan), give: LIQUIDITY_AMOUNT }); - - // Load the initial protocol revenues. - uint128 initialProtocolRevenues = flashLoan.protocolRevenues(dai); - - // Load the flash fee. - uint256 fee = flashLoan.flashFee({ asset: address(dai), amount: LIQUIDITY_AMOUNT }); - - // Mint the flash fee to the receiver so that they can repay the flash loan. - deal({ token: address(dai), to: address(goodFlashLoanReceiver), give: fee }); - - // Expect `amount` of assets to be transferred to the receiver. - expectCallToTransfer({ to: address(goodFlashLoanReceiver), amount: LIQUIDITY_AMOUNT }); - - // Expect `amount+fee` of assets to be transferred back from the receiver. - uint256 returnAmount = LIQUIDITY_AMOUNT + fee; - expectCallToTransferFrom({ from: address(goodFlashLoanReceiver), to: address(flashLoan), amount: returnAmount }); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(flashLoan) }); - bytes memory data = bytes("Hello World"); - emit FlashLoan({ - initiator: users.admin, - receiver: goodFlashLoanReceiver, - asset: dai, - amount: LIQUIDITY_AMOUNT, - feeAmount: fee, - data: data - }); - - // Execute the flash loan. - bool response = flashLoan.flashLoan({ - receiver: goodFlashLoanReceiver, - asset: address(dai), - amount: LIQUIDITY_AMOUNT, - data: data - }); - - // Assert that the returned response is `true`. - assertTrue(response, "flashLoan response"); - - // Assert that the protocol fee has been recorded. - uint128 actualProtocolRevenues = flashLoan.protocolRevenues(dai); - uint128 expectedProtocolRevenues = initialProtocolRevenues + uint128(fee); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - } -} diff --git a/test/integration/concrete/flash-loan/flash-loan/flashLoan.tree b/test/integration/concrete/flash-loan/flash-loan/flashLoan.tree deleted file mode 100644 index f2af3ec8b..000000000 --- a/test/integration/concrete/flash-loan/flash-loan/flashLoan.tree +++ /dev/null @@ -1,26 +0,0 @@ -flashLoan.t.sol -├── when delegate called -│ └── it should revert -└── when not delegate called - ├── when the flash loan amount is too high - │ └── it should revert - └── when the flash loan amount is not too high - ├── given the asset is not flash loanable - │ └── it should revert - └── given the asset is flash loanable - ├── when the calculated fee is too high - │ └── it should revert - └── when the calculated fee is not too high - ├── when the flash loan amount is greater than the available liquidity - │ └── it should revert - └── when the flash loan amount is less than or equal to the available liquidity - ├── when the receiver does not return the correct response - │ └── it should revert - └── when the receiver returns the correct response - ├── when there is reentrancy - │ └── it should revert - └── when there is no reentrancy - ├── it should execute the flash loan - ├── it should make the ERC-20 transfers - ├── it should update the protocol revenues - └── it should emit a {FlashLoan} event diff --git a/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.t.sol b/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.t.sol deleted file mode 100644 index 2f52a376c..000000000 --- a/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { FlashLoan_Integration_Shared_Test } from "../../../shared/flash-loan/FlashLoan.t.sol"; - -contract MaxFlashLoan_Integration_Concrete_Test is FlashLoan_Integration_Shared_Test { - function test_MaxFlashLoan_AssetNotFlashLoanable() external { - uint256 actualAmount = flashLoan.maxFlashLoan(address(dai)); - uint256 expectedAmount = 0; - assertEq(actualAmount, expectedAmount, "maxFlashLoan amount"); - } - - modifier givenAssetFlashLoanable() { - comptroller.toggleFlashAsset(dai); - _; - } - - function test_MaxFlashLoan() external givenAssetFlashLoanable { - uint256 dealAmount = 14_607_904e18; - deal({ token: address(dai), to: address(flashLoan), give: dealAmount }); - uint256 actualAmount = flashLoan.maxFlashLoan(address(dai)); - uint256 expectedAmount = dealAmount; - assertEq(actualAmount, expectedAmount, "maxFlashLoan amount"); - } -} diff --git a/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.tree b/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.tree deleted file mode 100644 index 7eedfcdd9..000000000 --- a/test/integration/concrete/flash-loan/max-flash-loan/maxFlashLoan.tree +++ /dev/null @@ -1,5 +0,0 @@ -maxFlashLoan.t.sol -├── given the asset is not flash loanable -│ └── it should revert -└── given the asset is flash loanable - └── it should return the correct max flash loan diff --git a/test/integration/concrete/lockup-dynamic/LockupDynamic.t.sol b/test/integration/concrete/lockup-dynamic/LockupDynamic.t.sol index c5ff9c6e7..cca53bda0 100644 --- a/test/integration/concrete/lockup-dynamic/LockupDynamic.t.sol +++ b/test/integration/concrete/lockup-dynamic/LockupDynamic.t.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { LockupDynamic_Integration_Shared_Test } from "../../shared/lockup-dynamic/LockupDynamic.t.sol"; import { Integration_Test } from "../../Integration.t.sol"; +import { AllowToHook_Integration_Concrete_Test } from "../lockup/allow-to-hook/allowToHook.t.sol"; import { Burn_Integration_Concrete_Test } from "../lockup/burn/burn.t.sol"; import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; import { CancelMultiple_Integration_Concrete_Test } from "../lockup/cancel-multiple/cancelMultiple.t.sol"; -import { ClaimProtocolRevenues_Integration_Concrete_Test } from - "../lockup/claim-protocol-revenues/claimProtocolRevenues.t.sol"; import { GetAsset_Integration_Concrete_Test } from "../lockup/get-asset/getAsset.t.sol"; import { GetDepositedAmount_Integration_Concrete_Test } from "../lockup/get-deposited-amount/getDepositedAmount.t.sol"; import { GetEndTime_Integration_Concrete_Test } from "../lockup/get-end-time/getEndTime.t.sol"; @@ -18,21 +17,21 @@ import { GetRefundedAmount_Integration_Concrete_Test } from "../lockup/get-refun import { GetSender_Integration_Concrete_Test } from "../lockup/get-sender/getSender.t.sol"; import { GetStartTime_Integration_Concrete_Test } from "../lockup/get-start-time/getStartTime.t.sol"; import { GetWithdrawnAmount_Integration_Concrete_Test } from "../lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol"; +import { IsAllowedToHook_Integration_Concrete_Test } from "../lockup/is-allowed-to-hook/isAllowedToHook.t.sol"; import { IsCancelable_Integration_Concrete_Test } from "../lockup/is-cancelable/isCancelable.t.sol"; import { IsCold_Integration_Concrete_Test } from "../lockup/is-cold/isCold.t.sol"; import { IsDepleted_Integration_Concrete_Test } from "../lockup/is-depleted/isDepleted.t.sol"; import { IsStream_Integration_Concrete_Test } from "../lockup/is-stream/isStream.t.sol"; import { IsTransferable_Integration_Concrete_Test } from "../lockup/is-transferable/isTransferable.t.sol"; import { IsWarm_Integration_Concrete_Test } from "../lockup/is-warm/isWarm.t.sol"; -import { ProtocolRevenues_Integration_Concrete_Test } from "../lockup/protocol-revenues/protocolRevenues.t.sol"; import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; -import { SetComptroller_Integration_Concrete_Test } from "../lockup/set-comptroller/setComptroller.t.sol"; import { SetNFTDescriptor_Integration_Concrete_Test } from "../lockup/set-nft-descriptor/setNFTDescriptor.t.sol"; import { StatusOf_Integration_Concrete_Test } from "../lockup/status-of/statusOf.t.sol"; import { TransferFrom_Integration_Concrete_Test } from "../lockup/transfer-from/transferFrom.t.sol"; -import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; import { WasCanceled_Integration_Concrete_Test } from "../lockup/was-canceled/wasCanceled.t.sol"; +import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; +import { WithdrawHooks_Integration_Concrete_Test } from "../lockup/withdraw-hooks/withdrawHooks.t.sol"; import { WithdrawMax_Integration_Concrete_Test } from "../lockup/withdraw-max/withdrawMax.t.sol"; import { WithdrawMaxAndTransfer_Integration_Concrete_Test } from "../lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol"; @@ -50,8 +49,7 @@ abstract contract LockupDynamic_Integration_Concrete_Test is Integration_Test, L Integration_Test.setUp(); LockupDynamic_Integration_Shared_Test.setUp(); - // Cast the LockupDynamic contract as {ISablierV2Base} and {ISablierV2Lockup}. - base = ISablierV2Lockup(lockupDynamic); + // Cast the {LockupDynamic} contract as {ISablierV2Lockup}. lockup = ISablierV2Lockup(lockupDynamic); } } @@ -60,6 +58,20 @@ abstract contract LockupDynamic_Integration_Concrete_Test is Integration_Test, L SHARED TESTS //////////////////////////////////////////////////////////////////////////*/ +contract AllowToHook_LockupDynamic_Integration_Concrete_Test is + LockupDynamic_Integration_Concrete_Test, + AllowToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupDynamic_Integration_Concrete_Test, AllowToHook_Integration_Concrete_Test) + { + LockupDynamic_Integration_Concrete_Test.setUp(); + AllowToHook_Integration_Concrete_Test.setUp(); + } +} + contract Burn_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, Burn_Integration_Concrete_Test @@ -98,20 +110,6 @@ contract CancelMultiple_LockupDynamic_Integration_Concrete_Test is } } -contract ClaimProtocolRevenues_LockupDynamic_Integration_Concrete_Test is - LockupDynamic_Integration_Concrete_Test, - ClaimProtocolRevenues_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupDynamic_Integration_Concrete_Test, ClaimProtocolRevenues_Integration_Concrete_Test) - { - LockupDynamic_Integration_Concrete_Test.setUp(); - ClaimProtocolRevenues_Integration_Concrete_Test.setUp(); - } -} - contract GetAsset_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, GetAsset_Integration_Concrete_Test @@ -224,6 +222,20 @@ contract GetWithdrawnAmount_LockupDynamic_Integration_Concrete_Test is } } +contract IsAllowedToHook_LockupDynamic_Integration_Concrete_Test is + LockupDynamic_Integration_Concrete_Test, + IsAllowedToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupDynamic_Integration_Concrete_Test, IsAllowedToHook_Integration_Concrete_Test) + { + LockupDynamic_Integration_Concrete_Test.setUp(); + IsAllowedToHook_Integration_Concrete_Test.setUp(); + } +} + contract IsCancelable_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, IsCancelable_Integration_Concrete_Test @@ -308,20 +320,6 @@ contract IsWarm_LockupDynamic_Integration_Concrete_Test is } } -contract ProtocolRevenues_LockupDynamic_Integration_Concrete_Test is - LockupDynamic_Integration_Concrete_Test, - ProtocolRevenues_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupDynamic_Integration_Concrete_Test, ProtocolRevenues_Integration_Concrete_Test) - { - LockupDynamic_Integration_Concrete_Test.setUp(); - ProtocolRevenues_Integration_Concrete_Test.setUp(); - } -} - contract RefundableAmountOf_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, RefundableAmountOf_Integration_Concrete_Test @@ -350,20 +348,6 @@ contract Renounce_LockupDynamic_Integration_Concrete_Test is } } -contract SetComptroller_LockupDynamic_Integration_Concrete_Test is - LockupDynamic_Integration_Concrete_Test, - SetComptroller_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupDynamic_Integration_Concrete_Test, SetComptroller_Integration_Concrete_Test) - { - LockupDynamic_Integration_Concrete_Test.setUp(); - SetComptroller_Integration_Concrete_Test.setUp(); - } -} - contract SetNFTDescriptor_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, SetNFTDescriptor_Integration_Concrete_Test @@ -434,6 +418,20 @@ contract Withdraw_LockupDynamic_Integration_Concrete_Test is } } +contract WithdrawHooks_LockupDynamic_Integration_Concrete_Test is + LockupDynamic_Integration_Concrete_Test, + WithdrawHooks_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupDynamic_Integration_Concrete_Test, WithdrawHooks_Integration_Concrete_Test) + { + LockupDynamic_Integration_Concrete_Test.setUp(); + WithdrawHooks_Integration_Concrete_Test.setUp(); + } +} + contract WithdrawMax_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, WithdrawMax_Integration_Concrete_Test diff --git a/test/integration/concrete/lockup-dynamic/constructor.t.sol b/test/integration/concrete/lockup-dynamic/constructor.t.sol index 01cb03c8b..89bb8c07f 100644 --- a/test/integration/concrete/lockup-dynamic/constructor.t.sol +++ b/test/integration/concrete/lockup-dynamic/constructor.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { SablierV2LockupDynamic } from "src/SablierV2LockupDynamic.sol"; import { LockupDynamic_Integration_Concrete_Test } from "./LockupDynamic.t.sol"; @@ -14,25 +15,31 @@ contract Constructor_LockupDynamic_Integration_Concrete_Test is LockupDynamic_In // Construct the contract. SablierV2LockupDynamic constructedLockupDynamic = new SablierV2LockupDynamic({ initialAdmin: users.admin, - initialComptroller: comptroller, initialNFTDescriptor: nftDescriptor, maxSegmentCount: defaults.MAX_SEGMENT_COUNT() }); - // {SablierV2Base.constructor} + // {SablierV2Lockup.constant} + UD60x18 actualMaxBrokerFee = constructedLockupDynamic.MAX_BROKER_FEE(); + UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18); + assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE"); + + // {SablierV2Lockup.constructor} address actualAdmin = constructedLockupDynamic.admin(); address expectedAdmin = users.admin; assertEq(actualAdmin, expectedAdmin, "admin"); - address actualComptroller = address(constructedLockupDynamic.comptroller()); - address expectedComptroller = address(comptroller); - assertEq(actualComptroller, expectedComptroller, "comptroller"); - - // {SablierV2Lockup.constructor} uint256 actualStreamId = constructedLockupDynamic.nextStreamId(); uint256 expectedStreamId = 1; assertEq(actualStreamId, expectedStreamId, "nextStreamId"); + address actualNFTDescriptor = address(constructedLockupDynamic.nftDescriptor()); + address expectedNFTDescriptor = address(nftDescriptor); + assertEq(actualNFTDescriptor, expectedNFTDescriptor, "nftDescriptor"); + + // {SablierV2Lockup.supportsInterface} + assertTrue(constructedLockupDynamic.supportsInterface(0x49064906), "ERC-4906 interface ID"); + // {SablierV2LockupDynamic.constructor} uint256 actualMaxSegmentCount = constructedLockupDynamic.MAX_SEGMENT_COUNT(); uint256 expectedMaxSegmentCount = defaults.MAX_SEGMENT_COUNT(); diff --git a/test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.t.sol b/test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.t.sol deleted file mode 100644 index cd79d99a4..000000000 --- a/test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.t.sol +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { ud2x18 } from "@prb/math/src/UD2x18.sol"; - -import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; -import { Errors } from "src/libraries/Errors.sol"; -import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; - -import { CreateWithDeltas_Integration_Shared_Test } from "../../../shared/lockup-dynamic/createWithDeltas.t.sol"; -import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; - -contract CreateWithDeltas_LockupDynamic_Integration_Concrete_Test is - LockupDynamic_Integration_Concrete_Test, - CreateWithDeltas_Integration_Shared_Test -{ - function setUp() - public - virtual - override(LockupDynamic_Integration_Concrete_Test, CreateWithDeltas_Integration_Shared_Test) - { - LockupDynamic_Integration_Concrete_Test.setUp(); - CreateWithDeltas_Integration_Shared_Test.setUp(); - streamId = lockupDynamic.nextStreamId(); - } - - /// @dev it should revert. - function test_RevertWhen_DelegateCalled() external { - bytes memory callData = abi.encodeCall(ISablierV2LockupDynamic.createWithDeltas, defaults.createWithDeltas()); - (bool success, bytes memory returnData) = address(lockupDynamic).delegatecall(callData); - expectRevertDueToDelegateCall(success, returnData); - } - - /// @dev it should revert. - function test_RevertWhen_LoopCalculationOverflowsBlockGasLimit() external whenNotDelegateCalled { - LockupDynamic.SegmentWithDelta[] memory segments = new LockupDynamic.SegmentWithDelta[](250_000); - vm.expectRevert(bytes("")); - createDefaultStreamWithDeltas(segments); - } - - function test_RevertWhen_DeltasZero() - external - whenNotDelegateCalled - whenLoopCalculationsDoNotOverflowBlockGasLimit - { - uint40 startTime = getBlockTimestamp(); - LockupDynamic.SegmentWithDelta[] memory segments = defaults.createWithDeltas().segments; - segments[1].delta = 0; - uint256 index = 1; - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_SegmentMilestonesNotOrdered.selector, - index, - startTime + segments[0].delta, - startTime + segments[0].delta - ) - ); - createDefaultStreamWithDeltas(segments); - } - - function test_RevertWhen_MilestonesCalculationsOverflows_StartTimeNotLessThanFirstSegmentMilestone() - external - whenNotDelegateCalled - whenLoopCalculationsDoNotOverflowBlockGasLimit - whenDeltasNotZero - { - unchecked { - uint40 startTime = getBlockTimestamp(); - LockupDynamic.SegmentWithDelta[] memory segments = defaults.createWithDeltas().segments; - segments[0].delta = MAX_UINT40; - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone.selector, - startTime, - startTime + segments[0].delta - ) - ); - createDefaultStreamWithDeltas(segments); - } - } - - function test_RevertWhen_MilestonesCalculationsOverflows_SegmentMilestonesNotOrdered() - external - whenNotDelegateCalled - whenLoopCalculationsDoNotOverflowBlockGasLimit - whenDeltasNotZero - { - unchecked { - uint40 startTime = getBlockTimestamp(); - - // Create new segments that overflow when the milestones are eventually calculated. - LockupDynamic.SegmentWithDelta[] memory segments = new LockupDynamic.SegmentWithDelta[](2); - segments[0] = - LockupDynamic.SegmentWithDelta({ amount: 0, exponent: ud2x18(1e18), delta: startTime + 1 seconds }); - segments[1] = defaults.segmentsWithDeltas()[0]; - segments[1].delta = MAX_UINT40; - - // Expect the relevant error to be thrown. - uint256 index = 1; - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_SegmentMilestonesNotOrdered.selector, - index, - startTime + segments[0].delta, - startTime + segments[0].delta + segments[1].delta - ) - ); - - // Create the stream. - createDefaultStreamWithDeltas(segments); - } - } - - function test_CreateWithDeltas() - external - whenNotDelegateCalled - whenLoopCalculationsDoNotOverflowBlockGasLimit - whenDeltasNotZero - whenMilestonesCalculationsDoNotOverflow - { - // Make the Sender the stream's funder - address funder = users.sender; - - // Load the initial protocol revenues. - uint128 initialProtocolRevenues = lockupDynamic.protocolRevenues(dai); - - // Declare the range. - uint40 currentTime = getBlockTimestamp(); - LockupDynamic.Range memory range = - LockupDynamic.Range({ start: currentTime, end: currentTime + defaults.TOTAL_DURATION() }); - - // Adjust the segments. - LockupDynamic.SegmentWithDelta[] memory segmentsWithDeltas = defaults.segmentsWithDeltas(); - LockupDynamic.Segment[] memory segments = defaults.segments(); - segments[0].milestone = range.start + segmentsWithDeltas[0].delta; - segments[1].milestone = segments[0].milestone + segmentsWithDeltas[1].delta; - - // Expect the assets to be transferred from the funder to {SablierV2LockupDynamic}. - expectCallToTransferFrom({ - from: funder, - to: address(lockupDynamic), - amount: defaults.DEPOSIT_AMOUNT() + defaults.PROTOCOL_FEE_AMOUNT() - }); - - // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, amount: defaults.BROKER_FEE_AMOUNT() }); - - // Expect the relevant events to be emitted. - vm.expectEmit({ emitter: address(lockupDynamic) }); - emit MetadataUpdate({ _tokenId: streamId }); - vm.expectEmit({ emitter: address(lockupDynamic) }); - emit CreateLockupDynamicStream({ - streamId: streamId, - funder: funder, - sender: users.sender, - recipient: users.recipient, - amounts: defaults.lockupCreateAmounts(), - asset: dai, - cancelable: true, - transferable: true, - segments: segments, - range: range, - broker: users.broker - }); - - // Create the stream. - createDefaultStreamWithDeltas(); - - // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); - expectedStream.endTime = range.end; - expectedStream.segments = segments; - expectedStream.startTime = range.start; - assertEq(actualStream, expectedStream); - - // Assert that the stream's status is "STREAMING". - Lockup.Status actualStatus = lockupDynamic.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.STREAMING; - assertEq(actualStatus, expectedStatus); - - // Assert that the next stream id has been bumped. - uint256 actualNextStreamId = lockupDynamic.nextStreamId(); - uint256 expectedNextStreamId = streamId + 1; - assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); - - // Assert that the protocol fee has been recorded. - uint128 actualProtocolRevenues = lockupDynamic.protocolRevenues(dai); - uint128 expectedProtocolRevenues = initialProtocolRevenues + defaults.PROTOCOL_FEE_AMOUNT(); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - - // Assert that the NFT has been minted. - address actualNFTOwner = lockupDynamic.ownerOf({ tokenId: streamId }); - address expectedNFTOwner = users.recipient; - assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); - } -} diff --git a/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol new file mode 100644 index 000000000..4edb555f3 --- /dev/null +++ b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; + +import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; +import { Errors } from "src/libraries/Errors.sol"; +import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; + +import { CreateWithDurations_Integration_Shared_Test } from "../../../shared/lockup/createWithDurations.t.sol"; +import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; + +contract CreateWithDurations_LockupDynamic_Integration_Concrete_Test is + LockupDynamic_Integration_Concrete_Test, + CreateWithDurations_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupDynamic_Integration_Concrete_Test, CreateWithDurations_Integration_Shared_Test) + { + LockupDynamic_Integration_Concrete_Test.setUp(); + CreateWithDurations_Integration_Shared_Test.setUp(); + streamId = lockupDynamic.nextStreamId(); + } + + /// @dev it should revert. + function test_RevertWhen_DelegateCalled() external { + bytes memory callData = + abi.encodeCall(ISablierV2LockupDynamic.createWithDurations, defaults.createWithDurationsLD()); + (bool success, bytes memory returnData) = address(lockupDynamic).delegatecall(callData); + expectRevertDueToDelegateCall(success, returnData); + } + + /// @dev it should revert. + function test_RevertWhen_SegmentCountTooHigh() external whenNotDelegateCalled { + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](25_000); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2LockupDynamic_SegmentCountTooHigh.selector, 25_000)); + createDefaultStreamWithDurations(segments); + } + + function test_RevertWhen_DurationsZero() external whenNotDelegateCalled whenSegmentCountNotTooHigh { + uint40 startTime = getBlockTimestamp(); + LockupDynamic.SegmentWithDuration[] memory segments = defaults.createWithDurationsLD().segments; + segments[1].duration = 0; + uint256 index = 1; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered.selector, + index, + startTime + segments[0].duration, + startTime + segments[0].duration + ) + ); + createDefaultStreamWithDurations(segments); + } + + function test_RevertWhen_TimestampsCalculationsOverflows_StartTimeNotLessThanFirstSegmentTimestamp() + external + whenNotDelegateCalled + whenSegmentCountNotTooHigh + whenDurationsNotZero + { + unchecked { + uint40 startTime = getBlockTimestamp(); + LockupDynamic.SegmentWithDuration[] memory segments = defaults.segmentsWithDurations(); + segments[0].duration = MAX_UINT40; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp.selector, + startTime, + startTime + segments[0].duration + ) + ); + createDefaultStreamWithDurations(segments); + } + } + + function test_RevertWhen_TimestampsCalculationsOverflows_SegmentTimestampsNotOrdered() + external + whenNotDelegateCalled + whenSegmentCountNotTooHigh + whenDurationsNotZero + { + unchecked { + uint40 startTime = getBlockTimestamp(); + + // Create new segments that overflow when the timestamps are eventually calculated. + LockupDynamic.SegmentWithDuration[] memory segments = new LockupDynamic.SegmentWithDuration[](2); + segments[0] = LockupDynamic.SegmentWithDuration({ + amount: 0, + exponent: ud2x18(1e18), + duration: startTime + 1 seconds + }); + segments[1] = defaults.segmentsWithDurations()[0]; + segments[1].duration = MAX_UINT40; + + // Expect the relevant error to be thrown. + uint256 index = 1; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered.selector, + index, + startTime + segments[0].duration, + startTime + segments[0].duration + segments[1].duration + ) + ); + + // Create the stream. + createDefaultStreamWithDurations(segments); + } + } + + function test_CreateWithDurations() + external + whenNotDelegateCalled + whenSegmentCountNotTooHigh + whenDurationsNotZero + whenTimestampsCalculationsDoNotOverflow + { + // Make the Sender the stream's funder + address funder = users.sender; + + // Declare the timestamps. + uint40 blockTimestamp = getBlockTimestamp(); + LockupDynamic.Timestamps memory timestamps = + LockupDynamic.Timestamps({ start: blockTimestamp, end: blockTimestamp + defaults.TOTAL_DURATION() }); + + // Adjust the segments. + LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations = defaults.segmentsWithDurations(); + LockupDynamic.Segment[] memory segments = defaults.segments(); + segments[0].timestamp = timestamps.start + segmentsWithDurations[0].duration; + segments[1].timestamp = segments[0].timestamp + segmentsWithDurations[1].duration; + + // Expect the assets to be transferred from the funder to {SablierV2LockupDynamic}. + expectCallToTransferFrom({ from: funder, to: address(lockupDynamic), value: defaults.DEPOSIT_AMOUNT() }); + + // Expect the broker fee to be paid to the broker. + expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupDynamic) }); + emit MetadataUpdate({ _tokenId: streamId }); + vm.expectEmit({ emitter: address(lockupDynamic) }); + emit CreateLockupDynamicStream({ + streamId: streamId, + funder: funder, + sender: users.sender, + recipient: users.recipient, + amounts: defaults.lockupCreateAmounts(), + asset: dai, + cancelable: true, + transferable: true, + segments: segments, + timestamps: timestamps, + broker: users.broker + }); + + // Create the stream. + createDefaultStreamWithDurations(); + + // Assert that the stream has been created. + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); + expectedStream.endTime = timestamps.end; + expectedStream.segments = segments; + expectedStream.startTime = timestamps.start; + assertEq(actualStream, expectedStream); + + // Assert that the stream's status is "STREAMING". + Lockup.Status actualStatus = lockupDynamic.statusOf(streamId); + Lockup.Status expectedStatus = Lockup.Status.STREAMING; + assertEq(actualStatus, expectedStatus); + + // Assert that the next stream ID has been bumped. + uint256 actualNextStreamId = lockupDynamic.nextStreamId(); + uint256 expectedNextStreamId = streamId + 1; + assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + address actualNFTOwner = lockupDynamic.ownerOf({ tokenId: streamId }); + address expectedNFTOwner = users.recipient; + assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); + } +} diff --git a/test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.tree b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.tree similarity index 51% rename from test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.tree rename to test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.tree index 3824747f3..fa917579a 100644 --- a/test/integration/concrete/lockup-dynamic/create-with-deltas/createWithDeltas.tree +++ b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.tree @@ -1,22 +1,21 @@ -createWithDeltas.t.sol +createWithDurations.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── when the loop calculations overflow the block gas limit + ├── when the segment count is too high │ └── it should revert - └── when the loop calculations do not overflow the block gas limit - ├── when at least one of the deltas at index one or greater is zero + └── when the segment count is not too high + ├── when at least one of the durations at index one or greater is zero │ └── it should revert - └── when none of the deltas is zero - ├── when the segment milestone calculations overflow uint256 - │ ├── when the start time is not less than the first segment milestone + └── when none of the durations is zero + ├── when the segment timestamp calculations overflow uint256 + │ ├── when the start time is not less than the first segment timestamp │ │ └── it should revert - │ └── when the segment milestones are not ordered + │ └── when the segment timestamps are not ordered │ └── it should revert - └── when the segment milestone calculations do not overflow uint256 + └── when the segment timestamp calculations do not overflow uint256 ├── it should create the stream - ├── it should bump the next stream id - ├── it should record the protocol fee + ├── it should bump the next stream ID ├── it should mint the NFT ├── it should emit a {MetadataUpdate} event ├── it should perform the ERC-20 transfers diff --git a/test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.t.sol b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol similarity index 68% rename from test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.t.sol rename to test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol index 46e62461a..c804413cf 100644 --- a/test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.t.sol +++ b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UD60x18, ud, ZERO } from "@prb/math/src/UD60x18.sol"; import { stdError } from "forge-std/src/StdError.sol"; @@ -9,48 +11,59 @@ import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic. import { Errors } from "src/libraries/Errors.sol"; import { Broker, Lockup, LockupDynamic } from "src/types/DataTypes.sol"; -import { CreateWithMilestones_Integration_Shared_Test } from "../../../shared/lockup-dynamic/createWithMilestones.t.sol"; +import { CreateWithTimestamps_Integration_Shared_Test } from "../../../shared/lockup/createWithTimestamps.t.sol"; import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; -contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is +contract CreateWithTimestamps_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test, - CreateWithMilestones_Integration_Shared_Test + CreateWithTimestamps_Integration_Shared_Test { function setUp() public virtual - override(LockupDynamic_Integration_Concrete_Test, CreateWithMilestones_Integration_Shared_Test) + override(LockupDynamic_Integration_Concrete_Test, CreateWithTimestamps_Integration_Shared_Test) { LockupDynamic_Integration_Concrete_Test.setUp(); - CreateWithMilestones_Integration_Shared_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); } function test_RevertWhen_DelegateCalled() external { bytes memory callData = - abi.encodeCall(ISablierV2LockupDynamic.createWithMilestones, defaults.createWithMilestones()); + abi.encodeCall(ISablierV2LockupDynamic.createWithTimestamps, defaults.createWithTimestampsLD()); (bool success, bytes memory returnData) = address(lockupDynamic).delegatecall(callData); expectRevertDueToDelegateCall(success, returnData); } function test_RevertWhen_RecipientZeroAddress() external whenNotDelegateCalled { - vm.expectRevert("ERC721: mint to the zero address"); address recipient = address(0); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, recipient)); createDefaultStreamWithRecipient(recipient); } function test_RevertWhen_DepositAmountZero() external whenNotDelegateCalled whenRecipientNonZeroAddress { - // It is not possible to obtain a zero deposit amount from a non-zero total amount, because the `MAX_FEE` + // It is not possible to obtain a zero deposit amount from a non-zero total amount, because the `MAX_BROKER_FEE` // is hard coded to 10%. vm.expectRevert(Errors.SablierV2Lockup_DepositAmountZero.selector); uint128 totalAmount = 0; createDefaultStreamWithTotalAmount(totalAmount); } + function test_RevertWhen_StartTimeZero() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + { + vm.expectRevert(Errors.SablierV2Lockup_StartTimeZero.selector); + createDefaultStreamWithStartTime(0); + } + function test_RevertWhen_SegmentCountZero() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero { LockupDynamic.Segment[] memory segments; vm.expectRevert(Errors.SablierV2LockupDynamic_SegmentCountZero.selector); @@ -62,6 +75,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero { uint256 segmentCount = defaults.MAX_SEGMENT_COUNT() + 1; @@ -77,6 +91,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh { @@ -87,25 +102,26 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is createDefaultStreamWithSegments(segments); } - function test_RevertWhen_StartTimeGreaterThanFirstSegmentMilestone() + function test_RevertWhen_StartTimeGreaterThanFirstSegmentTimestamp() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow { - // Change the milestone of the first segment. + // Change the timestamp of the first segment. LockupDynamic.Segment[] memory segments = defaults.segments(); - segments[0].milestone = defaults.START_TIME() - 1 seconds; + segments[0].timestamp = defaults.START_TIME() - 1 seconds; // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone.selector, + Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp.selector, defaults.START_TIME(), - segments[0].milestone + segments[0].timestamp ) ); @@ -113,25 +129,26 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is createDefaultStreamWithSegments(segments); } - function test_RevertWhen_StartTimeEqualToFirstSegmentMilestone() + function test_RevertWhen_StartTimeEqualToFirstSegmentTimestamp() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow { - // Change the milestone of the first segment. + // Change the timestamp of the first segment. LockupDynamic.Segment[] memory segments = defaults.segments(); - segments[0].milestone = defaults.START_TIME(); + segments[0].timestamp = defaults.START_TIME(); // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone.selector, + Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp.selector, defaults.START_TIME(), - segments[0].milestone + segments[0].timestamp ) ); @@ -139,28 +156,29 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is createDefaultStreamWithSegments(segments); } - function test_RevertWhen_SegmentMilestonesNotOrdered() + function test_RevertWhen_SegmentTimestampsNotOrdered() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone + whenStartTimeLessThanFirstSegmentTimestamp { - // Swap the segment milestones. + // Swap the segment timestamps. LockupDynamic.Segment[] memory segments = defaults.segments(); - (segments[0].milestone, segments[1].milestone) = (segments[1].milestone, segments[0].milestone); + (segments[0].timestamp, segments[1].timestamp) = (segments[1].timestamp, segments[0].timestamp); // Expect the relevant error to be thrown. uint256 index = 1; vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_SegmentMilestonesNotOrdered.selector, + Errors.SablierV2LockupDynamic_SegmentTimestampsNotOrdered.selector, index, - segments[0].milestone, - segments[1].milestone + segments[0].timestamp, + segments[1].timestamp ) ); @@ -173,14 +191,15 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered { uint40 endTime = defaults.END_TIME(); - vm.warp({ timestamp: endTime }); + vm.warp({ newTimestamp: endTime }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_EndTimeNotInTheFuture.selector, endTime, endTime)); createDefaultStream(); } @@ -190,25 +209,23 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture { - // Disable both the protocol and the broker fee so that they don't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); UD60x18 brokerFee = ZERO; - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + 100; // Prepare the params. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: brokerFee }); params.totalAmount = depositAmount; @@ -222,52 +239,27 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is ); // Create the stream. - lockupDynamic.createWithMilestones(params); + lockupDynamic.createWithTimestamps(params); } - function test_RevertGiven_ProtocolFeeTooHigh() + function test_RevertWhen_BrokerFeeTooHigh() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum { - UD60x18 protocolFee = MAX_FEE + ud(1); - - // Set the protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); - changePrank({ msgSender: users.sender }); - - // Run the test. + UD60x18 brokerFee = MAX_BROKER_FEE + ud(1); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_ProtocolFeeTooHigh.selector, protocolFee, MAX_FEE) + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, brokerFee, MAX_BROKER_FEE) ); - createDefaultStream(); - } - - function test_RevertWhen_BrokerFeeTooHigh() - external - whenNotDelegateCalled - whenRecipientNonZeroAddress - whenDepositAmountNotZero - whenSegmentCountNotZero - whenSegmentCountNotTooHigh - whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered - whenEndTimeInTheFuture - whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh - { - UD60x18 brokerFee = MAX_FEE + ud(1); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, brokerFee, MAX_FEE)); createDefaultStreamWithBroker(Broker({ account: users.broker, fee: brokerFee })); } @@ -276,70 +268,66 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh { address nonContract = address(8128); - // Set the default protocol fee so that the test does not revert due to the deposit amount not being - // equal to the sum of the segment amounts. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee(IERC20(nonContract), defaults.PROTOCOL_FEE()); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); // Run the test. - vm.expectRevert("Address: call to non-contract"); + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, nonContract)); createDefaultStreamWithAsset(IERC20(nonContract)); } - function test_CreateWithMilestones_AssetMissingReturnValue() + function test_CreateWithTimestamps_AssetMissingReturnValue() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract { - testCreateWithMilestones(address(usdt)); + testCreateWithTimestamps(address(usdt)); } - function test_CreateWithMilestones() + function test_CreateWithTimestamps() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract whenAssetERC20 { - testCreateWithMilestones(address(dai)); + testCreateWithTimestamps(address(dai)); } - /// @dev Shared logic between {test_CreateWithMilestones_AssetMissingReturnValue} and {test_CreateWithMilestones}. - function testCreateWithMilestones(address asset) internal { + /// @dev Shared logic between {test_CreateWithTimestamps_AssetMissingReturnValue} and {test_CreateWithTimestamps}. + function testCreateWithTimestamps(address asset) internal { // Make the Sender the stream's funder. address funder = users.sender; @@ -348,7 +336,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is asset: IERC20(asset), from: funder, to: address(lockupDynamic), - amount: defaults.DEPOSIT_AMOUNT() + defaults.PROTOCOL_FEE_AMOUNT() + value: defaults.DEPOSIT_AMOUNT() }); // Expect the broker fee to be paid to the broker. @@ -356,7 +344,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is asset: IERC20(asset), from: funder, to: users.broker, - amount: defaults.BROKER_FEE_AMOUNT() + value: defaults.BROKER_FEE_AMOUNT() }); // Expect the relevant events to be emitted. @@ -373,7 +361,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is asset: IERC20(asset), cancelable: true, transferable: true, - range: defaults.lockupDynamicRange(), + timestamps: defaults.lockupDynamicTimestamps(), broker: users.broker }); @@ -381,8 +369,8 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is streamId = createDefaultStreamWithAsset(IERC20(asset)); // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); expectedStream.asset = IERC20(asset); assertEq(actualStream, expectedStream); @@ -391,7 +379,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Concrete_Test is Lockup.Status expectedStatus = Lockup.Status.PENDING; assertEq(actualStatus, expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. uint256 actualNextStreamId = lockupDynamic.nextStreamId(); uint256 expectedNextStreamId = streamId + 1; assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); diff --git a/test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.tree b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.tree similarity index 61% rename from test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.tree rename to test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.tree index c90aecfc5..8105493e3 100644 --- a/test/integration/concrete/lockup-dynamic/create-with-milestones/createWithMilestones.tree +++ b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.tree @@ -1,4 +1,4 @@ -createWithMilestones.t.sol +createWithTimestamps.t.sol ├── when delegate called │ └── it should revert └── when not delegate called @@ -8,32 +8,32 @@ createWithMilestones.t.sol ├── when the deposit amount is zero │ └── it should revert └── when the deposit amount is not zero - ├── when the segment count is zero + ├── when the start time is zero │ └── it should revert - └── when the segment count is not zero - ├── when the segment count is too high + └── when the start time is not zero + ├── when the segment count is zero │ └── it should revert - └── when the segment count is not too high - ├── when the segment amounts sum overflows + └── when the segment count is not zero + ├── when the segment count is too high │ └── it should revert - └── when the segment amounts sum does not overflow - ├── when the start time is greater than the first segment milestone + └── when the segment count is not too high + ├── when the segment amounts sum overflows │ └── it should revert - ├── when the start time is equal to the first segment milestone - │ └── it should revert - └── when the start time is less than the first segment milestone - ├── when the segment milestones are not ordered + └── when the segment amounts sum does not overflow + ├── when the start time is greater than the first segment timestamp + │ └── it should revert + ├── when the start time is equal to the first segment timestamp │ └── it should revert - └── when the segment milestones are ordered - ├── when the end time is not in the future + └── when the start time is less than the first segment timestamp + ├── when the segment timestamps are not ordered │ └── it should revert - └── when the end time is in the future - ├── when the deposit amount is not equal to the segment amounts sum + └── when the segment timestamps are ordered + ├── when the end time is not in the future │ └── it should revert - └── when the deposit amount is equal to the segment amounts sum - ├── given the protocol fee is too high + └── when the end time is in the future + ├── when the deposit amount is not equal to the segment amounts sum │ └── it should revert - └── given the protocol fee is not too high + └── when the deposit amount is equal to the segment amounts sum ├── when the broker fee is too high │ └── it should revert └── when the broker fee is not too high @@ -42,16 +42,14 @@ createWithMilestones.t.sol └── when the asset is a contract ├── when the asset misses the ERC-20 return value │ ├── it should create the stream - │ ├── it should bump the next stream id - │ ├── it should record the protocol fee + │ ├── it should bump the next stream ID │ ├── it should mint the NFT │ ├── it should emit a {MetadataUpdate} event │ ├── it should perform the ERC-20 transfers │ └── it should emit a {CreateLockupDynamicStream} event └── when the asset does not miss the ERC-20 return value ├── it should create the stream - ├── it should bump the next stream id - ├── it should record the protocol fee + ├── it should bump the next stream ID ├── it should mint the NFT ├── it should emit a {MetadataUpdate} event ├── it should perform the ERC-20 transfers diff --git a/test/integration/concrete/lockup-dynamic/get-range/getRange.tree b/test/integration/concrete/lockup-dynamic/get-range/getRange.tree deleted file mode 100644 index 88dcc9707..000000000 --- a/test/integration/concrete/lockup-dynamic/get-range/getRange.tree +++ /dev/null @@ -1,5 +0,0 @@ -getRange.t.sol -├── given the id references a null stream -│ └── it should revert -└── given the id does not reference a null stream - └── it should return the correct range diff --git a/test/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol b/test/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol index f46eee599..b94535d00 100644 --- a/test/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol +++ b/test/integration/concrete/lockup-dynamic/get-segments/getSegments.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { LockupDynamic } from "src/types/DataTypes.sol"; diff --git a/test/integration/concrete/lockup-dynamic/get-segments/getSegments.tree b/test/integration/concrete/lockup-dynamic/get-segments/getSegments.tree index b7032eba6..6ac298106 100644 --- a/test/integration/concrete/lockup-dynamic/get-segments/getSegments.tree +++ b/test/integration/concrete/lockup-dynamic/get-segments/getSegments.tree @@ -1,5 +1,5 @@ getSegments.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct segments diff --git a/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol b/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol index 5c01e7898..c079a3a0e 100644 --- a/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol +++ b/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { LockupDynamic } from "src/types/DataTypes.sol"; @@ -25,9 +25,9 @@ contract GetStream_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Inte } function test_GetStream_StatusSettled() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(defaultStreamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + vm.warp({ newTimestamp: defaults.END_TIME() }); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(defaultStreamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); expectedStream.isCancelable = false; assertEq(actualStream, expectedStream); } @@ -38,8 +38,8 @@ contract GetStream_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Inte function test_GetStream() external givenNotNull givenStatusNotSettled { uint256 streamId = createDefaultStream(); - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); assertEq(actualStream, expectedStream); } } diff --git a/test/integration/concrete/lockup-dynamic/get-stream/getStream.tree b/test/integration/concrete/lockup-dynamic/get-stream/getStream.tree index c3278db89..bde21f7b4 100644 --- a/test/integration/concrete/lockup-dynamic/get-stream/getStream.tree +++ b/test/integration/concrete/lockup-dynamic/get-stream/getStream.tree @@ -1,7 +1,7 @@ getStream.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is settled │ └── it should adjust the `isCancelable` flag and return the stream └── given the stream is not settled diff --git a/test/integration/concrete/lockup-dynamic/get-range/getRange.t.sol b/test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.t.sol similarity index 51% rename from test/integration/concrete/lockup-dynamic/get-range/getRange.t.sol rename to test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.t.sol index 892864707..889c0cf50 100644 --- a/test/integration/concrete/lockup-dynamic/get-range/getRange.t.sol +++ b/test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.t.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { LockupDynamic } from "src/types/DataTypes.sol"; import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; -contract GetRange_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test { +contract GetTimestamps_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test { function test_RevertGiven_Null() external { uint256 nullStreamId = 1729; vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); - lockupDynamic.getRange(nullStreamId); + lockupDynamic.getTimestamps(nullStreamId); } modifier givenNotNull() { _; } - function test_GetRange() external givenNotNull { + function test_GetTimestamps() external givenNotNull { uint256 streamId = createDefaultStream(); - LockupDynamic.Range memory actualRange = lockupDynamic.getRange(streamId); - LockupDynamic.Range memory expectedRange = defaults.lockupDynamicRange(); - assertEq(actualRange, expectedRange); + LockupDynamic.Timestamps memory actualTimestamps = lockupDynamic.getTimestamps(streamId); + LockupDynamic.Timestamps memory expectedTimestamps = defaults.lockupDynamicTimestamps(); + assertEq(actualTimestamps, expectedTimestamps); } } diff --git a/test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.tree b/test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.tree new file mode 100644 index 000000000..49e6bf2a0 --- /dev/null +++ b/test/integration/concrete/lockup-dynamic/get-timestamps/getTimestamps.tree @@ -0,0 +1,5 @@ +getTimestamps.t.sol +├── given the ID references a null stream +│ └── it should revert +└── given the ID does not reference a null stream + └── it should return the correct timestamps diff --git a/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol b/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol index 922cf3fc7..8682ce12b 100644 --- a/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LockupDynamic } from "src/types/DataTypes.sol"; @@ -25,7 +25,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is givenStreamHasNotBeenCanceled givenStatusStreaming { - vm.warp({ timestamp: 0 }); + vm.warp({ newTimestamp: 0 }); uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); @@ -37,7 +37,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is givenStreamHasNotBeenCanceled givenStatusStreaming { - vm.warp({ timestamp: defaults.START_TIME() }); + vm.warp({ newTimestamp: defaults.START_TIME() }); uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); @@ -51,14 +51,14 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is whenStartTimeInThePast { // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + 2000 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + 2000 seconds }); // Create an array with one segment. LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](1); segments[0] = LockupDynamic.Segment({ amount: defaults.DEPOSIT_AMOUNT(), exponent: defaults.segments()[1].exponent, - milestone: defaults.END_TIME() + timestamp: defaults.END_TIME() }); // Create the stream. @@ -74,7 +74,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is _; } - function test_StreamedAmountOf_CurrentMilestone1st() + function test_StreamedAmountOf_CurrentTimestamp1st() external givenNotNull givenStreamHasNotBeenCanceled @@ -83,7 +83,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is whenStartTimeInThePast { // Warp 1 second to the future. - vm.warp({ timestamp: defaults.START_TIME() + 1 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); // Run the test. uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(defaultStreamId); @@ -91,21 +91,21 @@ contract StreamedAmountOf_LockupDynamic_Integration_Concrete_Test is assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } - modifier givenCurrentMilestoneNot1st() { + modifier givenCurrentTimestampNot1st() { _; } - function test_StreamedAmountOf_CurrentMilestoneNot1st() + function test_StreamedAmountOf_CurrentTimestampNot1st() external givenNotNull givenStreamHasNotBeenCanceled givenStatusStreaming whenStartTimeInThePast givenMultipleSegments - givenCurrentMilestoneNot1st + givenCurrentTimestampNot1st { // Simulate the passage of time. 750 seconds is ~10% of the way in the second segment. - vm.warp({ timestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 750 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 750 seconds }); // Run the test. uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(defaultStreamId); diff --git a/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree b/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree index 50815136d..8a231f4a0 100644 --- a/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree +++ b/test/integration/concrete/lockup-dynamic/streamed-amount-of/streamedAmountOf.tree @@ -8,7 +8,7 @@ streamedAmountOf.t.sol ├── given there is one segment │ └── it should return the correct streamed amount └── given there are multiple segments - ├── given the current milestone is the 1st in the array + ├── given the current timestamp is the 1st in the array │ └── it should return the correct streamed amount - └── given the current milestone is not the 1st in the array + └── given the current timestamp is not the 1st in the array └── it should return the correct streamed amount diff --git a/test/integration/concrete/lockup-dynamic/token-uri/tokenURI.t.sol b/test/integration/concrete/lockup-dynamic/token-uri/tokenURI.t.sol index 80f06f345..4c4e4ad9a 100644 --- a/test/integration/concrete/lockup-dynamic/token-uri/tokenURI.t.sol +++ b/test/integration/concrete/lockup-dynamic/token-uri/tokenURI.t.sol @@ -1,24 +1,22 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-line-length,no-console,quotes -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { console2 } from "forge-std/src/console2.sol"; -import { LibString } from "solady/src/utils/LibString.sol"; import { StdStyle } from "forge-std/src/StdStyle.sol"; import { Base64 } from "solady/src/utils/Base64.sol"; import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; /// @dev Requirements for these tests to work: -/// - The stream id must be 1 +/// - The stream ID must be 1 /// - The stream's sender must be `0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca`, i.e. `makeAddr("Sender")` /// - The stream asset must have the DAI symbol /// - The contract deployer, i.e. the `sender` config option in `foundry.toml`, must have the default value /// 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38 so that the deployed contracts have the same addresses as /// the values hard coded in the tests below contract TokenURI_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integration_Concrete_Test { - using LibString for string; - address internal constant LOCKUP_DYNAMIC = 0xDB25A7b768311dE128BBDa7B8426c3f9C74f3240; uint256 internal defaultStreamId; @@ -33,13 +31,13 @@ contract TokenURI_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integ function test_RevertGiven_NFTDoesNotExist() external { uint256 nullStreamId = 1729; - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); lockupDynamic.tokenURI({ tokenId: nullStreamId }); } modifier givenNFTExists() { defaultStreamId = createDefaultStream(); - vm.warp({ timestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); _; } @@ -48,7 +46,7 @@ contract TokenURI_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Integ /// 2. Remember to escape the EOL character \n with \\n. function test_TokenURI_Decoded() external skipOnMismatch givenNFTExists { string memory tokenURI = lockupDynamic.tokenURI(defaultStreamId); - tokenURI = tokenURI.replace({ search: "data:application/json;base64,", replacement: "" }); + tokenURI = vm.replace({ input: tokenURI, from: "data:application/json;base64,", to: "" }); string memory actualDecodedTokenURI = string(Base64.decode(tokenURI)); string memory expectedDecodedTokenURI = unicode'{"attributes":[{"trait_type":"Asset","value":"DAI"},{"trait_type":"Sender","value":"0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca"},{"trait_type":"Status","value":"Streaming"}],"description":"This NFT represents a payment stream in a Sablier V2 Lockup Dynamic contract. The owner of this NFT can withdraw the streamed assets, which are denominated in DAI.\\n\\n- Stream ID: 1\\n- Lockup Dynamic Address: 0xdb25a7b768311de128bbda7b8426c3f9c74f3240\\n- DAI Address: 0x03a6a84cd762d9707a21605b548aaab891562aab\\n\\n⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient.","external_url":"https://sablier.com","name":"Sablier V2 Lockup Dynamic #1","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAwIiBoZWlnaHQ9IjEwMDAiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCNOb2lzZSkiLz48cmVjdCB4PSI3MCIgeT0iNzAiIHdpZHRoPSI4NjAiIGhlaWdodD0iODYwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjQ1IiByeT0iNDUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48ZGVmcz48Y2lyY2xlIGlkPSJHbG93IiByPSI1MDAiIGZpbGw9InVybCgjUmFkaWFsR2xvdykiLz48ZmlsdGVyIGlkPSJOb2lzZSI+PGZlRmxvb2QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmxvb2QtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiIGZsb29kLW9wYWNpdHk9IjEiIHJlc3VsdD0iZmxvb2RGaWxsIi8+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuNCIgbnVtT2N0YXZlcz0iMyIgcmVzdWx0PSJOb2lzZSIgdHlwZT0iZnJhY3RhbE5vaXNlIi8+PGZlQmxlbmQgaW49Ik5vaXNlIiBpbjI9ImZsb29kRmlsbCIgbW9kZT0ic29mdC1saWdodCIvPjwvZmlsdGVyPjxwYXRoIGlkPSJMb2dvIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4xIiBkPSJtMTMzLjU1OSwxMjQuMDM0Yy0uMDEzLDIuNDEyLTEuMDU5LDQuODQ4LTIuOTIzLDYuNDAyLTIuNTU4LDEuODE5LTUuMTY4LDMuNDM5LTcuODg4LDQuOTk2LTE0LjQ0LDguMjYyLTMxLjA0NywxMi41NjUtNDcuNjc0LDEyLjU2OS04Ljg1OC4wMzYtMTcuODM4LTEuMjcyLTI2LjMyOC0zLjY2My05LjgwNi0yLjc2Ni0xOS4wODctNy4xMTMtMjcuNTYyLTEyLjc3OC0xMy44NDItOC4wMjUsOS40NjgtMjguNjA2LDE2LjE1My0zNS4yNjVoMGMyLjAzNS0xLjgzOCw0LjI1Mi0zLjU0Niw2LjQ2My01LjIyNGgwYzYuNDI5LTUuNjU1LDE2LjIxOC0yLjgzNSwyMC4zNTgsNC4xNyw0LjE0Myw1LjA1Nyw4LjgxNiw5LjY0OSwxMy45MiwxMy43MzRoLjAzN2M1LjczNiw2LjQ2MSwxNS4zNTctMi4yNTMsOS4zOC04LjQ4LDAsMC0zLjUxNS0zLjUxNS0zLjUxNS0zLjUxNS0xMS40OS0xMS40NzgtNTIuNjU2LTUyLjY2NC02NC44MzctNjQuODM3bC4wNDktLjAzN2MtMS43MjUtMS42MDYtMi43MTktMy44NDctMi43NTEtNi4yMDRoMGMtLjA0Ni0yLjM3NSwxLjA2Mi00LjU4MiwyLjcyNi02LjIyOWgwbC4xODUtLjE0OGgwYy4wOTktLjA2MiwuMjIyLS4xNDgsLjM3LS4yNTloMGMyLjA2LTEuMzYyLDMuOTUxLTIuNjIxLDYuMDQ0LTMuODQyQzU3Ljc2My0zLjQ3Myw5Ny43Ni0yLjM0MSwxMjguNjM3LDE4LjMzMmMxNi42NzEsOS45NDYtMjYuMzQ0LDU0LjgxMy0zOC42NTEsNDAuMTk5LTYuMjk5LTYuMDk2LTE4LjA2My0xNy43NDMtMTkuNjY4LTE4LjgxMS02LjAxNi00LjA0Ny0xMy4wNjEsNC43NzYtNy43NTIsOS43NTFsNjguMjU0LDY4LjM3MWMxLjcyNCwxLjYwMSwyLjcxNCwzLjg0LDIuNzM4LDYuMTkyWiIvPjxwYXRoIGlkPSJGbG9hdGluZ1RleHQiIGZpbGw9Im5vbmUiIGQ9Ik0xMjUgNDVoNzUwczgwIDAgODAgODB2NzUwczAgODAgLTgwIDgwaC03NTBzLTgwIDAgLTgwIC04MHYtNzUwczAgLTgwIDgwIC04MCIvPjxyYWRpYWxHcmFkaWVudCBpZD0iUmFkaWFsR2xvdyI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iaHNsKDYxLDg4JSw0MCUpIiBzdG9wLW9wYWNpdHk9Ii42Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iU2FuZFRvcCIgeDE9IjAlIiB5MT0iMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9ImhzbCg2MSw4OCUsNDAlKSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iaHNsKDIzMCwyMSUsMTElKSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJTYW5kQm90dG9tIiB4MT0iMTAwJSIgeTE9IjEwMCUiPjxzdG9wIG9mZnNldD0iMTAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woNjEsODglLDQwJSkiLz48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4MSIgZHVyPSI2cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzAlOzYwJTsxMjAlOzYwJTszMCU7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkhvdXJnbGFzc1N0cm9rZSIgZ3JhZGllbnRUcmFuc2Zvcm09InJvdGF0ZSg5MCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iaHNsKDYxLDg4JSw0MCUpIi8+PHN0b3Agb2Zmc2V0PSI4MCUiIHN0b3AtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiLz48L2xpbmVhckdyYWRpZW50PjxnIGlkPSJIb3VyZ2xhc3MiPjxwYXRoIGQ9Ik0gNTAsMzYwIGEgMzAwLDMwMCAwIDEsMSA2MDAsMCBhIDMwMCwzMDAgMCAxLDEgLTYwMCwwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMiIgc3Ryb2tlPSJ1cmwoI0hvdXJnbGFzc1N0cm9rZSkiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Im01NjYsMTYxLjIwMXYtNTMuOTI0YzAtMTkuMzgyLTIyLjUxMy0zNy41NjMtNjMuMzk4LTUxLjE5OC00MC43NTYtMTMuNTkyLTk0Ljk0Ni0yMS4wNzktMTUyLjU4Ny0yMS4wNzlzLTExMS44MzgsNy40ODctMTUyLjYwMiwyMS4wNzljLTQwLjg5MywxMy42MzYtNjMuNDEzLDMxLjgxNi02My40MTMsNTEuMTk4djUzLjkyNGMwLDE3LjE4MSwxNy43MDQsMzMuNDI3LDUwLjIyMyw0Ni4zOTR2Mjg0LjgwOWMtMzIuNTE5LDEyLjk2LTUwLjIyMywyOS4yMDYtNTAuMjIzLDQ2LjM5NHY1My45MjRjMCwxOS4zODIsMjIuNTIsMzcuNTYzLDYzLjQxMyw1MS4xOTgsNDAuNzYzLDEzLjU5Miw5NC45NTQsMjEuMDc5LDE1Mi42MDIsMjEuMDc5czExMS44MzEtNy40ODcsMTUyLjU4Ny0yMS4wNzljNDAuODg2LTEzLjYzNiw2My4zOTgtMzEuODE2LDYzLjM5OC01MS4xOTh2LTUzLjkyNGMwLTE3LjE5Ni0xNy43MDQtMzMuNDM1LTUwLjIyMy00Ni40MDFWMjA3LjYwM2MzMi41MTktMTIuOTY3LDUwLjIyMy0yOS4yMDYsNTAuMjIzLTQ2LjQwMVptLTM0Ny40NjIsNTcuNzkzbDEzMC45NTksMTMxLjAyNy0xMzAuOTU5LDEzMS4wMTNWMjE4Ljk5NFptMjYyLjkyNC4wMjJ2MjYyLjAxOGwtMTMwLjkzNy0xMzEuMDA2LDEzMC45MzctMTMxLjAxM1oiIGZpbGw9IiMxNjE4MjIiPjwvcGF0aD48cG9seWdvbiBwb2ludHM9IjM1MCAzNTAuMDI2IDQxNS4wMyAyODQuOTc4IDI4NSAyODQuOTc4IDM1MCAzNTAuMDI2IiBmaWxsPSJ1cmwoI1NhbmRCb3R0b20pIi8+PHBhdGggZD0ibTQxNi4zNDEsMjgxLjk3NWMwLC45MTQtLjM1NCwxLjgwOS0xLjAzNSwyLjY4LTUuNTQyLDcuMDc2LTMyLjY2MSwxMi40NS02NS4yOCwxMi40NS0zMi42MjQsMC01OS43MzgtNS4zNzQtNjUuMjgtMTIuNDUtLjY4MS0uODcyLTEuMDM1LTEuNzY3LTEuMDM1LTIuNjgsMC0uOTE0LjM1NC0xLjgwOCwxLjAzNS0yLjY3Niw1LjU0Mi03LjA3NiwzMi42NTYtMTIuNDUsNjUuMjgtMTIuNDUsMzIuNjE5LDAsNTkuNzM4LDUuMzc0LDY1LjI4LDEyLjQ1LjY4MS44NjcsMS4wMzUsMS43NjIsMS4wMzUsMi42NzZaIiBmaWxsPSJ1cmwoI1NhbmRUb3ApIi8+PHBhdGggZD0ibTQ4MS40Niw1MDQuMTAxdjU4LjQ0OWMtMi4zNS43Ny00LjgyLDEuNTEtNy4zOSwyLjIzLTMwLjMsOC41NC03NC42NSwxMy45Mi0xMjQuMDYsMTMuOTItNTMuNiwwLTEwMS4yNC02LjMzLTEzMS40Ny0xNi4xNnYtNTguNDM5aDI2Mi45MloiIGZpbGw9InVybCgjU2FuZEJvdHRvbSkiLz48ZWxsaXBzZSBjeD0iMzUwIiBjeT0iNTA0LjEwMSIgcng9IjEzMS40NjIiIHJ5PSIyOC4xMDgiIGZpbGw9InVybCgjU2FuZFRvcCkiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9InVybCgjSG91cmdsYXNzU3Ryb2tlKSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iNCI+PHBhdGggZD0ibTU2NS42NDEsMTA3LjI4YzAsOS41MzctNS41NiwxOC42MjktMTUuNjc2LDI2Ljk3M2gtLjAyM2MtOS4yMDQsNy41OTYtMjIuMTk0LDE0LjU2Mi0zOC4xOTcsMjAuNTkyLTM5LjUwNCwxNC45MzYtOTcuMzI1LDI0LjM1NS0xNjEuNzMzLDI0LjM1NS05MC40OCwwLTE2Ny45NDgtMTguNTgyLTE5OS45NTMtNDQuOTQ4aC0uMDIzYy0xMC4xMTUtOC4zNDQtMTUuNjc2LTE3LjQzNy0xNS42NzYtMjYuOTczLDAtMzkuNzM1LDk2LjU1NC03MS45MjEsMjE1LjY1Mi03MS45MjFzMjE1LjYyOSwzMi4xODUsMjE1LjYyOSw3MS45MjFaIi8+PHBhdGggZD0ibTEzNC4zNiwxNjEuMjAzYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iMTYxLjIwMyIgeDI9IjEzNC4zNiIgeTI9IjEwNy4yOCIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSIxNjEuMjAzIiB4Mj0iNTY1LjY0IiB5Mj0iMTA3LjI4Ii8+PGxpbmUgeDE9IjE4NC41ODQiIHkxPSIyMDYuODIzIiB4Mj0iMTg0LjU4NSIgeTI9IjUzNy41NzkiLz48bGluZSB4MT0iMjE4LjE4MSIgeTE9IjIxOC4xMTgiIHgyPSIyMTguMTgxIiB5Mj0iNTYyLjUzNyIvPjxsaW5lIHgxPSI0ODEuODE4IiB5MT0iMjE4LjE0MiIgeDI9IjQ4MS44MTkiIHkyPSI1NjIuNDI4Ii8+PGxpbmUgeDE9IjUxNS40MTUiIHkxPSIyMDcuMzUyIiB4Mj0iNTE1LjQxNiIgeTI9IjUzNy41NzkiLz48cGF0aCBkPSJtMTg0LjU4LDUzNy41OGMwLDUuNDUsNC4yNywxMC42NSwxMi4wMywxNS40MmguMDJjNS41MSwzLjM5LDEyLjc5LDYuNTUsMjEuNTUsOS40MiwzMC4yMSw5LjksNzguMDIsMTYuMjgsMTMxLjgzLDE2LjI4LDQ5LjQxLDAsOTMuNzYtNS4zOCwxMjQuMDYtMTMuOTIsMi43LS43Niw1LjI5LTEuNTQsNy43NS0yLjM1LDguNzctMi44NywxNi4wNS02LjA0LDIxLjU2LTkuNDNoMGM3Ljc2LTQuNzcsMTIuMDQtOS45NywxMi4wNC0xNS40MiIvPjxwYXRoIGQ9Im0xODQuNTgyLDQ5Mi42NTZjLTMxLjM1NCwxMi40ODUtNTAuMjIzLDI4LjU4LTUwLjIyMyw0Ni4xNDIsMCw5LjUzNiw1LjU2NCwxOC42MjcsMTUuNjc3LDI2Ljk2OWguMDIyYzguNTAzLDcuMDA1LDIwLjIxMywxMy40NjMsMzQuNTI0LDE5LjE1OSw5Ljk5OSwzLjk5MSwyMS4yNjksNy42MDksMzMuNTk3LDEwLjc4OCwzNi40NSw5LjQwNyw4Mi4xODEsMTUuMDAyLDEzMS44MzUsMTUuMDAyczk1LjM2My01LjU5NSwxMzEuODA3LTE1LjAwMmMxMC44NDctMi43OSwyMC44NjctNS45MjYsMjkuOTI0LTkuMzQ5LDEuMjQ0LS40NjcsMi40NzMtLjk0MiwzLjY3My0xLjQyNCwxNC4zMjYtNS42OTYsMjYuMDM1LTEyLjE2MSwzNC41MjQtMTkuMTczaC4wMjJjMTAuMTE0LTguMzQyLDE1LjY3Ny0xNy40MzMsMTUuNjc3LTI2Ljk2OSwwLTE3LjU2Mi0xOC44NjktMzMuNjY1LTUwLjIyMy00Ni4xNSIvPjxwYXRoIGQ9Im0xMzQuMzYsNTkyLjcyYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iNTkyLjcyIiB4Mj0iMTM0LjM2IiB5Mj0iNTM4Ljc5NyIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSI1OTIuNzIiIHgyPSI1NjUuNjQiIHkyPSI1MzguNzk3Ii8+PHBvbHlsaW5lIHBvaW50cz0iNDgxLjgyMiA0ODEuOTAxIDQ4MS43OTggNDgxLjg3NyA0ODEuNzc1IDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDIxOC4xODUgMjE4LjEyOSIvPjxwb2x5bGluZSBwb2ludHM9IjIxOC4xODUgNDgxLjkwMSAyMTguMjMxIDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDQ4MS44MjIgMjE4LjE1MiIvPjwvZz48L2c+PGcgaWQ9IlByb2dyZXNzIiBmaWxsPSIjZmZmIj48cmVjdCB3aWR0aD0iMjA4IiBoZWlnaHQ9IjEwMCIgZmlsbC1vcGFjaXR5PSIuMDMiIHJ4PSIxNSIgcnk9IjE1IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIuMSIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHRleHQgeD0iMjAiIHk9IjM0IiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjJweCI+UHJvZ3Jlc3M8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+MjUlPC90ZXh0PjxnIGZpbGw9Im5vbmUiPjxjaXJjbGUgY3g9IjE2NiIgY3k9IjUwIiByPSIyMiIgc3Ryb2tlPSJoc2woMjMwLDIxJSwxMSUpIiBzdHJva2Utd2lkdGg9IjEwIi8+PGNpcmNsZSBjeD0iMTY2IiBjeT0iNTAiIHBhdGhMZW5ndGg9IjEwMDAwIiByPSIyMiIgc3Ryb2tlPSJoc2woNjEsODglLDQwJSkiIHN0cm9rZS1kYXNoYXJyYXk9IjEwMDAwIiBzdHJva2UtZGFzaG9mZnNldD0iNzUwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjUiIHRyYW5zZm9ybT0icm90YXRlKC05MCkiIHRyYW5zZm9ybS1vcmlnaW49IjE2NiA1MCIvPjwvZz48L2c+PGcgaWQ9IlN0YXR1cyIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE4NCIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPlN0YXR1czwvdGV4dD48dGV4dCB4PSIyMCIgeT0iNzIiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyNnB4Ij5TdHJlYW1pbmc8L3RleHQ+PC9nPjxnIGlkPSJBbW91bnQiIGZpbGw9IiNmZmYiPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iMTAwIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjE1IiByeT0iMTUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48dGV4dCB4PSIyMCIgeT0iMzQiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyMnB4Ij5BbW91bnQ8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+JiM4ODA1OyAxMEs8L3RleHQ+PC9nPjxnIGlkPSJEdXJhdGlvbiIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE1MiIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPkR1cmF0aW9uPC90ZXh0Pjx0ZXh0IHg9IjIwIiB5PSI3MiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjI2cHgiPiZsdDsgMSBEYXk8L3RleHQ+PC9nPjwvZGVmcz48dGV4dCB0ZXh0LXJlbmRlcmluZz0ib3B0aW1pemVTcGVlZCI+PHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSItMTAwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHhkYjI1YTdiNzY4MzExZGUxMjhiYmRhN2I4NDI2YzNmOWM3NGYzMjQwIOKAoiBTYWJsaWVyIFYyIExvY2t1cCBEeW5hbWljPC90ZXh0UGF0aD48dGV4dFBhdGggc3RhcnRPZmZzZXQ9IjAlIiBocmVmPSIjRmxvYXRpbmdUZXh0IiBmaWxsPSIjZmZmIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZpbGwtb3BhY2l0eT0iLjgiIGZvbnQtc2l6ZT0iMjZweCI+PGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGJlZ2luPSIwcyIgZHVyPSI1MHMiIGZyb209IjAlIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdG89IjEwMCUiLz4weGRiMjVhN2I3NjgzMTFkZTEyOGJiZGE3Yjg0MjZjM2Y5Yzc0ZjMyNDAg4oCiIFNhYmxpZXIgVjIgTG9ja3VwIER5bmFtaWM8L3RleHRQYXRoPjx0ZXh0UGF0aCBzdGFydE9mZnNldD0iLTUwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHgwM2E2YTg0Y2Q3NjJkOTcwN2EyMTYwNWI1NDhhYWFiODkxNTYyYWFiIOKAoiBEQUk8L3RleHRQYXRoPjx0ZXh0UGF0aCBzdGFydE9mZnNldD0iNTAlIiBocmVmPSIjRmxvYXRpbmdUZXh0IiBmaWxsPSIjZmZmIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZpbGwtb3BhY2l0eT0iLjgiIGZvbnQtc2l6ZT0iMjZweCI+PGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGJlZ2luPSIwcyIgZHVyPSI1MHMiIGZyb209IjAlIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdG89IjEwMCUiLz4weDAzYTZhODRjZDc2MmQ5NzA3YTIxNjA1YjU0OGFhYWI4OTE1NjJhYWIg4oCiIERBSTwvdGV4dFBhdGg+PC90ZXh0Pjx1c2UgaHJlZj0iI0dsb3ciIGZpbGwtb3BhY2l0eT0iLjkiLz48dXNlIGhyZWY9IiNHbG93IiB4PSIxMDAwIiB5PSIxMDAwIiBmaWxsLW9wYWNpdHk9Ii45Ii8+PHVzZSBocmVmPSIjTG9nbyIgeD0iMTcwIiB5PSIxNzAiIHRyYW5zZm9ybT0ic2NhbGUoLjYpIi8+PHVzZSBocmVmPSIjSG91cmdsYXNzIiB4PSIxNTAiIHk9IjkwIiB0cmFuc2Zvcm09InJvdGF0ZSgxMCkiIHRyYW5zZm9ybS1vcmlnaW49IjUwMCA1MDAiLz48dXNlIGhyZWY9IiNQcm9ncmVzcyIgeD0iMTQ0IiB5PSI3OTAiLz48dXNlIGhyZWY9IiNTdGF0dXMiIHg9IjM2OCIgeT0iNzkwIi8+PHVzZSBocmVmPSIjQW1vdW50IiB4PSI1NjgiIHk9Ijc5MCIvPjx1c2UgaHJlZj0iI0R1cmF0aW9uIiB4PSI3MDQiIHk9Ijc5MCIvPjwvc3ZnPg=="}'; diff --git a/test/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol index f6532410a..2362e29d9 100644 --- a/test/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/integration/concrete/lockup-dynamic/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LockupDynamic_Integration_Concrete_Test } from "../LockupDynamic.t.sol"; import { WithdrawableAmountOf_Integration_Concrete_Test } from @@ -24,7 +24,7 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test is givenStreamHasNotBeenCanceled givenStatusStreaming { - vm.warp({ timestamp: defaults.START_TIME() }); + vm.warp({ newTimestamp: defaults.START_TIME() }); uint128 actualWithdrawableAmount = lockupDynamic.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); @@ -42,7 +42,7 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test is givenStartTimeInThePast { // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); // Run the test. uint128 actualWithdrawableAmount = lockupDynamic.withdrawableAmountOf(defaultStreamId); @@ -64,7 +64,7 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Concrete_Test is whenWithWithdrawals { // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() + 3750 seconds }); // Make the withdrawal. lockupDynamic.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); diff --git a/test/integration/concrete/lockup-linear/LockupLinear.t.sol b/test/integration/concrete/lockup-linear/LockupLinear.t.sol index e347f38a7..5f6a3b246 100644 --- a/test/integration/concrete/lockup-linear/LockupLinear.t.sol +++ b/test/integration/concrete/lockup-linear/LockupLinear.t.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Base } from "src/interfaces/ISablierV2Base.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { LockupLinear_Integration_Shared_Test } from "../../shared/lockup-linear/LockupLinear.t.sol"; import { Integration_Test } from "../../Integration.t.sol"; +import { AllowToHook_Integration_Concrete_Test } from "../lockup/allow-to-hook/allowToHook.t.sol"; import { Burn_Integration_Concrete_Test } from "../lockup/burn/burn.t.sol"; import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; import { CancelMultiple_Integration_Concrete_Test } from "../lockup/cancel-multiple/cancelMultiple.t.sol"; -import { ClaimProtocolRevenues_Integration_Concrete_Test } from - "../lockup/claim-protocol-revenues/claimProtocolRevenues.t.sol"; import { GetAsset_Integration_Concrete_Test } from "../lockup/get-asset/getAsset.t.sol"; import { GetDepositedAmount_Integration_Concrete_Test } from "../lockup/get-deposited-amount/getDepositedAmount.t.sol"; import { GetEndTime_Integration_Concrete_Test } from "../lockup/get-end-time/getEndTime.t.sol"; @@ -19,21 +17,21 @@ import { GetRecipient_Integration_Concrete_Test } from "../lockup/get-recipient/ import { GetSender_Integration_Concrete_Test } from "../lockup/get-sender/getSender.t.sol"; import { GetStartTime_Integration_Concrete_Test } from "../lockup/get-start-time/getStartTime.t.sol"; import { GetWithdrawnAmount_Integration_Concrete_Test } from "../lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol"; +import { IsAllowedToHook_Integration_Concrete_Test } from "../lockup/is-allowed-to-hook/isAllowedToHook.t.sol"; import { IsCancelable_Integration_Concrete_Test } from "../lockup/is-cancelable/isCancelable.t.sol"; import { IsCold_Integration_Concrete_Test } from "../lockup/is-cold/isCold.t.sol"; import { IsDepleted_Integration_Concrete_Test } from "../lockup/is-depleted/isDepleted.t.sol"; import { IsStream_Integration_Concrete_Test } from "../lockup/is-stream/isStream.t.sol"; import { IsTransferable_Integration_Concrete_Test } from "../lockup/is-transferable/isTransferable.t.sol"; import { IsWarm_Integration_Concrete_Test } from "../lockup/is-warm/isWarm.t.sol"; -import { ProtocolRevenues_Integration_Concrete_Test } from "../lockup/protocol-revenues/protocolRevenues.t.sol"; import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; -import { SetComptroller_Integration_Concrete_Test } from "../lockup/set-comptroller/setComptroller.t.sol"; import { SetNFTDescriptor_Integration_Concrete_Test } from "../lockup/set-nft-descriptor/setNFTDescriptor.t.sol"; import { StatusOf_Integration_Concrete_Test } from "../lockup/status-of/statusOf.t.sol"; import { TransferFrom_Integration_Concrete_Test } from "../lockup/transfer-from/transferFrom.t.sol"; import { WasCanceled_Integration_Concrete_Test } from "../lockup/was-canceled/wasCanceled.t.sol"; import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; +import { WithdrawHooks_Integration_Concrete_Test } from "../lockup/withdraw-hooks/withdrawHooks.t.sol"; import { WithdrawMax_Integration_Concrete_Test } from "../lockup/withdraw-max/withdrawMax.t.sol"; import { WithdrawMaxAndTransfer_Integration_Concrete_Test } from "../lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol"; @@ -51,8 +49,7 @@ abstract contract LockupLinear_Integration_Concrete_Test is Integration_Test, Lo Integration_Test.setUp(); LockupLinear_Integration_Shared_Test.setUp(); - // Cast the LockupLinear contract as {ISablierV2Base} and {ISablierV2Lockup}. - base = ISablierV2Base(lockupLinear); + // Cast the {LockupLinear} contract as {ISablierV2Lockup}. lockup = ISablierV2Lockup(lockupLinear); } } @@ -61,6 +58,20 @@ abstract contract LockupLinear_Integration_Concrete_Test is Integration_Test, Lo SHARED TESTS //////////////////////////////////////////////////////////////////////////*/ +contract AllowToHook_LockupLinear_Integration_Concrete_Test is + LockupLinear_Integration_Concrete_Test, + AllowToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupLinear_Integration_Concrete_Test, AllowToHook_Integration_Concrete_Test) + { + LockupLinear_Integration_Concrete_Test.setUp(); + AllowToHook_Integration_Concrete_Test.setUp(); + } +} + contract Burn_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, Burn_Integration_Concrete_Test @@ -99,20 +110,6 @@ contract CancelMultiple_LockupLinear_Integration_Concrete_Test is } } -contract ClaimProtocolRevenues_LockupLinear_Integration_Concrete_Test is - LockupLinear_Integration_Concrete_Test, - ClaimProtocolRevenues_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupLinear_Integration_Concrete_Test, ClaimProtocolRevenues_Integration_Concrete_Test) - { - LockupLinear_Integration_Concrete_Test.setUp(); - ClaimProtocolRevenues_Integration_Concrete_Test.setUp(); - } -} - contract GetAsset_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, GetAsset_Integration_Concrete_Test @@ -225,6 +222,20 @@ contract GetWithdrawnAmount_LockupLinear_Integration_Concrete_Test is } } +contract IsAllowedToHook_LockupLinear_Integration_Concrete_Test is + LockupLinear_Integration_Concrete_Test, + IsAllowedToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupLinear_Integration_Concrete_Test, IsAllowedToHook_Integration_Concrete_Test) + { + LockupLinear_Integration_Concrete_Test.setUp(); + IsAllowedToHook_Integration_Concrete_Test.setUp(); + } +} + contract IsCancelable_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, IsCancelable_Integration_Concrete_Test @@ -309,20 +320,6 @@ contract IsWarm_LockupLinear_Integration_Concrete_Test is } } -contract ProtocolRevenues_LockupLinear_Integration_Concrete_Test is - LockupLinear_Integration_Concrete_Test, - ProtocolRevenues_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupLinear_Integration_Concrete_Test, ProtocolRevenues_Integration_Concrete_Test) - { - LockupLinear_Integration_Concrete_Test.setUp(); - ProtocolRevenues_Integration_Concrete_Test.setUp(); - } -} - contract Renounce_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, Renounce_Integration_Concrete_Test @@ -351,20 +348,6 @@ contract RefundableAmountOf_LockupLinear_Integration_Concrete_Test is } } -contract SetComptroller_LockupLinear_Integration_Concrete_Test is - LockupLinear_Integration_Concrete_Test, - SetComptroller_Integration_Concrete_Test -{ - function setUp() - public - virtual - override(LockupLinear_Integration_Concrete_Test, SetComptroller_Integration_Concrete_Test) - { - LockupLinear_Integration_Concrete_Test.setUp(); - SetComptroller_Integration_Concrete_Test.setUp(); - } -} - contract SetNFTDescriptor_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, SetNFTDescriptor_Integration_Concrete_Test @@ -435,6 +418,20 @@ contract Withdraw_LockupLinear_Integration_Concrete_Test is } } +contract WithdrawHooks_LockupLinear_Integration_Concrete_Test is + LockupLinear_Integration_Concrete_Test, + WithdrawHooks_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupLinear_Integration_Concrete_Test, WithdrawHooks_Integration_Concrete_Test) + { + LockupLinear_Integration_Concrete_Test.setUp(); + WithdrawHooks_Integration_Concrete_Test.setUp(); + } +} + contract WithdrawMax_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, WithdrawMax_Integration_Concrete_Test diff --git a/test/integration/concrete/lockup-linear/constructor.t.sol b/test/integration/concrete/lockup-linear/constructor.t.sol index 12e7fb8f7..323f5fb60 100644 --- a/test/integration/concrete/lockup-linear/constructor.t.sol +++ b/test/integration/concrete/lockup-linear/constructor.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { SablierV2LockupLinear } from "src/SablierV2LockupLinear.sol"; import { LockupLinear_Integration_Concrete_Test } from "./LockupLinear.t.sol"; @@ -12,24 +13,28 @@ contract Constructor_LockupLinear_Integration_Concrete_Test is LockupLinear_Inte emit TransferAdmin({ oldAdmin: address(0), newAdmin: users.admin }); // Construct the contract. - SablierV2LockupLinear constructedLockupLinear = new SablierV2LockupLinear({ - initialAdmin: users.admin, - initialComptroller: comptroller, - initialNFTDescriptor: nftDescriptor - }); + SablierV2LockupLinear constructedLockupLinear = + new SablierV2LockupLinear({ initialAdmin: users.admin, initialNFTDescriptor: nftDescriptor }); - // {SablierV2Base.constructor} + // {SablierV2Lockup.constant} + UD60x18 actualMaxBrokerFee = constructedLockupLinear.MAX_BROKER_FEE(); + UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18); + assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE"); + + // {SablierV2Lockup.constructor} address actualAdmin = constructedLockupLinear.admin(); address expectedAdmin = users.admin; assertEq(actualAdmin, expectedAdmin, "admin"); - address actualComptroller = address(constructedLockupLinear.comptroller()); - address expectedComptroller = address(comptroller); - assertEq(actualComptroller, expectedComptroller, "comptroller"); - - // {SablierV2Lockup.constructor} uint256 actualStreamId = constructedLockupLinear.nextStreamId(); uint256 expectedStreamId = 1; assertEq(actualStreamId, expectedStreamId, "nextStreamId"); + + address actualNFTDescriptor = address(constructedLockupLinear.nftDescriptor()); + address expectedNFTDescriptor = address(nftDescriptor); + assertEq(actualNFTDescriptor, expectedNFTDescriptor, "nftDescriptor"); + + // {SablierV2Lockup.supportsInterface} + assertTrue(constructedLockupLinear.supportsInterface(0x49064906), "ERC-4906 interface ID"); } } diff --git a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol index 02f9c53ab..38fd9daae 100644 --- a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol +++ b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { CreateWithDurations_Integration_Shared_Test } from "../../../shared/lockup-linear/createWithDurations.t.sol"; +import { CreateWithDurations_Integration_Shared_Test } from "../../../shared/lockup/createWithDurations.t.sol"; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is @@ -23,14 +23,15 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is function test_RevertWhen_DelegateCalled() external { bytes memory callData = - abi.encodeCall(ISablierV2LockupLinear.createWithDurations, defaults.createWithDurations()); + abi.encodeCall(ISablierV2LockupLinear.createWithDurations, defaults.createWithDurationsLL()); (bool success, bytes memory returnData) = address(lockupLinear).delegatecall(callData); expectRevertDueToDelegateCall(success, returnData); } function test_RevertWhen_CliffDurationCalculationOverflows() external whenNotDelegateCalled { uint40 startTime = getBlockTimestamp(); - uint40 cliffDuration = MAX_UINT40 - startTime + 1 seconds; + uint40 cliffDuration = MAX_UINT40 - startTime + 2 seconds; + uint40 totalDuration = defaults.TOTAL_DURATION(); // Calculate the end time. Needs to be "unchecked" to avoid an overflow. uint40 cliffTime; @@ -41,13 +42,10 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime.selector, startTime, cliffTime ) ); - // Set the total duration to be the same as the cliff duration. - uint40 totalDuration = cliffDuration; - // Create the stream. createDefaultStreamWithDurations(LockupLinear.Durations({ cliff: cliffDuration, total: totalDuration })); } @@ -61,7 +59,7 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is LockupLinear.Durations memory durations = LockupLinear.Durations({ cliff: 0, total: MAX_UINT40 - startTime + 1 seconds }); - // Calculate the cliff time and the end time. Needs to be "unchecked" to avoid an overflow. + // Calculate the cliff time and the end time. Needs to be "unchecked" to allow an overflow. uint40 cliffTime; uint40 endTime; unchecked { @@ -72,7 +70,7 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime.selector, startTime, endTime ) ); @@ -89,26 +87,19 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is // Make the Sender the stream's funder address funder = users.sender; - // Load the initial protocol revenues. - uint128 initialProtocolRevenues = lockupLinear.protocolRevenues(dai); - - // Declare the range. - uint40 currentTime = getBlockTimestamp(); - LockupLinear.Range memory range = LockupLinear.Range({ - start: currentTime, - cliff: currentTime + defaults.CLIFF_DURATION(), - end: currentTime + defaults.TOTAL_DURATION() + // Declare the timestamps. + uint40 blockTimestamp = getBlockTimestamp(); + LockupLinear.Timestamps memory timestamps = LockupLinear.Timestamps({ + start: blockTimestamp, + cliff: blockTimestamp + defaults.CLIFF_DURATION(), + end: blockTimestamp + defaults.TOTAL_DURATION() }); // Expect the assets to be transferred from the funder to {SablierV2LockupLinear}. - expectCallToTransferFrom({ - from: funder, - to: address(lockupLinear), - amount: defaults.DEPOSIT_AMOUNT() + defaults.PROTOCOL_FEE_AMOUNT() - }); + expectCallToTransferFrom({ from: funder, to: address(lockupLinear), value: defaults.DEPOSIT_AMOUNT() }); // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, amount: defaults.BROKER_FEE_AMOUNT() }); + expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockupLinear) }); @@ -123,7 +114,7 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is asset: dai, cancelable: true, transferable: true, - range: range, + timestamps: timestamps, broker: users.broker }); @@ -131,11 +122,11 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is createDefaultStreamWithDurations(); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); - expectedStream.startTime = range.start; - expectedStream.cliffTime = range.cliff; - expectedStream.endTime = range.end; + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); + expectedStream.startTime = timestamps.start; + expectedStream.cliffTime = timestamps.cliff; + expectedStream.endTime = timestamps.end; assertEq(actualStream, expectedStream); // Assert that the stream's status is "STREAMING". @@ -143,16 +134,11 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. uint256 actualNextStreamId = lockupLinear.nextStreamId(); uint256 expectedNextStreamId = streamId + 1; assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); - // Assert that the protocol fee has been recorded. - uint128 actualProtocolRevenues = lockupLinear.protocolRevenues(dai); - uint128 expectedProtocolRevenues = initialProtocolRevenues + defaults.PROTOCOL_FEE_AMOUNT(); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - // Assert that the NFT has been minted. address actualNFTOwner = lockupLinear.ownerOf({ tokenId: streamId }); address expectedNFTOwner = users.recipient; diff --git a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree index ca9a10630..e769f8a06 100644 --- a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree +++ b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree @@ -6,11 +6,10 @@ createWithDurations.t.sol │ └── it should revert due to the start time being greater than the cliff time └── when the cliff duration calculation does not overflow uint256 ├── when the total duration calculation overflows uint256 - │ └── it should revert + │ └── it should revert └── when the total duration calculation does not overflow uint256 - ├── it should create the stream - ├── it should bump the next stream id - ├── it should record the protocol fee - ├── it should mint the NFT - ├── it should perform the ERC-20 transfers - └── it should emit a {CreateLockupLinearStream} event + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupLinearStream} event diff --git a/test/integration/concrete/lockup-linear/create-with-range/createWithRange.tree b/test/integration/concrete/lockup-linear/create-with-range/createWithRange.tree deleted file mode 100644 index 6d810042a..000000000 --- a/test/integration/concrete/lockup-linear/create-with-range/createWithRange.tree +++ /dev/null @@ -1,45 +0,0 @@ -createWithRange.t.sol -├── when delegate called -│ └── it should revert -└── when not delegate called - ├── when the recipient is the zero address - │ └── it should revert - └── when the recipient is not the zero address - ├── when the deposit amount is zero - │ └── it should revert - └── when the deposit amount is not zero - ├── when the start time is greater than the cliff time - │ └── it should revert - └── when the start time is not greater than the cliff time - ├── when the cliff time is not less than the end time - │ └── it should revert - └── when the cliff time is less than the end time - ├── when the end time is not in the future - │ └── it should revert - └── when the end time is in the future - ├── given the protocol fee is too high - │ └── it should revert - └── given the protocol fee is not too high - ├── when the broker fee is too high - │ └── it should revert - └── when the broker fee is not too high - ├── when the asset is not a contract - │ └── it should revert - └── when the asset is a contract - ├── when the asset misses the ERC-20 return value - │ ├── it should create the stream - │ ├── it should bump the next stream id - │ ├── it should record the protocol fee - │ ├── it should mint the NFT - │ ├── it should emit a {MetadataUpdate} event - │ ├── it should perform the ERC-20 transfers - │ └── it should emit a {CreateLockupLinearStream} event - └── when the asset does not miss the ERC-20 return value - ├── it should create the stream - ├── it should bump the next stream id - ├── it should record the protocol fee - ├── it should mint the NFT - ├── it should emit a {MetadataUpdate} event - ├── it should perform the ERC-20 transfers - └── it should emit a {CreateLockupLinearStream} event - diff --git a/test/integration/concrete/lockup-linear/create-with-range/createWithRange.t.sol b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol similarity index 54% rename from test/integration/concrete/lockup-linear/create-with-range/createWithRange.t.sol rename to test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol index 65a988b08..5ce293a85 100644 --- a/test/integration/concrete/lockup-linear/create-with-range/createWithRange.t.sol +++ b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; @@ -8,55 +10,121 @@ import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.so import { Errors } from "src/libraries/Errors.sol"; import { Broker, Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { CreateWithRange_Integration_Shared_Test } from "../../../shared/lockup-linear/createWithRange.t.sol"; +import { CreateWithTimestamps_Integration_Shared_Test } from "../../../shared/lockup/createWithTimestamps.t.sol"; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; -contract CreateWithRange_LockupLinear_Integration_Concrete_Test is +contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test, - CreateWithRange_Integration_Shared_Test + CreateWithTimestamps_Integration_Shared_Test { function setUp() public virtual - override(LockupLinear_Integration_Concrete_Test, CreateWithRange_Integration_Shared_Test) + override(LockupLinear_Integration_Concrete_Test, CreateWithTimestamps_Integration_Shared_Test) { LockupLinear_Integration_Concrete_Test.setUp(); - CreateWithRange_Integration_Shared_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); } function test_RevertWhen_DelegateCalled() external { - bytes memory callData = abi.encodeCall(ISablierV2LockupLinear.createWithRange, defaults.createWithRange()); + bytes memory callData = + abi.encodeCall(ISablierV2LockupLinear.createWithTimestamps, defaults.createWithTimestampsLL()); (bool success, bytes memory returnData) = address(lockupLinear).delegatecall(callData); expectRevertDueToDelegateCall(success, returnData); } function test_RevertWhen_RecipientZeroAddress() external whenNotDelegateCalled { - vm.expectRevert("ERC721: mint to the zero address"); - createDefaultStreamWithRecipient({ recipient: address(0) }); + address recipient = address(0); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, recipient)); + createDefaultStreamWithRecipient(recipient); } /// @dev It is not possible to obtain a zero deposit amount from a non-zero total amount, because the - /// `MAX_FEE` is hard coded to 10%. + /// `MAX_BROKER_FEE` is hard coded to 10%. function test_RevertWhen_DepositAmountZero() external whenNotDelegateCalled whenRecipientNonZeroAddress { vm.expectRevert(Errors.SablierV2Lockup_DepositAmountZero.selector); createDefaultStreamWithTotalAmount(0); } + function test_RevertWhen_StartTimeZero() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + { + uint40 cliffTime = defaults.CLIFF_TIME(); + uint40 endTime = defaults.END_TIME(); + + vm.expectRevert(Errors.SablierV2Lockup_StartTimeZero.selector); + createDefaultStreamWithTimestamps(LockupLinear.Timestamps({ start: 0, cliff: cliffTime, end: endTime })); + } + + function test_RevertWhen_StartTimeNotLessThanEndTime() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenCliffTimeZero + { + uint40 startTime = defaults.END_TIME(); + uint40 endTime = defaults.START_TIME(); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime.selector, startTime, endTime + ) + ); + createDefaultStreamWithTimestamps(LockupLinear.Timestamps({ start: startTime, cliff: 0, end: endTime })); + } + + function test_CreateWithTimestamps_StartTimeLessThanEndTime() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenCliffTimeZero + { + createDefaultStreamWithTimestamps( + LockupLinear.Timestamps({ start: defaults.START_TIME(), cliff: 0, end: defaults.END_TIME() }) + ); + + // Assert that the stream has been created. + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); + expectedStream.cliffTime = 0; + assertEq(actualStream, expectedStream); + + // Assert that the next stream ID has been bumped. + uint256 actualNextStreamId = lockupLinear.nextStreamId(); + uint256 expectedNextStreamId = streamId + 1; + assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + address actualNFTOwner = lockupLinear.ownerOf({ tokenId: streamId }); + address expectedNFTOwner = users.recipient; + assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); + } + function test_RevertWhen_StartTimeGreaterThanCliffTime() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime { uint40 startTime = defaults.CLIFF_TIME(); uint40 cliffTime = defaults.START_TIME(); uint40 endTime = defaults.END_TIME(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime.selector, startTime, cliffTime ) ); - createDefaultStreamWithRange(LockupLinear.Range({ start: startTime, cliff: cliffTime, end: endTime })); + createDefaultStreamWithTimestamps(LockupLinear.Timestamps({ start: startTime, cliff: cliffTime, end: endTime })); } function test_RevertWhen_CliffTimeNotLessThanEndTime() @@ -64,7 +132,9 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime { uint40 startTime = defaults.START_TIME(); uint40 cliffTime = defaults.END_TIME(); @@ -74,7 +144,7 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime ) ); - createDefaultStreamWithRange(LockupLinear.Range({ start: startTime, cliff: cliffTime, end: endTime })); + createDefaultStreamWithTimestamps(LockupLinear.Timestamps({ start: startTime, cliff: cliffTime, end: endTime })); } function test_RevertGiven_EndTimeNotInTheFuture() @@ -82,51 +152,33 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture { uint40 endTime = defaults.END_TIME(); - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_EndTimeNotInTheFuture.selector, endTime, endTime)); createDefaultStream(); } - function test_RevertGiven_ProtocolFeeTooHigh() + function test_RevertWhen_BrokerFeeTooHigh() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture { - UD60x18 protocolFee = MAX_FEE + ud(1); - - // Set the protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); - changePrank({ msgSender: users.sender }); - - // Run the test. + UD60x18 brokerFee = MAX_BROKER_FEE + ud(1); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_ProtocolFeeTooHigh.selector, protocolFee, MAX_FEE) + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, brokerFee, MAX_BROKER_FEE) ); - createDefaultStream(); - } - - function test_RevertWhen_BrokerFeeTooHigh() - external - whenNotDelegateCalled - whenRecipientNonZeroAddress - whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime - whenCliffTimeLessThanEndTime - whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh - { - UD60x18 brokerFee = MAX_FEE + ud(1); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, brokerFee, MAX_FEE)); createDefaultStreamWithBroker(Broker({ account: users.broker, fee: brokerFee })); } @@ -135,49 +187,52 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh { address nonContract = address(8128); - vm.expectRevert("Address: call to non-contract"); + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, nonContract)); createDefaultStreamWithAsset(IERC20(nonContract)); } - function test_CreateWithRange_AssetMissingReturnValue() + function test_CreateWithTimestamps_AssetMissingReturnValue() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract { - testCreateWithRange(address(usdt)); + testCreateWithTimestamps(address(usdt)); } - function test_CreateWithRange() + function test_CreateWithTimestamps() external whenNotDelegateCalled whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero + whenCliffTimeGreaterThanZero + whenStartTimeLessThanEndTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract whenAssetERC20 { - testCreateWithRange(address(dai)); + testCreateWithTimestamps(address(dai)); } - /// @dev Shared logic between {test_CreateWithRange_AssetMissingReturnValue} and {test_CreateWithRange}. - function testCreateWithRange(address asset) internal { + /// @dev Shared logic between {test_CreateWithTimestamps_AssetMissingReturnValue} and {test_CreateWithTimestamps}. + function testCreateWithTimestamps(address asset) internal { // Make the Sender the stream's funder. address funder = users.sender; @@ -186,7 +241,7 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is asset: IERC20(asset), from: funder, to: address(lockupLinear), - amount: defaults.DEPOSIT_AMOUNT() + defaults.PROTOCOL_FEE_AMOUNT() + value: defaults.DEPOSIT_AMOUNT() }); // Expect the broker fee to be paid to the broker. @@ -194,7 +249,7 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is asset: IERC20(asset), from: funder, to: users.broker, - amount: defaults.BROKER_FEE_AMOUNT() + value: defaults.BROKER_FEE_AMOUNT() }); // Expect the relevant events to be emitted. @@ -210,7 +265,7 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is asset: IERC20(asset), cancelable: true, transferable: true, - range: defaults.lockupLinearRange(), + timestamps: defaults.lockupLinearTimestamps(), broker: users.broker }); @@ -218,8 +273,8 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is createDefaultStreamWithAsset(IERC20(asset)); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.asset = IERC20(asset); assertEq(actualStream, expectedStream); @@ -228,7 +283,7 @@ contract CreateWithRange_LockupLinear_Integration_Concrete_Test is Lockup.Status expectedStatus = Lockup.Status.PENDING; assertEq(actualStatus, expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. uint256 actualNextStreamId = lockupLinear.nextStreamId(); uint256 expectedNextStreamId = streamId + 1; assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); diff --git a/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree new file mode 100644 index 000000000..6729ea211 --- /dev/null +++ b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree @@ -0,0 +1,48 @@ +createWithTimestamps.t.sol +├── when delegate called +│ └── it should revert +└── when not delegate called + ├── when the recipient is the zero address + │ └── it should revert + └── when the recipient is not the zero address + ├── when the deposit amount is zero + │ └── it should revert + └── when the deposit amount is not zero + ├── when the start time is zero + │ └── it should revert + └── when the start time is not zero + ├── when the cliff time is zero + │ ├── when the start time is not less than the end time + │ │ └── it should revert + │ └── when the start time is less than the end time + │ └── it should create the stream + └── when the cliff time is greater than zero + ├── when the start time is not less than the cliff time + │ └── it should revert + └── when the start time is less than the cliff time + ├── when the cliff time is not less than the end time + │ └── it should revert + └── when the cliff time is less than the end time + ├── when the end time is not in the future + │ └── it should revert + └── when the end time is in the future + ├── when the broker fee is too high + │ └── it should revert + └── when the broker fee is not too high + ├── when the asset is not a contract + │ └── it should revert + └── when the asset is a contract + ├── when the asset misses the ERC-20 return value + │ ├── it should create the stream + │ ├── it should bump the next stream ID + │ ├── it should mint the NFT + │ ├── it should emit a {MetadataUpdate} event + │ ├── it should perform the ERC-20 transfers + │ └── it should emit a {CreateLockupLinearStream} event + └── when the asset does not miss the ERC-20 return value + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should emit a {MetadataUpdate} event + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupLinearStream} event diff --git a/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol b/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol index 2a22e894b..3207a55f2 100644 --- a/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol +++ b/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.tree b/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.tree index afa854032..d588c8587 100644 --- a/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.tree +++ b/test/integration/concrete/lockup-linear/get-cliff-time/getCliffTime.tree @@ -1,5 +1,5 @@ getCliffTime.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct cliff time diff --git a/test/integration/concrete/lockup-linear/get-range/getRange.tree b/test/integration/concrete/lockup-linear/get-range/getRange.tree deleted file mode 100644 index 88dcc9707..000000000 --- a/test/integration/concrete/lockup-linear/get-range/getRange.tree +++ /dev/null @@ -1,5 +0,0 @@ -getRange.t.sol -├── given the id references a null stream -│ └── it should revert -└── given the id does not reference a null stream - └── it should return the correct range diff --git a/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol b/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol index d182bbf03..6c4b0a5fb 100644 --- a/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol +++ b/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { LockupLinear } from "src/types/DataTypes.sol"; @@ -25,9 +25,9 @@ contract GetStream_LockupLinear_Integration_Concrete_Test is LockupLinear_Integr } function test_GetStream_StatusSettled() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); - LockupLinear.Stream memory actualStream = lockupLinear.getStream(defaultStreamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + vm.warp({ newTimestamp: defaults.END_TIME() }); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(defaultStreamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.isCancelable = false; assertEq(actualStream, expectedStream); } @@ -37,8 +37,8 @@ contract GetStream_LockupLinear_Integration_Concrete_Test is LockupLinear_Integr } function test_GetStream() external givenNotNull givenStatusNotSettled { - LockupLinear.Stream memory actualStream = lockupLinear.getStream(defaultStreamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(defaultStreamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); assertEq(actualStream, expectedStream); } } diff --git a/test/integration/concrete/lockup-linear/get-stream/getStream.tree b/test/integration/concrete/lockup-linear/get-stream/getStream.tree index c3278db89..bde21f7b4 100644 --- a/test/integration/concrete/lockup-linear/get-stream/getStream.tree +++ b/test/integration/concrete/lockup-linear/get-stream/getStream.tree @@ -1,7 +1,7 @@ getStream.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is settled │ └── it should adjust the `isCancelable` flag and return the stream └── given the stream is not settled diff --git a/test/integration/concrete/lockup-linear/get-range/getRange.t.sol b/test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.t.sol similarity index 51% rename from test/integration/concrete/lockup-linear/get-range/getRange.t.sol rename to test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.t.sol index de45282ca..a0de39a5e 100644 --- a/test/integration/concrete/lockup-linear/get-range/getRange.t.sol +++ b/test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.t.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { LockupLinear } from "src/types/DataTypes.sol"; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; -contract GetRange_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test { +contract GetTimestamps_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test { function test_RevertGiven_Null() external { uint256 nullStreamId = 1729; vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); - lockupLinear.getRange(nullStreamId); + lockupLinear.getTimestamps(nullStreamId); } modifier givenNotNull() { _; } - function test_GetRange() external givenNotNull { + function test_GetTimestamps() external givenNotNull { uint256 streamId = createDefaultStream(); - LockupLinear.Range memory actualRange = lockupLinear.getRange(streamId); - LockupLinear.Range memory expectedRange = defaults.lockupLinearRange(); - assertEq(actualRange, expectedRange); + LockupLinear.Timestamps memory actualTimestamps = lockupLinear.getTimestamps(streamId); + LockupLinear.Timestamps memory expectedTimestamps = defaults.lockupLinearTimestamps(); + assertEq(actualTimestamps, expectedTimestamps); } } diff --git a/test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.tree b/test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.tree new file mode 100644 index 000000000..49e6bf2a0 --- /dev/null +++ b/test/integration/concrete/lockup-linear/get-timestamps/getTimestamps.tree @@ -0,0 +1,5 @@ +getTimestamps.t.sol +├── given the ID references a null stream +│ └── it should revert +└── given the ID does not reference a null stream + └── it should return the correct timestamps diff --git a/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol b/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol index 4320a4aac..beda0bc05 100644 --- a/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.t.sol @@ -1,5 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; + +import { LockupLinear } from "src/types/DataTypes.sol"; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup/streamed-amount-of/streamedAmountOf.t.sol"; @@ -17,10 +19,32 @@ contract StreamedAmountOf_LockupLinear_Integration_Concrete_Test is StreamedAmountOf_Integration_Concrete_Test.setUp(); } - function test_StreamedAmountOf_CliffTimeInThePast() + modifier givenStatusPending() { + _; + } + + function test_StreamedAmountOf_CliffTimeZero() external givenNotNull givenStreamHasNotBeenCanceled + givenStatusPending + { + vm.warp({ newTimestamp: defaults.START_TIME() - 1 }); + + LockupLinear.Timestamps memory timestamps = defaults.lockupLinearTimestamps(); + timestamps.cliff = 0; + uint256 streamId = createDefaultStreamWithTimestamps(timestamps); + + uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(streamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + function test_StreamedAmountOf_CliffTimeInTheFuture() + external + view + givenNotNull + givenStreamHasNotBeenCanceled givenStatusStreaming { uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(defaultStreamId); @@ -34,19 +58,19 @@ contract StreamedAmountOf_LockupLinear_Integration_Concrete_Test is givenStreamHasNotBeenCanceled givenStatusStreaming { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } - function test_StreamedAmountOf_CliffTimeInTheFuture() + function test_StreamedAmountOf_CliffTimeInThePast() external givenNotNull givenStreamHasNotBeenCanceled givenStatusStreaming { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 2600e18; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); diff --git a/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree b/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree index 9c23e5349..e87b5df2f 100644 --- a/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree +++ b/test/integration/concrete/lockup-linear/streamed-amount-of/streamedAmountOf.tree @@ -1,8 +1,11 @@ streamedAmountOf.t.sol +├── given the stream's status is "PENDING" +│ └── given the cliff time is zero +│ └── it should return zero └── given the stream's status is "STREAMING" - ├── given the cliff time is in the past + ├── given the cliff time is in the future │ └── it should return zero ├── given the cliff time is in the present │ └── it should return the correct streamed amount - └── given the cliff time is not in the future + └── given the cliff time is in the past └── it should return the correct streamed amount diff --git a/test/integration/concrete/lockup-linear/token-uri/tokenURI.t.sol b/test/integration/concrete/lockup-linear/token-uri/tokenURI.t.sol index 2f1d12906..2ce92568e 100644 --- a/test/integration/concrete/lockup-linear/token-uri/tokenURI.t.sol +++ b/test/integration/concrete/lockup-linear/token-uri/tokenURI.t.sol @@ -1,24 +1,22 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-line-length,no-console,quotes -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { console2 } from "forge-std/src/console2.sol"; -import { LibString } from "solady/src/utils/LibString.sol"; import { StdStyle } from "forge-std/src/StdStyle.sol"; import { Base64 } from "solady/src/utils/Base64.sol"; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; /// @dev Requirements for these tests to work: -/// - The stream id must be 1 +/// - The stream ID must be 1 /// - The stream's sender must be `0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca`, i.e. `makeAddr("Sender")` /// - The stream asset must have the DAI symbol /// - The contract deployer, i.e. the `sender` config option in `foundry.toml`, must have the default value /// 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38 so that the deployed contracts have the same addresses as /// the values hard coded in the tests below contract TokenURI_LockupLinear_Integration_Concrete_Test is LockupLinear_Integration_Concrete_Test { - using LibString for string; - address internal constant LOCKUP_LINEAR = 0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f; uint256 internal defaultStreamId; @@ -33,13 +31,13 @@ contract TokenURI_LockupLinear_Integration_Concrete_Test is LockupLinear_Integra function test_RevertGiven_NFTDoesNotExist() external { uint256 nullStreamId = 1729; - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); lockupLinear.tokenURI({ tokenId: nullStreamId }); } modifier givenNFTExists() { defaultStreamId = createDefaultStream(); - vm.warp({ timestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); _; } @@ -48,7 +46,7 @@ contract TokenURI_LockupLinear_Integration_Concrete_Test is LockupLinear_Integra /// 2. Remember to escape the EOL character \n with \\n. function test_TokenURI_Decoded() external skipOnMismatch givenNFTExists { string memory tokenURI = lockupLinear.tokenURI(defaultStreamId); - tokenURI = tokenURI.replace({ search: "data:application/json;base64,", replacement: "" }); + tokenURI = vm.replace({ input: tokenURI, from: "data:application/json;base64,", to: "" }); string memory actualDecodedTokenURI = string(Base64.decode(tokenURI)); string memory expectedDecodedTokenURI = unicode'{"attributes":[{"trait_type":"Asset","value":"DAI"},{"trait_type":"Sender","value":"0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca"},{"trait_type":"Status","value":"Streaming"}],"description":"This NFT represents a payment stream in a Sablier V2 Lockup Linear contract. The owner of this NFT can withdraw the streamed assets, which are denominated in DAI.\\n\\n- Stream ID: 1\\n- Lockup Linear Address: 0x3381cd18e2fb4db236bf0525938ab6e43db0440f\\n- DAI Address: 0x03a6a84cd762d9707a21605b548aaab891562aab\\n\\n⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient.","external_url":"https://sablier.com","name":"Sablier V2 Lockup Linear #1","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAwIiBoZWlnaHQ9IjEwMDAiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCNOb2lzZSkiLz48cmVjdCB4PSI3MCIgeT0iNzAiIHdpZHRoPSI4NjAiIGhlaWdodD0iODYwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjQ1IiByeT0iNDUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48ZGVmcz48Y2lyY2xlIGlkPSJHbG93IiByPSI1MDAiIGZpbGw9InVybCgjUmFkaWFsR2xvdykiLz48ZmlsdGVyIGlkPSJOb2lzZSI+PGZlRmxvb2QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmxvb2QtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiIGZsb29kLW9wYWNpdHk9IjEiIHJlc3VsdD0iZmxvb2RGaWxsIi8+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuNCIgbnVtT2N0YXZlcz0iMyIgcmVzdWx0PSJOb2lzZSIgdHlwZT0iZnJhY3RhbE5vaXNlIi8+PGZlQmxlbmQgaW49Ik5vaXNlIiBpbjI9ImZsb29kRmlsbCIgbW9kZT0ic29mdC1saWdodCIvPjwvZmlsdGVyPjxwYXRoIGlkPSJMb2dvIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4xIiBkPSJtMTMzLjU1OSwxMjQuMDM0Yy0uMDEzLDIuNDEyLTEuMDU5LDQuODQ4LTIuOTIzLDYuNDAyLTIuNTU4LDEuODE5LTUuMTY4LDMuNDM5LTcuODg4LDQuOTk2LTE0LjQ0LDguMjYyLTMxLjA0NywxMi41NjUtNDcuNjc0LDEyLjU2OS04Ljg1OC4wMzYtMTcuODM4LTEuMjcyLTI2LjMyOC0zLjY2My05LjgwNi0yLjc2Ni0xOS4wODctNy4xMTMtMjcuNTYyLTEyLjc3OC0xMy44NDItOC4wMjUsOS40NjgtMjguNjA2LDE2LjE1My0zNS4yNjVoMGMyLjAzNS0xLjgzOCw0LjI1Mi0zLjU0Niw2LjQ2My01LjIyNGgwYzYuNDI5LTUuNjU1LDE2LjIxOC0yLjgzNSwyMC4zNTgsNC4xNyw0LjE0Myw1LjA1Nyw4LjgxNiw5LjY0OSwxMy45MiwxMy43MzRoLjAzN2M1LjczNiw2LjQ2MSwxNS4zNTctMi4yNTMsOS4zOC04LjQ4LDAsMC0zLjUxNS0zLjUxNS0zLjUxNS0zLjUxNS0xMS40OS0xMS40NzgtNTIuNjU2LTUyLjY2NC02NC44MzctNjQuODM3bC4wNDktLjAzN2MtMS43MjUtMS42MDYtMi43MTktMy44NDctMi43NTEtNi4yMDRoMGMtLjA0Ni0yLjM3NSwxLjA2Mi00LjU4MiwyLjcyNi02LjIyOWgwbC4xODUtLjE0OGgwYy4wOTktLjA2MiwuMjIyLS4xNDgsLjM3LS4yNTloMGMyLjA2LTEuMzYyLDMuOTUxLTIuNjIxLDYuMDQ0LTMuODQyQzU3Ljc2My0zLjQ3Myw5Ny43Ni0yLjM0MSwxMjguNjM3LDE4LjMzMmMxNi42NzEsOS45NDYtMjYuMzQ0LDU0LjgxMy0zOC42NTEsNDAuMTk5LTYuMjk5LTYuMDk2LTE4LjA2My0xNy43NDMtMTkuNjY4LTE4LjgxMS02LjAxNi00LjA0Ny0xMy4wNjEsNC43NzYtNy43NTIsOS43NTFsNjguMjU0LDY4LjM3MWMxLjcyNCwxLjYwMSwyLjcxNCwzLjg0LDIuNzM4LDYuMTkyWiIvPjxwYXRoIGlkPSJGbG9hdGluZ1RleHQiIGZpbGw9Im5vbmUiIGQ9Ik0xMjUgNDVoNzUwczgwIDAgODAgODB2NzUwczAgODAgLTgwIDgwaC03NTBzLTgwIDAgLTgwIC04MHYtNzUwczAgLTgwIDgwIC04MCIvPjxyYWRpYWxHcmFkaWVudCBpZD0iUmFkaWFsR2xvdyI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iaHNsKDE5LDIyJSw2MyUpIiBzdG9wLW9wYWNpdHk9Ii42Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iU2FuZFRvcCIgeDE9IjAlIiB5MT0iMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9ImhzbCgxOSwyMiUsNjMlKSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iaHNsKDIzMCwyMSUsMTElKSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJTYW5kQm90dG9tIiB4MT0iMTAwJSIgeTE9IjEwMCUiPjxzdG9wIG9mZnNldD0iMTAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woMTksMjIlLDYzJSkiLz48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4MSIgZHVyPSI2cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzAlOzYwJTsxMjAlOzYwJTszMCU7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkhvdXJnbGFzc1N0cm9rZSIgZ3JhZGllbnRUcmFuc2Zvcm09InJvdGF0ZSg5MCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iaHNsKDE5LDIyJSw2MyUpIi8+PHN0b3Agb2Zmc2V0PSI4MCUiIHN0b3AtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiLz48L2xpbmVhckdyYWRpZW50PjxnIGlkPSJIb3VyZ2xhc3MiPjxwYXRoIGQ9Ik0gNTAsMzYwIGEgMzAwLDMwMCAwIDEsMSA2MDAsMCBhIDMwMCwzMDAgMCAxLDEgLTYwMCwwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMiIgc3Ryb2tlPSJ1cmwoI0hvdXJnbGFzc1N0cm9rZSkiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Im01NjYsMTYxLjIwMXYtNTMuOTI0YzAtMTkuMzgyLTIyLjUxMy0zNy41NjMtNjMuMzk4LTUxLjE5OC00MC43NTYtMTMuNTkyLTk0Ljk0Ni0yMS4wNzktMTUyLjU4Ny0yMS4wNzlzLTExMS44MzgsNy40ODctMTUyLjYwMiwyMS4wNzljLTQwLjg5MywxMy42MzYtNjMuNDEzLDMxLjgxNi02My40MTMsNTEuMTk4djUzLjkyNGMwLDE3LjE4MSwxNy43MDQsMzMuNDI3LDUwLjIyMyw0Ni4zOTR2Mjg0LjgwOWMtMzIuNTE5LDEyLjk2LTUwLjIyMywyOS4yMDYtNTAuMjIzLDQ2LjM5NHY1My45MjRjMCwxOS4zODIsMjIuNTIsMzcuNTYzLDYzLjQxMyw1MS4xOTgsNDAuNzYzLDEzLjU5Miw5NC45NTQsMjEuMDc5LDE1Mi42MDIsMjEuMDc5czExMS44MzEtNy40ODcsMTUyLjU4Ny0yMS4wNzljNDAuODg2LTEzLjYzNiw2My4zOTgtMzEuODE2LDYzLjM5OC01MS4xOTh2LTUzLjkyNGMwLTE3LjE5Ni0xNy43MDQtMzMuNDM1LTUwLjIyMy00Ni40MDFWMjA3LjYwM2MzMi41MTktMTIuOTY3LDUwLjIyMy0yOS4yMDYsNTAuMjIzLTQ2LjQwMVptLTM0Ny40NjIsNTcuNzkzbDEzMC45NTksMTMxLjAyNy0xMzAuOTU5LDEzMS4wMTNWMjE4Ljk5NFptMjYyLjkyNC4wMjJ2MjYyLjAxOGwtMTMwLjkzNy0xMzEuMDA2LDEzMC45MzctMTMxLjAxM1oiIGZpbGw9IiMxNjE4MjIiPjwvcGF0aD48cG9seWdvbiBwb2ludHM9IjM1MCAzNTAuMDI2IDQxNS4wMyAyODQuOTc4IDI4NSAyODQuOTc4IDM1MCAzNTAuMDI2IiBmaWxsPSJ1cmwoI1NhbmRCb3R0b20pIi8+PHBhdGggZD0ibTQxNi4zNDEsMjgxLjk3NWMwLC45MTQtLjM1NCwxLjgwOS0xLjAzNSwyLjY4LTUuNTQyLDcuMDc2LTMyLjY2MSwxMi40NS02NS4yOCwxMi40NS0zMi42MjQsMC01OS43MzgtNS4zNzQtNjUuMjgtMTIuNDUtLjY4MS0uODcyLTEuMDM1LTEuNzY3LTEuMDM1LTIuNjgsMC0uOTE0LjM1NC0xLjgwOCwxLjAzNS0yLjY3Niw1LjU0Mi03LjA3NiwzMi42NTYtMTIuNDUsNjUuMjgtMTIuNDUsMzIuNjE5LDAsNTkuNzM4LDUuMzc0LDY1LjI4LDEyLjQ1LjY4MS44NjcsMS4wMzUsMS43NjIsMS4wMzUsMi42NzZaIiBmaWxsPSJ1cmwoI1NhbmRUb3ApIi8+PHBhdGggZD0ibTQ4MS40Niw1MDQuMTAxdjU4LjQ0OWMtMi4zNS43Ny00LjgyLDEuNTEtNy4zOSwyLjIzLTMwLjMsOC41NC03NC42NSwxMy45Mi0xMjQuMDYsMTMuOTItNTMuNiwwLTEwMS4yNC02LjMzLTEzMS40Ny0xNi4xNnYtNTguNDM5aDI2Mi45MloiIGZpbGw9InVybCgjU2FuZEJvdHRvbSkiLz48ZWxsaXBzZSBjeD0iMzUwIiBjeT0iNTA0LjEwMSIgcng9IjEzMS40NjIiIHJ5PSIyOC4xMDgiIGZpbGw9InVybCgjU2FuZFRvcCkiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9InVybCgjSG91cmdsYXNzU3Ryb2tlKSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iNCI+PHBhdGggZD0ibTU2NS42NDEsMTA3LjI4YzAsOS41MzctNS41NiwxOC42MjktMTUuNjc2LDI2Ljk3M2gtLjAyM2MtOS4yMDQsNy41OTYtMjIuMTk0LDE0LjU2Mi0zOC4xOTcsMjAuNTkyLTM5LjUwNCwxNC45MzYtOTcuMzI1LDI0LjM1NS0xNjEuNzMzLDI0LjM1NS05MC40OCwwLTE2Ny45NDgtMTguNTgyLTE5OS45NTMtNDQuOTQ4aC0uMDIzYy0xMC4xMTUtOC4zNDQtMTUuNjc2LTE3LjQzNy0xNS42NzYtMjYuOTczLDAtMzkuNzM1LDk2LjU1NC03MS45MjEsMjE1LjY1Mi03MS45MjFzMjE1LjYyOSwzMi4xODUsMjE1LjYyOSw3MS45MjFaIi8+PHBhdGggZD0ibTEzNC4zNiwxNjEuMjAzYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iMTYxLjIwMyIgeDI9IjEzNC4zNiIgeTI9IjEwNy4yOCIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSIxNjEuMjAzIiB4Mj0iNTY1LjY0IiB5Mj0iMTA3LjI4Ii8+PGxpbmUgeDE9IjE4NC41ODQiIHkxPSIyMDYuODIzIiB4Mj0iMTg0LjU4NSIgeTI9IjUzNy41NzkiLz48bGluZSB4MT0iMjE4LjE4MSIgeTE9IjIxOC4xMTgiIHgyPSIyMTguMTgxIiB5Mj0iNTYyLjUzNyIvPjxsaW5lIHgxPSI0ODEuODE4IiB5MT0iMjE4LjE0MiIgeDI9IjQ4MS44MTkiIHkyPSI1NjIuNDI4Ii8+PGxpbmUgeDE9IjUxNS40MTUiIHkxPSIyMDcuMzUyIiB4Mj0iNTE1LjQxNiIgeTI9IjUzNy41NzkiLz48cGF0aCBkPSJtMTg0LjU4LDUzNy41OGMwLDUuNDUsNC4yNywxMC42NSwxMi4wMywxNS40MmguMDJjNS41MSwzLjM5LDEyLjc5LDYuNTUsMjEuNTUsOS40MiwzMC4yMSw5LjksNzguMDIsMTYuMjgsMTMxLjgzLDE2LjI4LDQ5LjQxLDAsOTMuNzYtNS4zOCwxMjQuMDYtMTMuOTIsMi43LS43Niw1LjI5LTEuNTQsNy43NS0yLjM1LDguNzctMi44NywxNi4wNS02LjA0LDIxLjU2LTkuNDNoMGM3Ljc2LTQuNzcsMTIuMDQtOS45NywxMi4wNC0xNS40MiIvPjxwYXRoIGQ9Im0xODQuNTgyLDQ5Mi42NTZjLTMxLjM1NCwxMi40ODUtNTAuMjIzLDI4LjU4LTUwLjIyMyw0Ni4xNDIsMCw5LjUzNiw1LjU2NCwxOC42MjcsMTUuNjc3LDI2Ljk2OWguMDIyYzguNTAzLDcuMDA1LDIwLjIxMywxMy40NjMsMzQuNTI0LDE5LjE1OSw5Ljk5OSwzLjk5MSwyMS4yNjksNy42MDksMzMuNTk3LDEwLjc4OCwzNi40NSw5LjQwNyw4Mi4xODEsMTUuMDAyLDEzMS44MzUsMTUuMDAyczk1LjM2My01LjU5NSwxMzEuODA3LTE1LjAwMmMxMC44NDctMi43OSwyMC44NjctNS45MjYsMjkuOTI0LTkuMzQ5LDEuMjQ0LS40NjcsMi40NzMtLjk0MiwzLjY3My0xLjQyNCwxNC4zMjYtNS42OTYsMjYuMDM1LTEyLjE2MSwzNC41MjQtMTkuMTczaC4wMjJjMTAuMTE0LTguMzQyLDE1LjY3Ny0xNy40MzMsMTUuNjc3LTI2Ljk2OSwwLTE3LjU2Mi0xOC44NjktMzMuNjY1LTUwLjIyMy00Ni4xNSIvPjxwYXRoIGQ9Im0xMzQuMzYsNTkyLjcyYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iNTkyLjcyIiB4Mj0iMTM0LjM2IiB5Mj0iNTM4Ljc5NyIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSI1OTIuNzIiIHgyPSI1NjUuNjQiIHkyPSI1MzguNzk3Ii8+PHBvbHlsaW5lIHBvaW50cz0iNDgxLjgyMiA0ODEuOTAxIDQ4MS43OTggNDgxLjg3NyA0ODEuNzc1IDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDIxOC4xODUgMjE4LjEyOSIvPjxwb2x5bGluZSBwb2ludHM9IjIxOC4xODUgNDgxLjkwMSAyMTguMjMxIDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDQ4MS44MjIgMjE4LjE1MiIvPjwvZz48L2c+PGcgaWQ9IlByb2dyZXNzIiBmaWxsPSIjZmZmIj48cmVjdCB3aWR0aD0iMjA4IiBoZWlnaHQ9IjEwMCIgZmlsbC1vcGFjaXR5PSIuMDMiIHJ4PSIxNSIgcnk9IjE1IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIuMSIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHRleHQgeD0iMjAiIHk9IjM0IiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjJweCI+UHJvZ3Jlc3M8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+MjUlPC90ZXh0PjxnIGZpbGw9Im5vbmUiPjxjaXJjbGUgY3g9IjE2NiIgY3k9IjUwIiByPSIyMiIgc3Ryb2tlPSJoc2woMjMwLDIxJSwxMSUpIiBzdHJva2Utd2lkdGg9IjEwIi8+PGNpcmNsZSBjeD0iMTY2IiBjeT0iNTAiIHBhdGhMZW5ndGg9IjEwMDAwIiByPSIyMiIgc3Ryb2tlPSJoc2woMTksMjIlLDYzJSkiIHN0cm9rZS1kYXNoYXJyYXk9IjEwMDAwIiBzdHJva2UtZGFzaG9mZnNldD0iNzUwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjUiIHRyYW5zZm9ybT0icm90YXRlKC05MCkiIHRyYW5zZm9ybS1vcmlnaW49IjE2NiA1MCIvPjwvZz48L2c+PGcgaWQ9IlN0YXR1cyIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE4NCIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPlN0YXR1czwvdGV4dD48dGV4dCB4PSIyMCIgeT0iNzIiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyNnB4Ij5TdHJlYW1pbmc8L3RleHQ+PC9nPjxnIGlkPSJBbW91bnQiIGZpbGw9IiNmZmYiPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iMTAwIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjE1IiByeT0iMTUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48dGV4dCB4PSIyMCIgeT0iMzQiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyMnB4Ij5BbW91bnQ8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+JiM4ODA1OyAxMEs8L3RleHQ+PC9nPjxnIGlkPSJEdXJhdGlvbiIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE1MiIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPkR1cmF0aW9uPC90ZXh0Pjx0ZXh0IHg9IjIwIiB5PSI3MiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjI2cHgiPiZsdDsgMSBEYXk8L3RleHQ+PC9nPjwvZGVmcz48dGV4dCB0ZXh0LXJlbmRlcmluZz0ib3B0aW1pemVTcGVlZCI+PHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSItMTAwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHgzMzgxY2QxOGUyZmI0ZGIyMzZiZjA1MjU5MzhhYjZlNDNkYjA0NDBmIOKAoiBTYWJsaWVyIFYyIExvY2t1cCBMaW5lYXI8L3RleHRQYXRoPjx0ZXh0UGF0aCBzdGFydE9mZnNldD0iMCUiIGhyZWY9IiNGbG9hdGluZ1RleHQiIGZpbGw9IiNmZmYiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZmlsbC1vcGFjaXR5PSIuOCIgZm9udC1zaXplPSIyNnB4Ij48YW5pbWF0ZSBhZGRpdGl2ZT0ic3VtIiBhdHRyaWJ1dGVOYW1lPSJzdGFydE9mZnNldCIgYmVnaW49IjBzIiBkdXI9IjUwcyIgZnJvbT0iMCUiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB0bz0iMTAwJSIvPjB4MzM4MWNkMThlMmZiNGRiMjM2YmYwNTI1OTM4YWI2ZTQzZGIwNDQwZiDigKIgU2FibGllciBWMiBMb2NrdXAgTGluZWFyPC90ZXh0UGF0aD48dGV4dFBhdGggc3RhcnRPZmZzZXQ9Ii01MCUiIGhyZWY9IiNGbG9hdGluZ1RleHQiIGZpbGw9IiNmZmYiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZmlsbC1vcGFjaXR5PSIuOCIgZm9udC1zaXplPSIyNnB4Ij48YW5pbWF0ZSBhZGRpdGl2ZT0ic3VtIiBhdHRyaWJ1dGVOYW1lPSJzdGFydE9mZnNldCIgYmVnaW49IjBzIiBkdXI9IjUwcyIgZnJvbT0iMCUiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB0bz0iMTAwJSIvPjB4MDNhNmE4NGNkNzYyZDk3MDdhMjE2MDViNTQ4YWFhYjg5MTU2MmFhYiDigKIgREFJPC90ZXh0UGF0aD48dGV4dFBhdGggc3RhcnRPZmZzZXQ9IjUwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHgwM2E2YTg0Y2Q3NjJkOTcwN2EyMTYwNWI1NDhhYWFiODkxNTYyYWFiIOKAoiBEQUk8L3RleHRQYXRoPjwvdGV4dD48dXNlIGhyZWY9IiNHbG93IiBmaWxsLW9wYWNpdHk9Ii45Ii8+PHVzZSBocmVmPSIjR2xvdyIgeD0iMTAwMCIgeT0iMTAwMCIgZmlsbC1vcGFjaXR5PSIuOSIvPjx1c2UgaHJlZj0iI0xvZ28iIHg9IjE3MCIgeT0iMTcwIiB0cmFuc2Zvcm09InNjYWxlKC42KSIvPjx1c2UgaHJlZj0iI0hvdXJnbGFzcyIgeD0iMTUwIiB5PSI5MCIgdHJhbnNmb3JtPSJyb3RhdGUoMTApIiB0cmFuc2Zvcm0tb3JpZ2luPSI1MDAgNTAwIi8+PHVzZSBocmVmPSIjUHJvZ3Jlc3MiIHg9IjE0NCIgeT0iNzkwIi8+PHVzZSBocmVmPSIjU3RhdHVzIiB4PSIzNjgiIHk9Ijc5MCIvPjx1c2UgaHJlZj0iI0Ftb3VudCIgeD0iNTY4IiB5PSI3OTAiLz48dXNlIGhyZWY9IiNEdXJhdGlvbiIgeD0iNzA0IiB5PSI3OTAiLz48L3N2Zz4="}'; diff --git a/test/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol index d193126c9..c5a6d9d44 100644 --- a/test/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/integration/concrete/lockup-linear/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LockupLinear_Integration_Concrete_Test } from "../LockupLinear.t.sol"; import { WithdrawableAmountOf_Integration_Concrete_Test } from @@ -30,7 +30,7 @@ contract WithdrawableAmountOf_LockupLinear_Integration_Concrete_Test is } modifier givenCliffTimeNotInTheFuture() { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); _; } diff --git a/test/integration/concrete/lockup-tranched/LockupTranched.t.sol b/test/integration/concrete/lockup-tranched/LockupTranched.t.sol new file mode 100644 index 000000000..b19b02ffc --- /dev/null +++ b/test/integration/concrete/lockup-tranched/LockupTranched.t.sol @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; + +import { LockupTranched_Integration_Shared_Test } from "../../shared/lockup-tranched/LockupTranched.t.sol"; +import { Integration_Test } from "../../Integration.t.sol"; +import { AllowToHook_Integration_Concrete_Test } from "../lockup/allow-to-hook/allowToHook.t.sol"; +import { Burn_Integration_Concrete_Test } from "../lockup/burn/burn.t.sol"; +import { Cancel_Integration_Concrete_Test } from "../lockup/cancel/cancel.t.sol"; +import { CancelMultiple_Integration_Concrete_Test } from "../lockup/cancel-multiple/cancelMultiple.t.sol"; +import { GetAsset_Integration_Concrete_Test } from "../lockup/get-asset/getAsset.t.sol"; +import { GetDepositedAmount_Integration_Concrete_Test } from "../lockup/get-deposited-amount/getDepositedAmount.t.sol"; +import { GetEndTime_Integration_Concrete_Test } from "../lockup/get-end-time/getEndTime.t.sol"; +import { GetRecipient_Integration_Concrete_Test } from "../lockup/get-recipient/getRecipient.t.sol"; +import { GetRefundedAmount_Integration_Concrete_Test } from "../lockup/get-refunded-amount/getRefundedAmount.t.sol"; +import { GetSender_Integration_Concrete_Test } from "../lockup/get-sender/getSender.t.sol"; +import { GetStartTime_Integration_Concrete_Test } from "../lockup/get-start-time/getStartTime.t.sol"; +import { GetWithdrawnAmount_Integration_Concrete_Test } from "../lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol"; +import { IsAllowedToHook_Integration_Concrete_Test } from "../lockup/is-allowed-to-hook/isAllowedToHook.t.sol"; +import { IsCancelable_Integration_Concrete_Test } from "../lockup/is-cancelable/isCancelable.t.sol"; +import { IsCold_Integration_Concrete_Test } from "../lockup/is-cold/isCold.t.sol"; +import { IsDepleted_Integration_Concrete_Test } from "../lockup/is-depleted/isDepleted.t.sol"; +import { IsStream_Integration_Concrete_Test } from "../lockup/is-stream/isStream.t.sol"; +import { IsTransferable_Integration_Concrete_Test } from "../lockup/is-transferable/isTransferable.t.sol"; +import { IsWarm_Integration_Concrete_Test } from "../lockup/is-warm/isWarm.t.sol"; +import { RefundableAmountOf_Integration_Concrete_Test } from "../lockup/refundable-amount-of/refundableAmountOf.t.sol"; +import { Renounce_Integration_Concrete_Test } from "../lockup/renounce/renounce.t.sol"; +import { SetNFTDescriptor_Integration_Concrete_Test } from "../lockup/set-nft-descriptor/setNFTDescriptor.t.sol"; +import { StatusOf_Integration_Concrete_Test } from "../lockup/status-of/statusOf.t.sol"; +import { TransferFrom_Integration_Concrete_Test } from "../lockup/transfer-from/transferFrom.t.sol"; +import { WasCanceled_Integration_Concrete_Test } from "../lockup/was-canceled/wasCanceled.t.sol"; +import { Withdraw_Integration_Concrete_Test } from "../lockup/withdraw/withdraw.t.sol"; +import { WithdrawHooks_Integration_Concrete_Test } from "../lockup/withdraw-hooks/withdrawHooks.t.sol"; +import { WithdrawMax_Integration_Concrete_Test } from "../lockup/withdraw-max/withdrawMax.t.sol"; +import { WithdrawMaxAndTransfer_Integration_Concrete_Test } from + "../lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol"; +import { WithdrawMultiple_Integration_Concrete_Test } from "../lockup/withdraw-multiple/withdrawMultiple.t.sol"; + +/*////////////////////////////////////////////////////////////////////////// + NON-SHARED ABSTRACT TEST +//////////////////////////////////////////////////////////////////////////*/ + +/// @notice Common testing logic needed across {SablierV2LockupTranched} integration concrete tests. +abstract contract LockupTranched_Integration_Concrete_Test is + Integration_Test, + LockupTranched_Integration_Shared_Test +{ + function setUp() public virtual override(Integration_Test, LockupTranched_Integration_Shared_Test) { + // Both of these contracts inherit from {Base_Test}, which is fine because multiple inheritance is + // allowed in Solidity, and {Base_Test-setUp} will only be called once. + Integration_Test.setUp(); + LockupTranched_Integration_Shared_Test.setUp(); + + // Cast the {LockupTranched} contract as {ISablierV2Lockup}. + lockup = ISablierV2Lockup(lockupTranched); + } +} + +/*////////////////////////////////////////////////////////////////////////// + SHARED TESTS +//////////////////////////////////////////////////////////////////////////*/ + +contract AllowToHook_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + AllowToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, AllowToHook_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + AllowToHook_Integration_Concrete_Test.setUp(); + } +} + +contract Burn_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + Burn_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, Burn_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + Burn_Integration_Concrete_Test.setUp(); + } +} + +contract Cancel_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + Cancel_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, Cancel_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + Cancel_Integration_Concrete_Test.setUp(); + } +} + +contract CancelMultiple_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + CancelMultiple_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, CancelMultiple_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + CancelMultiple_Integration_Concrete_Test.setUp(); + } +} + +contract GetAsset_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetAsset_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetAsset_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetAsset_Integration_Concrete_Test.setUp(); + } +} + +contract GetDepositedAmount_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetDepositedAmount_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetDepositedAmount_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetDepositedAmount_Integration_Concrete_Test.setUp(); + } +} + +contract GetEndTime_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetEndTime_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetEndTime_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetEndTime_Integration_Concrete_Test.setUp(); + } +} + +contract GetRecipient_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetRecipient_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetRecipient_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetRecipient_Integration_Concrete_Test.setUp(); + } +} + +contract GetRefundedAmount_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetRefundedAmount_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetRefundedAmount_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetRefundedAmount_Integration_Concrete_Test.setUp(); + } +} + +contract GetSender_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetSender_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetSender_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetSender_Integration_Concrete_Test.setUp(); + } +} + +contract GetStartTime_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetStartTime_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetStartTime_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetStartTime_Integration_Concrete_Test.setUp(); + } +} + +contract GetWithdrawnAmount_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + GetWithdrawnAmount_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, GetWithdrawnAmount_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + GetWithdrawnAmount_Integration_Concrete_Test.setUp(); + } +} + +contract IsAllowedToHook_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsAllowedToHook_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsAllowedToHook_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsAllowedToHook_Integration_Concrete_Test.setUp(); + } +} + +contract IsCancelable_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsCancelable_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsCancelable_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsCancelable_Integration_Concrete_Test.setUp(); + } +} + +contract IsCold_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsCold_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsCold_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsCold_Integration_Concrete_Test.setUp(); + } +} + +contract IsDepleted_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsDepleted_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsDepleted_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsDepleted_Integration_Concrete_Test.setUp(); + } +} + +contract IsStream_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsStream_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsStream_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsStream_Integration_Concrete_Test.setUp(); + } +} + +contract IsTransferable_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsTransferable_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsTransferable_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsTransferable_Integration_Concrete_Test.setUp(); + } +} + +contract IsWarm_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + IsWarm_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, IsWarm_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + IsWarm_Integration_Concrete_Test.setUp(); + } +} + +contract RefundableAmountOf_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + RefundableAmountOf_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, RefundableAmountOf_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + RefundableAmountOf_Integration_Concrete_Test.setUp(); + } +} + +contract Renounce_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + Renounce_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, Renounce_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + Renounce_Integration_Concrete_Test.setUp(); + } +} + +contract SetNFTDescriptor_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + SetNFTDescriptor_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, SetNFTDescriptor_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + SetNFTDescriptor_Integration_Concrete_Test.setUp(); + } +} + +contract StatusOf_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + StatusOf_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, StatusOf_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + StatusOf_Integration_Concrete_Test.setUp(); + } +} + +contract TransferFrom_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + TransferFrom_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, TransferFrom_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + TransferFrom_Integration_Concrete_Test.setUp(); + } +} + +contract WasCanceled_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WasCanceled_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WasCanceled_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WasCanceled_Integration_Concrete_Test.setUp(); + } +} + +contract Withdraw_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + Withdraw_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, Withdraw_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + Withdraw_Integration_Concrete_Test.setUp(); + } +} + +contract WithdrawHooks_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WithdrawHooks_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WithdrawHooks_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WithdrawHooks_Integration_Concrete_Test.setUp(); + } +} + +contract WithdrawMax_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WithdrawMax_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WithdrawMax_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WithdrawMax_Integration_Concrete_Test.setUp(); + } +} + +contract WithdrawMaxAndTransfer_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WithdrawMaxAndTransfer_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WithdrawMaxAndTransfer_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WithdrawMaxAndTransfer_Integration_Concrete_Test.setUp(); + } +} + +contract WithdrawMultiple_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WithdrawMultiple_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WithdrawMultiple_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WithdrawMultiple_Integration_Concrete_Test.setUp(); + } +} diff --git a/test/integration/concrete/lockup-tranched/constructor.t.sol b/test/integration/concrete/lockup-tranched/constructor.t.sol new file mode 100644 index 000000000..5bf824fb5 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/constructor.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; +import { SablierV2LockupTranched } from "src/SablierV2LockupTranched.sol"; + +import { LockupTranched_Integration_Concrete_Test } from "./LockupTranched.t.sol"; + +contract Constructor_LockupTranched_Integration_Concrete_Test is LockupTranched_Integration_Concrete_Test { + function test_Constructor() external { + // Expect the relevant event to be emitted. + vm.expectEmit(); + emit TransferAdmin({ oldAdmin: address(0), newAdmin: users.admin }); + + // Construct the contract. + SablierV2LockupTranched constructedLockupTranched = new SablierV2LockupTranched({ + initialAdmin: users.admin, + initialNFTDescriptor: nftDescriptor, + maxTrancheCount: defaults.MAX_TRANCHE_COUNT() + }); + + // {SablierV2Lockup.constant} + UD60x18 actualMaxBrokerFee = constructedLockupTranched.MAX_BROKER_FEE(); + UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18); + assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE"); + + // {SablierV2Lockup.constructor} + address actualAdmin = constructedLockupTranched.admin(); + address expectedAdmin = users.admin; + assertEq(actualAdmin, expectedAdmin, "admin"); + + uint256 actualStreamId = constructedLockupTranched.nextStreamId(); + uint256 expectedStreamId = 1; + assertEq(actualStreamId, expectedStreamId, "nextStreamId"); + + address actualNFTDescriptor = address(constructedLockupTranched.nftDescriptor()); + address expectedNFTDescriptor = address(nftDescriptor); + assertEq(actualNFTDescriptor, expectedNFTDescriptor, "nftDescriptor"); + + // {SablierV2Lockup.supportsInterface} + assertTrue(constructedLockupTranched.supportsInterface(0x49064906), "ERC-4906 interface ID"); + + // {SablierV2LockupTranched.constructor} + uint256 actualMaxTrancheCount = constructedLockupTranched.MAX_TRANCHE_COUNT(); + uint256 expectedMaxTrancheCount = defaults.MAX_TRANCHE_COUNT(); + assertEq(actualMaxTrancheCount, expectedMaxTrancheCount, "MAX_TRANCHE_COUNT"); + } +} diff --git a/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.t.sol b/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.t.sol new file mode 100644 index 000000000..9db841bb2 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2LockupTranched } from "src/interfaces/ISablierV2LockupTranched.sol"; +import { Errors } from "src/libraries/Errors.sol"; +import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { CreateWithDurations_Integration_Shared_Test } from "../../../shared/lockup/createWithDurations.t.sol"; +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +contract CreateWithDurations_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + CreateWithDurations_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, CreateWithDurations_Integration_Shared_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + CreateWithDurations_Integration_Shared_Test.setUp(); + streamId = lockupTranched.nextStreamId(); + } + + /// @dev it should revert. + function test_RevertWhen_DelegateCalled() external { + bytes memory callData = + abi.encodeCall(ISablierV2LockupTranched.createWithDurations, defaults.createWithDurationsLT()); + (bool success, bytes memory returnData) = address(lockupTranched).delegatecall(callData); + expectRevertDueToDelegateCall(success, returnData); + } + + /// @dev it should revert. + function test_RevertWhen_TrancheCountTooHigh() external whenNotDelegateCalled { + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](25_000); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2LockupTranched_TrancheCountTooHigh.selector, 25_000)); + createDefaultStreamWithDurations(tranches); + } + + function test_RevertWhen_DurationsZero() external whenNotDelegateCalled whenTrancheCountNotTooHigh { + uint40 startTime = getBlockTimestamp(); + LockupTranched.TrancheWithDuration[] memory tranches = defaults.createWithDurationsLT().tranches; + tranches[2].duration = 0; + uint256 index = 2; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered.selector, + index, + startTime + tranches[0].duration + tranches[1].duration, + startTime + tranches[0].duration + tranches[1].duration + ) + ); + createDefaultStreamWithDurations(tranches); + } + + function test_RevertWhen_TimestampsCalculationsOverflows_StartTimeNotLessThanFirstTrancheTimestamp() + external + whenNotDelegateCalled + whenTrancheCountNotTooHigh + whenDurationsNotZero + { + unchecked { + uint40 startTime = getBlockTimestamp(); + LockupTranched.TrancheWithDuration[] memory tranches = defaults.tranchesWithDurations(); + tranches[0].duration = MAX_UINT40; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp.selector, + startTime, + startTime + tranches[0].duration + ) + ); + createDefaultStreamWithDurations(tranches); + } + } + + function test_RevertWhen_TimestampsCalculationsOverflows_TrancheTimestampsNotOrdered() + external + whenNotDelegateCalled + whenTrancheCountNotTooHigh + whenDurationsNotZero + { + unchecked { + uint40 startTime = getBlockTimestamp(); + + // Create new tranches that overflow when the timestamps are eventually calculated. + LockupTranched.TrancheWithDuration[] memory tranches = new LockupTranched.TrancheWithDuration[](2); + tranches[0] = LockupTranched.TrancheWithDuration({ amount: 0, duration: startTime + 1 seconds }); + tranches[1] = defaults.tranchesWithDurations()[0]; + tranches[1].duration = MAX_UINT40; + + // Expect the relevant error to be thrown. + uint256 index = 1; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered.selector, + index, + startTime + tranches[0].duration, + startTime + tranches[0].duration + tranches[1].duration + ) + ); + + // Create the stream. + createDefaultStreamWithDurations(tranches); + } + } + + function test_CreateWithDurations() + external + whenNotDelegateCalled + whenTrancheCountNotTooHigh + whenDurationsNotZero + whenTimestampsCalculationsDoNotOverflow + { + // Make the Sender the stream's funder + address funder = users.sender; + + // Declare the timestamps. + uint40 blockTimestamp = getBlockTimestamp(); + LockupTranched.Timestamps memory timestamps = + LockupTranched.Timestamps({ start: blockTimestamp, end: blockTimestamp + defaults.TOTAL_DURATION() }); + + LockupTranched.TrancheWithDuration[] memory tranchesWithDurations = defaults.tranchesWithDurations(); + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].timestamp = timestamps.start + tranchesWithDurations[0].duration; + tranches[1].timestamp = tranches[0].timestamp + tranchesWithDurations[1].duration; + tranches[2].timestamp = tranches[1].timestamp + tranchesWithDurations[2].duration; + + // Expect the assets to be transferred from the funder to {SablierV2LockupTranched}. + expectCallToTransferFrom({ from: funder, to: address(lockupTranched), value: defaults.DEPOSIT_AMOUNT() }); + + // Expect the broker fee to be paid to the broker. + expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: streamId }); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit CreateLockupTranchedStream({ + streamId: streamId, + funder: funder, + sender: users.sender, + recipient: users.recipient, + amounts: defaults.lockupCreateAmounts(), + asset: dai, + cancelable: true, + transferable: true, + tranches: tranches, + timestamps: timestamps, + broker: users.broker + }); + + // Create the stream. + createDefaultStreamWithDurations(); + + // Assert that the stream has been created. + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(streamId); + LockupTranched.StreamLT memory expectedStream = defaults.lockupTranchedStream(); + expectedStream.endTime = timestamps.end; + expectedStream.startTime = timestamps.start; + expectedStream.tranches = tranches; + assertEq(actualStream, expectedStream); + + // Assert that the stream's status is "STREAMING". + Lockup.Status actualStatus = lockupTranched.statusOf(streamId); + Lockup.Status expectedStatus = Lockup.Status.STREAMING; + assertEq(actualStatus, expectedStatus); + + // Assert that the next stream ID has been bumped. + uint256 actualNextStreamId = lockupTranched.nextStreamId(); + uint256 expectedNextStreamId = streamId + 1; + assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + address actualNFTOwner = lockupTranched.ownerOf({ tokenId: streamId }); + address expectedNFTOwner = users.recipient; + assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); + } +} diff --git a/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.tree b/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.tree new file mode 100644 index 000000000..d3f633068 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/create-with-durations/createWithDurations.tree @@ -0,0 +1,22 @@ +createWithDurations.t.sol +├── when delegate called +│ └── it should revert +└── when not delegate called + ├── when the tranche count is too high + │ └── it should revert + └── when the tranche count is not too high + ├── when at least one of the durations at index one or greater is zero + │ └── it should revert + └── when none of the durations is zero + ├── when the tranche timestamp calculations overflow uint256 + │ ├── when the start time is not less than the first tranche timestamp + │ │ └── it should revert + │ └── when the tranche timestamps are not ordered + │ └── it should revert + └── when the tranche timestamp calculations do not overflow uint256 + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should emit a {MetadataUpdate} event + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupTranchedStream} event diff --git a/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.t.sol b/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.t.sol new file mode 100644 index 000000000..cb1ce9a56 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.t.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UD60x18, ud, ZERO } from "@prb/math/src/UD60x18.sol"; +import { stdError } from "forge-std/src/StdError.sol"; + +import { ISablierV2LockupTranched } from "src/interfaces/ISablierV2LockupTranched.sol"; +import { Errors } from "src/libraries/Errors.sol"; +import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { CreateWithTimestamps_Integration_Shared_Test } from "../../../shared/lockup/createWithTimestamps.t.sol"; +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +contract CreateWithTimestamps_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + CreateWithTimestamps_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, CreateWithTimestamps_Integration_Shared_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); + } + + function test_RevertWhen_DelegateCalled() external { + bytes memory callData = + abi.encodeCall(ISablierV2LockupTranched.createWithTimestamps, defaults.createWithTimestampsLT()); + (bool success, bytes memory returnData) = address(lockupTranched).delegatecall(callData); + expectRevertDueToDelegateCall(success, returnData); + } + + function test_RevertWhen_RecipientZeroAddress() external whenNotDelegateCalled { + address recipient = address(0); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, recipient)); + createDefaultStreamWithRecipient(recipient); + } + + function test_RevertWhen_DepositAmountZero() external whenNotDelegateCalled whenRecipientNonZeroAddress { + // It is not possible to obtain a zero deposit amount from a non-zero total amount, because the `MAX_BROKER_FEE` + // is hard coded to 10%. + vm.expectRevert(Errors.SablierV2Lockup_DepositAmountZero.selector); + uint128 totalAmount = 0; + createDefaultStreamWithTotalAmount(totalAmount); + } + + function test_RevertWhen_StartTimeZero() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + { + vm.expectRevert(Errors.SablierV2Lockup_StartTimeZero.selector); + createDefaultStreamWithStartTime(0); + } + + function test_RevertWhen_TrancheCountZero() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + { + LockupTranched.Tranche[] memory tranches; + vm.expectRevert(Errors.SablierV2LockupTranched_TrancheCountZero.selector); + createDefaultStreamWithTranches(tranches); + } + + function test_RevertWhen_TrancheCountTooHigh() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + { + uint256 trancheCount = defaults.MAX_TRANCHE_COUNT() + 1; + LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](trancheCount); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2LockupTranched_TrancheCountTooHigh.selector, trancheCount) + ); + createDefaultStreamWithTranches(tranches); + } + + function test_RevertWhen_TrancheAmountsSumOverflows() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + { + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].amount = MAX_UINT128; + tranches[1].amount = 1; + vm.expectRevert(stdError.arithmeticError); + createDefaultStreamWithTranches(tranches); + } + + function test_RevertWhen_StartTimeGreaterThanFirstTrancheTimestamp() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + { + // Change the timestamp of the first tranche. + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].timestamp = defaults.START_TIME() - 1 seconds; + + // Expect the relevant error to be thrown. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp.selector, + defaults.START_TIME(), + tranches[0].timestamp + ) + ); + + // Create the stream. + createDefaultStreamWithTranches(tranches); + } + + function test_RevertWhen_StartTimeEqualToFirstTrancheTimestamp() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + { + // Change the timestamp of the first tranche. + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].timestamp = defaults.START_TIME(); + + // Expect the relevant error to be thrown. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp.selector, + defaults.START_TIME(), + tranches[0].timestamp + ) + ); + + // Create the stream. + createDefaultStreamWithTranches(tranches); + } + + function test_RevertWhen_TrancheTimestampsNotOrdered() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + { + // Swap the tranche timestamps. + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + (tranches[0].timestamp, tranches[1].timestamp) = (tranches[1].timestamp, tranches[0].timestamp); + + // Expect the relevant error to be thrown. + uint256 index = 1; + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_TrancheTimestampsNotOrdered.selector, + index, + tranches[0].timestamp, + tranches[1].timestamp + ) + ); + + // Create the stream. + createDefaultStreamWithTranches(tranches); + } + + function test_RevertGiven_EndTimeNotInTheFuture() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + { + uint40 endTime = defaults.END_TIME(); + vm.warp({ newTimestamp: endTime }); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_EndTimeNotInTheFuture.selector, endTime, endTime)); + createDefaultStream(); + } + + function test_RevertWhen_DepositAmountNotEqualToTrancheAmountsSum() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + { + UD60x18 brokerFee = ZERO; + resetPrank({ msgSender: users.sender }); + + // Adjust the default deposit amount. + uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); + uint128 depositAmount = defaultDepositAmount + 100; + + // Prepare the params. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: brokerFee }); + params.totalAmount = depositAmount; + + // Expect the relevant error to be thrown. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_DepositAmountNotEqualToTrancheAmountsSum.selector, + depositAmount, + defaultDepositAmount + ) + ); + + // Create the stream. + lockupTranched.createWithTimestamps(params); + } + + function test_RevertWhen_BrokerFeeTooHigh() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + { + UD60x18 brokerFee = MAX_BROKER_FEE + ud(1); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, brokerFee, MAX_BROKER_FEE) + ); + createDefaultStreamWithBroker(Broker({ account: users.broker, fee: brokerFee })); + } + + function test_RevertWhen_AssetNotContract() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + whenBrokerFeeNotTooHigh + { + address nonContract = address(8128); + + resetPrank({ msgSender: users.sender }); + + // Run the test. + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, nonContract)); + createDefaultStreamWithAsset(IERC20(nonContract)); + } + + function test_CreateWithTimestamps_AssetMissingReturnValue() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + whenBrokerFeeNotTooHigh + whenAssetContract + { + testCreateWithTimestamps(address(usdt)); + } + + function test_CreateWithTimestamps() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + whenBrokerFeeNotTooHigh + whenAssetContract + whenAssetERC20 + { + testCreateWithTimestamps(address(dai)); + } + + /// @dev Shared logic between {test_CreateWithTimestamps_AssetMissingReturnValue} and {test_CreateWithTimestamps}. + function testCreateWithTimestamps(address asset) internal { + // Make the Sender the stream's funder. + address funder = users.sender; + + // Expect the assets to be transferred from the funder to {SablierV2LockupTranched}. + expectCallToTransferFrom({ + asset: IERC20(asset), + from: funder, + to: address(lockupTranched), + value: defaults.DEPOSIT_AMOUNT() + }); + + // Expect the broker fee to be paid to the broker. + expectCallToTransferFrom({ + asset: IERC20(asset), + from: funder, + to: users.broker, + value: defaults.BROKER_FEE_AMOUNT() + }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: streamId }); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit CreateLockupTranchedStream({ + streamId: streamId, + funder: funder, + sender: users.sender, + recipient: users.recipient, + amounts: defaults.lockupCreateAmounts(), + tranches: defaults.tranches(), + asset: IERC20(asset), + cancelable: true, + transferable: true, + timestamps: defaults.lockupTranchedTimestamps(), + broker: users.broker + }); + + // Create the stream. + streamId = createDefaultStreamWithAsset(IERC20(asset)); + + // Assert that the stream has been created. + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(streamId); + LockupTranched.StreamLT memory expectedStream = defaults.lockupTranchedStream(); + expectedStream.asset = IERC20(asset); + assertEq(actualStream, expectedStream); + + // Assert that the stream's status is "PENDING". + Lockup.Status actualStatus = lockupTranched.statusOf(streamId); + Lockup.Status expectedStatus = Lockup.Status.PENDING; + assertEq(actualStatus, expectedStatus); + + // Assert that the next stream ID has been bumped. + uint256 actualNextStreamId = lockupTranched.nextStreamId(); + uint256 expectedNextStreamId = streamId + 1; + assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + address actualNFTOwner = lockupTranched.ownerOf({ tokenId: streamId }); + address expectedNFTOwner = users.recipient; + assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); + } +} diff --git a/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.tree b/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.tree new file mode 100644 index 000000000..3c4205768 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/create-with-timestamps/createWithTimestamps.tree @@ -0,0 +1,56 @@ +createWithTimestamps.t.sol +├── when delegate called +│ └── it should revert +└── when not delegate called + ├── when the recipient is the zero address + │ └── it should revert + └── when the recipient is not the zero address + ├── when the deposit amount is zero + │ └── it should revert + └── when the deposit amount is not zero + ├── when the start time is zero + │ └── it should revert + └── when the start time is not zero + ├── when the tranche count is zero + │ └── it should revert + └── when the tranche count is not zero + ├── when the tranche count is too high + │ └── it should revert + └── when the tranche count is not too high + ├── when the tranche amounts sum overflows + │ └── it should revert + └── when the tranche amounts sum does not overflow + ├── when the start time is greater than the first tranche timestamp + │ └── it should revert + ├── when the start time is equal to the first tranche timestamp + │ └── it should revert + └── when the start time is less than the first tranche timestamp + ├── when the tranche timestamps are not ordered + │ └── it should revert + └── when the tranche timestamps are ordered + ├── when the end time is not in the future + │ └── it should revert + └── when the end time is in the future + ├── when the deposit amount is not equal to the tranche amounts sum + │ └── it should revert + └── when the deposit amount is equal to the tranche amounts sum + ├── when the broker fee is too high + │ └── it should revert + └── when the broker fee is not too high + ├── when the asset is not a contract + │ └── it should revert + └── when the asset is a contract + ├── when the asset misses the ERC-20 return value + │ ├── it should create the stream + │ ├── it should bump the next stream ID + │ ├── it should mint the NFT + │ ├── it should emit a {MetadataUpdate} event + │ ├── it should perform the ERC-20 transfers + │ └── it should emit a {CreateLockupTranchedStream} event + └── when the asset does not miss the ERC-20 return value + ├── it should create the stream + ├── it should bump the next stream ID + ├── it should mint the NFT + ├── it should emit a {MetadataUpdate} event + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupTranchedStream} event diff --git a/test/integration/concrete/lockup-tranched/get-stream/getStream.t.sol b/test/integration/concrete/lockup-tranched/get-stream/getStream.t.sol new file mode 100644 index 000000000..3c9870943 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-stream/getStream.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { LockupTranched } from "src/types/DataTypes.sol"; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +contract GetStream_LockupTranched_Integration_Concrete_Test is LockupTranched_Integration_Concrete_Test { + uint256 internal defaultStreamId; + + function setUp() public virtual override { + LockupTranched_Integration_Concrete_Test.setUp(); + defaultStreamId = createDefaultStream(); + } + + function test_RevertGiven_Null() external { + uint256 nullStreamId = 1729; + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); + lockupTranched.getStream(nullStreamId); + } + + modifier givenNotNull() { + _; + } + + function test_GetStream_StatusSettled() external givenNotNull { + vm.warp({ newTimestamp: defaults.END_TIME() }); + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(defaultStreamId); + LockupTranched.StreamLT memory expectedStream = defaults.lockupTranchedStream(); + expectedStream.isCancelable = false; + assertEq(actualStream, expectedStream); + } + + modifier givenStatusNotSettled() { + _; + } + + function test_GetStream() external givenNotNull givenStatusNotSettled { + uint256 streamId = createDefaultStream(); + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(streamId); + LockupTranched.StreamLT memory expectedStream = defaults.lockupTranchedStream(); + assertEq(actualStream, expectedStream); + } +} diff --git a/test/integration/concrete/lockup-tranched/get-stream/getStream.tree b/test/integration/concrete/lockup-tranched/get-stream/getStream.tree new file mode 100644 index 000000000..bde21f7b4 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-stream/getStream.tree @@ -0,0 +1,8 @@ +getStream.t.sol +├── given the ID references a null stream +│ └── it should revert +└── given the ID does not reference a null stream + ├── given the stream is settled + │ └── it should adjust the `isCancelable` flag and return the stream + └── given the stream is not settled + └── it should return the stream diff --git a/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.t.sol b/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.t.sol new file mode 100644 index 000000000..3205fd3ed --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { LockupTranched } from "src/types/DataTypes.sol"; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +contract GetTimestamps_LockupTranched_Integration_Concrete_Test is LockupTranched_Integration_Concrete_Test { + function test_RevertGiven_Null() external { + uint256 nullStreamId = 1729; + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); + lockupTranched.getTimestamps(nullStreamId); + } + + modifier givenNotNull() { + _; + } + + function test_GetTimestamps() external givenNotNull { + uint256 streamId = createDefaultStream(); + LockupTranched.Timestamps memory actualTimestamps = lockupTranched.getTimestamps(streamId); + LockupTranched.Timestamps memory expectedTimestamps = defaults.lockupTranchedTimestamps(); + assertEq(actualTimestamps, expectedTimestamps); + } +} diff --git a/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.tree b/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.tree new file mode 100644 index 000000000..49e6bf2a0 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-timestamps/getTimestamps.tree @@ -0,0 +1,5 @@ +getTimestamps.t.sol +├── given the ID references a null stream +│ └── it should revert +└── given the ID does not reference a null stream + └── it should return the correct timestamps diff --git a/test/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol b/test/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol new file mode 100644 index 000000000..eaad2a69b --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-tranches/getTranches.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; +import { LockupTranched } from "src/types/DataTypes.sol"; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +contract GetTranches_LockupTranched_Integration_Concrete_Test is LockupTranched_Integration_Concrete_Test { + function test_RevertGiven_Null() external { + uint256 nullStreamId = 1729; + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); + lockupTranched.getTranches(nullStreamId); + } + + modifier givenNotNull() { + _; + } + + function test_GetTranches() external givenNotNull { + uint256 streamId = createDefaultStream(); + LockupTranched.Tranche[] memory actualTranches = lockupTranched.getTranches(streamId); + LockupTranched.Tranche[] memory expectedTranches = defaults.tranches(); + assertEq(actualTranches, expectedTranches, "tranches"); + } +} diff --git a/test/integration/concrete/lockup-tranched/get-tranches/getTranches.tree b/test/integration/concrete/lockup-tranched/get-tranches/getTranches.tree new file mode 100644 index 000000000..4a8884be1 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/get-tranches/getTranches.tree @@ -0,0 +1,5 @@ +getTranches.t.sol +├── given the ID references a null stream +│ └── it should revert +└── given the ID does not reference a null stream + └── it should return the correct tranches diff --git a/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol b/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol new file mode 100644 index 000000000..cd145d590 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; +import { StreamedAmountOf_Integration_Concrete_Test } from "../../lockup/streamed-amount-of/streamedAmountOf.t.sol"; + +contract StreamedAmountOf_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + StreamedAmountOf_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, StreamedAmountOf_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + StreamedAmountOf_Integration_Concrete_Test.setUp(); + } + + function test_StreamedAmountOf_StartTimeInTheFuture() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + { + vm.warp({ newTimestamp: 0 }); + uint128 actualStreamedAmount = lockupTranched.streamedAmountOf(defaultStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + function test_StreamedAmountOf_StartTimeInThePresent() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + { + vm.warp({ newTimestamp: defaults.START_TIME() }); + uint128 actualStreamedAmount = lockupTranched.streamedAmountOf(defaultStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + modifier givenMultipleTranches() { + _; + } + + function test_StreamedAmountOf_CurrentTimestamp1st() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + givenMultipleTranches + whenStartTimeInThePast + { + // Warp 1 second to the future. + vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); + + // Run the test. + uint128 actualStreamedAmount = lockupTranched.streamedAmountOf(defaultStreamId); + uint128 expectedStreamedAmount = 0; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + modifier givenCurrentTimestampNot1st() { + _; + } + + function test_StreamedAmountOf() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + whenStartTimeInThePast + givenMultipleTranches + givenCurrentTimestampNot1st + { + vm.warp({ newTimestamp: defaults.END_TIME() - 1 seconds }); + + // Run the test. + uint128 actualStreamedAmount = lockupTranched.streamedAmountOf(defaultStreamId); + uint128 expectedStreamedAmount = defaults.tranches()[0].amount + defaults.tranches()[1].amount; + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } +} diff --git a/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree b/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree new file mode 100644 index 000000000..7c8644b98 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/streamed-amount-of/streamedAmountOf.tree @@ -0,0 +1,11 @@ +streamedAmountOf.t.sol +└── given the stream's status is "STREAMING" + ├── given the start time is in the future + │ └── it should return zero + ├── given the start time is in the present + │ └── it should return zero + └── given the start time is in the past + ├── given the current timestamp is the 1st in the array + │ └── it should return the correct streamed amount + └── given the current timestamp is not the 1st in the array + └── it should return the correct streamed amount diff --git a/test/integration/concrete/lockup-tranched/token-uri/tokenURI.t.sol b/test/integration/concrete/lockup-tranched/token-uri/tokenURI.t.sol new file mode 100644 index 000000000..04b8aeb06 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/token-uri/tokenURI.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +// solhint-disable max-line-length,no-console,quotes +pragma solidity >=0.8.22 <0.9.0; + +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import { console2 } from "forge-std/src/console2.sol"; +import { StdStyle } from "forge-std/src/StdStyle.sol"; +import { Base64 } from "solady/src/utils/Base64.sol"; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; + +/// @dev Requirements for these tests to work: +/// - The stream ID must be 1 +/// - The stream's sender must be `0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca`, i.e. `makeAddr("Sender")` +/// - The stream asset must have the DAI symbol +/// - The contract deployer, i.e. the `sender` config option in `foundry.toml`, must have the default value +/// 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38 so that the deployed contracts have the same addresses as +/// the values hard coded in the tests below +contract TokenURI_LockupTranched_Integration_Concrete_Test is LockupTranched_Integration_Concrete_Test { + address internal constant LOCKUP_TRANCHED = 0xDB25A7b768311dE128BBDa7B8426c3f9C74f3240; + uint256 internal defaultStreamId; + + /// @dev To make these tests noninvasive, they are run only when the contract address matches the hard coded value. + modifier skipOnMismatch() { + if (address(lockupTranched) == LOCKUP_TRANCHED) { + _; + } else { + console2.log(StdStyle.yellow('Warning: "lockupTranched.tokenURI" tests skipped due to address mismatch')); + } + } + + function test_RevertGiven_NFTDoesNotExist() external { + uint256 nullStreamId = 1729; + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); + lockupTranched.tokenURI({ tokenId: nullStreamId }); + } + + modifier givenNFTExists() { + defaultStreamId = createDefaultStream(); + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.TOTAL_DURATION() / 4 }); + _; + } + + /// @dev If you need to update the hard-coded token URI: + /// 1. Use "vm.writeFile" to log the strings to a file. + /// 2. Remember to escape the EOL character \n with \\n. + function test_TokenURI_Decoded() external skipOnMismatch givenNFTExists { + string memory tokenURI = lockupTranched.tokenURI(defaultStreamId); + tokenURI = vm.replace({ input: tokenURI, from: "data:application/json;base64,", to: "" }); + string memory actualDecodedTokenURI = string(Base64.decode(tokenURI)); + string memory expectedDecodedTokenURI = + unicode'{"attributes":[{"trait_type":"Asset","value":"DAI"},{"trait_type":"Sender","value":"0x6332e7b1deb1f1a0b77b2bb18b144330c7291bca"},{"trait_type":"Status","value":"Streaming"}],"description":"This NFT represents a payment stream in a Sablier V2 Lockup Tranched contract. The owner of this NFT can withdraw the streamed assets, which are denominated in DAI.\\n\\n- Stream ID: 1\\n- Lockup Tranched Address: 0xdb25a7b768311de128bbda7b8426c3f9c74f3240\\n- DAI Address: 0x03a6a84cd762d9707a21605b548aaab891562aab\\n\\n⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient.","external_url":"https://sablier.com","name":"Sablier V2 Lockup Tranched #1","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAwIiBoZWlnaHQ9IjEwMDAiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCNOb2lzZSkiLz48cmVjdCB4PSI3MCIgeT0iNzAiIHdpZHRoPSI4NjAiIGhlaWdodD0iODYwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjQ1IiByeT0iNDUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48ZGVmcz48Y2lyY2xlIGlkPSJHbG93IiByPSI1MDAiIGZpbGw9InVybCgjUmFkaWFsR2xvdykiLz48ZmlsdGVyIGlkPSJOb2lzZSI+PGZlRmxvb2QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmxvb2QtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiIGZsb29kLW9wYWNpdHk9IjEiIHJlc3VsdD0iZmxvb2RGaWxsIi8+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuNCIgbnVtT2N0YXZlcz0iMyIgcmVzdWx0PSJOb2lzZSIgdHlwZT0iZnJhY3RhbE5vaXNlIi8+PGZlQmxlbmQgaW49Ik5vaXNlIiBpbjI9ImZsb29kRmlsbCIgbW9kZT0ic29mdC1saWdodCIvPjwvZmlsdGVyPjxwYXRoIGlkPSJMb2dvIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4xIiBkPSJtMTMzLjU1OSwxMjQuMDM0Yy0uMDEzLDIuNDEyLTEuMDU5LDQuODQ4LTIuOTIzLDYuNDAyLTIuNTU4LDEuODE5LTUuMTY4LDMuNDM5LTcuODg4LDQuOTk2LTE0LjQ0LDguMjYyLTMxLjA0NywxMi41NjUtNDcuNjc0LDEyLjU2OS04Ljg1OC4wMzYtMTcuODM4LTEuMjcyLTI2LjMyOC0zLjY2My05LjgwNi0yLjc2Ni0xOS4wODctNy4xMTMtMjcuNTYyLTEyLjc3OC0xMy44NDItOC4wMjUsOS40NjgtMjguNjA2LDE2LjE1My0zNS4yNjVoMGMyLjAzNS0xLjgzOCw0LjI1Mi0zLjU0Niw2LjQ2My01LjIyNGgwYzYuNDI5LTUuNjU1LDE2LjIxOC0yLjgzNSwyMC4zNTgsNC4xNyw0LjE0Myw1LjA1Nyw4LjgxNiw5LjY0OSwxMy45MiwxMy43MzRoLjAzN2M1LjczNiw2LjQ2MSwxNS4zNTctMi4yNTMsOS4zOC04LjQ4LDAsMC0zLjUxNS0zLjUxNS0zLjUxNS0zLjUxNS0xMS40OS0xMS40NzgtNTIuNjU2LTUyLjY2NC02NC44MzctNjQuODM3bC4wNDktLjAzN2MtMS43MjUtMS42MDYtMi43MTktMy44NDctMi43NTEtNi4yMDRoMGMtLjA0Ni0yLjM3NSwxLjA2Mi00LjU4MiwyLjcyNi02LjIyOWgwbC4xODUtLjE0OGgwYy4wOTktLjA2MiwuMjIyLS4xNDgsLjM3LS4yNTloMGMyLjA2LTEuMzYyLDMuOTUxLTIuNjIxLDYuMDQ0LTMuODQyQzU3Ljc2My0zLjQ3Myw5Ny43Ni0yLjM0MSwxMjguNjM3LDE4LjMzMmMxNi42NzEsOS45NDYtMjYuMzQ0LDU0LjgxMy0zOC42NTEsNDAuMTk5LTYuMjk5LTYuMDk2LTE4LjA2My0xNy43NDMtMTkuNjY4LTE4LjgxMS02LjAxNi00LjA0Ny0xMy4wNjEsNC43NzYtNy43NTIsOS43NTFsNjguMjU0LDY4LjM3MWMxLjcyNCwxLjYwMSwyLjcxNCwzLjg0LDIuNzM4LDYuMTkyWiIvPjxwYXRoIGlkPSJGbG9hdGluZ1RleHQiIGZpbGw9Im5vbmUiIGQ9Ik0xMjUgNDVoNzUwczgwIDAgODAgODB2NzUwczAgODAgLTgwIDgwaC03NTBzLTgwIDAgLTgwIC04MHYtNzUwczAgLTgwIDgwIC04MCIvPjxyYWRpYWxHcmFkaWVudCBpZD0iUmFkaWFsR2xvdyI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iaHNsKDYxLDg4JSw0MCUpIiBzdG9wLW9wYWNpdHk9Ii42Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iU2FuZFRvcCIgeDE9IjAlIiB5MT0iMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9ImhzbCg2MSw4OCUsNDAlKSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iaHNsKDIzMCwyMSUsMTElKSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJTYW5kQm90dG9tIiB4MT0iMTAwJSIgeTE9IjEwMCUiPjxzdG9wIG9mZnNldD0iMTAlIiBzdG9wLWNvbG9yPSJoc2woMjMwLDIxJSwxMSUpIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSJoc2woNjEsODglLDQwJSkiLz48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ4MSIgZHVyPSI2cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzAlOzYwJTsxMjAlOzYwJTszMCU7Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkhvdXJnbGFzc1N0cm9rZSIgZ3JhZGllbnRUcmFuc2Zvcm09InJvdGF0ZSg5MCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iaHNsKDYxLDg4JSw0MCUpIi8+PHN0b3Agb2Zmc2V0PSI4MCUiIHN0b3AtY29sb3I9ImhzbCgyMzAsMjElLDExJSkiLz48L2xpbmVhckdyYWRpZW50PjxnIGlkPSJIb3VyZ2xhc3MiPjxwYXRoIGQ9Ik0gNTAsMzYwIGEgMzAwLDMwMCAwIDEsMSA2MDAsMCBhIDMwMCwzMDAgMCAxLDEgLTYwMCwwIiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9Ii4wMiIgc3Ryb2tlPSJ1cmwoI0hvdXJnbGFzc1N0cm9rZSkiIHN0cm9rZS13aWR0aD0iNCIvPjxwYXRoIGQ9Im01NjYsMTYxLjIwMXYtNTMuOTI0YzAtMTkuMzgyLTIyLjUxMy0zNy41NjMtNjMuMzk4LTUxLjE5OC00MC43NTYtMTMuNTkyLTk0Ljk0Ni0yMS4wNzktMTUyLjU4Ny0yMS4wNzlzLTExMS44MzgsNy40ODctMTUyLjYwMiwyMS4wNzljLTQwLjg5MywxMy42MzYtNjMuNDEzLDMxLjgxNi02My40MTMsNTEuMTk4djUzLjkyNGMwLDE3LjE4MSwxNy43MDQsMzMuNDI3LDUwLjIyMyw0Ni4zOTR2Mjg0LjgwOWMtMzIuNTE5LDEyLjk2LTUwLjIyMywyOS4yMDYtNTAuMjIzLDQ2LjM5NHY1My45MjRjMCwxOS4zODIsMjIuNTIsMzcuNTYzLDYzLjQxMyw1MS4xOTgsNDAuNzYzLDEzLjU5Miw5NC45NTQsMjEuMDc5LDE1Mi42MDIsMjEuMDc5czExMS44MzEtNy40ODcsMTUyLjU4Ny0yMS4wNzljNDAuODg2LTEzLjYzNiw2My4zOTgtMzEuODE2LDYzLjM5OC01MS4xOTh2LTUzLjkyNGMwLTE3LjE5Ni0xNy43MDQtMzMuNDM1LTUwLjIyMy00Ni40MDFWMjA3LjYwM2MzMi41MTktMTIuOTY3LDUwLjIyMy0yOS4yMDYsNTAuMjIzLTQ2LjQwMVptLTM0Ny40NjIsNTcuNzkzbDEzMC45NTksMTMxLjAyNy0xMzAuOTU5LDEzMS4wMTNWMjE4Ljk5NFptMjYyLjkyNC4wMjJ2MjYyLjAxOGwtMTMwLjkzNy0xMzEuMDA2LDEzMC45MzctMTMxLjAxM1oiIGZpbGw9IiMxNjE4MjIiPjwvcGF0aD48cG9seWdvbiBwb2ludHM9IjM1MCAzNTAuMDI2IDQxNS4wMyAyODQuOTc4IDI4NSAyODQuOTc4IDM1MCAzNTAuMDI2IiBmaWxsPSJ1cmwoI1NhbmRCb3R0b20pIi8+PHBhdGggZD0ibTQxNi4zNDEsMjgxLjk3NWMwLC45MTQtLjM1NCwxLjgwOS0xLjAzNSwyLjY4LTUuNTQyLDcuMDc2LTMyLjY2MSwxMi40NS02NS4yOCwxMi40NS0zMi42MjQsMC01OS43MzgtNS4zNzQtNjUuMjgtMTIuNDUtLjY4MS0uODcyLTEuMDM1LTEuNzY3LTEuMDM1LTIuNjgsMC0uOTE0LjM1NC0xLjgwOCwxLjAzNS0yLjY3Niw1LjU0Mi03LjA3NiwzMi42NTYtMTIuNDUsNjUuMjgtMTIuNDUsMzIuNjE5LDAsNTkuNzM4LDUuMzc0LDY1LjI4LDEyLjQ1LjY4MS44NjcsMS4wMzUsMS43NjIsMS4wMzUsMi42NzZaIiBmaWxsPSJ1cmwoI1NhbmRUb3ApIi8+PHBhdGggZD0ibTQ4MS40Niw1MDQuMTAxdjU4LjQ0OWMtMi4zNS43Ny00LjgyLDEuNTEtNy4zOSwyLjIzLTMwLjMsOC41NC03NC42NSwxMy45Mi0xMjQuMDYsMTMuOTItNTMuNiwwLTEwMS4yNC02LjMzLTEzMS40Ny0xNi4xNnYtNTguNDM5aDI2Mi45MloiIGZpbGw9InVybCgjU2FuZEJvdHRvbSkiLz48ZWxsaXBzZSBjeD0iMzUwIiBjeT0iNTA0LjEwMSIgcng9IjEzMS40NjIiIHJ5PSIyOC4xMDgiIGZpbGw9InVybCgjU2FuZFRvcCkiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9InVybCgjSG91cmdsYXNzU3Ryb2tlKSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iNCI+PHBhdGggZD0ibTU2NS42NDEsMTA3LjI4YzAsOS41MzctNS41NiwxOC42MjktMTUuNjc2LDI2Ljk3M2gtLjAyM2MtOS4yMDQsNy41OTYtMjIuMTk0LDE0LjU2Mi0zOC4xOTcsMjAuNTkyLTM5LjUwNCwxNC45MzYtOTcuMzI1LDI0LjM1NS0xNjEuNzMzLDI0LjM1NS05MC40OCwwLTE2Ny45NDgtMTguNTgyLTE5OS45NTMtNDQuOTQ4aC0uMDIzYy0xMC4xMTUtOC4zNDQtMTUuNjc2LTE3LjQzNy0xNS42NzYtMjYuOTczLDAtMzkuNzM1LDk2LjU1NC03MS45MjEsMjE1LjY1Mi03MS45MjFzMjE1LjYyOSwzMi4xODUsMjE1LjYyOSw3MS45MjFaIi8+PHBhdGggZD0ibTEzNC4zNiwxNjEuMjAzYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iMTYxLjIwMyIgeDI9IjEzNC4zNiIgeTI9IjEwNy4yOCIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSIxNjEuMjAzIiB4Mj0iNTY1LjY0IiB5Mj0iMTA3LjI4Ii8+PGxpbmUgeDE9IjE4NC41ODQiIHkxPSIyMDYuODIzIiB4Mj0iMTg0LjU4NSIgeTI9IjUzNy41NzkiLz48bGluZSB4MT0iMjE4LjE4MSIgeTE9IjIxOC4xMTgiIHgyPSIyMTguMTgxIiB5Mj0iNTYyLjUzNyIvPjxsaW5lIHgxPSI0ODEuODE4IiB5MT0iMjE4LjE0MiIgeDI9IjQ4MS44MTkiIHkyPSI1NjIuNDI4Ii8+PGxpbmUgeDE9IjUxNS40MTUiIHkxPSIyMDcuMzUyIiB4Mj0iNTE1LjQxNiIgeTI9IjUzNy41NzkiLz48cGF0aCBkPSJtMTg0LjU4LDUzNy41OGMwLDUuNDUsNC4yNywxMC42NSwxMi4wMywxNS40MmguMDJjNS41MSwzLjM5LDEyLjc5LDYuNTUsMjEuNTUsOS40MiwzMC4yMSw5LjksNzguMDIsMTYuMjgsMTMxLjgzLDE2LjI4LDQ5LjQxLDAsOTMuNzYtNS4zOCwxMjQuMDYtMTMuOTIsMi43LS43Niw1LjI5LTEuNTQsNy43NS0yLjM1LDguNzctMi44NywxNi4wNS02LjA0LDIxLjU2LTkuNDNoMGM3Ljc2LTQuNzcsMTIuMDQtOS45NywxMi4wNC0xNS40MiIvPjxwYXRoIGQ9Im0xODQuNTgyLDQ5Mi42NTZjLTMxLjM1NCwxMi40ODUtNTAuMjIzLDI4LjU4LTUwLjIyMyw0Ni4xNDIsMCw5LjUzNiw1LjU2NCwxOC42MjcsMTUuNjc3LDI2Ljk2OWguMDIyYzguNTAzLDcuMDA1LDIwLjIxMywxMy40NjMsMzQuNTI0LDE5LjE1OSw5Ljk5OSwzLjk5MSwyMS4yNjksNy42MDksMzMuNTk3LDEwLjc4OCwzNi40NSw5LjQwNyw4Mi4xODEsMTUuMDAyLDEzMS44MzUsMTUuMDAyczk1LjM2My01LjU5NSwxMzEuODA3LTE1LjAwMmMxMC44NDctMi43OSwyMC44NjctNS45MjYsMjkuOTI0LTkuMzQ5LDEuMjQ0LS40NjcsMi40NzMtLjk0MiwzLjY3My0xLjQyNCwxNC4zMjYtNS42OTYsMjYuMDM1LTEyLjE2MSwzNC41MjQtMTkuMTczaC4wMjJjMTAuMTE0LTguMzQyLDE1LjY3Ny0xNy40MzMsMTUuNjc3LTI2Ljk2OSwwLTE3LjU2Mi0xOC44NjktMzMuNjY1LTUwLjIyMy00Ni4xNSIvPjxwYXRoIGQ9Im0xMzQuMzYsNTkyLjcyYzAsMzkuNzM1LDk2LjU1NCw3MS45MjEsMjE1LjY1Miw3MS45MjFzMjE1LjYyOS0zMi4xODYsMjE1LjYyOS03MS45MjEiLz48bGluZSB4MT0iMTM0LjM2IiB5MT0iNTkyLjcyIiB4Mj0iMTM0LjM2IiB5Mj0iNTM4Ljc5NyIvPjxsaW5lIHgxPSI1NjUuNjQiIHkxPSI1OTIuNzIiIHgyPSI1NjUuNjQiIHkyPSI1MzguNzk3Ii8+PHBvbHlsaW5lIHBvaW50cz0iNDgxLjgyMiA0ODEuOTAxIDQ4MS43OTggNDgxLjg3NyA0ODEuNzc1IDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDIxOC4xODUgMjE4LjEyOSIvPjxwb2x5bGluZSBwb2ludHM9IjIxOC4xODUgNDgxLjkwMSAyMTguMjMxIDQ4MS44NTQgMzUwLjAxNSAzNTAuMDI2IDQ4MS44MjIgMjE4LjE1MiIvPjwvZz48L2c+PGcgaWQ9IlByb2dyZXNzIiBmaWxsPSIjZmZmIj48cmVjdCB3aWR0aD0iMjA4IiBoZWlnaHQ9IjEwMCIgZmlsbC1vcGFjaXR5PSIuMDMiIHJ4PSIxNSIgcnk9IjE1IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIuMSIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHRleHQgeD0iMjAiIHk9IjM0IiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjJweCI+UHJvZ3Jlc3M8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+MjUlPC90ZXh0PjxnIGZpbGw9Im5vbmUiPjxjaXJjbGUgY3g9IjE2NiIgY3k9IjUwIiByPSIyMiIgc3Ryb2tlPSJoc2woMjMwLDIxJSwxMSUpIiBzdHJva2Utd2lkdGg9IjEwIi8+PGNpcmNsZSBjeD0iMTY2IiBjeT0iNTAiIHBhdGhMZW5ndGg9IjEwMDAwIiByPSIyMiIgc3Ryb2tlPSJoc2woNjEsODglLDQwJSkiIHN0cm9rZS1kYXNoYXJyYXk9IjEwMDAwIiBzdHJva2UtZGFzaG9mZnNldD0iNzUwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjUiIHRyYW5zZm9ybT0icm90YXRlKC05MCkiIHRyYW5zZm9ybS1vcmlnaW49IjE2NiA1MCIvPjwvZz48L2c+PGcgaWQ9IlN0YXR1cyIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE4NCIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPlN0YXR1czwvdGV4dD48dGV4dCB4PSIyMCIgeT0iNzIiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyNnB4Ij5TdHJlYW1pbmc8L3RleHQ+PC9nPjxnIGlkPSJBbW91bnQiIGZpbGw9IiNmZmYiPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iMTAwIiBmaWxsLW9wYWNpdHk9Ii4wMyIgcng9IjE1IiByeT0iMTUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9IjQiLz48dGV4dCB4PSIyMCIgeT0iMzQiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLEFyaWFsLG1vbm9zcGFjZSIgZm9udC1zaXplPSIyMnB4Ij5BbW91bnQ8L3RleHQ+PHRleHQgeD0iMjAiIHk9IjcyIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjZweCI+JiM4ODA1OyAxMEs8L3RleHQ+PC9nPjxnIGlkPSJEdXJhdGlvbiIgZmlsbD0iI2ZmZiI+PHJlY3Qgd2lkdGg9IjE1MiIgaGVpZ2h0PSIxMDAiIGZpbGwtb3BhY2l0eT0iLjAzIiByeD0iMTUiIHJ5PSIxNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZS13aWR0aD0iNCIvPjx0ZXh0IHg9IjIwIiB5PSIzNCIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjIycHgiPkR1cmF0aW9uPC90ZXh0Pjx0ZXh0IHg9IjIwIiB5PSI3MiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmb250LXNpemU9IjI2cHgiPiZsdDsgMSBEYXk8L3RleHQ+PC9nPjwvZGVmcz48dGV4dCB0ZXh0LXJlbmRlcmluZz0ib3B0aW1pemVTcGVlZCI+PHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSItMTAwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHhkYjI1YTdiNzY4MzExZGUxMjhiYmRhN2I4NDI2YzNmOWM3NGYzMjQwIOKAoiBTYWJsaWVyIFYyIExvY2t1cCBEeW5hbWljPC90ZXh0UGF0aD48dGV4dFBhdGggc3RhcnRPZmZzZXQ9IjAlIiBocmVmPSIjRmxvYXRpbmdUZXh0IiBmaWxsPSIjZmZmIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZpbGwtb3BhY2l0eT0iLjgiIGZvbnQtc2l6ZT0iMjZweCI+PGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGJlZ2luPSIwcyIgZHVyPSI1MHMiIGZyb209IjAlIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdG89IjEwMCUiLz4weGRiMjVhN2I3NjgzMTFkZTEyOGJiZGE3Yjg0MjZjM2Y5Yzc0ZjMyNDAg4oCiIFNhYmxpZXIgVjIgTG9ja3VwIER5bmFtaWM8L3RleHRQYXRoPjx0ZXh0UGF0aCBzdGFydE9mZnNldD0iLTUwJSIgaHJlZj0iI0Zsb2F0aW5nVGV4dCIgZmlsbD0iI2ZmZiIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsQXJpYWwsbW9ub3NwYWNlIiBmaWxsLW9wYWNpdHk9Ii44IiBmb250LXNpemU9IjI2cHgiPjxhbmltYXRlIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InN0YXJ0T2Zmc2V0IiBiZWdpbj0iMHMiIGR1cj0iNTBzIiBmcm9tPSIwJSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIxMDAlIi8+MHgwM2E2YTg0Y2Q3NjJkOTcwN2EyMTYwNWI1NDhhYWFiODkxNTYyYWFiIOKAoiBEQUk8L3RleHRQYXRoPjx0ZXh0UGF0aCBzdGFydE9mZnNldD0iNTAlIiBocmVmPSIjRmxvYXRpbmdUZXh0IiBmaWxsPSIjZmZmIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JyxBcmlhbCxtb25vc3BhY2UiIGZpbGwtb3BhY2l0eT0iLjgiIGZvbnQtc2l6ZT0iMjZweCI+PGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGJlZ2luPSIwcyIgZHVyPSI1MHMiIGZyb209IjAlIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgdG89IjEwMCUiLz4weDAzYTZhODRjZDc2MmQ5NzA3YTIxNjA1YjU0OGFhYWI4OTE1NjJhYWIg4oCiIERBSTwvdGV4dFBhdGg+PC90ZXh0Pjx1c2UgaHJlZj0iI0dsb3ciIGZpbGwtb3BhY2l0eT0iLjkiLz48dXNlIGhyZWY9IiNHbG93IiB4PSIxMDAwIiB5PSIxMDAwIiBmaWxsLW9wYWNpdHk9Ii45Ii8+PHVzZSBocmVmPSIjTG9nbyIgeD0iMTcwIiB5PSIxNzAiIHRyYW5zZm9ybT0ic2NhbGUoLjYpIi8+PHVzZSBocmVmPSIjSG91cmdsYXNzIiB4PSIxNTAiIHk9IjkwIiB0cmFuc2Zvcm09InJvdGF0ZSgxMCkiIHRyYW5zZm9ybS1vcmlnaW49IjUwMCA1MDAiLz48dXNlIGhyZWY9IiNQcm9ncmVzcyIgeD0iMTQ0IiB5PSI3OTAiLz48dXNlIGhyZWY9IiNTdGF0dXMiIHg9IjM2OCIgeT0iNzkwIi8+PHVzZSBocmVmPSIjQW1vdW50IiB4PSI1NjgiIHk9Ijc5MCIvPjx1c2UgaHJlZj0iI0R1cmF0aW9uIiB4PSI3MDQiIHk9Ijc5MCIvPjwvc3ZnPg=="}'; + assertEq(actualDecodedTokenURI, expectedDecodedTokenURI, "decoded token URI"); + } + + function test_TokenURI_Full() external skipOnMismatch givenNFTExists { + string memory actualTokenURI = lockupTranched.tokenURI(defaultStreamId); + string memory expectedTokenURI = + "data:application/json;base64,eyJhdHRyaWJ1dGVzIjpbeyJ0cmFpdF90eXBlIjoiQXNzZXQiLCJ2YWx1ZSI6IkRBSSJ9LHsidHJhaXRfdHlwZSI6IlNlbmRlciIsInZhbHVlIjoiMHg2MzMyZTdiMWRlYjFmMWEwYjc3YjJiYjE4YjE0NDMzMGM3MjkxYmNhIn0seyJ0cmFpdF90eXBlIjoiU3RhdHVzIiwidmFsdWUiOiJTdHJlYW1pbmcifV0sImRlc2NyaXB0aW9uIjoiVGhpcyBORlQgcmVwcmVzZW50cyBhIHBheW1lbnQgc3RyZWFtIGluIGEgU2FibGllciBWMiBMb2NrdXAgRHluYW1pYyBjb250cmFjdC4gVGhlIG93bmVyIG9mIHRoaXMgTkZUIGNhbiB3aXRoZHJhdyB0aGUgc3RyZWFtZWQgYXNzZXRzLCB3aGljaCBhcmUgZGVub21pbmF0ZWQgaW4gREFJLlxuXG4tIFN0cmVhbSBJRDogMVxuLSBMb2NrdXAgRHluYW1pYyBBZGRyZXNzOiAweGRiMjVhN2I3NjgzMTFkZTEyOGJiZGE3Yjg0MjZjM2Y5Yzc0ZjMyNDBcbi0gREFJIEFkZHJlc3M6IDB4MDNhNmE4NGNkNzYyZDk3MDdhMjE2MDViNTQ4YWFhYjg5MTU2MmFhYlxuXG7imqDvuI8gV0FSTklORzogVHJhbnNmZXJyaW5nIHRoZSBORlQgbWFrZXMgdGhlIG5ldyBvd25lciB0aGUgcmVjaXBpZW50IG9mIHRoZSBzdHJlYW0uIFRoZSBmdW5kcyBhcmUgbm90IGF1dG9tYXRpY2FsbHkgd2l0aGRyYXduIGZvciB0aGUgcHJldmlvdXMgcmVjaXBpZW50LiIsImV4dGVybmFsX3VybCI6Imh0dHBzOi8vc2FibGllci5jb20iLCJuYW1lIjoiU2FibGllciBWMiBMb2NrdXAgRHluYW1pYyAjMSIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0l4TURBd0lpQm9aV2xuYUhROUlqRXdNREFpSUhacFpYZENiM2c5SWpBZ01DQXhNREF3SURFd01EQWlQanh5WldOMElIZHBaSFJvUFNJeE1EQWxJaUJvWldsbmFIUTlJakV3TUNVaUlHWnBiSFJsY2owaWRYSnNLQ05PYjJselpTa2lMejQ4Y21WamRDQjRQU0kzTUNJZ2VUMGlOekFpSUhkcFpIUm9QU0k0TmpBaUlHaGxhV2RvZEQwaU9EWXdJaUJtYVd4c1BTSWpabVptSWlCbWFXeHNMVzl3WVdOcGRIazlJaTR3TXlJZ2NuZzlJalExSWlCeWVUMGlORFVpSUhOMGNtOXJaVDBpSTJabVppSWdjM1J5YjJ0bExXOXdZV05wZEhrOUlpNHhJaUJ6ZEhKdmEyVXRkMmxrZEdnOUlqUWlMejQ4WkdWbWN6NDhZMmx5WTJ4bElHbGtQU0pIYkc5M0lpQnlQU0kxTURBaUlHWnBiR3c5SW5WeWJDZ2pVbUZrYVdGc1IyeHZkeWtpTHo0OFptbHNkR1Z5SUdsa1BTSk9iMmx6WlNJK1BHWmxSbXh2YjJRZ2VEMGlNQ0lnZVQwaU1DSWdkMmxrZEdnOUlqRXdNQ1VpSUdobGFXZG9kRDBpTVRBd0pTSWdabXh2YjJRdFkyOXNiM0k5SW1oemJDZ3lNekFzTWpFbExERXhKU2tpSUdac2IyOWtMVzl3WVdOcGRIazlJakVpSUhKbGMzVnNkRDBpWm14dmIyUkdhV3hzSWk4K1BHWmxWSFZ5WW5Wc1pXNWpaU0JpWVhObFJuSmxjWFZsYm1ONVBTSXVOQ0lnYm5WdFQyTjBZWFpsY3owaU15SWdjbVZ6ZFd4MFBTSk9iMmx6WlNJZ2RIbHdaVDBpWm5KaFkzUmhiRTV2YVhObElpOCtQR1psUW14bGJtUWdhVzQ5SWs1dmFYTmxJaUJwYmpJOUltWnNiMjlrUm1sc2JDSWdiVzlrWlQwaWMyOW1kQzFzYVdkb2RDSXZQand2Wm1sc2RHVnlQanh3WVhSb0lHbGtQU0pNYjJkdklpQm1hV3hzUFNJalptWm1JaUJtYVd4c0xXOXdZV05wZEhrOUlpNHhJaUJrUFNKdE1UTXpMalUxT1N3eE1qUXVNRE0wWXkwdU1ERXpMREl1TkRFeUxURXVNRFU1TERRdU9EUTRMVEl1T1RJekxEWXVOREF5TFRJdU5UVTRMREV1T0RFNUxUVXVNVFk0TERNdU5ETTVMVGN1T0RnNExEUXVPVGsyTFRFMExqUTBMRGd1TWpZeUxUTXhMakEwTnl3eE1pNDFOalV0TkRjdU5qYzBMREV5TGpVMk9TMDRMamcxT0M0d016WXRNVGN1T0RNNExURXVNamN5TFRJMkxqTXlPQzB6TGpZMk15MDVMamd3TmkweUxqYzJOaTB4T1M0d09EY3ROeTR4TVRNdE1qY3VOVFl5TFRFeUxqYzNPQzB4TXk0NE5ESXRPQzR3TWpVc09TNDBOamd0TWpndU5qQTJMREUyTGpFMU15MHpOUzR5TmpWb01HTXlMakF6TlMweExqZ3pPQ3cwTGpJMU1pMHpMalUwTml3MkxqUTJNeTAxTGpJeU5HZ3dZell1TkRJNUxUVXVOalUxTERFMkxqSXhPQzB5TGpnek5Td3lNQzR6TlRnc05DNHhOeXcwTGpFME15dzFMakExTnl3NExqZ3hOaXc1TGpZME9Td3hNeTQ1TWl3eE15NDNNelJvTGpBek4yTTFMamN6Tml3MkxqUTJNU3d4TlM0ek5UY3RNaTR5TlRNc09TNHpPQzA0TGpRNExEQXNNQzB6TGpVeE5TMHpMalV4TlMwekxqVXhOUzB6TGpVeE5TMHhNUzQwT1MweE1TNDBOemd0TlRJdU5qVTJMVFV5TGpZMk5DMDJOQzQ0TXpjdE5qUXVPRE0zYkM0d05Ea3RMakF6TjJNdE1TNDNNalV0TVM0Mk1EWXRNaTQzTVRrdE15NDRORGN0TWk0M05URXROaTR5TURSb01HTXRMakEwTmkweUxqTTNOU3d4TGpBMk1pMDBMalU0TWl3eUxqY3lOaTAyTGpJeU9XZ3diQzR4T0RVdExqRTBPR2d3WXk0d09Ua3RMakEyTWl3dU1qSXlMUzR4TkRnc0xqTTNMUzR5TlRsb01HTXlMakEyTFRFdU16WXlMRE11T1RVeExUSXVOakl4TERZdU1EUTBMVE11T0RReVF6VTNMamMyTXkwekxqUTNNeXc1Tnk0M05pMHlMak0wTVN3eE1qZ3VOak0zTERFNExqTXpNbU14Tmk0Mk56RXNPUzQ1TkRZdE1qWXVNelEwTERVMExqZ3hNeTB6T0M0Mk5URXNOREF1TVRrNUxUWXVNams1TFRZdU1EazJMVEU0TGpBMk15MHhOeTQzTkRNdE1Ua3VOalk0TFRFNExqZ3hNUzAyTGpBeE5pMDBMakEwTnkweE15NHdOakVzTkM0M056WXROeTQzTlRJc09TNDNOVEZzTmpndU1qVTBMRFk0TGpNM01XTXhMamN5TkN3eExqWXdNU3d5TGpjeE5Dd3pMamcwTERJdU56TTRMRFl1TVRreVdpSXZQanh3WVhSb0lHbGtQU0pHYkc5aGRHbHVaMVJsZUhRaUlHWnBiR3c5SW01dmJtVWlJR1E5SWsweE1qVWdORFZvTnpVd2N6Z3dJREFnT0RBZ09EQjJOelV3Y3pBZ09EQWdMVGd3SURnd2FDMDNOVEJ6TFRnd0lEQWdMVGd3SUMwNE1IWXROelV3Y3pBZ0xUZ3dJRGd3SUMwNE1DSXZQanh5WVdScFlXeEhjbUZrYVdWdWRDQnBaRDBpVW1Ga2FXRnNSMnh2ZHlJK1BITjBiM0FnYjJabWMyVjBQU0l3SlNJZ2MzUnZjQzFqYjJ4dmNqMGlhSE5zS0RZeExEZzRKU3cwTUNVcElpQnpkRzl3TFc5d1lXTnBkSGs5SWk0MklpOCtQSE4wYjNBZ2IyWm1jMlYwUFNJeE1EQWxJaUJ6ZEc5d0xXTnZiRzl5UFNKb2Myd29Nak13TERJeEpTd3hNU1VwSWlCemRHOXdMVzl3WVdOcGRIazlJakFpTHo0OEwzSmhaR2xoYkVkeVlXUnBaVzUwUGp4c2FXNWxZWEpIY21Ga2FXVnVkQ0JwWkQwaVUyRnVaRlJ2Y0NJZ2VERTlJakFsSWlCNU1UMGlNQ1VpUGp4emRHOXdJRzltWm5ObGREMGlNQ1VpSUhOMGIzQXRZMjlzYjNJOUltaHpiQ2cyTVN3NE9DVXNOREFsS1NJdlBqeHpkRzl3SUc5bVpuTmxkRDBpTVRBd0pTSWdjM1J2Y0MxamIyeHZjajBpYUhOc0tESXpNQ3d5TVNVc01URWxLU0l2UGp3dmJHbHVaV0Z5UjNKaFpHbGxiblErUEd4cGJtVmhja2R5WVdScFpXNTBJR2xrUFNKVFlXNWtRbTkwZEc5dElpQjRNVDBpTVRBd0pTSWdlVEU5SWpFd01DVWlQanh6ZEc5d0lHOW1abk5sZEQwaU1UQWxJaUJ6ZEc5d0xXTnZiRzl5UFNKb2Myd29Nak13TERJeEpTd3hNU1VwSWk4K1BITjBiM0FnYjJabWMyVjBQU0l4TURBbElpQnpkRzl3TFdOdmJHOXlQU0pvYzJ3b05qRXNPRGdsTERRd0pTa2lMejQ4WVc1cGJXRjBaU0JoZEhSeWFXSjFkR1ZPWVcxbFBTSjRNU0lnWkhWeVBTSTJjeUlnY21Wd1pXRjBRMjkxYm5ROUltbHVaR1ZtYVc1cGRHVWlJSFpoYkhWbGN6MGlNekFsT3pZd0pUc3hNakFsT3pZd0pUc3pNQ1U3SWk4K1BDOXNhVzVsWVhKSGNtRmthV1Z1ZEQ0OGJHbHVaV0Z5UjNKaFpHbGxiblFnYVdROUlraHZkWEpuYkdGemMxTjBjbTlyWlNJZ1ozSmhaR2xsYm5SVWNtRnVjMlp2Y20wOUluSnZkR0YwWlNnNU1Da2lJR2R5WVdScFpXNTBWVzVwZEhNOUluVnpaWEpUY0dGalpVOXVWWE5sSWo0OGMzUnZjQ0J2Wm1aelpYUTlJalV3SlNJZ2MzUnZjQzFqYjJ4dmNqMGlhSE5zS0RZeExEZzRKU3cwTUNVcElpOCtQSE4wYjNBZ2IyWm1jMlYwUFNJNE1DVWlJSE4wYjNBdFkyOXNiM0k5SW1oemJDZ3lNekFzTWpFbExERXhKU2tpTHo0OEwyeHBibVZoY2tkeVlXUnBaVzUwUGp4bklHbGtQU0pJYjNWeVoyeGhjM01pUGp4d1lYUm9JR1E5SWswZ05UQXNNell3SUdFZ016QXdMRE13TUNBd0lERXNNU0EyTURBc01DQmhJRE13TUN3ek1EQWdNQ0F4TERFZ0xUWXdNQ3d3SWlCbWFXeHNQU0lqWm1abUlpQm1hV3hzTFc5d1lXTnBkSGs5SWk0d01pSWdjM1J5YjJ0bFBTSjFjbXdvSTBodmRYSm5iR0Z6YzFOMGNtOXJaU2tpSUhOMGNtOXJaUzEzYVdSMGFEMGlOQ0l2UGp4d1lYUm9JR1E5SW0wMU5qWXNNVFl4TGpJd01YWXROVE11T1RJMFl6QXRNVGt1TXpneUxUSXlMalV4TXkwek55NDFOak10TmpNdU16azRMVFV4TGpFNU9DMDBNQzQzTlRZdE1UTXVOVGt5TFRrMExqazBOaTB5TVM0d056a3RNVFV5TGpVNE55MHlNUzR3TnpsekxURXhNUzQ0TXpnc055NDBPRGN0TVRVeUxqWXdNaXd5TVM0d056bGpMVFF3TGpnNU15d3hNeTQyTXpZdE5qTXVOREV6TERNeExqZ3hOaTAyTXk0ME1UTXNOVEV1TVRrNGRqVXpMamt5TkdNd0xERTNMakU0TVN3eE55NDNNRFFzTXpNdU5ESTNMRFV3TGpJeU15dzBOaTR6T1RSMk1qZzBMamd3T1dNdE16SXVOVEU1TERFeUxqazJMVFV3TGpJeU15d3lPUzR5TURZdE5UQXVNakl6TERRMkxqTTVOSFkxTXk0NU1qUmpNQ3d4T1M0ek9ESXNNakl1TlRJc016Y3VOVFl6TERZekxqUXhNeXcxTVM0eE9UZ3NOREF1TnpZekxERXpMalU1TWl3NU5DNDVOVFFzTWpFdU1EYzVMREUxTWk0Mk1ESXNNakV1TURjNWN6RXhNUzQ0TXpFdE55NDBPRGNzTVRVeUxqVTROeTB5TVM0d056bGpOREF1T0RnMkxURXpMall6Tml3Mk15NHpPVGd0TXpFdU9ERTJMRFl6TGpNNU9DMDFNUzR4T1RoMkxUVXpMamt5TkdNd0xURTNMakU1TmkweE55NDNNRFF0TXpNdU5ETTFMVFV3TGpJeU15MDBOaTQwTURGV01qQTNMall3TTJNek1pNDFNVGt0TVRJdU9UWTNMRFV3TGpJeU15MHlPUzR5TURZc05UQXVNakl6TFRRMkxqUXdNVnB0TFRNME55NDBOaklzTlRjdU56a3piREV6TUM0NU5Ua3NNVE14TGpBeU55MHhNekF1T1RVNUxERXpNUzR3TVROV01qRTRMams1TkZwdE1qWXlMamt5TkM0d01qSjJNall5TGpBeE9Hd3RNVE13TGprek55MHhNekV1TURBMkxERXpNQzQ1TXpjdE1UTXhMakF4TTFvaUlHWnBiR3c5SWlNeE5qRTRNaklpUGp3dmNHRjBhRDQ4Y0c5c2VXZHZiaUJ3YjJsdWRITTlJak0xTUNBek5UQXVNREkySURReE5TNHdNeUF5T0RRdU9UYzRJREk0TlNBeU9EUXVPVGM0SURNMU1DQXpOVEF1TURJMklpQm1hV3hzUFNKMWNtd29JMU5oYm1SQ2IzUjBiMjBwSWk4K1BIQmhkR2dnWkQwaWJUUXhOaTR6TkRFc01qZ3hMamszTldNd0xDNDVNVFF0TGpNMU5Dd3hMamd3T1MweExqQXpOU3d5TGpZNExUVXVOVFF5TERjdU1EYzJMVE15TGpZMk1Td3hNaTQwTlMwMk5TNHlPQ3d4TWk0ME5TMHpNaTQyTWpRc01DMDFPUzQzTXpndE5TNHpOelF0TmpVdU1qZ3RNVEl1TkRVdExqWTRNUzB1T0RjeUxURXVNRE0xTFRFdU56WTNMVEV1TURNMUxUSXVOamdzTUMwdU9URTBMak0xTkMweExqZ3dPQ3d4TGpBek5TMHlMalkzTml3MUxqVTBNaTAzTGpBM05pd3pNaTQyTlRZdE1USXVORFVzTmpVdU1qZ3RNVEl1TkRVc016SXVOakU1TERBc05Ua3VOek00TERVdU16YzBMRFkxTGpJNExERXlMalExTGpZNE1TNDROamNzTVM0d016VXNNUzQzTmpJc01TNHdNelVzTWk0Mk56WmFJaUJtYVd4c1BTSjFjbXdvSTFOaGJtUlViM0FwSWk4K1BIQmhkR2dnWkQwaWJUUTRNUzQwTml3MU1EUXVNVEF4ZGpVNExqUTBPV010TWk0ek5TNDNOeTAwTGpneUxERXVOVEV0Tnk0ek9Td3lMakl6TFRNd0xqTXNPQzQxTkMwM05DNDJOU3d4TXk0NU1pMHhNalF1TURZc01UTXVPVEl0TlRNdU5pd3dMVEV3TVM0eU5DMDJMak16TFRFek1TNDBOeTB4Tmk0eE5uWXROVGd1TkRNNWFESTJNaTQ1TWxvaUlHWnBiR3c5SW5WeWJDZ2pVMkZ1WkVKdmRIUnZiU2tpTHo0OFpXeHNhWEJ6WlNCamVEMGlNelV3SWlCamVUMGlOVEEwTGpFd01TSWdjbmc5SWpFek1TNDBOaklpSUhKNVBTSXlPQzR4TURnaUlHWnBiR3c5SW5WeWJDZ2pVMkZ1WkZSdmNDa2lMejQ4WnlCbWFXeHNQU0p1YjI1bElpQnpkSEp2YTJVOUluVnliQ2dqU0c5MWNtZHNZWE56VTNSeWIydGxLU0lnYzNSeWIydGxMV3hwYm1WallYQTlJbkp2ZFc1a0lpQnpkSEp2YTJVdGJXbDBaWEpzYVcxcGREMGlNVEFpSUhOMGNtOXJaUzEzYVdSMGFEMGlOQ0krUEhCaGRHZ2daRDBpYlRVMk5TNDJOREVzTVRBM0xqSTRZekFzT1M0MU16Y3ROUzQxTml3eE9DNDJNamt0TVRVdU5qYzJMREkyTGprM00yZ3RMakF5TTJNdE9TNHlNRFFzTnk0MU9UWXRNakl1TVRrMExERTBMalUyTWkwek9DNHhPVGNzTWpBdU5Ua3lMVE01TGpVd05Dd3hOQzQ1TXpZdE9UY3VNekkxTERJMExqTTFOUzB4TmpFdU56TXpMREkwTGpNMU5TMDVNQzQwT0N3d0xURTJOeTQ1TkRndE1UZ3VOVGd5TFRFNU9TNDVOVE10TkRRdU9UUTRhQzB1TURJell5MHhNQzR4TVRVdE9DNHpORFF0TVRVdU5qYzJMVEUzTGpRek55MHhOUzQyTnpZdE1qWXVPVGN6TERBdE16a3VOek0xTERrMkxqVTFOQzAzTVM0NU1qRXNNakUxTGpZMU1pMDNNUzQ1TWpGek1qRTFMall5T1N3ek1pNHhPRFVzTWpFMUxqWXlPU3czTVM0NU1qRmFJaTgrUEhCaGRHZ2daRDBpYlRFek5DNHpOaXd4TmpFdU1qQXpZekFzTXprdU56TTFMRGsyTGpVMU5DdzNNUzQ1TWpFc01qRTFMalkxTWl3M01TNDVNakZ6TWpFMUxqWXlPUzB6TWk0eE9EWXNNakUxTGpZeU9TMDNNUzQ1TWpFaUx6NDhiR2x1WlNCNE1UMGlNVE0wTGpNMklpQjVNVDBpTVRZeExqSXdNeUlnZURJOUlqRXpOQzR6TmlJZ2VUSTlJakV3Tnk0eU9DSXZQanhzYVc1bElIZ3hQU0kxTmpVdU5qUWlJSGt4UFNJeE5qRXVNakF6SWlCNE1qMGlOVFkxTGpZMElpQjVNajBpTVRBM0xqSTRJaTgrUEd4cGJtVWdlREU5SWpFNE5DNDFPRFFpSUhreFBTSXlNRFl1T0RJeklpQjRNajBpTVRnMExqVTROU0lnZVRJOUlqVXpOeTQxTnpraUx6NDhiR2x1WlNCNE1UMGlNakU0TGpFNE1TSWdlVEU5SWpJeE9DNHhNVGdpSUhneVBTSXlNVGd1TVRneElpQjVNajBpTlRZeUxqVXpOeUl2UGp4c2FXNWxJSGd4UFNJME9ERXVPREU0SWlCNU1UMGlNakU0TGpFME1pSWdlREk5SWpRNE1TNDRNVGtpSUhreVBTSTFOakl1TkRJNElpOCtQR3hwYm1VZ2VERTlJalV4TlM0ME1UVWlJSGt4UFNJeU1EY3VNelV5SWlCNE1qMGlOVEUxTGpReE5pSWdlVEk5SWpVek55NDFOemtpTHo0OGNHRjBhQ0JrUFNKdE1UZzBMalU0TERVek55NDFPR013TERVdU5EVXNOQzR5Tnl3eE1DNDJOU3d4TWk0d015d3hOUzQwTW1ndU1ESmpOUzQxTVN3ekxqTTVMREV5TGpjNUxEWXVOVFVzTWpFdU5UVXNPUzQwTWl3ek1DNHlNU3c1TGprc056Z3VNRElzTVRZdU1qZ3NNVE14TGpnekxERTJMakk0TERRNUxqUXhMREFzT1RNdU56WXROUzR6T0N3eE1qUXVNRFl0TVRNdU9USXNNaTQzTFM0M05pdzFMakk1TFRFdU5UUXNOeTQzTlMweUxqTTFMRGd1TnpjdE1pNDROeXd4Tmk0d05TMDJMakEwTERJeExqVTJMVGt1TkROb01HTTNMamMyTFRRdU56Y3NNVEl1TURRdE9TNDVOeXd4TWk0d05DMHhOUzQwTWlJdlBqeHdZWFJvSUdROUltMHhPRFF1TlRneUxEUTVNaTQyTlRaakxUTXhMak0xTkN3eE1pNDBPRFV0TlRBdU1qSXpMREk0TGpVNExUVXdMakl5TXl3ME5pNHhORElzTUN3NUxqVXpOaXcxTGpVMk5Dd3hPQzQyTWpjc01UVXVOamMzTERJMkxqazJPV2d1TURJeVl6Z3VOVEF6TERjdU1EQTFMREl3TGpJeE15d3hNeTQwTmpNc016UXVOVEkwTERFNUxqRTFPU3c1TGprNU9Td3pMams1TVN3eU1TNHlOamtzTnk0Mk1Ea3NNek11TlRrM0xERXdMamM0T0N3ek5pNDBOU3c1TGpRd055dzRNaTR4T0RFc01UVXVNREF5TERFek1TNDRNelVzTVRVdU1EQXljemsxTGpNMk15MDFMalU1TlN3eE16RXVPREEzTFRFMUxqQXdNbU14TUM0NE5EY3RNaTQzT1N3eU1DNDROamN0TlM0NU1qWXNNamt1T1RJMExUa3VNelE1TERFdU1qUTBMUzQwTmpjc01pNDBOek10TGprME1pd3pMalkzTXkweExqUXlOQ3d4TkM0ek1qWXROUzQyT1RZc01qWXVNRE0xTFRFeUxqRTJNU3d6TkM0MU1qUXRNVGt1TVRjemFDNHdNakpqTVRBdU1URTBMVGd1TXpReUxERTFMalkzTnkweE55NDBNek1zTVRVdU5qYzNMVEkyTGprMk9Td3dMVEUzTGpVMk1pMHhPQzQ0TmprdE16TXVOalkxTFRVd0xqSXlNeTAwTmk0eE5TSXZQanh3WVhSb0lHUTlJbTB4TXpRdU16WXNOVGt5TGpjeVl6QXNNemt1TnpNMUxEazJMalUxTkN3M01TNDVNakVzTWpFMUxqWTFNaXczTVM0NU1qRnpNakUxTGpZeU9TMHpNaTR4T0RZc01qRTFMall5T1MwM01TNDVNakVpTHo0OGJHbHVaU0I0TVQwaU1UTTBMak0ySWlCNU1UMGlOVGt5TGpjeUlpQjRNajBpTVRNMExqTTJJaUI1TWowaU5UTTRMamM1TnlJdlBqeHNhVzVsSUhneFBTSTFOalV1TmpRaUlIa3hQU0kxT1RJdU56SWlJSGd5UFNJMU5qVXVOalFpSUhreVBTSTFNemd1TnprM0lpOCtQSEJ2Ykhsc2FXNWxJSEJ2YVc1MGN6MGlORGd4TGpneU1pQTBPREV1T1RBeElEUTRNUzQzT1RnZ05EZ3hMamczTnlBME9ERXVOemMxSURRNE1TNDROVFFnTXpVd0xqQXhOU0F6TlRBdU1ESTJJREl4T0M0eE9EVWdNakU0TGpFeU9TSXZQanh3YjJ4NWJHbHVaU0J3YjJsdWRITTlJakl4T0M0eE9EVWdORGd4TGprd01TQXlNVGd1TWpNeElEUTRNUzQ0TlRRZ016VXdMakF4TlNBek5UQXVNREkySURRNE1TNDRNaklnTWpFNExqRTFNaUl2UGp3dlp6NDhMMmMrUEdjZ2FXUTlJbEJ5YjJkeVpYTnpJaUJtYVd4c1BTSWpabVptSWo0OGNtVmpkQ0IzYVdSMGFEMGlNakE0SWlCb1pXbG5hSFE5SWpFd01DSWdabWxzYkMxdmNHRmphWFI1UFNJdU1ETWlJSEo0UFNJeE5TSWdjbms5SWpFMUlpQnpkSEp2YTJVOUlpTm1abVlpSUhOMGNtOXJaUzF2Y0dGamFYUjVQU0l1TVNJZ2MzUnliMnRsTFhkcFpIUm9QU0kwSWk4K1BIUmxlSFFnZUQwaU1qQWlJSGs5SWpNMElpQm1iMjUwTFdaaGJXbHNlVDBpSjBOdmRYSnBaWElnVG1WM0p5eEJjbWxoYkN4dGIyNXZjM0JoWTJVaUlHWnZiblF0YzJsNlpUMGlNakp3ZUNJK1VISnZaM0psYzNNOEwzUmxlSFErUEhSbGVIUWdlRDBpTWpBaUlIazlJamN5SWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl4QmNtbGhiQ3h0YjI1dmMzQmhZMlVpSUdadmJuUXRjMmw2WlQwaU1qWndlQ0krTWpVbFBDOTBaWGgwUGp4bklHWnBiR3c5SW01dmJtVWlQanhqYVhKamJHVWdZM2c5SWpFMk5pSWdZM2s5SWpVd0lpQnlQU0l5TWlJZ2MzUnliMnRsUFNKb2Myd29Nak13TERJeEpTd3hNU1VwSWlCemRISnZhMlV0ZDJsa2RHZzlJakV3SWk4K1BHTnBjbU5zWlNCamVEMGlNVFkySWlCamVUMGlOVEFpSUhCaGRHaE1aVzVuZEdnOUlqRXdNREF3SWlCeVBTSXlNaUlnYzNSeWIydGxQU0pvYzJ3b05qRXNPRGdsTERRd0pTa2lJSE4wY205clpTMWtZWE5vWVhKeVlYazlJakV3TURBd0lpQnpkSEp2YTJVdFpHRnphRzltWm5ObGREMGlOelV3TUNJZ2MzUnliMnRsTFd4cGJtVmpZWEE5SW5KdmRXNWtJaUJ6ZEhKdmEyVXRkMmxrZEdnOUlqVWlJSFJ5WVc1elptOXliVDBpY205MFlYUmxLQzA1TUNraUlIUnlZVzV6Wm05eWJTMXZjbWxuYVc0OUlqRTJOaUExTUNJdlBqd3ZaejQ4TDJjK1BHY2dhV1E5SWxOMFlYUjFjeUlnWm1sc2JEMGlJMlptWmlJK1BISmxZM1FnZDJsa2RHZzlJakU0TkNJZ2FHVnBaMmgwUFNJeE1EQWlJR1pwYkd3dGIzQmhZMmwwZVQwaUxqQXpJaUJ5ZUQwaU1UVWlJSEo1UFNJeE5TSWdjM1J5YjJ0bFBTSWpabVptSWlCemRISnZhMlV0YjNCaFkybDBlVDBpTGpFaUlITjBjbTlyWlMxM2FXUjBhRDBpTkNJdlBqeDBaWGgwSUhnOUlqSXdJaUI1UFNJek5DSWdabTl1ZEMxbVlXMXBiSGs5SWlkRGIzVnlhV1Z5SUU1bGR5Y3NRWEpwWVd3c2JXOXViM053WVdObElpQm1iMjUwTFhOcGVtVTlJakl5Y0hnaVBsTjBZWFIxY3p3dmRHVjRkRDQ4ZEdWNGRDQjRQU0l5TUNJZ2VUMGlOeklpSUdadmJuUXRabUZ0YVd4NVBTSW5RMjkxY21sbGNpQk9aWGNuTEVGeWFXRnNMRzF2Ym05emNHRmpaU0lnWm05dWRDMXphWHBsUFNJeU5uQjRJajVUZEhKbFlXMXBibWM4TDNSbGVIUStQQzluUGp4bklHbGtQU0pCYlc5MWJuUWlJR1pwYkd3OUlpTm1abVlpUGp4eVpXTjBJSGRwWkhSb1BTSXhNakFpSUdobGFXZG9kRDBpTVRBd0lpQm1hV3hzTFc5d1lXTnBkSGs5SWk0d015SWdjbmc5SWpFMUlpQnllVDBpTVRVaUlITjBjbTlyWlQwaUkyWm1aaUlnYzNSeWIydGxMVzl3WVdOcGRIazlJaTR4SWlCemRISnZhMlV0ZDJsa2RHZzlJalFpTHo0OGRHVjRkQ0I0UFNJeU1DSWdlVDBpTXpRaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxFRnlhV0ZzTEcxdmJtOXpjR0ZqWlNJZ1ptOXVkQzF6YVhwbFBTSXlNbkI0SWo1QmJXOTFiblE4TDNSbGVIUStQSFJsZUhRZ2VEMGlNakFpSUhrOUlqY3lJaUJtYjI1MExXWmhiV2xzZVQwaUowTnZkWEpwWlhJZ1RtVjNKeXhCY21saGJDeHRiMjV2YzNCaFkyVWlJR1p2Ym5RdGMybDZaVDBpTWpad2VDSStKaU00T0RBMU95QXhNRXM4TDNSbGVIUStQQzluUGp4bklHbGtQU0pFZFhKaGRHbHZiaUlnWm1sc2JEMGlJMlptWmlJK1BISmxZM1FnZDJsa2RHZzlJakUxTWlJZ2FHVnBaMmgwUFNJeE1EQWlJR1pwYkd3dGIzQmhZMmwwZVQwaUxqQXpJaUJ5ZUQwaU1UVWlJSEo1UFNJeE5TSWdjM1J5YjJ0bFBTSWpabVptSWlCemRISnZhMlV0YjNCaFkybDBlVDBpTGpFaUlITjBjbTlyWlMxM2FXUjBhRDBpTkNJdlBqeDBaWGgwSUhnOUlqSXdJaUI1UFNJek5DSWdabTl1ZEMxbVlXMXBiSGs5SWlkRGIzVnlhV1Z5SUU1bGR5Y3NRWEpwWVd3c2JXOXViM053WVdObElpQm1iMjUwTFhOcGVtVTlJakl5Y0hnaVBrUjFjbUYwYVc5dVBDOTBaWGgwUGp4MFpYaDBJSGc5SWpJd0lpQjVQU0kzTWlJZ1ptOXVkQzFtWVcxcGJIazlJaWREYjNWeWFXVnlJRTVsZHljc1FYSnBZV3dzYlc5dWIzTndZV05sSWlCbWIyNTBMWE5wZW1VOUlqSTJjSGdpUGlac2REc2dNU0JFWVhrOEwzUmxlSFErUEM5blBqd3ZaR1ZtY3o0OGRHVjRkQ0IwWlhoMExYSmxibVJsY21sdVp6MGliM0IwYVcxcGVtVlRjR1ZsWkNJK1BIUmxlSFJRWVhSb0lITjBZWEowVDJabWMyVjBQU0l0TVRBd0pTSWdhSEpsWmowaUkwWnNiMkYwYVc1blZHVjRkQ0lnWm1sc2JEMGlJMlptWmlJZ1ptOXVkQzFtWVcxcGJIazlJaWREYjNWeWFXVnlJRTVsZHljc1FYSnBZV3dzYlc5dWIzTndZV05sSWlCbWFXeHNMVzl3WVdOcGRIazlJaTQ0SWlCbWIyNTBMWE5wZW1VOUlqSTJjSGdpUGp4aGJtbHRZWFJsSUdGa1pHbDBhWFpsUFNKemRXMGlJR0YwZEhKcFluVjBaVTVoYldVOUluTjBZWEowVDJabWMyVjBJaUJpWldkcGJqMGlNSE1pSUdSMWNqMGlOVEJ6SWlCbWNtOXRQU0l3SlNJZ2NtVndaV0YwUTI5MWJuUTlJbWx1WkdWbWFXNXBkR1VpSUhSdlBTSXhNREFsSWk4K01IaGtZakkxWVRkaU56WTRNekV4WkdVeE1qaGlZbVJoTjJJNE5ESTJZek5tT1dNM05HWXpNalF3SU9LQW9pQlRZV0pzYVdWeUlGWXlJRXh2WTJ0MWNDQkVlVzVoYldsalBDOTBaWGgwVUdGMGFENDhkR1Y0ZEZCaGRHZ2djM1JoY25SUFptWnpaWFE5SWpBbElpQm9jbVZtUFNJalJteHZZWFJwYm1kVVpYaDBJaUJtYVd4c1BTSWpabVptSWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl4QmNtbGhiQ3h0YjI1dmMzQmhZMlVpSUdacGJHd3RiM0JoWTJsMGVUMGlMamdpSUdadmJuUXRjMmw2WlQwaU1qWndlQ0krUEdGdWFXMWhkR1VnWVdSa2FYUnBkbVU5SW5OMWJTSWdZWFIwY21saWRYUmxUbUZ0WlQwaWMzUmhjblJQWm1aelpYUWlJR0psWjJsdVBTSXdjeUlnWkhWeVBTSTFNSE1pSUdaeWIyMDlJakFsSWlCeVpYQmxZWFJEYjNWdWREMGlhVzVrWldacGJtbDBaU0lnZEc4OUlqRXdNQ1VpTHo0d2VHUmlNalZoTjJJM05qZ3pNVEZrWlRFeU9HSmlaR0UzWWpnME1qWmpNMlk1WXpjMFpqTXlOREFnNG9DaUlGTmhZbXhwWlhJZ1ZqSWdURzlqYTNWd0lFUjVibUZ0YVdNOEwzUmxlSFJRWVhSb1BqeDBaWGgwVUdGMGFDQnpkR0Z5ZEU5bVpuTmxkRDBpTFRVd0pTSWdhSEpsWmowaUkwWnNiMkYwYVc1blZHVjRkQ0lnWm1sc2JEMGlJMlptWmlJZ1ptOXVkQzFtWVcxcGJIazlJaWREYjNWeWFXVnlJRTVsZHljc1FYSnBZV3dzYlc5dWIzTndZV05sSWlCbWFXeHNMVzl3WVdOcGRIazlJaTQ0SWlCbWIyNTBMWE5wZW1VOUlqSTJjSGdpUGp4aGJtbHRZWFJsSUdGa1pHbDBhWFpsUFNKemRXMGlJR0YwZEhKcFluVjBaVTVoYldVOUluTjBZWEowVDJabWMyVjBJaUJpWldkcGJqMGlNSE1pSUdSMWNqMGlOVEJ6SWlCbWNtOXRQU0l3SlNJZ2NtVndaV0YwUTI5MWJuUTlJbWx1WkdWbWFXNXBkR1VpSUhSdlBTSXhNREFsSWk4K01IZ3dNMkUyWVRnMFkyUTNOakprT1Rjd04yRXlNVFl3TldJMU5EaGhZV0ZpT0RreE5UWXlZV0ZpSU9LQW9pQkVRVWs4TDNSbGVIUlFZWFJvUGp4MFpYaDBVR0YwYUNCemRHRnlkRTltWm5ObGREMGlOVEFsSWlCb2NtVm1QU0lqUm14dllYUnBibWRVWlhoMElpQm1hV3hzUFNJalptWm1JaUJtYjI1MExXWmhiV2xzZVQwaUowTnZkWEpwWlhJZ1RtVjNKeXhCY21saGJDeHRiMjV2YzNCaFkyVWlJR1pwYkd3dGIzQmhZMmwwZVQwaUxqZ2lJR1p2Ym5RdGMybDZaVDBpTWpad2VDSStQR0Z1YVcxaGRHVWdZV1JrYVhScGRtVTlJbk4xYlNJZ1lYUjBjbWxpZFhSbFRtRnRaVDBpYzNSaGNuUlBabVp6WlhRaUlHSmxaMmx1UFNJd2N5SWdaSFZ5UFNJMU1ITWlJR1p5YjIwOUlqQWxJaUJ5WlhCbFlYUkRiM1Z1ZEQwaWFXNWtaV1pwYm1sMFpTSWdkRzg5SWpFd01DVWlMejR3ZURBellUWmhPRFJqWkRjMk1tUTVOekEzWVRJeE5qQTFZalUwT0dGaFlXSTRPVEUxTmpKaFlXSWc0b0NpSUVSQlNUd3ZkR1Y0ZEZCaGRHZytQQzkwWlhoMFBqeDFjMlVnYUhKbFpqMGlJMGRzYjNjaUlHWnBiR3d0YjNCaFkybDBlVDBpTGpraUx6NDhkWE5sSUdoeVpXWTlJaU5IYkc5M0lpQjRQU0l4TURBd0lpQjVQU0l4TURBd0lpQm1hV3hzTFc5d1lXTnBkSGs5SWk0NUlpOCtQSFZ6WlNCb2NtVm1QU0lqVEc5bmJ5SWdlRDBpTVRjd0lpQjVQU0l4TnpBaUlIUnlZVzV6Wm05eWJUMGljMk5oYkdVb0xqWXBJaTgrUEhWelpTQm9jbVZtUFNJalNHOTFjbWRzWVhOeklpQjRQU0l4TlRBaUlIazlJamt3SWlCMGNtRnVjMlp2Y20wOUluSnZkR0YwWlNneE1Da2lJSFJ5WVc1elptOXliUzF2Y21sbmFXNDlJalV3TUNBMU1EQWlMejQ4ZFhObElHaHlaV1k5SWlOUWNtOW5jbVZ6Y3lJZ2VEMGlNVFEwSWlCNVBTSTNPVEFpTHo0OGRYTmxJR2h5WldZOUlpTlRkR0YwZFhNaUlIZzlJak0yT0NJZ2VUMGlOemt3SWk4K1BIVnpaU0JvY21WbVBTSWpRVzF2ZFc1MElpQjRQU0kxTmpnaUlIazlJamM1TUNJdlBqeDFjMlVnYUhKbFpqMGlJMFIxY21GMGFXOXVJaUI0UFNJM01EUWlJSGs5SWpjNU1DSXZQand2YzNablBnPT0ifQ=="; + assertEq(actualTokenURI, expectedTokenURI, "token URI"); + } +} diff --git a/test/integration/concrete/lockup-tranched/token-uri/tokenURI.tree b/test/integration/concrete/lockup-tranched/token-uri/tokenURI.tree new file mode 100644 index 000000000..6a58d7e09 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/token-uri/tokenURI.tree @@ -0,0 +1,5 @@ +tokenURI.t.sol +├── given the NFT does not exist +│ └── it should revert +└── given the NFT exists + └── it should return the correct token URI diff --git a/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol new file mode 100644 index 000000000..0f1ea9bd2 --- /dev/null +++ b/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { LockupTranched_Integration_Concrete_Test } from "../LockupTranched.t.sol"; +import { WithdrawableAmountOf_Integration_Concrete_Test } from + "../../lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol"; + +contract WithdrawableAmountOf_LockupTranched_Integration_Concrete_Test is + LockupTranched_Integration_Concrete_Test, + WithdrawableAmountOf_Integration_Concrete_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Concrete_Test, WithdrawableAmountOf_Integration_Concrete_Test) + { + LockupTranched_Integration_Concrete_Test.setUp(); + WithdrawableAmountOf_Integration_Concrete_Test.setUp(); + } + + function test_WithdrawableAmountOf_StartTimeInThePresent() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + { + vm.warp({ newTimestamp: defaults.START_TIME() }); + uint128 actualWithdrawableAmount = lockupTranched.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawableAmount = 0; + assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); + } + + modifier givenStartTimeInThePast() { + _; + } + + function test_WithdrawableAmountOf_NoPreviousWithdrawals() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + givenStartTimeInThePast + { + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() }); + + // Run the test. + uint128 actualWithdrawableAmount = lockupTranched.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount; + assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); + } + + modifier whenWithWithdrawals() { + _; + } + + function test_WithdrawableAmountOf() + external + givenNotNull + givenStreamHasNotBeenCanceled + givenStatusStreaming + givenStartTimeInThePast + whenWithWithdrawals + { + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.START_TIME() + defaults.CLIFF_DURATION() }); + + // Make the withdrawal. + lockupTranched.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.CLIFF_AMOUNT() }); + + // Run the test. + uint128 actualWithdrawableAmount = lockupTranched.withdrawableAmountOf(defaultStreamId); + + uint128 expectedWithdrawableAmount = defaults.tranches()[0].amount - defaults.CLIFF_AMOUNT(); + assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); + } +} diff --git a/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.tree b/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.tree new file mode 100644 index 000000000..fe3ff45ef --- /dev/null +++ b/test/integration/concrete/lockup-tranched/withdrawable-amount-of/withdrawableAmountOf.tree @@ -0,0 +1,9 @@ +withdrawableAmountOf.t.sol +└── given the stream's status is "STREAMING" + ├── given the start time is in the present + │ └── it should return zero + └── given the start time is in the past + ├── given there are no previous withdrawals + │ └── it should return the correct withdrawable amount + └── given there are previous withdrawals + └── it should return the correct withdrawable amount diff --git a/test/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol b/test/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol new file mode 100644 index 000000000..c30daca79 --- /dev/null +++ b/test/integration/concrete/lockup/allow-to-hook/allowToHook.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; + +import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; +import { Integration_Test } from "../../../Integration.t.sol"; + +abstract contract AllowToHook_Integration_Concrete_Test is Integration_Test, Lockup_Integration_Shared_Test { + uint256 internal defaultStreamId; + + function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { + defaultStreamId = createDefaultStream(); + } + + function test_RevertWhen_CallerNotAdmin() external { + // Make Eve the caller in this test. + resetPrank({ msgSender: users.eve }); + + // Run the test. + vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); + lockup.allowToHook(users.eve); + } + + modifier whenCallerAdmin() { + // Make the Admin the caller in the rest of this test suite. + resetPrank({ msgSender: users.admin }); + _; + } + + function test_RevertWhen_ProvidedAddressNoCode() external whenCallerAdmin { + address eoa = vm.addr({ privateKey: 1 }); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_AllowToHookZeroCodeSize.selector, eoa)); + lockup.allowToHook(eoa); + } + + modifier whenProvidedAddressHasCode() { + _; + } + + function test_RevertWhen_ProvidedAddressUnsupportedInterface() + external + whenCallerAdmin + whenProvidedAddressHasCode + { + // Incorrect interface ID. + address recipient = address(recipientInterfaceIDIncorrect); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2Lockup_AllowToHookUnsupportedInterface.selector, recipient) + ); + lockup.allowToHook(recipient); + + // Missing interface ID. + recipient = address(recipientInterfaceIDMissing); + vm.expectRevert(bytes("")); + lockup.allowToHook(recipient); + } + + modifier whenProvidedAddressSupportsInterface() { + _; + } + + function test_AllowToHook() + external + whenCallerAdmin + whenProvidedAddressHasCode + whenProvidedAddressSupportsInterface + { + // Expect the relevant event to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + emit AllowToHook(users.admin, address(recipientGood)); + + // Allow the provided address to hook. + lockup.allowToHook(address(recipientGood)); + + // Assert that the provided address has been put on the allowlist. + bool isAllowedToHook = lockup.isAllowedToHook(address(recipientGood)); + assertTrue(isAllowedToHook, "address not put on the allowlist"); + } +} diff --git a/test/integration/concrete/lockup/allow-to-hook/allowToHook.tree b/test/integration/concrete/lockup/allow-to-hook/allowToHook.tree new file mode 100644 index 000000000..e8541e138 --- /dev/null +++ b/test/integration/concrete/lockup/allow-to-hook/allowToHook.tree @@ -0,0 +1,12 @@ +allowToHook.tree +├── when the caller is not the admin +│ └── it should revert +└── when the caller is the admin + ├── when the provided address doesn't have any code + │ └── it should revert + └── when the provided address has code + ├── when the provided address does not support the recipient interface + │ └── it should revert + └── when the provided address supports the recipient interface + ├── it should put the address on the allowlist + └── it should emit a {AllowToHook} event diff --git a/test/integration/concrete/lockup/burn/burn.t.sol b/test/integration/concrete/lockup/burn/burn.t.sol index f73493e41..f16ffa248 100644 --- a/test/integration/concrete/lockup/burn/burn.t.sol +++ b/test/integration/concrete/lockup/burn/burn.t.sol @@ -1,5 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; + +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -16,7 +18,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int notTransferableStreamId = createDefaultStreamNotTransferable(); // Make the Recipient (owner of the NFT) the caller in this test suite. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } function test_RevertWhen_DelegateCalled() external { @@ -49,7 +51,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int givenNotNull givenStreamHasNotBeenDepleted { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamNotDepleted.selector, streamId)); lockup.burn(streamId); } @@ -60,7 +62,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int givenNotNull givenStreamHasNotBeenDepleted { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamNotDepleted.selector, streamId)); lockup.burn(streamId); } @@ -71,7 +73,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int givenNotNull givenStreamHasNotBeenDepleted { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamNotDepleted.selector, streamId)); lockup.burn(streamId); } @@ -82,16 +84,16 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int givenNotNull givenStreamHasNotBeenDepleted { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); - changePrank({ msgSender: users.sender }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); + resetPrank({ msgSender: users.sender }); lockup.cancel(streamId); - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamNotDepleted.selector, streamId)); lockup.burn(streamId); } modifier givenStreamHasBeenDepleted(uint256 streamId_) { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: streamId_, to: users.recipient }); _; } @@ -102,7 +104,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int givenNotNull givenStreamHasBeenDepleted(streamId) { - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, streamId, users.eve)); lockup.burn(streamId); } @@ -122,7 +124,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int lockup.burn(streamId); // Run the test. - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, streamId)); lockup.burn(streamId); } @@ -142,7 +144,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: notTransferableStreamId }); lockup.burn(notTransferableStreamId); - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, notTransferableStreamId)); lockup.getRecipient(notTransferableStreamId); } @@ -163,7 +165,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int lockup.approve({ to: users.operator, tokenId: streamId }); // Make the approved operator the caller in this test. - changePrank({ msgSender: users.operator }); + resetPrank({ msgSender: users.operator }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -173,7 +175,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int lockup.burn(streamId); // Assert that the NFT has been burned. - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, streamId)); lockup.getRecipient(streamId); } @@ -190,7 +192,7 @@ abstract contract Burn_Integration_Concrete_Test is Integration_Test, Lockup_Int vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: streamId }); lockup.burn(streamId); - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, streamId)); lockup.getRecipient(streamId); } } diff --git a/test/integration/concrete/lockup/burn/burn.tree b/test/integration/concrete/lockup/burn/burn.tree index 90efa59eb..5ec96014b 100644 --- a/test/integration/concrete/lockup/burn/burn.tree +++ b/test/integration/concrete/lockup/burn/burn.tree @@ -2,9 +2,9 @@ burn.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── given the id references a null stream + ├── given the ID references a null stream │ └── it should revert - └── given the id does not reference a null stream + └── given the ID does not reference a null stream ├── given the stream has not been depleted │ ├── given the stream's status is "PENDING" │ │ └── it should revert diff --git a/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol b/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol index 0e69e0b64..e0f601a1d 100644 --- a/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol +++ b/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Solarray } from "solarray/src/Solarray.sol"; @@ -42,14 +42,14 @@ abstract contract CancelMultiple_Integration_Concrete_Test is } function test_RevertGiven_AllStreamsCold() external whenNotDelegateCalled whenArrayCountNotZero givenNoNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamSettled.selector, testStreamIds[0])); lockup.cancelMultiple({ streamIds: testStreamIds }); } function test_RevertGiven_SomeStreamsCold() external whenNotDelegateCalled whenArrayCountNotZero givenNoNull { uint256 earlyStreamId = createDefaultStreamWithEndTime({ endTime: defaults.CLIFF_TIME() + 1 seconds }); - vm.warp({ timestamp: defaults.CLIFF_TIME() + 1 seconds }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 1 seconds }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamSettled.selector, earlyStreamId)); lockup.cancelMultiple({ streamIds: Solarray.uint256s(testStreamIds[0], earlyStreamId) }); } @@ -63,7 +63,7 @@ abstract contract CancelMultiple_Integration_Concrete_Test is whenCallerUnauthorized { // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Run the test. vm.expectRevert( @@ -81,7 +81,7 @@ abstract contract CancelMultiple_Integration_Concrete_Test is whenCallerUnauthorized { // Make the Recipient the caller in this test. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Run the test. vm.expectRevert( @@ -98,7 +98,7 @@ abstract contract CancelMultiple_Integration_Concrete_Test is givenAllStreamsWarm whenCallerUnauthorized { - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Create a stream with Eve as the stream's sender. uint256 eveStreamId = createDefaultStreamWithSender(users.eve); @@ -120,7 +120,7 @@ abstract contract CancelMultiple_Integration_Concrete_Test is whenCallerUnauthorized { // Make the Recipient the caller in this test. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Run the test. vm.expectRevert( @@ -168,13 +168,13 @@ abstract contract CancelMultiple_Integration_Concrete_Test is givenAllStreamsCancelable { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Expect the assets to be refunded to the stream's sender. uint128 senderAmount0 = lockup.refundableAmountOf(testStreamIds[0]); - expectCallToTransfer({ to: users.sender, amount: senderAmount0 }); + expectCallToTransfer({ to: users.sender, value: senderAmount0 }); uint128 senderAmount1 = lockup.refundableAmountOf(testStreamIds[1]); - expectCallToTransfer({ to: users.sender, amount: senderAmount1 }); + expectCallToTransfer({ to: users.sender, value: senderAmount1 }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree b/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree index 2cd6a411f..b039e509f 100644 --- a/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree +++ b/test/integration/concrete/lockup/cancel-multiple/cancelMultiple.tree @@ -5,11 +5,11 @@ cancelMultiple.t.sol ├── when the array count is zero │ └── it should do nothing └── when the array count is not zero - ├── given the stream ids array references only null streams + ├── given the stream IDs array references only null streams │ └── it should revert - ├── given the stream ids array references some null streams + ├── given the stream IDs array references some null streams │ └── it should revert - └── given the stream ids array references only streams that are not null + └── given the stream IDs array references only streams that are not null ├── given all streams are cold │ └── it should revert ├── given some streams are cold diff --git a/test/integration/concrete/lockup/cancel/cancel.t.sol b/test/integration/concrete/lockup/cancel/cancel.t.sol index 616e864ee..cca169686 100644 --- a/test/integration/concrete/lockup/cancel/cancel.t.sol +++ b/test/integration/concrete/lockup/cancel/cancel.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; -import { ISablierV2LockupRecipient } from "src/interfaces/hooks/ISablierV2LockupRecipient.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Lockup } from "src/types/DataTypes.sol"; @@ -28,21 +28,21 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I } function test_RevertGiven_StatusDepleted() external whenNotDelegateCalled givenNotNull givenStreamCold { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamDepleted.selector, defaultStreamId)); lockup.cancel(defaultStreamId); } function test_RevertGiven_StatusCanceled() external whenNotDelegateCalled givenNotNull givenStreamCold { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamCanceled.selector, defaultStreamId)); lockup.cancel(defaultStreamId); } function test_RevertGiven_StatusSettled() external whenNotDelegateCalled givenNotNull givenStreamCold { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamSettled.selector, defaultStreamId)); lockup.cancel(defaultStreamId); } @@ -55,7 +55,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerUnauthorized { // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Run the test. vm.expectRevert( @@ -72,7 +72,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerUnauthorized { // Make the Recipient the caller in this test. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Run the test. vm.expectRevert( @@ -95,7 +95,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I function test_Cancel_StatusPending() external { // Warp to the past. - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); // Cancel the stream. lockup.cancel(defaultStreamId); @@ -110,7 +110,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I assertFalse(isCancelable, "isCancelable"); } - function test_Cancel_RecipientNotContract() + function test_Cancel_RecipientNotAllowedToHook() external whenNotDelegateCalled givenNotNull @@ -119,13 +119,30 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I givenStreamCancelable givenStatusStreaming { - lockup.cancel(defaultStreamId); - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + // Create the stream with a recipient contract that implements {ISablierLockupRecipient}. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); + + // Expect Sablier to NOT run the recipient hook. + uint128 senderAmount = lockup.refundableAmountOf(streamId); + uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupCancel, (streamId, users.sender, senderAmount, recipientAmount) + ), + count: 0 + }); + + // Cancel the stream. + lockup.cancel(streamId); + + // Assert that the stream has been canceled. + Lockup.Status actualStatus = lockup.statusOf(streamId); Lockup.Status expectedStatus = Lockup.Status.CANCELED; assertEq(actualStatus, expectedStatus); } - function test_Cancel_RecipientDoesNotImplementHook() + function test_Cancel_RecipientReverting() external whenNotDelegateCalled givenNotNull @@ -133,31 +150,24 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerAuthorized givenStreamCancelable givenStatusStreaming - givenRecipientContract + givenRecipientAllowedToHook { - // Create the stream with a no-op contract as the recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(noop)); + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientReverting)); + resetPrank({ msgSender: users.sender }); - // Expect a call to the hook. - uint128 senderAmount = lockup.refundableAmountOf(streamId); - uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); - vm.expectCall( - address(noop), - abi.encodeCall( - ISablierV2LockupRecipient.onStreamCanceled, (streamId, users.sender, senderAmount, recipientAmount) - ) - ); + // Create the stream with a reverting contract as the stream's recipient. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientReverting)); + + // Expect a revert. + vm.expectRevert("You shall not pass"); // Cancel the stream. lockup.cancel(streamId); - - // Assert that the stream has been canceled. - Lockup.Status actualStatus = lockup.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.CANCELED; - assertEq(actualStatus, expectedStatus); } - function test_Cancel_RecipientReverts() + function test_Cancel_RecipientReturnsInvalidSelector() external whenNotDelegateCalled givenNotNull @@ -165,32 +175,29 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerAuthorized givenStreamCancelable givenStatusStreaming - givenRecipientContract - givenRecipientImplementsHook + givenRecipientAllowedToHook + whenRecipientNotReverting { - // Create the stream with a reverting contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(revertingRecipient)); + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientInvalidSelector)); + resetPrank({ msgSender: users.sender }); - // Expect a call to the hook. - uint128 senderAmount = lockup.refundableAmountOf(streamId); - uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); - vm.expectCall( - address(revertingRecipient), - abi.encodeCall( - ISablierV2LockupRecipient.onStreamCanceled, (streamId, users.sender, senderAmount, recipientAmount) + // Create the stream with a recipient contract that returns invalid selector bytes on the hook call. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientInvalidSelector)); + + // Expect a revert. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2Lockup_InvalidHookSelector.selector, address(recipientInvalidSelector) ) ); // Cancel the stream. lockup.cancel(streamId); - - // Assert that the stream has been canceled. - Lockup.Status actualStatus = lockup.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.CANCELED; - assertEq(actualStatus, expectedStatus); } - function test_Cancel_RecipientReentrancy() + function test_Cancel_RecipientReentrant() external whenNotDelegateCalled givenNotNull @@ -198,30 +205,45 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerAuthorized givenStreamCancelable givenStatusStreaming - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert + givenRecipientAllowedToHook + whenRecipientNotReverting + whenRecipientReturnsSelector { + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientReentrant)); + resetPrank({ msgSender: users.sender }); + // Create the stream with a reentrant contract as the recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(reentrantRecipient)); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientReentrant)); - // Expect a call to the hook. + // Expect Sablier to run the recipient hook. uint128 senderAmount = lockup.refundableAmountOf(streamId); uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); vm.expectCall( - address(reentrantRecipient), + address(recipientReentrant), abi.encodeCall( - ISablierV2LockupRecipient.onStreamCanceled, (streamId, users.sender, senderAmount, recipientAmount) + ISablierLockupRecipient.onSablierLockupCancel, (streamId, users.sender, senderAmount, recipientAmount) ) ); + // Expect a reentrant call to the Lockup contract. + vm.expectCall( + address(lockup), + abi.encodeCall(ISablierV2Lockup.withdraw, (streamId, address(recipientReentrant), recipientAmount)) + ); + // Cancel the stream. lockup.cancel(streamId); - // Assert that the stream has been canceled. + // Assert that the stream has been depleted. The reentrant recipient withdrew all the funds. Lockup.Status actualStatus = lockup.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.CANCELED; + Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); + + // Assert that the withdrawn amount has been updated. + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(streamId); + assertEq(actualWithdrawnAmount, recipientAmount, "withdrawnAmount"); } function test_Cancel() @@ -232,30 +254,35 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I whenCallerAuthorized givenStreamCancelable givenStatusStreaming - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - whenNoRecipientReentrancy + givenRecipientAllowedToHook + whenRecipientNotReverting + whenRecipientReturnsSelector + whenRecipientNotReentrant { + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientGood)); + resetPrank({ msgSender: users.sender }); + // Create the stream. - uint256 streamId = createDefaultStreamWithRecipient(address(goodRecipient)); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); // Expect the assets to be refunded to the Sender. uint128 senderAmount = lockup.refundableAmountOf(streamId); - expectCallToTransfer({ to: users.sender, amount: senderAmount }); + expectCallToTransfer({ to: users.sender, value: senderAmount }); - // Expect a call to the hook. + // Expect Sablier to run the recipient hook. uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); vm.expectCall( - address(goodRecipient), + address(recipientGood), abi.encodeCall( - ISablierV2LockupRecipient.onStreamCanceled, (streamId, users.sender, senderAmount, recipientAmount) + ISablierLockupRecipient.onSablierLockupCancel, (streamId, users.sender, senderAmount, recipientAmount) ) ); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit CancelLockupStream(streamId, users.sender, address(goodRecipient), dai, senderAmount, recipientAmount); + emit CancelLockupStream(streamId, users.sender, address(recipientGood), dai, senderAmount, recipientAmount); vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: streamId }); @@ -278,7 +305,7 @@ abstract contract Cancel_Integration_Concrete_Test is Integration_Test, Cancel_I // Assert that the NFT has not been burned. address actualNFTOwner = lockup.ownerOf({ tokenId: streamId }); - address expectedNFTOwner = address(goodRecipient); + address expectedNFTOwner = address(recipientGood); assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); } } diff --git a/test/integration/concrete/lockup/cancel/cancel.tree b/test/integration/concrete/lockup/cancel/cancel.tree index 491c3f2e3..dab8a48a2 100644 --- a/test/integration/concrete/lockup/cancel/cancel.tree +++ b/test/integration/concrete/lockup/cancel/cancel.tree @@ -2,9 +2,9 @@ cancel.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── given the id references a null stream + ├── given the ID references a null stream │ └── it should revert - └── given the id does not reference a null stream + └── given the ID does not reference a null stream ├── given the stream is cold │ ├── given the stream's status is "DEPLETED" │ │ └── it should revert @@ -27,34 +27,29 @@ cancel.t.sol │ ├── it should mark the stream as depleted │ └── it should make the stream not cancelable └── given the stream's status is "STREAMING" - ├── given the recipient is not a contract + ├── given the recipient is not allowed to hook │ ├── it should cancel the stream - │ └── it should mark the stream as canceled - └── given the recipient is a contract - ├── given the recipient does not implement the hook - │ ├── it should cancel the stream - │ ├── it should mark the stream as canceled - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── given the recipient implements the hook - ├── when the recipient reverts + │ ├── it should mark the stream as canceled + │ └── it should not make Sablier run the recipient hook + └── given the recipient is allowed to hook + ├── when the recipient reverts + │ └── it should revert the entire transaction + └── when the recipient does not revert + ├── when the recipient hook does not return a valid selector + │ └── it should revert + └── when the recipient hook returns a valid selector + ├── when there is reentrancy │ ├── it should cancel the stream │ ├── it should mark the stream as canceled - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── when the recipient does not revert - ├── when there is reentrancy - │ ├── it should cancel the stream - │ ├── it should mark the stream as canceled - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── when there is no reentrancy - ├── it should cancel the stream - ├── it should mark the stream as canceled - ├── it should make the stream not cancelable - ├── it should update the refunded amount - ├── it should refund the sender - ├── it should call the recipient hook - ├── it should emit a {MetadataUpdate} event - └── it should emit a {CancelLockupStream} event - \ No newline at end of file + │ ├── it should make Sablier run the recipient hook + │ ├── it should perform a reentrancy call to the Lockup contract + │ └── it should make the withdrawal via the reentrancy + └── when there is no reentrancy + ├── it should cancel the stream + ├── it should mark the stream as canceled + ├── it should make the stream not cancelable + ├── it should update the refunded amount + ├── it should refund the sender + ├── it should make Sablier run the recipient hook + ├── it should emit a {MetadataUpdate} event + └── it should emit a {CancelLockupStream} event diff --git a/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.t.sol b/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.t.sol deleted file mode 100644 index 820ad51f9..000000000 --- a/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.t.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; - -import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; -import { Integration_Test } from "../../../Integration.t.sol"; - -abstract contract ClaimProtocolRevenues_Integration_Concrete_Test is - Integration_Test, - Lockup_Integration_Shared_Test -{ - function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { } - - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - base.claimProtocolRevenues(dai); - } - - modifier whenCallerAdmin() { - // Make the Admin the caller in the rest of this test suite. - changePrank({ msgSender: users.admin }); - _; - } - - function test_RevertGiven_ProtocolRevenuesZero() external whenCallerAdmin { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Base_NoProtocolRevenues.selector, dai)); - base.claimProtocolRevenues(dai); - } - - modifier givenProtocolRevenuesNotZero() { - // Create the default stream, which will accrue revenues for the protocol. - changePrank({ msgSender: users.sender }); - createDefaultStream(); - changePrank({ msgSender: users.admin }); - _; - } - - function test_ClaimProtocolRevenues() external whenCallerAdmin givenProtocolRevenuesNotZero { - // Expect the protocol revenues to be claimed. - uint128 protocolRevenues = defaults.PROTOCOL_FEE_AMOUNT(); - expectCallToTransfer({ to: users.admin, amount: protocolRevenues }); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(base) }); - emit ClaimProtocolRevenues(users.admin, dai, protocolRevenues); - - // Claim the protocol revenues. - base.claimProtocolRevenues(dai); - - // Assert that the protocol revenues have been set to zero. - uint128 actualProtocolRevenues = base.protocolRevenues(dai); - uint128 expectedProtocolRevenues = 0; - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - } -} diff --git a/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.tree b/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.tree deleted file mode 100644 index 46d8c91c0..000000000 --- a/test/integration/concrete/lockup/claim-protocol-revenues/claimProtocolRevenues.tree +++ /dev/null @@ -1,10 +0,0 @@ -claimProtocolRevenues.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── given the protocol revenues are zero - │ └── it should revert - └── given the protocol revenues are not zero - ├── it should claim the protocol revenues - ├── it should update the protocol revenues - └── it should emit a {ClaimProtocolRevenues} event diff --git a/test/integration/concrete/lockup/get-asset/getAsset.t.sol b/test/integration/concrete/lockup/get-asset/getAsset.t.sol index b43d23a58..082560891 100644 --- a/test/integration/concrete/lockup/get-asset/getAsset.t.sol +++ b/test/integration/concrete/lockup/get-asset/getAsset.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/test/integration/concrete/lockup/get-asset/getAsset.tree b/test/integration/concrete/lockup/get-asset/getAsset.tree index 01bde9485..fd072b636 100644 --- a/test/integration/concrete/lockup/get-asset/getAsset.tree +++ b/test/integration/concrete/lockup/get-asset/getAsset.tree @@ -1,5 +1,5 @@ getAsset.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct address of the asset diff --git a/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.t.sol b/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.t.sol index 97fcfd666..e418e69db 100644 --- a/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.t.sol +++ b/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.tree b/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.tree index 5254069e5..7aab0ae56 100644 --- a/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.tree +++ b/test/integration/concrete/lockup/get-deposited-amount/getDepositedAmount.tree @@ -1,5 +1,5 @@ getDepositedAmount.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct deposited amount diff --git a/test/integration/concrete/lockup/get-end-time/getEndTime.t.sol b/test/integration/concrete/lockup/get-end-time/getEndTime.t.sol index f6c6090dc..ed3575642 100644 --- a/test/integration/concrete/lockup/get-end-time/getEndTime.t.sol +++ b/test/integration/concrete/lockup/get-end-time/getEndTime.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/get-end-time/getEndTime.tree b/test/integration/concrete/lockup/get-end-time/getEndTime.tree index 20359c089..235fe5270 100644 --- a/test/integration/concrete/lockup/get-end-time/getEndTime.tree +++ b/test/integration/concrete/lockup/get-end-time/getEndTime.tree @@ -1,5 +1,5 @@ getEndTime.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct end time diff --git a/test/integration/concrete/lockup/get-recipient/getRecipient.t.sol b/test/integration/concrete/lockup/get-recipient/getRecipient.t.sol index 3e6c06d47..8bfcd06ae 100644 --- a/test/integration/concrete/lockup/get-recipient/getRecipient.t.sol +++ b/test/integration/concrete/lockup/get-recipient/getRecipient.t.sol @@ -1,5 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; + +import { IERC721Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -13,7 +15,7 @@ abstract contract GetRecipient_Integration_Concrete_Test is Integration_Test, Lo function test_RevertGiven_Null() external { uint256 nullStreamId = 1729; - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, nullStreamId)); lockup.getRecipient(nullStreamId); } @@ -23,10 +25,10 @@ abstract contract GetRecipient_Integration_Concrete_Test is Integration_Test, Lo function test_RevertGiven_NFTBurned() external { // Simulate the passage of time. - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); // Make the Recipient the caller. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Deplete the stream. lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); @@ -35,7 +37,7 @@ abstract contract GetRecipient_Integration_Concrete_Test is Integration_Test, Lo lockup.burn(defaultStreamId); // Expect the relevant error when retrieving the recipient. - vm.expectRevert("ERC721: invalid token ID"); + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, defaultStreamId)); lockup.getRecipient(defaultStreamId); } @@ -43,7 +45,7 @@ abstract contract GetRecipient_Integration_Concrete_Test is Integration_Test, Lo _; } - function test_GetRecipient() external givenNotNull givenNFTNotBurned { + function test_GetRecipient() external view givenNotNull givenNFTNotBurned { address actualRecipient = lockup.getRecipient(defaultStreamId); address expectedRecipient = users.recipient; assertEq(actualRecipient, expectedRecipient, "recipient"); diff --git a/test/integration/concrete/lockup/get-recipient/getRecipient.tree b/test/integration/concrete/lockup/get-recipient/getRecipient.tree index 5b33ed4a0..075fc23bd 100644 --- a/test/integration/concrete/lockup/get-recipient/getRecipient.tree +++ b/test/integration/concrete/lockup/get-recipient/getRecipient.tree @@ -1,7 +1,7 @@ getRecipient.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the NFT has been burned │ └── it should revert └── given the NFT has not been burned diff --git a/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.t.sol b/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.t.sol index 70951a7e8..c35abea37 100644 --- a/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.t.sol +++ b/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -31,7 +31,7 @@ abstract contract GetRefundedAmount_Integration_Concrete_Test is Integration_Tes givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); uint128 expectedRefundedAmount = defaults.REFUND_AMOUNT(); @@ -43,7 +43,7 @@ abstract contract GetRefundedAmount_Integration_Concrete_Test is Integration_Tes givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); @@ -56,28 +56,28 @@ abstract contract GetRefundedAmount_Integration_Concrete_Test is Integration_Tes } function test_GetRefundedAmount_StatusPending() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } function test_GetRefundedAmount_StatusStreaming() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } function test_GetRefundedAmount_StatusSettled() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); uint128 expectedRefundedAmount = 0; assertEq(actualRefundedAmount, expectedRefundedAmount, "refundedAmount"); } function test_GetRefundedAmount_StatusDepleted() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 actualRefundedAmount = lockup.getRefundedAmount(defaultStreamId); uint128 expectedRefundedAmount = 0; diff --git a/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.tree b/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.tree index 3a8aa8645..0ab67e853 100644 --- a/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.tree +++ b/test/integration/concrete/lockup/get-refunded-amount/getRefundedAmount.tree @@ -1,7 +1,7 @@ getRefundedAmount.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream has been canceled │ ├── given the stream's status is "CANCELED" │ │ └── it should return the correct refunded amount diff --git a/test/integration/concrete/lockup/get-sender/getSender.t.sol b/test/integration/concrete/lockup/get-sender/getSender.t.sol index f4a008439..713c4f1eb 100644 --- a/test/integration/concrete/lockup/get-sender/getSender.t.sol +++ b/test/integration/concrete/lockup/get-sender/getSender.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/get-sender/getSender.tree b/test/integration/concrete/lockup/get-sender/getSender.tree index 50160b661..65b9b412a 100644 --- a/test/integration/concrete/lockup/get-sender/getSender.tree +++ b/test/integration/concrete/lockup/get-sender/getSender.tree @@ -1,5 +1,5 @@ getSender.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct sender diff --git a/test/integration/concrete/lockup/get-start-time/getStartTime.t.sol b/test/integration/concrete/lockup/get-start-time/getStartTime.t.sol index 910091a40..e31b8d734 100644 --- a/test/integration/concrete/lockup/get-start-time/getStartTime.t.sol +++ b/test/integration/concrete/lockup/get-start-time/getStartTime.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/get-start-time/getStartTime.tree b/test/integration/concrete/lockup/get-start-time/getStartTime.tree index d65e107ab..a0c4f6da7 100644 --- a/test/integration/concrete/lockup/get-start-time/getStartTime.tree +++ b/test/integration/concrete/lockup/get-start-time/getStartTime.tree @@ -1,5 +1,5 @@ getStartTime.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return the correct start time diff --git a/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol b/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol index a7d636336..d7cc37828 100644 --- a/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol +++ b/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -22,7 +22,7 @@ abstract contract GetWithdrawnAmount_Integration_Concrete_Test is function test_GetWithdrawnAmount_NoPreviousWithdrawals() external givenNotNull { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Assert that the withdrawn amount has been updated. uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); @@ -32,7 +32,7 @@ abstract contract GetWithdrawnAmount_Integration_Concrete_Test is function test_GetWithdrawnAmount() external givenNotNull givenPreviousWithdrawals { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Set the withdraw amount to the streamed amount. uint128 withdrawAmount = lockup.streamedAmountOf(defaultStreamId); diff --git a/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.tree b/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.tree index 2a88d73c9..ca78cb978 100644 --- a/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.tree +++ b/test/integration/concrete/lockup/get-withdrawn-amount/getWithdrawnAmount.tree @@ -1,7 +1,7 @@ getWithdrawnAmount.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given there are no previous withdrawals │ └── it should return zero └── given there are previous withdrawals diff --git a/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.t.sol b/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.t.sol new file mode 100644 index 000000000..ed92d5fcb --- /dev/null +++ b/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; +import { Integration_Test } from "../../../Integration.t.sol"; + +abstract contract IsAllowedToHook_Integration_Concrete_Test is Integration_Test, Lockup_Integration_Shared_Test { + uint256 internal defaultStreamId; + + function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { + defaultStreamId = createDefaultStream(); + } + + function test_IsAllowedToHook_GivenProvidedAddressIsNotAllowedToHook() external view { + bool result = lockup.isAllowedToHook(address(recipientGood)); + assertFalse(result, "isAllowedToHook"); + } + + modifier givenProvidedAddressIsAllowedToHook() { + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientGood)); + _; + } + + function test_IsAllowedToHook() external givenProvidedAddressIsAllowedToHook { + bool result = lockup.isAllowedToHook(address(recipientGood)); + assertTrue(result, "isAllowedToHook"); + } +} diff --git a/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.tree b/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.tree new file mode 100644 index 000000000..9196064ef --- /dev/null +++ b/test/integration/concrete/lockup/is-allowed-to-hook/isAllowedToHook.tree @@ -0,0 +1,5 @@ +isAllowedToHook.tree +├── given the provided address is not allowed to hook +│ └── it should return false +└── given the provided address is allowed to hook + └── it should return true diff --git a/test/integration/concrete/lockup/is-cancelable/isCancelable.t.sol b/test/integration/concrete/lockup/is-cancelable/isCancelable.t.sol index b773ade31..5f0bf9c48 100644 --- a/test/integration/concrete/lockup/is-cancelable/isCancelable.t.sol +++ b/test/integration/concrete/lockup/is-cancelable/isCancelable.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -23,7 +23,7 @@ abstract contract IsCancelable_Integration_Concrete_Test is Integration_Test, Lo } function test_IsCancelable_Cold() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); // settled status + vm.warp({ newTimestamp: defaults.END_TIME() }); // settled status bool isCancelable = lockup.isCancelable(defaultStreamId); assertFalse(isCancelable, "isCancelable"); } diff --git a/test/integration/concrete/lockup/is-cancelable/isCancelable.tree b/test/integration/concrete/lockup/is-cancelable/isCancelable.tree index 8a36d6dbe..c75cda4f3 100644 --- a/test/integration/concrete/lockup/is-cancelable/isCancelable.tree +++ b/test/integration/concrete/lockup/is-cancelable/isCancelable.tree @@ -1,7 +1,7 @@ isCancelable.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is cold │ └── it should return true └── given the stream is not cold diff --git a/test/integration/concrete/lockup/is-cold/isCold.t.sol b/test/integration/concrete/lockup/is-cold/isCold.t.sol index 214512228..ebb99badb 100644 --- a/test/integration/concrete/lockup/is-cold/isCold.t.sol +++ b/test/integration/concrete/lockup/is-cold/isCold.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -23,32 +23,32 @@ abstract contract IsCold_Integration_Concrete_Test is Integration_Test, Lockup_I } function test_IsCold_StatusPending() external givenNotNull { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); bool isCold = lockup.isCold(defaultStreamId); assertFalse(isCold, "isCold"); } function test_IsCold_StatusStreaming() external givenNotNull { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); bool isCold = lockup.isCold(defaultStreamId); assertFalse(isCold, "isCold"); } function test_IsCold_StatusSettled() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); bool isCold = lockup.isCold(defaultStreamId); assertTrue(isCold, "isCold"); } function test_IsCold_StatusCanceled() external givenNotNull { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); bool isCold = lockup.isCold(defaultStreamId); assertTrue(isCold, "isCold"); } function test_IsCold_StatusDepleted() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); bool isCold = lockup.isCold(defaultStreamId); assertTrue(isCold, "isCold"); diff --git a/test/integration/concrete/lockup/is-cold/isCold.tree b/test/integration/concrete/lockup/is-cold/isCold.tree index 15bfe3fdc..5902b8b9f 100644 --- a/test/integration/concrete/lockup/is-cold/isCold.tree +++ b/test/integration/concrete/lockup/is-cold/isCold.tree @@ -1,7 +1,7 @@ isCold.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream's status is pending │ └── it should return false ├── given the stream's status is streaming diff --git a/test/integration/concrete/lockup/is-depleted/isDepleted.t.sol b/test/integration/concrete/lockup/is-depleted/isDepleted.t.sol index 6d969a5e8..0ba5d78c6 100644 --- a/test/integration/concrete/lockup/is-depleted/isDepleted.t.sol +++ b/test/integration/concrete/lockup/is-depleted/isDepleted.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -28,7 +28,7 @@ abstract contract IsDepleted_Integration_Concrete_Test is Integration_Test, Lock } modifier givenStreamDepleted() { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); _; } diff --git a/test/integration/concrete/lockup/is-depleted/isDepleted.tree b/test/integration/concrete/lockup/is-depleted/isDepleted.tree index 43149d5aa..3b796bd0c 100644 --- a/test/integration/concrete/lockup/is-depleted/isDepleted.tree +++ b/test/integration/concrete/lockup/is-depleted/isDepleted.tree @@ -1,7 +1,7 @@ isDepleted.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is not depleted │ └── it should return false └── given the stream is depleted diff --git a/test/integration/concrete/lockup/is-stream/isStream.t.sol b/test/integration/concrete/lockup/is-stream/isStream.t.sol index fd44ba2ce..ac4f349a2 100644 --- a/test/integration/concrete/lockup/is-stream/isStream.t.sol +++ b/test/integration/concrete/lockup/is-stream/isStream.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; import { Integration_Test } from "../../../Integration.t.sol"; @@ -9,7 +9,7 @@ abstract contract IsStream_Integration_Concrete_Test is Integration_Test, Lockup function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { } - function test_IsStream_Null() external { + function test_IsStream_Null() external view { uint256 nullStreamId = 1729; bool isStream = lockup.isStream(nullStreamId); assertFalse(isStream, "isStream"); diff --git a/test/integration/concrete/lockup/is-stream/isStream.tree b/test/integration/concrete/lockup/is-stream/isStream.tree index 5846352f8..83ae45765 100644 --- a/test/integration/concrete/lockup/is-stream/isStream.tree +++ b/test/integration/concrete/lockup/is-stream/isStream.tree @@ -1,5 +1,5 @@ isStream.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should return false -└── given the id does not reference a null stream +└── given the ID does not reference a null stream └── it should return true diff --git a/test/integration/concrete/lockup/is-transferable/isTransferable.t.sol b/test/integration/concrete/lockup/is-transferable/isTransferable.t.sol index e36599786..e32d1f0f9 100644 --- a/test/integration/concrete/lockup/is-transferable/isTransferable.t.sol +++ b/test/integration/concrete/lockup/is-transferable/isTransferable.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/is-transferable/isTransferable.tree b/test/integration/concrete/lockup/is-transferable/isTransferable.tree index 2fefdc265..aba57c19d 100644 --- a/test/integration/concrete/lockup/is-transferable/isTransferable.tree +++ b/test/integration/concrete/lockup/is-transferable/isTransferable.tree @@ -1,7 +1,7 @@ isTransferable.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is not transferable │ └── it should return false └── given the stream is transferable diff --git a/test/integration/concrete/lockup/is-warm/isWarm.t.sol b/test/integration/concrete/lockup/is-warm/isWarm.t.sol index 91e2c934d..e784adc4c 100644 --- a/test/integration/concrete/lockup/is-warm/isWarm.t.sol +++ b/test/integration/concrete/lockup/is-warm/isWarm.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -23,32 +23,32 @@ abstract contract IsWarm_Integration_Concrete_Test is Integration_Test, Lockup_I } function test_IsWarm_StatusPending() external givenNotNull { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); bool isWarm = lockup.isWarm(defaultStreamId); assertTrue(isWarm, "isWarm"); } function test_IsWarm_StatusStreaming() external givenNotNull { - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); bool isWarm = lockup.isWarm(defaultStreamId); assertTrue(isWarm, "isWarm"); } function test_IsWarm_StatusSettled() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); bool isWarm = lockup.isWarm(defaultStreamId); assertFalse(isWarm, "isWarm"); } function test_IsWarm_StatusCanceled() external givenNotNull { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); bool isWarm = lockup.isWarm(defaultStreamId); assertFalse(isWarm, "isWarm"); } function test_IsWarm_StatusDepleted() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); bool isWarm = lockup.isWarm(defaultStreamId); assertFalse(isWarm, "isWarm"); diff --git a/test/integration/concrete/lockup/is-warm/isWarm.tree b/test/integration/concrete/lockup/is-warm/isWarm.tree index be21fe1d1..7044432bb 100644 --- a/test/integration/concrete/lockup/is-warm/isWarm.tree +++ b/test/integration/concrete/lockup/is-warm/isWarm.tree @@ -1,7 +1,7 @@ isWarm.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream's status is pending │ └── it should return true ├── given the stream's status is streaming diff --git a/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.t.sol b/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.t.sol deleted file mode 100644 index b42cbe56b..000000000 --- a/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.t.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; -import { Integration_Test } from "../../../Integration.t.sol"; - -abstract contract ProtocolRevenues_Integration_Concrete_Test is Integration_Test, Lockup_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { } - - function test_ProtocolRevenues_ProtocolRevenuesZero() external { - uint128 actualProtocolRevenues = base.protocolRevenues(dai); - uint128 expectedProtocolRevenues = 0; - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - } - - modifier givenProtocolRevenuesNotZero() { - // Create the default stream, which will accrue revenues for the protocol. - changePrank({ msgSender: users.sender }); - createDefaultStream(); - changePrank({ msgSender: users.admin }); - _; - } - - function test_ProtocolRevenues() external givenProtocolRevenuesNotZero { - uint128 actualProtocolRevenues = base.protocolRevenues(dai); - uint128 expectedProtocolRevenues = defaults.PROTOCOL_FEE_AMOUNT(); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - } -} diff --git a/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.tree b/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.tree deleted file mode 100644 index 7c575e2f0..000000000 --- a/test/integration/concrete/lockup/protocol-revenues/protocolRevenues.tree +++ /dev/null @@ -1,5 +0,0 @@ -protocolRevenues.t.sol -├── given the protocol revenues are zero -│ └── it should return zero -└── given the protocol revenues are not zero - └── it should return the correct protocol revenues diff --git a/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol b/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol index df7d7381e..698e03894 100644 --- a/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol +++ b/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -24,7 +24,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te function test_RefundableAmountOf_StreamNotCancelable() external givenNotNull { uint256 streamId = createDefaultStreamNotCancelable(); - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); uint128 actualRefundableAmount = lockup.refundableAmountOf(streamId); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); @@ -44,7 +44,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedRefundableAmount = 0; @@ -58,10 +58,10 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ timestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedRefundableAmount = 0; assertEq(actualRefundableAmount, expectedRefundableAmount, "refundableAmount"); @@ -77,7 +77,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedReturnableAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); @@ -89,7 +89,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedReturnableAmount = defaults.REFUND_AMOUNT(); assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); @@ -101,7 +101,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedReturnableAmount = 0; assertEq(actualRefundableAmount, expectedReturnableAmount, "refundableAmount"); @@ -113,7 +113,7 @@ abstract contract RefundableAmountOf_Integration_Concrete_Test is Integration_Te givenStreamIsCancelable givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 actualRefundableAmount = lockup.refundableAmountOf(defaultStreamId); uint128 expectedReturnableAmount = 0; diff --git a/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree b/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree index ebfdbcdad..e46acad0d 100644 --- a/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree +++ b/test/integration/concrete/lockup/refundable-amount-of/refundableAmountOf.tree @@ -1,7 +1,7 @@ refundableAmountOf.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream is not cancelable │ └── it should return zero └── given the stream is cancelable diff --git a/test/integration/concrete/lockup/renounce/renounce.t.sol b/test/integration/concrete/lockup/renounce/renounce.t.sol index 7340dc66b..b6b91fed1 100644 --- a/test/integration/concrete/lockup/renounce/renounce.t.sol +++ b/test/integration/concrete/lockup/renounce/renounce.t.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; -import { ISablierV2LockupRecipient } from "src/interfaces/hooks/ISablierV2LockupRecipient.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; @@ -40,37 +39,37 @@ abstract contract Renounce_Integration_Concrete_Test is Integration_Test, Lockup } function test_RevertGiven_StatusDepleted() external whenNotDelegateCalled givenStreamCold { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamDepleted.selector, defaultStreamId)); lockup.renounce(defaultStreamId); } function test_RevertGiven_StatusCanceled() external whenNotDelegateCalled givenStreamCold { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamCanceled.selector, defaultStreamId)); lockup.renounce(defaultStreamId); } function test_RevertGiven_StatusSettled() external whenNotDelegateCalled givenStreamCold { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamSettled.selector, defaultStreamId)); lockup.renounce(defaultStreamId); } /// @dev This modifier runs the test twice: once with a "PENDING" status, and once with a "STREAMING" status. modifier givenStreamWarm() { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); _; - vm.warp({ timestamp: defaults.START_TIME() }); + vm.warp({ newTimestamp: defaults.START_TIME() }); defaultStreamId = createDefaultStream(); _; } function test_RevertWhen_CallerNotSender() external whenNotDelegateCalled givenStreamWarm { // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Run the test. vm.expectRevert( @@ -98,123 +97,9 @@ abstract contract Renounce_Integration_Concrete_Test is Integration_Test, Lockup _; } - function test_Renounce_RecipientNotContract() - external - whenNotDelegateCalled - givenStreamWarm - whenCallerSender - givenStreamCancelable - { - lockup.renounce(defaultStreamId); - bool isCancelable = lockup.isCancelable(defaultStreamId); - assertFalse(isCancelable, "isCancelable"); - } - - modifier givenRecipientContract() { - _; - } - - function test_Renounce_RecipientDoesNotImplementHook() - external - whenNotDelegateCalled - givenStreamWarm - whenCallerSender - givenStreamCancelable - givenRecipientContract - { - // Create the stream with a no-op contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(noop)); - - // Expect a call to the hook. - vm.expectCall(address(noop), abi.encodeCall(ISablierV2LockupRecipient.onStreamRenounced, (streamId))); - - // Renounce the stream. - lockup.renounce(streamId); - - // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(streamId); - assertFalse(isCancelable, "isCancelable"); - } - - modifier givenRecipientImplementsHook() { - _; - } - - function test_Renounce_RecipientReverts() - external - whenNotDelegateCalled - givenStreamWarm - whenCallerSender - givenStreamCancelable - givenRecipientContract - givenRecipientImplementsHook - { - // Create the stream with a reverting contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(revertingRecipient)); - - // Expect a call to the hook. - vm.expectCall( - address(revertingRecipient), abi.encodeCall(ISablierV2LockupRecipient.onStreamRenounced, (streamId)) - ); - - // Renounce the stream. - lockup.renounce(streamId); - - // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(streamId); - assertFalse(isCancelable, "isCancelable"); - } - - modifier whenRecipientDoesNotRevert() { - _; - } - - function test_Renounce_RecipientReentrancy() - external - whenNotDelegateCalled - givenStreamWarm - whenCallerSender - givenStreamCancelable - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - { - // Create the stream with a reentrant contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(reentrantRecipient)); - - // Expect a call to the hook. - vm.expectCall( - address(reentrantRecipient), abi.encodeCall(ISablierV2LockupRecipient.onStreamRenounced, (streamId)) - ); - - // Renounce the stream. - lockup.renounce(streamId); - - // Assert that the stream is not cancelable anymore. - bool isCancelable = lockup.isCancelable(streamId); - assertFalse(isCancelable, "isCancelable"); - } - - modifier whenNoRecipientReentrancy() { - _; - } - - function test_Renounce() - external - whenNotDelegateCalled - givenStreamWarm - whenCallerSender - givenStreamCancelable - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - whenNoRecipientReentrancy - { + function test_Renounce() external whenNotDelegateCalled givenStreamWarm whenCallerSender givenStreamCancelable { // Create the stream with a contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(goodRecipient)); - - // Expect a call to the hook. - vm.expectCall(address(goodRecipient), abi.encodeCall(ISablierV2LockupRecipient.onStreamRenounced, (streamId))); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/concrete/lockup/renounce/renounce.tree b/test/integration/concrete/lockup/renounce/renounce.tree index be03dbc40..f354e61c6 100644 --- a/test/integration/concrete/lockup/renounce/renounce.tree +++ b/test/integration/concrete/lockup/renounce/renounce.tree @@ -2,9 +2,9 @@ renounce.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── given the id references a null stream + ├── given the ID references a null stream │ └── it should revert - └── given the id does not reference a null stream + └── given the ID does not reference a null stream ├── given the stream is cold │ ├── given the stream's status is "DEPLETED" │ │ └── it should revert @@ -16,25 +16,9 @@ renounce.t.sol ├── when the caller is not the sender │ └── it should revert └── when the caller is the sender - ├── given the recipient is not a contract - │ └── it should renounce the stream - └── given the recipient is a contract - ├── given the recipient does not implement the hook - │ ├── it should renounce the stream - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── given the recipient implements the hook - ├── when the recipient reverts - │ ├── it should renounce the stream - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── when the recipient does not revert - ├── when there is reentrancy - │ ├── it should renounce the stream - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── when there is no reentrancy - ├── it should renounce the stream - ├── it should call the recipient hook - ├── it should emit a {RenounceLockupStream} event - └── it should emit a {MetadataUpdate} event + ├── given the stream is not cancelable + │ └── it should revert + └── given the stream is cancelable + ├── it should renounce the stream + ├── it should emit a {RenounceLockupStream} event + └── it should emit a {MetadataUpdate} event diff --git a/test/integration/concrete/lockup/set-comptroller/setComptroller.t.sol b/test/integration/concrete/lockup/set-comptroller/setComptroller.t.sol deleted file mode 100644 index 0773070bc..000000000 --- a/test/integration/concrete/lockup/set-comptroller/setComptroller.t.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { ISablierV2Comptroller } from "src/interfaces/ISablierV2Comptroller.sol"; -import { Errors } from "src/libraries/Errors.sol"; -import { SablierV2Comptroller } from "src/SablierV2Comptroller.sol"; - -import { Lockup_Integration_Shared_Test } from "../../../shared/lockup/Lockup.t.sol"; -import { Integration_Test } from "../../../Integration.t.sol"; - -abstract contract SetComptroller_Integration_Concrete_Test is Integration_Test, Lockup_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { } - - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - base.setComptroller(ISablierV2Comptroller(users.eve)); - } - - modifier whenCallerAdmin() { - // Make the Admin the caller in the rest of this test suite. - changePrank({ msgSender: users.admin }); - _; - } - - function test_SetComptroller_SameComptroller() external whenCallerAdmin { - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(base) }); - emit SetComptroller(users.admin, comptroller, comptroller); - - // Re-set the comptroller. - base.setComptroller(comptroller); - - // Assert that the comptroller has not been changed. - address actualComptroller = address(base.comptroller()); - address expectedComptroller = address(comptroller); - assertEq(actualComptroller, expectedComptroller, "comptroller"); - } - - function test_SetComptroller_NewComptroller() external whenCallerAdmin { - // Deploy the new comptroller. - ISablierV2Comptroller newComptroller = new SablierV2Comptroller({ initialAdmin: users.admin }); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(base) }); - emit SetComptroller(users.admin, comptroller, newComptroller); - - // Set the new comptroller. - base.setComptroller(newComptroller); - - // Assert that the new comptroller has been set. - address actualComptroller = address(base.comptroller()); - address expectedComptroller = address(newComptroller); - assertEq(actualComptroller, expectedComptroller, "comptroller"); - } -} diff --git a/test/integration/concrete/lockup/set-comptroller/setComptroller.tree b/test/integration/concrete/lockup/set-comptroller/setComptroller.tree deleted file mode 100644 index fae51fdb9..000000000 --- a/test/integration/concrete/lockup/set-comptroller/setComptroller.tree +++ /dev/null @@ -1,10 +0,0 @@ -setComptroller.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── when the new comptroller is the same as the current comptroller - │ ├── it should re-set the comptroller - │ └── it should emit a {SetComptroller} event - └── when the new comptroller is not the same as the current comptroller - ├── it should set the new comptroller - └── it should emit a {SetComptroller} event diff --git a/test/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol b/test/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol index 1ca11acef..2b1c52cf6 100644 --- a/test/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol +++ b/test/integration/concrete/lockup/set-nft-descriptor/setNFTDescriptor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2NFTDescriptor } from "src/interfaces/ISablierV2NFTDescriptor.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -17,7 +17,7 @@ abstract contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test function test_RevertWhen_CallerNotAdmin() external { // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Run the test. vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); @@ -26,7 +26,7 @@ abstract contract SetNFTDescriptor_Integration_Concrete_Test is Integration_Test modifier whenCallerAdmin() { // Make the Admin the caller in the rest of this test suite. - changePrank({ msgSender: users.admin }); + resetPrank({ msgSender: users.admin }); _; } diff --git a/test/integration/concrete/lockup/status-of/statusOf.t.sol b/test/integration/concrete/lockup/status-of/statusOf.t.sol index 75775ebe3..43b275e39 100644 --- a/test/integration/concrete/lockup/status-of/statusOf.t.sol +++ b/test/integration/concrete/lockup/status-of/statusOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { Lockup } from "src/types/DataTypes.sol"; @@ -24,7 +24,7 @@ abstract contract StatusOf_Integration_Concrete_Test is Integration_Test, Lockup } function test_StatusOf_AssetsFullyWithdrawn() external givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; @@ -36,7 +36,7 @@ abstract contract StatusOf_Integration_Concrete_Test is Integration_Test, Lockup } function test_StatusOf_StreamCanceled() external givenNotNull givenAssetsNotFullyWithdrawn { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.CANCELED; @@ -53,7 +53,7 @@ abstract contract StatusOf_Integration_Concrete_Test is Integration_Test, Lockup givenAssetsNotFullyWithdrawn givenStreamNotCanceled { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.PENDING; assertEq(actualStatus, expectedStatus); @@ -70,7 +70,7 @@ abstract contract StatusOf_Integration_Concrete_Test is Integration_Test, Lockup givenStreamNotCanceled givenStartTimeNotInTheFuture { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.SETTLED; assertEq(actualStatus, expectedStatus); @@ -88,7 +88,7 @@ abstract contract StatusOf_Integration_Concrete_Test is Integration_Test, Lockup givenStartTimeNotInTheFuture givenRefundableAmountNotZero { - vm.warp({ timestamp: defaults.START_TIME() + 1 seconds }); + vm.warp({ newTimestamp: defaults.START_TIME() + 1 seconds }); Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); diff --git a/test/integration/concrete/lockup/status-of/statusOf.tree b/test/integration/concrete/lockup/status-of/statusOf.tree index 6d0bfda29..03c12a2ef 100644 --- a/test/integration/concrete/lockup/status-of/statusOf.tree +++ b/test/integration/concrete/lockup/status-of/statusOf.tree @@ -1,7 +1,7 @@ statusOf.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given assets have been fully withdrawn │ └── it should return DEPLETED └── given assets have not been fully withdrawn diff --git a/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol b/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol index caaa72956..acf7d0f17 100644 --- a/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol +++ b/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -25,7 +25,7 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint256 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); @@ -38,31 +38,31 @@ abstract contract StreamedAmountOf_Integration_Concrete_Test is givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ timestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = defaults.CLIFF_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } function test_StreamedAmountOf_StatusPending() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } function test_StreamedAmountOf_StatusSettled() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } function test_StreamedAmountOf_StatusDepleted() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 actualStreamedAmount = lockup.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = defaults.DEPOSIT_AMOUNT(); diff --git a/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree b/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree index dbc0970ab..f0784e8ea 100644 --- a/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree +++ b/test/integration/concrete/lockup/streamed-amount-of/streamedAmountOf.tree @@ -1,7 +1,7 @@ streamedAmountOf.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream has been canceled │ ├── given the stream's status is "CANCELED" │ │ └── it should return the correct streamed amount diff --git a/test/integration/concrete/lockup/transfer-from/transferFrom.t.sol b/test/integration/concrete/lockup/transfer-from/transferFrom.t.sol index 648ee00ce..31987fd0d 100644 --- a/test/integration/concrete/lockup/transfer-from/transferFrom.t.sol +++ b/test/integration/concrete/lockup/transfer-from/transferFrom.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -8,7 +8,7 @@ import { Integration_Test } from "../../../Integration.t.sol"; abstract contract TransferFrom_Integration_Concrete_Test is Integration_Test, Lockup_Integration_Shared_Test { function setUp() public virtual override(Integration_Test, Lockup_Integration_Shared_Test) { - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } function test_RevertGiven_StreamNotTransferable() external { @@ -29,9 +29,9 @@ abstract contract TransferFrom_Integration_Concrete_Test is Integration_Test, Lo // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit Transfer({ from: users.recipient, to: users.alice, tokenId: streamId }); - vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: streamId }); + vm.expectEmit({ emitter: address(lockup) }); + emit Transfer({ from: users.recipient, to: users.alice, tokenId: streamId }); // Transfer the NFT. lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: streamId }); diff --git a/test/integration/concrete/lockup/was-canceled/wasCanceled.t.sol b/test/integration/concrete/lockup/was-canceled/wasCanceled.t.sol index 3eea3c362..fb532ea8b 100644 --- a/test/integration/concrete/lockup/was-canceled/wasCanceled.t.sol +++ b/test/integration/concrete/lockup/was-canceled/wasCanceled.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; diff --git a/test/integration/concrete/lockup/was-canceled/wasCanceled.tree b/test/integration/concrete/lockup/was-canceled/wasCanceled.tree index 5dd4e1e3a..0be9ecbda 100644 --- a/test/integration/concrete/lockup/was-canceled/wasCanceled.tree +++ b/test/integration/concrete/lockup/was-canceled/wasCanceled.tree @@ -1,7 +1,7 @@ wasCanceled.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream was not canceled │ └── it should return false └── given the stream was canceled diff --git a/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol b/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol new file mode 100644 index 000000000..a89bc2c07 --- /dev/null +++ b/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; +import { Withdraw_Integration_Shared_Test } from "../../../shared/lockup/withdraw.t.sol"; + +abstract contract WithdrawHooks_Integration_Concrete_Test is Integration_Test, Withdraw_Integration_Shared_Test { + uint128 internal withdrawAmount; + + function setUp() public virtual override(Integration_Test, Withdraw_Integration_Shared_Test) { + Withdraw_Integration_Shared_Test.setUp(); + withdrawAmount = defaults.WITHDRAW_AMOUNT(); + + // Allow the good recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientGood)); + resetPrank({ msgSender: users.sender }); + } + + function test_WithdrawHooks_GivenSameSenderAndRecipient() external { + // Create a stream with identical sender and recipient. + uint256 streamId = createDefaultStreamWithIdenticalUsers(users.sender); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Expect Sablier to NOT run the user hook. + vm.expectCall({ + callee: users.sender, + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, (streamId, users.sender, users.sender, withdrawAmount) + ), + count: 0 + }); + + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: users.sender, amount: withdrawAmount }); + } + + modifier givenDifferentSenderAndRecipient() { + _; + } + + function test_WithdrawHooks_CallerUnknown() external givenDifferentSenderAndRecipient { + // Create the test stream. + uint256 streamId = createDefaultStreamWithUsers({ recipient: address(recipientGood), sender: users.sender }); + + // Make the unknown address the caller in this test. + address unknownCaller = address(0xCAFE); + resetPrank({ msgSender: unknownCaller }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Expect Sablier to run the recipient hook. + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, unknownCaller, address(recipientGood), withdrawAmount) + ), + count: 1 + }); + + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); + } + + function test_WithdrawHooks_CallerApprovedOperator() external givenDifferentSenderAndRecipient { + // Create the test stream. + uint256 streamId = createDefaultStreamWithUsers({ recipient: address(recipientGood), sender: users.sender }); + + // Approve the operator to handle the stream. + resetPrank({ msgSender: address(recipientGood) }); + lockup.approve({ to: users.operator, tokenId: streamId }); + + // Make the operator the caller in this test. + resetPrank({ msgSender: users.operator }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Expect Sablier to run the recipient hook. + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, users.operator, address(recipientGood), withdrawAmount) + ), + count: 1 + }); + + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); + } + + function test_WithdrawHooks_CallerSender() external givenDifferentSenderAndRecipient { + // Create the test stream. + uint256 streamId = createDefaultStreamWithUsers({ recipient: address(recipientGood), sender: users.sender }); + + // Make the Sender the caller in this test. + resetPrank({ msgSender: users.sender }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Expect Sablier to run the recipient hook. + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, users.sender, address(recipientGood), withdrawAmount) + ), + count: 1 + }); + + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); + } + + function test_WithdrawHooks_CallerRecipient() external givenDifferentSenderAndRecipient { + // Create the test stream. + uint256 streamId = createDefaultStreamWithUsers({ recipient: address(recipientGood), sender: users.sender }); + + // Make the recipient contract the caller in this test. + resetPrank({ msgSender: address(recipientGood) }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Expect Sablier to NOT run the recipient hook. + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, address(recipientGood), address(recipientGood), withdrawAmount) + ), + count: 0 + }); + + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); + } +} diff --git a/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.tree b/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.tree new file mode 100644 index 000000000..dd552a01b --- /dev/null +++ b/test/integration/concrete/lockup/withdraw-hooks/withdrawHooks.tree @@ -0,0 +1,12 @@ +withdrawHooks.t.sol +├── given the recipient is the same as the sender +│ └── it should not make Sablier run the user hook +└── given the recipient is different than the sender + ├── when the caller is unknown + │ └── it should make Sablier run the recipient hook + ├── when the caller is an approved third party + │ └── it should make Sablier run the recipient hook + ├── when the caller is the sender + │ └── it should make Sablier run the recipient hook + └── when the caller is the recipient + └── it should not make Sablier run the recipient hook diff --git a/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol b/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol index 86bddab4f..98f62a5b4 100644 --- a/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol +++ b/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -29,7 +29,7 @@ abstract contract WithdrawMaxAndTransfer_Integration_Concrete_Test is function test_RevertWhen_CallerNotCurrentRecipient() external whenNotDelegateCalled givenNotNull { // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: users.eve }); // Run the test. vm.expectRevert( @@ -40,7 +40,7 @@ abstract contract WithdrawMaxAndTransfer_Integration_Concrete_Test is function test_RevertGiven_NFTBurned() external whenNotDelegateCalled givenNotNull whenCallerCurrentRecipient { // Deplete the stream. - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); // Burn the NFT. @@ -60,7 +60,7 @@ abstract contract WithdrawMaxAndTransfer_Integration_Concrete_Test is whenCallerCurrentRecipient givenNFTNotBurned { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); } @@ -90,33 +90,32 @@ abstract contract WithdrawMaxAndTransfer_Integration_Concrete_Test is givenStreamTransferable { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Get the withdraw amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(defaultStreamId); // Expect the assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: withdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: expectedWithdrawnAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit WithdrawFromLockupStream({ streamId: defaultStreamId, to: users.recipient, - amount: withdrawAmount, + amount: expectedWithdrawnAmount, asset: dai }); vm.expectEmit({ emitter: address(lockup) }); - emit Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); - vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: defaultStreamId }); + vm.expectEmit({ emitter: address(lockup) }); + emit Transfer({ from: users.recipient, to: users.alice, tokenId: defaultStreamId }); // Make the max withdrawal and transfer the NFT. - lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); + uint128 actualWithdrawnAmount = + lockup.withdrawMaxAndTransfer({ streamId: defaultStreamId, newRecipient: users.alice }); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); - uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that Alice is the new stream recipient (and NFT owner). diff --git a/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree b/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree index f53682e9e..91f754730 100644 --- a/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree +++ b/test/integration/concrete/lockup/withdraw-max-and-transfer/withdrawMaxAndTransfer.tree @@ -2,9 +2,9 @@ withdrawMaxAndTransfer.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── given the id references a null stream + ├── given the ID references a null stream │ └── it should revert - └── given the id does not reference a null stream + └── given the ID does not reference a null stream ├── given the stream is not transferable │ └── it should revert └── given the stream is transferable @@ -22,4 +22,5 @@ withdrawMaxAndTransfer.t.sol ├── it should transfer the NFT ├── it should emit a {WithdrawFromLockupStream} event ├── it should emit a {Transfer} event - └── it should emit a {MetadataUpdate} event + ├── it should emit a {MetadataUpdate} event + └── it should return the withdrawable amount diff --git a/test/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol b/test/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol index 2721c5654..e0f6eab91 100644 --- a/test/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol +++ b/test/integration/concrete/lockup/withdraw-max/withdrawMax.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; @@ -13,10 +13,10 @@ abstract contract WithdrawMax_Integration_Concrete_Test is Integration_Test, Wit function test_WithdrawMax_EndTimeNotInTheFuture() external { // Warp to the stream's end. - vm.warp({ timestamp: defaults.END_TIME() + 1 seconds }); + vm.warp({ newTimestamp: defaults.END_TIME() + 1 seconds }); // Expect the ERC-20 assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: defaults.DEPOSIT_AMOUNT() }); + expectCallToTransfer({ to: users.recipient, value: defaults.DEPOSIT_AMOUNT() }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -52,34 +52,32 @@ abstract contract WithdrawMax_Integration_Concrete_Test is Integration_Test, Wit function test_WithdrawMax() external givenEndTimeInTheFuture { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Get the withdraw amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); + uint128 expectedWithdrawnAmount = lockup.withdrawableAmountOf(defaultStreamId); // Expect the assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: withdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: expectedWithdrawnAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); emit WithdrawFromLockupStream({ streamId: defaultStreamId, to: users.recipient, - amount: withdrawAmount, + amount: expectedWithdrawnAmount, asset: dai }); // Make the max withdrawal. - lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + uint128 actualWithdrawnAmount = lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); + + // Assert that the withdrawn amount has been updated. + assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the stream's status is still "STREAMING". Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); - - // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); - uint128 expectedWithdrawnAmount = withdrawAmount; - assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } } diff --git a/test/integration/concrete/lockup/withdraw-max/withdrawMax.tree b/test/integration/concrete/lockup/withdraw-max/withdrawMax.tree index 46b7d1190..866c7f09f 100644 --- a/test/integration/concrete/lockup/withdraw-max/withdrawMax.tree +++ b/test/integration/concrete/lockup/withdraw-max/withdrawMax.tree @@ -4,8 +4,10 @@ withdrawMax.t.sol │ ├── it should update the withdrawn amount │ ├── it should mark the stream as depleted │ ├── it should make the stream not cancelable -│ └── it should emit a {WithdrawFromLockupStream} event +│ ├── it should emit a {WithdrawFromLockupStream} event +│ └── it should return the withdrawn amount └── given the end time is in the future ├── it should make the max withdrawal ├── it should update the withdrawn amount - └── it should emit a {WithdrawFromLockupStream} event + ├── it should emit a {WithdrawFromLockupStream} event + └── it should return the withdrawable amount diff --git a/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol b/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol index 308fc2b3a..fdc0347de 100644 --- a/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol +++ b/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Solarray } from "solarray/src/Solarray.sol"; @@ -19,8 +19,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is } function test_RevertWhen_DelegateCalled() external { - bytes memory callData = - abi.encodeCall(ISablierV2Lockup.withdrawMultiple, (testStreamIds, users.recipient, testAmounts)); + bytes memory callData = abi.encodeCall(ISablierV2Lockup.withdrawMultiple, (testStreamIds, testAmounts)); (bool success, bytes memory returnData) = address(lockup).delegatecall(callData); expectRevertDueToDelegateCall(success, returnData); } @@ -33,7 +32,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is Errors.SablierV2Lockup_WithdrawArrayCountsNotEqual.selector, streamIds.length, amounts.length ) ); - lockup.withdrawMultiple({ streamIds: streamIds, to: users.recipient, amounts: amounts }); + lockup.withdrawMultiple(streamIds, amounts); } modifier whenArrayCountsAreEqual() { @@ -43,7 +42,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is function test_WithdrawMultiple_ArrayCountsZero() external whenNotDelegateCalled whenArrayCountsAreEqual { uint256[] memory streamIds = new uint256[](0); uint128[] memory amounts = new uint128[](0); - lockup.withdrawMultiple({ streamIds: streamIds, to: users.recipient, amounts: amounts }); + lockup.withdrawMultiple(streamIds, amounts); } modifier whenArrayCountsNotZero() { @@ -61,7 +60,6 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); lockup.withdrawMultiple({ streamIds: Solarray.uint256s(nullStreamId), - to: users.recipient, amounts: Solarray.uint128s(withdrawAmount) }); } @@ -76,13 +74,13 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is uint256[] memory streamIds = Solarray.uint256s(testStreamIds[0], testStreamIds[1], nullStreamId); // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Expect the relevant error to be thrown. vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId)); // Withdraw from multiple streams. - lockup.withdrawMultiple({ streamIds: streamIds, to: users.recipient, amounts: testAmounts }); + lockup.withdrawMultiple({ streamIds: streamIds, amounts: testAmounts }); } function test_RevertGiven_AllStatusesDepleted() @@ -96,7 +94,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is uint128[] memory amounts = Solarray.uint128s(defaults.WITHDRAW_AMOUNT()); // Simulate the passage of time. - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); // Deplete the first test stream. lockup.withdrawMax({ streamId: testStreamIds[0], to: users.recipient }); @@ -105,7 +103,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamDepleted.selector, testStreamIds[0])); // Withdraw from multiple streams. - lockup.withdrawMultiple({ streamIds: streamIds, to: users.recipient, amounts: amounts }); + lockup.withdrawMultiple(streamIds, amounts); } function test_RevertGiven_SomeStatusesDepleted() @@ -116,7 +114,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is givenNoNull { // Simulate the passage of time. - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); // Deplete the first test stream. lockup.withdrawMax({ streamId: testStreamIds[0], to: users.recipient }); @@ -125,113 +123,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_StreamDepleted.selector, testStreamIds[0])); // Withdraw from multiple streams. - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: testAmounts }); - } - - function test_RevertWhen_CallerUnauthorizedAllStreams_MaliciousThirdParty() - external - whenNotDelegateCalled - whenArrayCountsAreEqual - whenArrayCountsNotZero - givenNoNull - givenNoDepletedStream - whenCallerUnauthorized - { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, testStreamIds[0], users.eve) - ); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: testAmounts }); - } - - function test_RevertWhen_CallerUnauthorizedAllStreams_FormerRecipient() - external - whenNotDelegateCalled - whenArrayCountsAreEqual - whenArrayCountsNotZero - givenNoNull - givenNoDepletedStream - whenCallerUnauthorized - { - // Transfer all streams to Alice. - changePrank({ msgSender: users.recipient }); - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: testStreamIds[0] }); - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: testStreamIds[1] }); - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: testStreamIds[2] }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, testStreamIds[0], users.recipient) - ); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: testAmounts }); - } - - function test_RevertWhen_CallerUnauthorizedSomeStreams_MaliciousThirdParty() - external - whenNotDelegateCalled - whenArrayCountsAreEqual - whenArrayCountsNotZero - givenNoNull - givenNoDepletedStream - whenCallerUnauthorized - { - // Create a stream with Eve as the stream's recipient. - uint256 eveStreamId = createDefaultStreamWithRecipient(users.eve); - - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); - - // Run the test. - uint256[] memory streamIds = Solarray.uint256s(eveStreamId, testStreamIds[0], testStreamIds[1]); - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, testStreamIds[0], users.eve) - ); - lockup.withdrawMultiple({ streamIds: streamIds, to: users.recipient, amounts: testAmounts }); - } - - function test_RevertWhen_CallerUnauthorizedSomeStreams_FormerRecipient() - external - whenNotDelegateCalled - whenArrayCountsAreEqual - whenArrayCountsNotZero - givenNoNull - givenNoDepletedStream - whenCallerUnauthorized - { - // Transfer one of the streams to Eve. - changePrank({ msgSender: users.recipient }); - lockup.transferFrom({ from: users.recipient, to: users.alice, tokenId: testStreamIds[0] }); - - // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, testStreamIds[0], users.recipient) - ); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: testAmounts }); - } - - function test_RevertWhen_ToZeroAddress() - external - whenNotDelegateCalled - whenArrayCountsAreEqual - whenArrayCountsNotZero - givenNoNull - givenNoDepletedStream - whenCallerAuthorizedAllStreams - { - if (caller == users.sender) { - return; - } - vm.expectRevert(Errors.SablierV2Lockup_WithdrawToZeroAddress.selector); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: address(0), amounts: testAmounts }); + lockup.withdrawMultiple({ streamIds: testStreamIds, amounts: testAmounts }); } function test_RevertWhen_SomeAmountsZero() @@ -241,16 +133,15 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is whenArrayCountsNotZero givenNoNull givenNoDepletedStream - whenCallerAuthorizedAllStreams whenToNonZeroAddress { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Run the test. uint128[] memory amounts = Solarray.uint128s(defaults.WITHDRAW_AMOUNT(), 0, 0); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_WithdrawAmountZero.selector, testStreamIds[1])); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: amounts }); + lockup.withdrawMultiple({ streamIds: testStreamIds, amounts: amounts }); } function test_RevertWhen_SomeAmountsOverdraw() @@ -260,12 +151,11 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is whenArrayCountsNotZero givenNoNull givenNoDepletedStream - whenCallerAuthorizedAllStreams whenToNonZeroAddress whenNoAmountZero { // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Run the test. uint128 withdrawableAmount = lockup.withdrawableAmountOf(testStreamIds[2]); @@ -275,7 +165,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is Errors.SablierV2Lockup_Overdraw.selector, testStreamIds[2], MAX_UINT128, withdrawableAmount ) ); - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: amounts }); + lockup.withdrawMultiple({ streamIds: testStreamIds, amounts: amounts }); } function test_WithdrawMultiple() @@ -285,25 +175,24 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is whenArrayCountsNotZero givenNoNull givenNoDepletedStream - whenCallerAuthorizedAllStreams whenToNonZeroAddress whenNoAmountZero whenNoAmountOverdraws { // Simulate the passage of time. - vm.warp({ timestamp: earlyStopTime }); + vm.warp({ newTimestamp: earlyStopTime }); // Cancel the 3rd stream. - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); lockup.cancel(testStreamIds[2]); // Run the test with the caller provided in {whenCallerAuthorizedAllStreams}. - changePrank({ msgSender: caller }); + resetPrank({ msgSender: caller }); // Expect the withdrawals to be made. - expectCallToTransfer({ to: users.recipient, amount: testAmounts[0] }); - expectCallToTransfer({ to: users.recipient, amount: testAmounts[1] }); - expectCallToTransfer({ to: users.recipient, amount: testAmounts[2] }); + expectCallToTransfer({ to: users.recipient, value: testAmounts[0] }); + expectCallToTransfer({ to: users.recipient, value: testAmounts[1] }); + expectCallToTransfer({ to: users.recipient, value: testAmounts[2] }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -329,7 +218,7 @@ abstract contract WithdrawMultiple_Integration_Concrete_Test is }); // Make the withdrawals. - lockup.withdrawMultiple({ streamIds: testStreamIds, to: users.recipient, amounts: testAmounts }); + lockup.withdrawMultiple({ streamIds: testStreamIds, amounts: testAmounts }); // Assert that the statuses have been updated. assertEq(lockup.statusOf(testStreamIds[0]), Lockup.Status.STREAMING, "status0"); diff --git a/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree b/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree index 52ca2f9e7..c9fbb0d35 100644 --- a/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree +++ b/test/integration/concrete/lockup/withdraw-multiple/withdrawMultiple.tree @@ -8,37 +8,23 @@ withdrawMultiple.t.sol ├── when the array counts are zero │ └── it should do nothing └── when the array counts are not zero - ├── given the stream ids array references only null streams + ├── given the stream IDs array references only null streams │ └── it should revert - ├── given the stream ids array references some null streams + ├── given the stream IDs array references some null streams │ └── it should revert - └── given the stream ids array references only non-null streams + └── given the stream IDs array references only non-null streams ├── given all streams' statuses are "DEPLETED" │ └── it should revert ├── given some streams' statuses are "DEPLETED" │ └── it should revert └── given no stream's status is "DEPLETED" - ├── when the caller is unauthorized for all streams - │ ├── when the caller is a malicious third party - │ │ └── it should revert - │ └── when the caller is a former recipient - │ └── it should revert - ├── when the caller is unauthorized for some streams - │ ├── when the caller is a malicious third party - │ │ └── it should revert - │ └── when the caller is a former recipient - │ └── it should revert - └── when the caller is authorized for all streams - ├── when the provided address is zero + ├── when some amounts are zero + │ └── it should revert + └── when none of the amounts are zero + ├── when some amounts overdraw │ └── it should revert - └── when the provided address is not zero - ├── when some amounts are zero - │ └── it should revert - └── when none of the amounts are zero - ├── when some amounts overdraw - │ └── it should revert - └── when no amount overdraws - ├── it should make the withdrawals - ├── it should update the statuses - ├── it should update the withdrawn amounts - └── it should emit multiple {WithdrawFromLockupStream} events + └── when no amount overdraws + ├── it should make the withdrawals + ├── it should update the statuses + ├── it should update the withdrawn amounts + └── it should emit multiple {WithdrawFromLockupStream} events diff --git a/test/integration/concrete/lockup/withdraw/withdraw.t.sol b/test/integration/concrete/lockup/withdraw/withdraw.t.sol index fbbeca2f6..66815be10 100644 --- a/test/integration/concrete/lockup/withdraw/withdraw.t.sol +++ b/test/integration/concrete/lockup/withdraw/withdraw.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; +import { ISablierLockupRecipient } from "src/interfaces/ISablierLockupRecipient.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; -import { ISablierV2LockupRecipient } from "src/interfaces/hooks/ISablierV2LockupRecipient.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Lockup } from "src/types/DataTypes.sol"; @@ -15,6 +15,10 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr Withdraw_Integration_Shared_Test.setUp(); } + /*////////////////////////////////////////////////////////////////////////// + TESTS + //////////////////////////////////////////////////////////////////////////*/ + function test_RevertWhen_DelegateCalled() external { uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); bytes memory callData = @@ -31,7 +35,7 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr } function test_RevertGiven_StreamDepleted() external whenNotDelegateCalled givenNotNull { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); @@ -39,120 +43,197 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); } - function test_RevertWhen_CallerUnauthorized_Sender() + function test_RevertWhen_ToZeroAddress() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted { + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_WithdrawToZeroAddress.selector, defaultStreamId)); + lockup.withdraw({ streamId: defaultStreamId, to: address(0), amount: withdrawAmount }); + } + + function test_RevertWhen_WithdrawAmountZero() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerUnauthorized + whenToNonZeroAddress { - // Make the Sender the caller in this test. - changePrank({ msgSender: users.sender }); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_WithdrawAmountZero.selector, defaultStreamId)); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: 0 }); + } - // Run the test. - uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + function test_RevertWhen_Overdraw() + external + whenNotDelegateCalled + givenNotNull + givenStreamNotDepleted + whenToNonZeroAddress + whenWithdrawAmountNotZero + { + uint128 withdrawableAmount = 0; vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2Lockup_InvalidSenderWithdrawal.selector, defaultStreamId, users.sender, users.sender + Errors.SablierV2Lockup_Overdraw.selector, defaultStreamId, MAX_UINT128, withdrawableAmount ) ); - lockup.withdraw({ streamId: defaultStreamId, to: users.sender, amount: withdrawAmount }); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: MAX_UINT128 }); } - function test_RevertWhen_CallerUnauthorized_MaliciousThirdParty() + function test_RevertWhen_CallerUnknown() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerUnauthorized + whenWithdrawAmountNotZero + whenNoOverdraw + whenWithdrawalAddressNotRecipient { + address unknownCaller = address(0xCAFE); + // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); + resetPrank({ msgSender: unknownCaller }); // Run the test. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, defaultStreamId, users.eve) + abi.encodeWithSelector( + Errors.SablierV2Lockup_WithdrawalAddressNotRecipient.selector, + defaultStreamId, + unknownCaller, + unknownCaller + ) ); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw({ streamId: defaultStreamId, to: unknownCaller, amount: withdrawAmount }); } - function test_RevertWhen_FormerRecipient() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted { - // Transfer the stream to Alice. - lockup.transferFrom(users.recipient, users.alice, defaultStreamId); + function test_RevertWhen_CallerSender() + external + whenNotDelegateCalled + givenNotNull + givenStreamNotDepleted + whenWithdrawAmountNotZero + whenNoOverdraw + whenWithdrawalAddressNotRecipient + { + // Make the Sender the caller in this test. + resetPrank({ msgSender: users.sender }); // Run the test. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_Unauthorized.selector, defaultStreamId, users.recipient) + abi.encodeWithSelector( + Errors.SablierV2Lockup_WithdrawalAddressNotRecipient.selector, + defaultStreamId, + users.sender, + users.sender + ) ); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); + lockup.withdraw({ streamId: defaultStreamId, to: users.sender, amount: withdrawAmount }); } - function test_RevertWhen_ToZeroAddress() + function test_RevertWhen_CallerFormerRecipient() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized + whenWithdrawAmountNotZero + whenNoOverdraw + whenWithdrawalAddressNotRecipient { + // Transfer the stream to Alice. + lockup.transferFrom(users.recipient, users.alice, defaultStreamId); + + // Run the test. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); - vm.expectRevert(Errors.SablierV2Lockup_WithdrawToZeroAddress.selector); - lockup.withdraw({ streamId: defaultStreamId, to: address(0), amount: withdrawAmount }); + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2Lockup_WithdrawalAddressNotRecipient.selector, + defaultStreamId, + users.recipient, + users.recipient + ) + ); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); } - function test_RevertWhen_WithdrawAmountZero() + function test_Withdraw_CallerApprovedOperator() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress + whenWithdrawAmountNotZero + whenNoOverdraw + whenWithdrawalAddressNotRecipient { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_WithdrawAmountZero.selector, defaultStreamId)); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: 0 }); + // Approve the operator to handle the stream. + lockup.approve({ to: users.operator, tokenId: defaultStreamId }); + + // Make the operator the caller in this test. + resetPrank({ msgSender: users.operator }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Make the withdrawal. + lockup.withdraw({ streamId: defaultStreamId, to: users.operator, amount: defaults.WITHDRAW_AMOUNT() }); + + // Assert that the withdrawn amount has been updated. + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); + assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - function test_RevertWhen_Overdraw() + function test_Withdraw_CallerRecipient1() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero + whenNoOverdraw + whenWithdrawalAddressNotRecipient { - uint128 withdrawableAmount = 0; - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2Lockup_Overdraw.selector, defaultStreamId, MAX_UINT128, withdrawableAmount - ) - ); - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: MAX_UINT128 }); - } + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Set the withdraw amount to the default amount. + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + + // Expect the assets to be transferred to Alice. + expectCallToTransfer({ to: users.alice, value: withdrawAmount }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockup) }); + emit WithdrawFromLockupStream({ streamId: defaultStreamId, to: users.alice, asset: dai, amount: withdrawAmount }); + vm.expectEmit({ emitter: address(lockup) }); + emit MetadataUpdate({ _tokenId: defaultStreamId }); + + // Make the withdrawal. + lockup.withdraw({ streamId: defaultStreamId, to: users.alice, amount: withdrawAmount }); - modifier whenNoOverdraw() { - _; + // Assert that the withdrawn amount has been updated. + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 expectedWithdrawnAmount = withdrawAmount; + assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - function test_Withdraw_CallerRecipient() + function test_Withdraw_CallerUnknown() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient { - // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + // Make the unknown address the caller in this test. + resetPrank({ msgSender: address(0xCAFE) }); - // Make Alice the `to` address in this test. - address to = users.alice; + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: to, amount: defaults.WITHDRAW_AMOUNT() }); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); // Assert that the withdrawn amount has been updated. uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); @@ -160,24 +241,18 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - function test_Withdraw_CallerApprovedOperator() + function test_Withdraw_CallerRecipient2() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient { - // Approve the operator to handle the stream. - lockup.approve({ to: users.operator, tokenId: defaultStreamId }); - - // Make the operator the caller in this test. - changePrank({ msgSender: users.operator }); - // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); @@ -188,24 +263,19 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - modifier whenCallerSender() { - changePrank({ msgSender: users.sender }); - _; - } - function test_Withdraw_EndTimeNotInTheFuture() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender { // Warp to the stream's end. - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); // Make the withdrawal. lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.DEPOSIT_AMOUNT() }); @@ -225,224 +295,173 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } - modifier givenEndTimeInTheFuture() { - // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); - _; - } - function test_Withdraw_StreamHasBeenCanceled() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - whenNoRecipientReentrancy { - // Create the stream with a contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(goodRecipient)); - // Cancel the stream. - lockup.cancel(streamId); + lockup.cancel(defaultStreamId); // Set the withdraw amount to the withdrawable amount. - uint128 withdrawAmount = lockup.withdrawableAmountOf(streamId); + uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: address(goodRecipient), amount: withdrawAmount }); + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); // Assert that the stream's status is "DEPLETED". - Lockup.Status actualStatus = lockup.statusOf(streamId); + Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); Lockup.Status expectedStatus = Lockup.Status.DEPLETED; assertEq(actualStatus, expectedStatus); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(streamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); // Assert that the NFT has not been burned. - address actualNFTowner = lockup.ownerOf({ tokenId: streamId }); - address expectedNFTOwner = address(goodRecipient); + address actualNFTowner = lockup.ownerOf({ tokenId: defaultStreamId }); + address expectedNFTOwner = users.recipient; assertEq(actualNFTowner, expectedNFTOwner, "NFT owner"); } - function test_Withdraw_RecipientNotContract() + function test_Withdraw_RecipientNotAllowedToHook() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture whenStreamHasNotBeenCanceled { - // Set the withdraw amount to the streamed amount. - uint128 withdrawAmount = lockup.streamedAmountOf(defaultStreamId); - - // Expect the assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: withdrawAmount }); + // Create the stream with a recipient contract that implements {ISablierLockupRecipient}. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(lockup) }); - emit WithdrawFromLockupStream({ - streamId: defaultStreamId, - to: users.recipient, - asset: dai, - amount: withdrawAmount + // Expect Sablier to NOT run the recipient hook. + uint128 withdrawAmount = lockup.withdrawableAmountOf(streamId); + vm.expectCall({ + callee: address(recipientGood), + data: abi.encodeCall( + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, users.sender, address(recipientGood), withdrawAmount) + ), + count: 0 }); // Make the withdrawal. - lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: withdrawAmount }); - - // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); - Lockup.Status expectedStatus = Lockup.Status.STREAMING; - assertEq(actualStatus, expectedStatus); + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(streamId); uint128 expectedWithdrawnAmount = withdrawAmount; assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - modifier givenRecipientContract() { - _; - } - - function test_Withdraw_RecipientDoesNotImplementHook() + function test_Withdraw_RecipientReverting() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture whenStreamHasNotBeenCanceled - givenRecipientContract + givenRecipientAllowedToHook { - // Create the stream with a no-op contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(noop)); + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientReverting)); + resetPrank({ msgSender: users.sender }); - // Expect a call to the hook. - vm.expectCall( - address(noop), - abi.encodeCall( - ISablierV2LockupRecipient.onStreamWithdrawn, - (streamId, users.sender, address(noop), defaults.WITHDRAW_AMOUNT()) - ) - ); - - // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: address(noop), amount: defaults.WITHDRAW_AMOUNT() }); - - // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.STREAMING; - assertEq(actualStatus, expectedStatus); + // Create the stream with a reverting contract as the stream's recipient. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientReverting)); - // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(streamId); - uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); - assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); - } + // Expect a revert. + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + vm.expectRevert("You shall not pass"); - modifier givenRecipientImplementsHook() { - _; + // Make the withdrawal. + lockup.withdraw({ streamId: streamId, to: address(recipientReverting), amount: withdrawAmount }); } - function test_Withdraw_RecipientReverts() + function test_Cancel_RecipientReturnsInvalidSelector() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture whenStreamHasNotBeenCanceled - givenRecipientContract - givenRecipientImplementsHook + givenRecipientAllowedToHook + whenRecipientNotReverting { - // Create the stream with a reverting contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(revertingRecipient)); + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientInvalidSelector)); + resetPrank({ msgSender: users.sender }); - // Expect a call to the hook. - vm.expectCall( - address(revertingRecipient), - abi.encodeCall( - ISablierV2LockupRecipient.onStreamWithdrawn, - (streamId, users.sender, address(revertingRecipient), defaults.WITHDRAW_AMOUNT()) + // Create the stream with a recipient contract that returns invalid selector bytes on the hook call. + uint256 streamId = createDefaultStreamWithRecipient(address(recipientInvalidSelector)); + + // Expect a revert. + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2Lockup_InvalidHookSelector.selector, address(recipientInvalidSelector) ) ); - // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: address(revertingRecipient), amount: defaults.WITHDRAW_AMOUNT() }); - - // Assert that the stream's status is still "STREAMING". - Lockup.Status actualStatus = lockup.statusOf(streamId); - Lockup.Status expectedStatus = Lockup.Status.STREAMING; - assertEq(actualStatus, expectedStatus); - - // Assert that the withdrawn amount has been updated. - uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(streamId); - uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); - assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); - } - - modifier whenRecipientDoesNotRevert() { - _; + // Cancel the stream. + lockup.withdraw({ streamId: streamId, to: address(recipientInvalidSelector), amount: withdrawAmount }); } - function test_Withdraw_RecipientReentrancy() + function test_Withdraw_RecipientReentrant() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture whenStreamHasNotBeenCanceled - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert + givenRecipientAllowedToHook + whenRecipientNotReverting + whenRecipientReturnsSelector { + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientReentrant)); + resetPrank({ msgSender: users.sender }); + // Create the stream with a reentrant contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(reentrantRecipient)); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientReentrant)); // Halve the withdraw amount so that the recipient can re-entry and make another withdrawal. uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT() / 2; - // Expect a call to the hook. - vm.expectCall( - address(reentrantRecipient), - abi.encodeCall( - ISablierV2LockupRecipient.onStreamWithdrawn, - (streamId, users.sender, address(reentrantRecipient), withdrawAmount) - ) - ); - // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: address(reentrantRecipient), amount: withdrawAmount }); + lockup.withdraw({ streamId: streamId, to: address(recipientReentrant), amount: withdrawAmount }); // Assert that the stream's status is still "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); @@ -455,42 +474,43 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); } - modifier whenNoRecipientReentrancy() { - _; - } - function test_Withdraw() external whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero whenNoOverdraw + whenWithdrawalAddressIsRecipient whenCallerSender givenEndTimeInTheFuture whenStreamHasNotBeenCanceled - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - whenNoRecipientReentrancy + givenRecipientAllowedToHook + whenRecipientNotReverting + whenRecipientReturnsSelector + whenRecipientNotReentrant { + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientGood)); + resetPrank({ msgSender: users.sender }); + // Create the stream with a contract as the stream's recipient. - uint256 streamId = createDefaultStreamWithRecipient(address(goodRecipient)); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); - // Set the withdraw amount to the streamed amount. - uint128 withdrawAmount = lockup.streamedAmountOf(streamId); + // Set the withdraw amount to the default amount. + uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT(); // Expect the assets to be transferred to the recipient contract. - expectCallToTransfer({ to: address(goodRecipient), amount: withdrawAmount }); + expectCallToTransfer({ to: address(recipientGood), value: withdrawAmount }); - // Expect a call to the hook. + // Expect a call to the hook if the recipient is a contract. vm.expectCall( - address(goodRecipient), + address(recipientGood), abi.encodeCall( - ISablierV2LockupRecipient.onStreamWithdrawn, - (streamId, users.sender, address(goodRecipient), withdrawAmount) + ISablierLockupRecipient.onSablierLockupWithdraw, + (streamId, users.sender, address(recipientGood), withdrawAmount) ) ); @@ -498,7 +518,7 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr vm.expectEmit({ emitter: address(lockup) }); emit WithdrawFromLockupStream({ streamId: streamId, - to: address(goodRecipient), + to: address(recipientGood), asset: dai, amount: withdrawAmount }); @@ -506,7 +526,7 @@ abstract contract Withdraw_Integration_Concrete_Test is Integration_Test, Withdr emit MetadataUpdate({ _tokenId: streamId }); // Make the withdrawal. - lockup.withdraw({ streamId: streamId, to: address(goodRecipient), amount: withdrawAmount }); + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); // Assert that the stream's status is still "STREAMING". Lockup.Status actualStatus = lockup.statusOf(streamId); diff --git a/test/integration/concrete/lockup/withdraw/withdraw.tree b/test/integration/concrete/lockup/withdraw/withdraw.tree index c36b88582..32a43c469 100644 --- a/test/integration/concrete/lockup/withdraw/withdraw.tree +++ b/test/integration/concrete/lockup/withdraw/withdraw.tree @@ -2,72 +2,75 @@ withdraw.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── given the id references a null stream - │ └── it should revert - └── given the id does not reference a null stream - ├── given the stream's status is "DEPLETED" - │ └── it should revert - └── given the stream's status is not "DEPLETED" - ├── when the caller is unauthorized - │ ├── when the caller is the sender - │ │ └── it should revert - │ ├── when the caller is a malicious third party - │ │ └── it should revert - │ └── when the caller is a former recipient - │ └── it should revert - └── when the caller is authorized - ├── when the provided address is zero - │ └── it should revert - └── when the provided address is not zero - ├── when the withdraw amount is zero - │ └── it should revert - └── when the withdraw amount is not zero - ├── when the withdraw amount overdraws - │ └── it should revert - └── when the withdraw amount does not overdraw - ├── when the caller is the recipient - │ ├── it should make the withdrawal - │ └── it should update the withdrawn amount - ├── when the caller is an approved third party - │ ├── it should make the withdrawal - │ └── it should update the withdrawn amount - └── when the caller is the sender - ├── given the end time is not in the future - │ ├── it should make the withdrawal - │ ├── it should mark the stream as depleted - │ └── it should make the stream not cancelable - └── given the end time is in the future - ├── given the stream has been canceled - │ ├── it should make the withdrawal - │ ├── it should mark the stream as depleted - │ ├── it should update the withdrawn amount - │ ├── it should call the recipient hook - │ ├── it should emit a {MetadataUpdate} event - │ └── it should emit a {WithdrawFromLockupStream} event - └── given the stream has not been canceled - ├── given the recipient is not a contract - │ └── it should make the withdrawal - │ └── it should update the withdrawn amount - └── given the recipient is a contract - ├── given the recipient does not implement the hook - │ ├── it should make the withdrawal - │ ├── it should update the withdrawn amount - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── given the recipient implements the hook - ├── when the recipient reverts - │ ├── it should make the withdrawal - │ ├── it should update the withdrawn amount - │ ├── it should call the recipient hook - │ └── it should ignore the revert - └── when the recipient does not revert - ├── when there is reentrancy - │ ├── it should make multiple withdrawals - │ ├── it should update the withdrawn amounts - │ └── it should call the recipient hooks - └── when there is no reentrancy - ├── it should make the withdrawal - ├── it should update the withdrawn amount - ├── it should call the recipient hook - ├── it should emit a {MetadataUpdate} event - └── it should emit a {WithdrawFromLockupStream} event + ├── given the ID references a null stream + │ └── it should revert + └── given the ID does not reference a null stream + ├── given the stream's status is "DEPLETED" + │ └── it should revert + └── given the stream's status is not "DEPLETED" + ├── when the provided address is zero + │ └── it should revert + └── when the provided address is not zero + ├── when the withdraw amount is zero + │ └── it should revert + └── when the withdraw amount is not zero + ├── when the withdraw amount overdraws + │ └── it should revert + └── when the withdraw amount does not overdraw + ├── when the withdrawal address is not the stream recipient + │ ├── when the caller is unknown + │ │ └── it should revert + │ ├── when the caller is the sender + │ │ └── it should revert + │ ├── when the caller is a former recipient + │ │ └── it should revert + │ ├── when the caller is an approved third party + │ │ ├── it should make the withdrawal + │ │ └── it should update the withdrawn amount + │ └── when the caller is the recipient + │ ├── it should make the withdrawal + │ ├── it should update the withdrawn amount + │ ├── it should emit a {MetadataUpdate} event + │ └── it should emit a {WithdrawFromLockupStream} event + └── when the withdrawal address is the stream recipient + ├── when the caller is unknown + │ ├── it should make the withdrawal + │ └── it should update the withdrawn amount + ├── when the caller is the recipient + │ ├── it should make the withdrawal + │ └── it should update the withdrawn amount + └── when the caller is the sender + ├── given the end time is not in the future + │ ├── it should make the withdrawal + │ ├── it should mark the stream as depleted + │ └── it should make the stream not cancelable + └── given the end time is in the future + ├── given the stream has been canceled + │ ├── it should make the withdrawal + │ ├── it should mark the stream as depleted + │ ├── it should update the withdrawn amount + │ ├── it should make Sablier run the recipient hook + │ ├── it should emit a {MetadataUpdate} event + │ └── it should emit a {WithdrawFromLockupStream} event + └── given the stream has not been canceled + ├── given the recipient is not allowed to hook + │ ├── it should make the withdrawal + │ ├── it should update the withdrawn amount + │ └── it should not make Sablier run the recipient hook + └── given the recipient is allowed to hook + ├── when the recipient reverts + │ └── it should revert the entire transaction + └── when the recipient does not revert + ├── when the recipient hook does not return a valid selector + │ └── it should revert + └── when the recipient hook returns a valid selector + ├── when there is reentrancy + │ ├── it should make multiple withdrawals + │ ├── it should update the withdrawn amounts + │ └── it should make Sablier run the recipient hook + └── when there is no reentrancy + ├── it should make the withdrawal + ├── it should update the withdrawn amount + ├── it should make Sablier run the recipient hook + ├── it should emit a {MetadataUpdate} event + └── it should emit a {WithdrawFromLockupStream} event diff --git a/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol b/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol index 5f38eceb8..7677fd2b2 100644 --- a/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol +++ b/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -25,7 +25,7 @@ abstract contract WithdrawableAmountOf_Integration_Concrete_Test is givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); uint256 expectedWithdrawableAmount = defaults.CLIFF_AMOUNT(); @@ -38,31 +38,31 @@ abstract contract WithdrawableAmountOf_Integration_Concrete_Test is givenNotNull givenStreamHasBeenCanceled { - vm.warp({ timestamp: defaults.CLIFF_TIME() }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() }); lockup.cancel(defaultStreamId); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); - vm.warp({ timestamp: defaults.CLIFF_TIME() + 10 seconds }); + vm.warp({ newTimestamp: defaults.CLIFF_TIME() + 10 seconds }); uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_WithdrawableAmountOf_StatusPending() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: getBlockTimestamp() - 1 seconds }); + vm.warp({ newTimestamp: getBlockTimestamp() - 1 seconds }); uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_WithdrawableAmountOf_StatusSettled() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = defaults.DEPOSIT_AMOUNT(); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } function test_WithdrawableAmountOf_StatusDepleted() external givenNotNull givenStreamHasNotBeenCanceled { - vm.warp({ timestamp: defaults.END_TIME() }); + vm.warp({ newTimestamp: defaults.END_TIME() }); lockup.withdrawMax({ streamId: defaultStreamId, to: users.recipient }); uint128 actualWithdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = 0; diff --git a/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree b/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree index 27c4896ef..183bddf1b 100644 --- a/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree +++ b/test/integration/concrete/lockup/withdrawable-amount-of/withdrawableAmountOf.tree @@ -1,7 +1,7 @@ withdrawableAmountOf.t.sol -├── given the id references a null stream +├── given the ID references a null stream │ └── it should revert -└── given the id does not reference a null stream +└── given the ID does not reference a null stream ├── given the stream has been canceled │ ├── given the stream's status is "CANCELED" │ │ └── it should return the correct withdrawable amount diff --git a/test/integration/concrete/nft-descriptor/generateAccentColor.t.sol b/test/integration/concrete/nft-descriptor/generateAccentColor.t.sol index 4e956be9c..d064e9c3f 100644 --- a/test/integration/concrete/nft-descriptor/generateAccentColor.t.sol +++ b/test/integration/concrete/nft-descriptor/generateAccentColor.t.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { NFTDescriptor_Integration_Concrete_Test } from "./NFTDescriptor.t.sol"; +import { NFTDescriptor_Integration_Shared_Test } from "../../shared/nft-descriptor/NFTDescriptor.t.sol"; -contract GenerateAccentColor_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test { - function test_GenerateAccentColor() external { +contract GenerateAccentColor_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test { + function test_GenerateAccentColor() external view { // Passing a dummy contract instead of a real Sablier contract to make this test easy to maintain. + // Note: the address of `noop` depends on the order of the state variables in {Base_Test}. string memory actualColor = nftDescriptorMock.generateAccentColor_({ sablier: address(noop), streamId: 1337 }); string memory expectedColor = "hsl(302,69%,44%)"; assertEq(actualColor, expectedColor, "accentColor"); diff --git a/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.t.sol b/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.t.sol new file mode 100644 index 000000000..95c4ea4a8 --- /dev/null +++ b/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol"; + +contract IsAllowedCharacter_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test { + function test_IsAllowedCharacter_EmptyString() external view { + string memory symbol = ""; + bool result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + } + + modifier whenNotEmptyString() { + _; + } + + function test_IsAllowedCharacter_ContainsUnsupportedCharacters() external view whenNotEmptyString { + string memory symbol = ""; + bool result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo/"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo\\"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo%"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo&"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo("; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo)"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo\""; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo'"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo`"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo;"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + + symbol = "foo%20"; // URL-encoded empty space + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertFalse(result, "isAllowedCharacter"); + } + + modifier whenOnlySupportedCharacters() { + _; + } + + function test_IsAllowedCharacter() external view whenNotEmptyString whenOnlySupportedCharacters { + string memory symbol = "foo"; + bool result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "Foo"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "Foo "; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "Foo Bar"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "Bar-Foo"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = " "; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "foo01234"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + + symbol = "123456789"; + result = nftDescriptorMock.isAllowedCharacter_(symbol); + assertTrue(result, "isAllowedCharacter"); + } +} diff --git a/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.tree b/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.tree new file mode 100644 index 000000000..924004a0a --- /dev/null +++ b/test/integration/concrete/nft-descriptor/is-allowed-character/IsAllowedCharacter.tree @@ -0,0 +1,8 @@ +isAllowedCharacter.t.sol +├── when the symbol is an empty string +│ └── it should return true +└── when the symbol is not an empty string + ├── given the symbol contain unsupported characters + │ └── it should return false + └── given the symbol contains only supported characters + └── it should return true diff --git a/test/integration/concrete/nft-descriptor/map-symbol/mapSymbol.t.sol b/test/integration/concrete/nft-descriptor/map-symbol/mapSymbol.t.sol index 16051f57d..e023c8e6b 100644 --- a/test/integration/concrete/nft-descriptor/map-symbol/mapSymbol.t.sol +++ b/test/integration/concrete/nft-descriptor/map-symbol/mapSymbol.t.sol @@ -1,32 +1,40 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { MockERC721 } from "forge-std/src/mocks/MockERC721.sol"; import { Errors } from "src/libraries/Errors.sol"; -import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol"; +import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol"; -contract MapSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test { +contract MapSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test { function test_RevertGiven_UnknownNFT() external { - ERC721 nft = new ERC721("Foo NFT", "FOO"); + MockERC721 nft = new MockERC721(); + nft.initialize("Foo", "FOO"); vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2NFTDescriptor_UnknownNFT.selector, nft, "FOO")); - nftDescriptorMock.mapSymbol_(nft); + nftDescriptorMock.mapSymbol_(IERC721Metadata(address(nft))); } modifier givenKnownNFT() { _; } - function test_MapSymbol_LockupDynamic() external givenKnownNFT { - string memory actualStreamingModel = nftDescriptorMock.mapSymbol_(lockupDynamic); - string memory expectedStreamingModel = "Lockup Dynamic"; - assertEq(actualStreamingModel, expectedStreamingModel, "streamingModel"); + function test_MapSymbol_LockupDynamic() external view givenKnownNFT { + string memory actualSablierModel = nftDescriptorMock.mapSymbol_(lockupDynamic); + string memory expectedSablierModel = "Lockup Dynamic"; + assertEq(actualSablierModel, expectedSablierModel, "sablierModel"); } - function test_MapSymbol_LockupLinear() external givenKnownNFT { - string memory actualStreamingModel = nftDescriptorMock.mapSymbol_(lockupLinear); - string memory expectedStreamingModel = "Lockup Linear"; - assertEq(actualStreamingModel, expectedStreamingModel, "streamingModel"); + function test_MapSymbol_LockupLinear() external view givenKnownNFT { + string memory actualSablierModel = nftDescriptorMock.mapSymbol_(lockupLinear); + string memory expectedSablierModel = "Lockup Linear"; + assertEq(actualSablierModel, expectedSablierModel, "sablierModel"); + } + + function test_MapSymbol_LockupTranched() external view givenKnownNFT { + string memory actualSablierModel = nftDescriptorMock.mapSymbol_(lockupTranched); + string memory expectedSablierModel = "Lockup Tranched"; + assertEq(actualSablierModel, expectedSablierModel, "sablierModel"); } } diff --git a/test/integration/concrete/nft-descriptor/safe-asset-decimals/safeAssetDecimals.t.sol b/test/integration/concrete/nft-descriptor/safe-asset-decimals/safeAssetDecimals.t.sol index e22d505e9..70e4677a6 100644 --- a/test/integration/concrete/nft-descriptor/safe-asset-decimals/safeAssetDecimals.t.sol +++ b/test/integration/concrete/nft-descriptor/safe-asset-decimals/safeAssetDecimals.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol"; +import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol"; -contract SafeAssetDecimals_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test { - function test_SafeAssetDecimals_EOA() external { +contract SafeAssetDecimals_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test { + function test_SafeAssetDecimals_EOA() external view { address eoa = vm.addr({ privateKey: 1 }); uint8 actualDecimals = nftDescriptorMock.safeAssetDecimals_(address(eoa)); uint8 expectedDecimals = 0; assertEq(actualDecimals, expectedDecimals, "decimals"); } - function test_SafeAssetDecimals_DecimalsNotImplemented() external { + function test_SafeAssetDecimals_DecimalsNotImplemented() external view { uint8 actualDecimals = nftDescriptorMock.safeAssetDecimals_(address(noop)); uint8 expectedDecimals = 0; assertEq(actualDecimals, expectedDecimals, "decimals"); @@ -21,7 +21,7 @@ contract SafeAssetDecimals_Integration_Concrete_Test is NFTDescriptor_Integratio _; } - function test_SafeAssetDecimals() external whenAssetDecimalsDefined { + function test_SafeAssetDecimals() external view whenAssetDecimalsDefined { uint8 actualDecimals = nftDescriptorMock.safeAssetDecimals_(address(dai)); uint8 expectedDecimals = dai.decimals(); assertEq(actualDecimals, expectedDecimals, "decimals"); diff --git a/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.t.sol b/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.t.sol index 74188c22a..f2e1e9a10 100644 --- a/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.t.sol +++ b/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.t.sol @@ -1,20 +1,19 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +pragma solidity >=0.8.22 <0.9.0; +import { ERC20Mock } from "../../../../mocks/erc20/ERC20Mock.sol"; import { ERC20Bytes32 } from "../../../../mocks/erc20/ERC20Bytes32.sol"; -import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol"; +import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol"; -contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test { - function test_SafeAssetSymbol_EOA() external { +contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test { + function test_SafeAssetSymbol_EOA() external view { address eoa = vm.addr({ privateKey: 1 }); string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(eoa)); string memory expectedSymbol = "ERC20"; assertEq(actualSymbol, expectedSymbol, "symbol"); } - function test_SafeAssetSymbol_SymbolNotImplemented() external { + function test_SafeAssetSymbol_SymbolNotImplemented() external view { string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(noop)); string memory expectedSymbol = "ERC20"; assertEq(actualSymbol, expectedSymbol, "symbol"); @@ -36,9 +35,9 @@ contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_ } function test_SafeAssetSymbol_LongSymbol() external whenERC20Contract givenSymbolString { - ERC20 asset = new ERC20({ - name_: "Token", - symbol_: "This symbol is has more than 30 characters and it should be ignored" + ERC20Mock asset = new ERC20Mock({ + name: "Token", + symbol: "This symbol is has more than 30 characters and it should be ignored" }); string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(asset)); string memory expectedSymbol = "Long Symbol"; @@ -49,7 +48,25 @@ contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_ _; } - function test_SafeAssetSymbol() external whenERC20Contract givenSymbolString givenSymbolNotLong { + function test_SafeAssetSymbol_NonAlphanumeric() external whenERC20Contract givenSymbolString givenSymbolNotLong { + ERC20Mock asset = new ERC20Mock({ name: "Token", symbol: "" }); + string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(asset)); + string memory expectedSymbol = "Unsupported Symbol"; + assertEq(actualSymbol, expectedSymbol, "symbol"); + } + + modifier givenSymbolAlphanumeric() { + _; + } + + function test_SafeAssetSymbol() + external + view + whenERC20Contract + givenSymbolString + givenSymbolNotLong + givenSymbolAlphanumeric + { string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(dai)); string memory expectedSymbol = dai.symbol(); assertEq(actualSymbol, expectedSymbol, "symbol"); diff --git a/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.tree b/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.tree index 792667a10..9082ee03b 100644 --- a/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.tree +++ b/test/integration/concrete/nft-descriptor/safe-asset-symbol/safeAssetSymbol.tree @@ -8,4 +8,7 @@ safeAssetSymbol.t.sol ├── given the symbol is longer than 30 characters │ └── it should return a hard-coded values └── given the symbol is not longer than 30 characters - └── it should return the correct symbol value + ├── given the symbol contains non-alphanumeric characters + │ └── it should return a hard-coded value + └── given the symbol contains only alphanumeric characters + └── it should return the correct symbol value diff --git a/test/integration/fuzz/comptroller/setFlashFee.t.sol b/test/integration/fuzz/comptroller/setFlashFee.t.sol deleted file mode 100644 index e8b27e0f0..000000000 --- a/test/integration/fuzz/comptroller/setFlashFee.t.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract SetFlashFee_Integration_Fuzz_Test is Integration_Test { - modifier givenCallerAdmin() { - _; - } - - function testFuzz_SetFlashFee(UD60x18 newFlashFee) external givenCallerAdmin { - newFlashFee = _bound(newFlashFee, 0, MAX_FEE); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetFlashFee({ admin: users.admin, oldFlashFee: ZERO, newFlashFee: newFlashFee }); - - // Set the new flash fee. - comptroller.setFlashFee(newFlashFee); - - // Assert that the flash fee has been updated. - UD60x18 actualFlashFee = comptroller.flashFee(); - UD60x18 expectedFlashFee = newFlashFee; - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } -} diff --git a/test/integration/fuzz/comptroller/setProtocolFee.t.sol b/test/integration/fuzz/comptroller/setProtocolFee.t.sol deleted file mode 100644 index 0db99800c..000000000 --- a/test/integration/fuzz/comptroller/setProtocolFee.t.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Integration_Test } from "../../Integration.t.sol"; - -contract SetProtocolFee_Integration_Fuzz_Test is Integration_Test { - modifier givenCallerAdmin() { - _; - } - - function testFuzz_SetProtocolFee(UD60x18 newProtocolFee) external givenCallerAdmin { - newProtocolFee = _bound(newProtocolFee, 1, MAX_FEE); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetProtocolFee({ admin: users.admin, asset: dai, oldProtocolFee: ZERO, newProtocolFee: newProtocolFee }); - - // Set the new protocol fee. - comptroller.setProtocolFee({ asset: dai, newProtocolFee: newProtocolFee }); - - // Assert that the protocol fee has been updated. - UD60x18 actualProtocolFee = comptroller.protocolFees(dai); - UD60x18 expectedProtocolFee = newProtocolFee; - assertEq(actualProtocolFee, expectedProtocolFee, "protocolFee"); - } -} diff --git a/test/integration/fuzz/flash-loan/flashFee.t.sol b/test/integration/fuzz/flash-loan/flashFee.t.sol deleted file mode 100644 index fb73de14a..000000000 --- a/test/integration/fuzz/flash-loan/flashFee.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { FlashLoan_Integration_Shared_Test } from "../../shared/flash-loan/FlashLoan.t.sol"; - -contract FlashFee_Integration_Fuzz_Test is FlashLoan_Integration_Shared_Test { - modifier givenAssetFlashLoanable() { - comptroller.toggleFlashAsset(dai); - _; - } - - /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: - /// - /// - Multiple values for the comptroller flash fee, including zero - /// - Multiple values for the flash loan amount, including zero - function testFuzz_FlashFee(UD60x18 comptrollerFlashFee, uint256 amount) external givenAssetFlashLoanable { - comptrollerFlashFee = _bound(comptrollerFlashFee, 0, MAX_FEE); - comptroller.setFlashFee(comptrollerFlashFee); - uint256 actualFlashFee = flashLoan.flashFee({ asset: address(dai), amount: amount }); - uint256 expectedFlashFee = ud(amount).mul(comptrollerFlashFee).intoUint256(); - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } -} diff --git a/test/integration/fuzz/flash-loan/flashLoan.t.sol b/test/integration/fuzz/flash-loan/flashLoan.t.sol deleted file mode 100644 index c783a2d90..000000000 --- a/test/integration/fuzz/flash-loan/flashLoan.t.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; - -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; - -import { IERC3156FlashBorrower } from "src/interfaces/erc3156/IERC3156FlashBorrower.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { FlashLoanFunction_Integration_Shared_Test } from "../../shared/flash-loan/flashLoanFunction.t.sol"; - -contract FlashLoanFunction_Integration_Fuzz_Test is FlashLoanFunction_Integration_Shared_Test { - function setUp() public virtual override { - FlashLoanFunction_Integration_Shared_Test.setUp(); - } - - function testFuzz_RevertWhen_AmountTooHigh(uint256 amount) external whenNotDelegateCalled { - amount = _bound(amount, uint256(MAX_UINT128) + 1, MAX_UINT256); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_AmountTooHigh.selector, amount)); - flashLoan.flashLoan({ - receiver: IERC3156FlashBorrower(address(0)), - asset: address(dai), - amount: amount, - data: bytes("") - }); - } - - function testFuzz_RevertWhen_CalculatedFeeTooHigh(UD60x18 flashFee) - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - { - // Bound the flash fee so that the calculated fee ends up being greater than 2^128. - flashFee = _bound(flashFee, ud(1.1e18), ud(10e18)); - comptroller.setFlashFee(flashFee); - - // Run the test. - uint256 fee = flashLoan.flashFee({ asset: address(dai), amount: MAX_UINT128 }); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2FlashLoan_CalculatedFeeTooHigh.selector, fee)); - flashLoan.flashLoan({ - receiver: IERC3156FlashBorrower(address(0)), - asset: address(dai), - amount: MAX_UINT128, - data: bytes("") - }); - } - - /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: - /// - /// - Multiple values for the comptroller flash fee, including zero - /// - Multiple values for the flash loan amount, including zero - /// - Multiple values for the data bytes array, including zero length - function testFuzz_FlashLoanFunction( - UD60x18 comptrollerFlashFee, - uint128 amount, - bytes calldata data - ) - external - whenNotDelegateCalled - whenAmountNotTooHigh - givenAssetFlashLoanable - whenCalculatedFeeNotTooHigh - whenBorrowDoesNotFail - whenNoReentrancy - { - comptrollerFlashFee = _bound(comptrollerFlashFee, 0, MAX_FEE); - comptroller.setFlashFee(comptrollerFlashFee); - - // Load the initial protocol revenues. - uint128 initialProtocolRevenues = flashLoan.protocolRevenues(dai); - - // Load the flash fee. - uint256 fee = flashLoan.flashFee({ asset: address(dai), amount: amount }); - - // Mint the flash loan amount to the contract. - deal({ token: address(dai), to: address(flashLoan), give: amount }); - - // Mint the flash fee to the receiver so that they can repay the flash loan. - deal({ token: address(dai), to: address(goodFlashLoanReceiver), give: fee }); - - // Expect `amount` of assets to be transferred from {SablierV2FlashLoan} to the receiver. - expectCallToTransfer({ to: address(goodFlashLoanReceiver), amount: amount }); - - // Expect `amount+fee` of assets to be transferred back from the receiver. - uint256 returnAmount = amount + fee; - expectCallToTransferFrom({ from: address(goodFlashLoanReceiver), to: address(flashLoan), amount: returnAmount }); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(flashLoan) }); - emit FlashLoan({ - initiator: users.admin, - receiver: goodFlashLoanReceiver, - asset: dai, - amount: amount, - feeAmount: fee, - data: data - }); - - // Execute the flash loan. - bool response = - flashLoan.flashLoan({ receiver: goodFlashLoanReceiver, asset: address(dai), amount: amount, data: data }); - - // Assert that the returned response is `true`. - assertTrue(response, "flashLoan response"); - - // Assert that the protocol fee has been recorded. - uint128 actualProtocolRevenues = flashLoan.protocolRevenues(dai); - uint128 expectedProtocolRevenues = initialProtocolRevenues + uint128(fee); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - } -} diff --git a/test/integration/fuzz/flash-loan/maxFlashLoan.t.sol b/test/integration/fuzz/flash-loan/maxFlashLoan.t.sol deleted file mode 100644 index 5afbd58d5..000000000 --- a/test/integration/fuzz/flash-loan/maxFlashLoan.t.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { FlashLoan_Integration_Shared_Test } from "../../shared/flash-loan/FlashLoan.t.sol"; - -contract MaxFlashLoan_Integration_Fuzz_Test is FlashLoan_Integration_Shared_Test { - modifier givenAssetFlashLoanable() { - comptroller.toggleFlashAsset(dai); - _; - } - - function testFuzz_MaxFlashLoan(uint256 dealAmount) external givenAssetFlashLoanable { - deal({ token: address(dai), to: address(flashLoan), give: dealAmount }); - uint256 actualAmount = flashLoan.maxFlashLoan(address(dai)); - uint256 expectedAmount = dealAmount; - assertEq(actualAmount, expectedAmount, "maxFlashLoan amount"); - } -} diff --git a/test/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol b/test/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol index 5da2393d5..b699733fc 100644 --- a/test/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol +++ b/test/integration/fuzz/lockup-dynamic/LockupDynamic.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Base } from "src/interfaces/ISablierV2Base.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { LockupDynamic_Integration_Shared_Test } from "../../shared/lockup-dynamic/LockupDynamic.t.sol"; @@ -26,8 +25,7 @@ abstract contract LockupDynamic_Integration_Fuzz_Test is Integration_Test, Locku Integration_Test.setUp(); LockupDynamic_Integration_Shared_Test.setUp(); - // Cast the LockupDynamic contract as {ISablierV2Base} and {ISablierV2Lockup}. - base = ISablierV2Base(lockupDynamic); + // Cast the LockupDynamic contract as {ISablierV2Lockup}. lockup = ISablierV2Lockup(lockupDynamic); } } diff --git a/test/integration/fuzz/lockup-dynamic/createWithDeltas.t.sol b/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol similarity index 61% rename from test/integration/fuzz/lockup-dynamic/createWithDeltas.t.sol rename to test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol index aaf2455ab..4bf6242b4 100644 --- a/test/integration/fuzz/lockup-dynamic/createWithDeltas.t.sol +++ b/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol @@ -1,84 +1,74 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; -import { CreateWithDeltas_Integration_Shared_Test } from "../../shared/lockup-dynamic/createWithDeltas.t.sol"; +import { CreateWithDurations_Integration_Shared_Test } from "../../shared/lockup/createWithDurations.t.sol"; import { LockupDynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; -contract CreateWithDeltas_LockupDynamic_Integration_Fuzz_Test is +contract CreateWithDurations_LockupDynamic_Integration_Fuzz_Test is LockupDynamic_Integration_Fuzz_Test, - CreateWithDeltas_Integration_Shared_Test + CreateWithDurations_Integration_Shared_Test { function setUp() public virtual - override(LockupDynamic_Integration_Fuzz_Test, CreateWithDeltas_Integration_Shared_Test) + override(LockupDynamic_Integration_Fuzz_Test, CreateWithDurations_Integration_Shared_Test) { LockupDynamic_Integration_Fuzz_Test.setUp(); - CreateWithDeltas_Integration_Shared_Test.setUp(); + CreateWithDurations_Integration_Shared_Test.setUp(); } struct Vars { uint256 actualNextStreamId; address actualNFTOwner; - uint256 actualProtocolRevenues; Lockup.Status actualStatus; Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; - uint256 expectedProtocolRevenues; Lockup.Status expectedStatus; address funder; - uint128 initialProtocolRevenues; bool isCancelable; bool isSettled; - LockupDynamic.Segment[] segmentsWithMilestones; + LockupDynamic.Segment[] segmentsWithTimestamps; uint128 totalAmount; } - function testFuzz_CreateWithDeltas(LockupDynamic.SegmentWithDelta[] memory segments) + function testFuzz_CreateWithDurations(LockupDynamic.SegmentWithDuration[] memory segments) external whenNotDelegateCalled - whenLoopCalculationsDoNotOverflowBlockGasLimit - whenDeltasNotZero - whenMilestonesCalculationsDoNotOverflow + whenSegmentCountNotTooHigh + whenDurationsNotZero + whenTimestampsCalculationsDoNotOverflow { vm.assume(segments.length != 0); - // Fuzz the deltas. + // Fuzz the durations. Vars memory vars; - fuzzSegmentDeltas(segments); + fuzzSegmentDurations(segments); - // Fuzz the segment amounts and calculate the create amounts (total, deposit, protocol fee, and broker fee). + // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts(segments); // Make the Sender the stream's funder (recall that the Sender is the default caller). vars.funder = users.sender; - // Load the initial protocol revenues. - vars.initialProtocolRevenues = lockupDynamic.protocolRevenues(dai); - // Mint enough assets to the fuzzed funder. deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); // Expect the assets to be transferred from the funder to {SablierV2LockupDynamic}. - expectCallToTransferFrom({ - from: vars.funder, - to: address(lockupDynamic), - amount: vars.createAmounts.deposit + vars.createAmounts.protocolFee - }); + expectCallToTransferFrom({ from: vars.funder, to: address(lockupDynamic), value: vars.createAmounts.deposit }); // Expect the broker fee to be paid to the broker, if not zero. if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: vars.funder, to: users.broker, amount: vars.createAmounts.brokerFee }); + expectCallToTransferFrom({ from: vars.funder, to: users.broker, value: vars.createAmounts.brokerFee }); } - // Create the range struct. - vars.segmentsWithMilestones = getSegmentsWithMilestones(segments); - LockupDynamic.Range memory range = LockupDynamic.Range({ + // Create the timestamps struct. + vars.segmentsWithTimestamps = getSegmentsWithTimestamps(segments); + LockupDynamic.Timestamps memory timestamps = LockupDynamic.Timestamps({ start: getBlockTimestamp(), - end: vars.segmentsWithMilestones[vars.segmentsWithMilestones.length - 1].milestone + end: vars.segmentsWithTimestamps[vars.segmentsWithTimestamps.length - 1].timestamp }); // Expect the relevant event to be emitted. @@ -92,17 +82,17 @@ contract CreateWithDeltas_LockupDynamic_Integration_Fuzz_Test is asset: dai, cancelable: true, transferable: true, - segments: vars.segmentsWithMilestones, - range: range, + segments: vars.segmentsWithTimestamps, + timestamps: timestamps, broker: users.broker }); // Create the stream. - LockupDynamic.CreateWithDeltas memory params = defaults.createWithDeltas(); + LockupDynamic.CreateWithDurations memory params = defaults.createWithDurationsLD(); params.segments = segments; params.totalAmount = vars.totalAmount; params.transferable = true; - lockupDynamic.createWithDeltas(params); + lockupDynamic.createWithDurations(params); // Check if the stream is settled. It is possible for a Lockup Dynamic stream to settle at the time of creation // because some segment amounts can be zero. @@ -110,17 +100,18 @@ contract CreateWithDeltas_LockupDynamic_Integration_Fuzz_Test is vars.isCancelable = vars.isSettled ? false : true; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); - assertEq(actualStream.endTime, range.end, "endTime"); + assertEq(actualStream.endTime, timestamps.end, "endTime"); assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); - assertEq(actualStream.isTransferable, true, "isTransferable"); assertEq(actualStream.isDepleted, false, "isDepleted"); assertEq(actualStream.isStream, true, "isStream"); - assertEq(actualStream.segments, vars.segmentsWithMilestones, "segments"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, users.recipient, "recipient"); + assertEq(actualStream.segments, vars.segmentsWithTimestamps, "segments"); assertEq(actualStream.sender, users.sender, "sender"); - assertEq(actualStream.startTime, range.start, "startTime"); + assertEq(actualStream.startTime, timestamps.start, "startTime"); assertEq(actualStream.wasCanceled, false, "wasCanceled"); // Assert that the stream's status is correct. @@ -128,16 +119,11 @@ contract CreateWithDeltas_LockupDynamic_Integration_Fuzz_Test is vars.expectedStatus = vars.isSettled ? Lockup.Status.SETTLED : Lockup.Status.STREAMING; assertEq(vars.actualStatus, vars.expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. vars.actualNextStreamId = lockupDynamic.nextStreamId(); vars.expectedNextStreamId = streamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); - // Assert that the protocol fee has been recorded. - vars.actualProtocolRevenues = lockupDynamic.protocolRevenues(dai); - vars.expectedProtocolRevenues = vars.initialProtocolRevenues + vars.createAmounts.protocolFee; - assertEq(vars.actualProtocolRevenues, vars.expectedProtocolRevenues, "protocolRevenues"); - // Assert that the NFT has been minted. vars.actualNFTOwner = lockupDynamic.ownerOf({ tokenId: streamId }); vars.expectedNFTOwner = users.recipient; diff --git a/test/integration/fuzz/lockup-dynamic/createWithMilestones.t.sol b/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol similarity index 65% rename from test/integration/fuzz/lockup-dynamic/createWithMilestones.t.sol rename to test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol index cbcf7d2c0..6b55b5d07 100644 --- a/test/integration/fuzz/lockup-dynamic/createWithMilestones.t.sol +++ b/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { MAX_UD60x18, UD60x18, ud, ZERO } from "@prb/math/src/UD60x18.sol"; import { stdError } from "forge-std/src/StdError.sol"; @@ -7,20 +7,20 @@ import { stdError } from "forge-std/src/StdError.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Broker, Lockup, LockupDynamic } from "src/types/DataTypes.sol"; -import { CreateWithMilestones_Integration_Shared_Test } from "../../shared/lockup-dynamic/createWithMilestones.t.sol"; +import { CreateWithTimestamps_Integration_Shared_Test } from "../../shared/lockup/createWithTimestamps.t.sol"; import { LockupDynamic_Integration_Fuzz_Test } from "./LockupDynamic.t.sol"; -contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is +contract CreateWithTimestamps_LockupDynamic_Integration_Fuzz_Test is LockupDynamic_Integration_Fuzz_Test, - CreateWithMilestones_Integration_Shared_Test + CreateWithTimestamps_Integration_Shared_Test { function setUp() public virtual - override(LockupDynamic_Integration_Fuzz_Test, CreateWithMilestones_Integration_Shared_Test) + override(LockupDynamic_Integration_Fuzz_Test, CreateWithTimestamps_Integration_Shared_Test) { LockupDynamic_Integration_Fuzz_Test.setUp(); - CreateWithMilestones_Integration_Shared_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); } function testFuzz_RevertWhen_SegmentCountTooHigh(uint256 segmentCount) @@ -30,7 +30,8 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is whenDepositAmountNotZero whenSegmentCountNotZero { - segmentCount = _bound(segmentCount, defaults.MAX_SEGMENT_COUNT() + 1 seconds, defaults.MAX_SEGMENT_COUNT() * 10); + uint256 defaultMax = defaults.MAX_SEGMENT_COUNT(); + segmentCount = _bound(segmentCount, defaultMax + 1, defaultMax * 2); LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](segmentCount); vm.expectRevert( abi.encodeWithSelector(Errors.SablierV2LockupDynamic_SegmentCountTooHigh.selector, segmentCount) @@ -58,7 +59,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is createDefaultStreamWithSegments(segments); } - function testFuzz_RevertWhen_StartTimeNotLessThanFirstSegmentMilestone(uint40 firstMilestone) + function testFuzz_RevertWhen_StartTimeNotLessThanFirstSegmentTimestamp(uint40 firstTimestamp) external whenNotDelegateCalled whenRecipientNonZeroAddress @@ -67,18 +68,18 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow { - firstMilestone = boundUint40(firstMilestone, 0, defaults.START_TIME()); + firstTimestamp = boundUint40(firstTimestamp, 0, defaults.START_TIME()); - // Change the milestone of the first segment. + // Change the timestamp of the first segment. LockupDynamic.Segment[] memory segments = defaults.segments(); - segments[0].milestone = firstMilestone; + segments[0].timestamp = firstTimestamp; // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentMilestone.selector, + Errors.SablierV2LockupDynamic_StartTimeNotLessThanFirstSegmentTimestamp.selector, defaults.START_TIME(), - segments[0].milestone + segments[0].timestamp ) ); @@ -94,24 +95,21 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture { depositDiff = boundUint128(depositDiff, 100, defaults.TOTAL_AMOUNT()); - // Disable both the protocol and the broker fee so that they don't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); UD60x18 brokerFee = ZERO; - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); // Adjust the default deposit amount. uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); uint128 depositAmount = defaultDepositAmount + depositDiff; // Prepare the params. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: brokerFee }); params.totalAmount = depositAmount; @@ -125,34 +123,7 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is ); // Create the stream. - lockupDynamic.createWithMilestones(params); - } - - function testFuzz_RevertWhen_ProtocolFeeTooHigh(UD60x18 protocolFee) - external - whenNotDelegateCalled - whenRecipientNonZeroAddress - whenDepositAmountNotZero - whenSegmentCountNotZero - whenSegmentCountNotTooHigh - whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered - whenEndTimeInTheFuture - whenDepositAmountEqualToSegmentAmountsSum - { - protocolFee = _bound(protocolFee, MAX_FEE + ud(1), MAX_UD60x18); - - // Set the protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); - changePrank({ msgSender: users.sender }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_ProtocolFeeTooHigh.selector, protocolFee, MAX_FEE) - ); - createDefaultStream(); + lockupDynamic.createWithTimestamps(params); } function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) @@ -163,27 +134,26 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh { vm.assume(broker.account != address(0)); - broker.fee = _bound(broker.fee, MAX_FEE + ud(1), MAX_UD60x18); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, broker.fee, MAX_FEE)); + broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) + ); createDefaultStreamWithBroker(broker); } struct Vars { uint256 actualNextStreamId; address actualNFTOwner; - uint256 actualProtocolRevenues; Lockup.Status actualStatus; Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; - uint256 expectedProtocolRevenues; Lockup.Status expectedStatus; bool isCancelable; bool isSettled; @@ -193,83 +163,73 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// /// - All possible permutations for the funder, sender, recipient, and broker - /// - Multiple values for the segment amounts, exponents, and milestones + /// - Multiple values for the segment amounts, exponents, and timestamps /// - Cancelable and not cancelable /// - Start time in the past /// - Start time in the present /// - Start time in the future - /// - Start time equal and not equal to the first segment milestone + /// - Start time equal and not equal to the first segment timestamp /// - Multiple values for the broker fee, including zero - /// - Multiple values for the protocol fee, including zero - function testFuzz_CreateWithMilestones( + function testFuzz_CreateWithTimestamps( address funder, - LockupDynamic.CreateWithMilestones memory params, - UD60x18 protocolFee + LockupDynamic.CreateWithTimestamps memory params ) external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenStartTimeNotZero whenSegmentCountNotZero whenSegmentCountNotTooHigh whenSegmentAmountsSumDoesNotOverflow - whenStartTimeLessThanFirstSegmentMilestone - whenSegmentMilestonesOrdered + whenStartTimeLessThanFirstSegmentTimestamp + whenSegmentTimestampsOrdered whenEndTimeInTheFuture whenDepositAmountEqualToSegmentAmountsSum - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract whenAssetERC20 { vm.assume(funder != address(0) && params.recipient != address(0) && params.broker.account != address(0)); vm.assume(params.segments.length != 0); - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - protocolFee = _bound(protocolFee, 0, MAX_FEE); - params.startTime = boundUint40(params.startTime, 0, defaults.START_TIME()); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.startTime = boundUint40(params.startTime, 1, defaults.START_TIME()); params.transferable = true; - // Fuzz the segment milestones. - fuzzSegmentMilestones(params.segments, params.startTime); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(params.segments, params.startTime); - // Fuzz the segment amounts and calculate the create amounts (total, deposit, protocol fee, and broker fee). + // Fuzz the segment amounts and calculate the total and create amounts (deposit and broker fee). Vars memory vars; (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: params.segments, - protocolFee: protocolFee, brokerFee: params.broker.fee }); - // Set the fuzzed protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); - // Make the fuzzed funder the caller in the rest of this test. - changePrank(funder); + resetPrank(funder); // Mint enough assets to the fuzzed funder. deal({ token: address(dai), to: funder, give: vars.totalAmount }); // Approve {SablierV2LockupDynamic} to transfer the assets from the fuzzed funder. - dai.approve({ spender: address(lockupDynamic), amount: MAX_UINT256 }); + dai.approve({ spender: address(lockupDynamic), value: MAX_UINT256 }); // Expect the assets to be transferred from the funder to {SablierV2LockupDynamic}. - expectCallToTransferFrom({ - from: funder, - to: address(lockupDynamic), - amount: vars.createAmounts.deposit + vars.createAmounts.protocolFee - }); + expectCallToTransferFrom({ from: funder, to: address(lockupDynamic), value: vars.createAmounts.deposit }); // Expect the broker fee to be paid to the broker, if not zero. if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: funder, to: params.broker.account, amount: vars.createAmounts.brokerFee }); + expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); } // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockupDynamic) }); - LockupDynamic.Range memory range = - LockupDynamic.Range({ start: params.startTime, end: params.segments[params.segments.length - 1].milestone }); + LockupDynamic.Timestamps memory timestamps = LockupDynamic.Timestamps({ + start: params.startTime, + end: params.segments[params.segments.length - 1].timestamp + }); emit CreateLockupDynamicStream({ streamId: streamId, funder: funder, @@ -280,22 +240,22 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is cancelable: params.cancelable, transferable: params.transferable, segments: params.segments, - range: range, + timestamps: timestamps, broker: params.broker.account }); // Create the stream. - lockupDynamic.createWithMilestones( - LockupDynamic.CreateWithMilestones({ + lockupDynamic.createWithTimestamps( + LockupDynamic.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: vars.totalAmount, asset: dai, - broker: params.broker, cancelable: params.cancelable, transferable: params.transferable, - recipient: params.recipient, - segments: params.segments, - sender: params.sender, startTime: params.startTime, - totalAmount: vars.totalAmount + segments: params.segments, + broker: params.broker }) ); @@ -305,17 +265,18 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is vars.isCancelable = vars.isSettled ? false : params.cancelable; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); - assertEq(actualStream.endTime, range.end, "endTime"); + assertEq(actualStream.endTime, timestamps.end, "endTime"); assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); - assertEq(actualStream.isTransferable, true, "isTransferable"); assertEq(actualStream.isDepleted, false, "isStream"); assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); assertEq(actualStream.sender, params.sender, "sender"); assertEq(actualStream.segments, params.segments, "segments"); - assertEq(actualStream.startTime, range.start, "startTime"); + assertEq(actualStream.startTime, timestamps.start, "startTime"); assertEq(actualStream.wasCanceled, false, "wasCanceled"); // Assert that the stream's status is correct. @@ -329,16 +290,11 @@ contract CreateWithMilestones_LockupDynamic_Integration_Fuzz_Test is } assertEq(vars.actualStatus, vars.expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. vars.actualNextStreamId = lockupDynamic.nextStreamId(); vars.expectedNextStreamId = streamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); - // Assert that the protocol fee has been recorded. - vars.actualProtocolRevenues = lockupDynamic.protocolRevenues(dai); - vars.expectedProtocolRevenues = vars.createAmounts.protocolFee; - assertEq(vars.actualProtocolRevenues, vars.expectedProtocolRevenues, "protocolRevenues"); - // Assert that the NFT has been minted. vars.actualNFTOwner = lockupDynamic.ownerOf({ tokenId: streamId }); vars.expectedNFTOwner = params.recipient; diff --git a/test/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol b/test/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol index d8755e6f8..0e1cc38ee 100644 --- a/test/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol +++ b/test/integration/fuzz/lockup-dynamic/streamedAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ZERO } from "@prb/math/src/UD60x18.sol"; import { Broker, LockupDynamic } from "src/types/DataTypes.sol"; @@ -19,10 +19,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is LockupDynamic_Integration_Fuzz_Test.setUp(); StreamedAmountOf_Integration_Shared_Test.setUp(); - // Disable the protocol fee so that it doesn't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: @@ -42,7 +39,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is whenStartTimeInThePast { vm.assume(segment.amount != 0); - segment.milestone = boundUint40(segment.milestone, defaults.CLIFF_TIME(), defaults.END_TIME()); + segment.timestamp = boundUint40(segment.timestamp, defaults.CLIFF_TIME(), defaults.END_TIME()); timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); // Create the single-segment array. @@ -53,19 +50,19 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is deal({ token: address(dai), to: users.sender, give: segment.amount }); // Create the stream with the fuzzed segment. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: ZERO }); params.segments = segments; params.totalAmount = segment.amount; - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); // Simulate the passage of time. - uint40 currentTime = defaults.START_TIME() + timeJump; - vm.warp({ timestamp: currentTime }); + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); // Run the test. uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateStreamedAmountForOneSegment(currentTime, segment); + uint128 expectedStreamedAmount = calculateStreamedAmountForOneSegment(blockTimestamp, segment); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -73,7 +70,7 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is _; } - modifier whenCurrentMilestoneNot1st() { + modifier whenCurrentTimestampNot1st() { _; } @@ -94,43 +91,40 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is givenStreamHasNotBeenCanceled whenStartTimeInThePast givenMultipleSegments - whenCurrentMilestoneNot1st + whenCurrentTimestampNot1st { vm.assume(segments.length > 1); - // Fuzz the segment milestones. - fuzzSegmentMilestones(segments, defaults.START_TIME()); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(segments, defaults.START_TIME()); // Fuzz the segment amounts. - (uint128 totalAmount,) = fuzzDynamicStreamAmounts({ - upperBound: MAX_UINT128, - segments: segments, - protocolFee: ZERO, - brokerFee: ZERO - }); + (uint128 totalAmount,) = + fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: ZERO }); // Bound the time jump. - uint40 firstSegmentDuration = segments[1].milestone - segments[0].milestone; - uint40 totalDuration = segments[segments.length - 1].milestone - defaults.START_TIME(); + uint40 firstSegmentDuration = segments[1].timestamp - segments[0].timestamp; + uint40 totalDuration = segments[segments.length - 1].timestamp - defaults.START_TIME(); timeJump = boundUint40(timeJump, firstSegmentDuration, totalDuration + 100 seconds); // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: totalAmount }); // Create the stream with the fuzzed segments. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: ZERO }); params.segments = segments; params.totalAmount = totalAmount; - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); // Simulate the passage of time. - uint40 currentTime = defaults.START_TIME() + timeJump; - vm.warp({ timestamp: currentTime }); + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); // Run the test. uint128 actualStreamedAmount = lockupDynamic.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateStreamedAmountForMultipleSegments(currentTime, segments, totalAmount); + uint128 expectedStreamedAmount = + calculateStreamedAmountForMultipleSegments(blockTimestamp, segments, totalAmount); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -145,24 +139,20 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is givenStreamHasNotBeenCanceled whenStartTimeInThePast givenMultipleSegments - whenCurrentMilestoneNot1st + whenCurrentTimestampNot1st { vm.assume(segments.length > 1); - // Fuzz the segment milestones. - fuzzSegmentMilestones(segments, defaults.START_TIME()); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(segments, defaults.START_TIME()); // Fuzz the segment amounts. - (uint128 totalAmount,) = fuzzDynamicStreamAmounts({ - upperBound: MAX_UINT128, - segments: segments, - protocolFee: ZERO, - brokerFee: ZERO - }); + (uint128 totalAmount,) = + fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: ZERO }); // Bound the time warps. - uint40 firstSegmentDuration = segments[1].milestone - segments[0].milestone; - uint40 totalDuration = segments[segments.length - 1].milestone - defaults.START_TIME(); + uint40 firstSegmentDuration = segments[1].timestamp - segments[0].timestamp; + uint40 totalDuration = segments[segments.length - 1].timestamp - defaults.START_TIME(); timeWarp0 = boundUint40(timeWarp0, firstSegmentDuration, totalDuration - 1); timeWarp1 = boundUint40(timeWarp1, timeWarp0, totalDuration); @@ -170,23 +160,23 @@ contract StreamedAmountOf_LockupDynamic_Integration_Fuzz_Test is deal({ token: address(dai), to: users.sender, give: totalAmount }); // Create the stream with the fuzzed segments. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: ZERO }); params.segments = segments; params.totalAmount = totalAmount; - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); // Warp to the future for the first time. - vm.warp({ timestamp: defaults.START_TIME() + timeWarp0 }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp0 }); // Calculate the streamed amount at this midpoint in time. uint128 streamedAmount0 = lockupDynamic.streamedAmountOf(streamId); // Warp to the future for the second time. - vm.warp({ timestamp: defaults.START_TIME() + timeWarp1 }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp1 }); // Assert that this streamed amount is greater than or equal to the previous streamed amount. uint128 streamedAmount1 = lockupDynamic.streamedAmountOf(streamId); - assertGte(streamedAmount1, streamedAmount0, "streamedAmount"); + assertGe(streamedAmount1, streamedAmount0, "streamedAmount"); } } diff --git a/test/integration/fuzz/lockup-dynamic/withdraw.t.sol b/test/integration/fuzz/lockup-dynamic/withdraw.t.sol index 196ac96cb..10d3fc893 100644 --- a/test/integration/fuzz/lockup-dynamic/withdraw.t.sol +++ b/test/integration/fuzz/lockup-dynamic/withdraw.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; @@ -44,10 +44,9 @@ contract Withdraw_LockupDynamic_Integration_Fuzz_Test is external whenNotDelegateCalled givenNotNull - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero - whenWithdrawAmountNotGreaterThanWithdrawableAmount + whenNoOverdraw { vm.assume(params.segments.length != 0); vm.assume(params.to != address(0)); @@ -56,31 +55,31 @@ contract Withdraw_LockupDynamic_Integration_Fuzz_Test is Vars memory vars; vars.funder = users.sender; - // Fuzz the segment milestones. - fuzzSegmentMilestones(params.segments, defaults.START_TIME()); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(params.segments, defaults.START_TIME()); // Fuzz the segment amounts. (vars.totalAmount, vars.createAmounts) = fuzzDynamicStreamAmounts(params.segments); // Bound the time jump. - vars.totalDuration = params.segments[params.segments.length - 1].milestone - defaults.START_TIME(); + vars.totalDuration = params.segments[params.segments.length - 1].timestamp - defaults.START_TIME(); params.timeJump = _bound(params.timeJump, 1 seconds, vars.totalDuration + 100 seconds); // Mint enough assets to the funder. deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); // Make the Sender the caller. - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); // Create the stream with the fuzzed segments. - LockupDynamic.CreateWithMilestones memory createParams = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory createParams = defaults.createWithTimestampsLD(); createParams.totalAmount = vars.totalAmount; createParams.segments = params.segments; - vars.streamId = lockupDynamic.createWithMilestones(createParams); + vars.streamId = lockupDynamic.createWithTimestamps(createParams); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + params.timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + params.timeJump }); // Query the withdrawable amount. vars.withdrawableAmount = lockupDynamic.withdrawableAmountOf(vars.streamId); @@ -94,7 +93,7 @@ contract Withdraw_LockupDynamic_Integration_Fuzz_Test is vars.withdrawAmount = boundUint128(vars.withdrawAmount, 1, vars.withdrawableAmount); // Expect the assets to be transferred to the fuzzed `to` address. - expectCallToTransfer({ to: params.to, amount: vars.withdrawAmount }); + expectCallToTransfer({ to: params.to, value: vars.withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockupDynamic) }); @@ -103,7 +102,7 @@ contract Withdraw_LockupDynamic_Integration_Fuzz_Test is emit MetadataUpdate({ _tokenId: vars.streamId }); // Make the Recipient the caller. - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Make the withdrawal. lockupDynamic.withdraw({ streamId: vars.streamId, to: params.to, amount: vars.withdrawAmount }); diff --git a/test/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol b/test/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol index aee569bb8..27751e524 100644 --- a/test/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol +++ b/test/integration/fuzz/lockup-dynamic/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ZERO } from "@prb/math/src/UD60x18.sol"; @@ -20,10 +20,7 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Fuzz_Test is LockupDynamic_Integration_Fuzz_Test.setUp(); WithdrawableAmountOf_Integration_Shared_Test.setUp(); - // Disable the protocol fee so that it doesn't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); } modifier whenStartTimeInThePast() { @@ -42,19 +39,19 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Fuzz_Test is // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with // the calculations. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: ZERO }); params.totalAmount = defaults.DEPOSIT_AMOUNT(); - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); // Simulate the passage of time. - uint40 currentTime = defaults.START_TIME() + timeJump; - vm.warp({ timestamp: currentTime }); + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); // Run the test. uint128 actualWithdrawableAmount = lockupDynamic.withdrawableAmountOf(streamId); uint128 expectedWithdrawableAmount = - calculateStreamedAmountForMultipleSegments(currentTime, defaults.segments(), defaults.DEPOSIT_AMOUNT()); + calculateStreamedAmountForMultipleSegments(blockTimestamp, defaults.segments(), defaults.DEPOSIT_AMOUNT()); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -82,23 +79,23 @@ contract WithdrawableAmountOf_LockupDynamic_Integration_Fuzz_Test is { timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); - // Define the current time. - uint40 currentTime = defaults.START_TIME() + timeJump; + // Define the block timestamp. + uint40 blockTimestamp = defaults.START_TIME() + timeJump; // Bound the withdraw amount. uint128 streamedAmount = - calculateStreamedAmountForMultipleSegments(currentTime, defaults.segments(), defaults.DEPOSIT_AMOUNT()); + calculateStreamedAmountForMultipleSegments(blockTimestamp, defaults.segments(), defaults.DEPOSIT_AMOUNT()); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with // the calculations. - LockupDynamic.CreateWithMilestones memory params = defaults.createWithMilestones(); + LockupDynamic.CreateWithTimestamps memory params = defaults.createWithTimestampsLD(); params.broker = Broker({ account: address(0), fee: ZERO }); params.totalAmount = defaults.DEPOSIT_AMOUNT(); - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); // Simulate the passage of time. - vm.warp({ timestamp: currentTime }); + vm.warp({ newTimestamp: blockTimestamp }); // Make the withdrawal. lockupDynamic.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); diff --git a/test/integration/fuzz/lockup-linear/LockupLinear.t.sol b/test/integration/fuzz/lockup-linear/LockupLinear.t.sol index 320ac7f85..0d47251ed 100644 --- a/test/integration/fuzz/lockup-linear/LockupLinear.t.sol +++ b/test/integration/fuzz/lockup-linear/LockupLinear.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { ISablierV2Base } from "src/interfaces/ISablierV2Base.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { LockupLinear_Integration_Shared_Test } from "../../shared/lockup-linear/LockupLinear.t.sol"; @@ -27,8 +26,7 @@ abstract contract LockupLinear_Integration_Fuzz_Test is Integration_Test, Lockup Integration_Test.setUp(); LockupLinear_Integration_Shared_Test.setUp(); - // Cast the lockupLinear contract as {ISablierV2Base} and {ISablierV2Lockup}. - base = ISablierV2Base(lockupLinear); + // Cast the lockupLinear contract as {ISablierV2Lockup}. lockup = ISablierV2Lockup(lockupLinear); } } diff --git a/test/integration/fuzz/lockup-linear/createWithDurations.t.sol b/test/integration/fuzz/lockup-linear/createWithDurations.t.sol index c84b7af53..9fa96dd32 100644 --- a/test/integration/fuzz/lockup-linear/createWithDurations.t.sol +++ b/test/integration/fuzz/lockup-linear/createWithDurations.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { CreateWithDurations_Integration_Shared_Test } from "../../shared/lockup-linear/createWithDurations.t.sol"; +import { CreateWithDurations_Integration_Shared_Test } from "../../shared/lockup/createWithDurations.t.sol"; import { LockupLinear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is @@ -20,43 +20,16 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is CreateWithDurations_Integration_Shared_Test.setUp(); } - function testFuzz_RevertWhen_CliffDurationCalculationOverflows(uint40 cliffDuration) - external - whenNotDelegateCalled - { - uint40 startTime = getBlockTimestamp(); - cliffDuration = boundUint40(cliffDuration, MAX_UINT40 - startTime + 1 seconds, MAX_UINT40); - - // Calculate the end time. Needs to be "unchecked" to avoid an overflow. - uint40 cliffTime; - unchecked { - cliffTime = startTime + cliffDuration; - } - - // Expect the relevant error to be thrown. - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime - ) - ); - - // Set the total duration to be the same as the cliff duration. - uint40 totalDuration = cliffDuration; - - // Create the stream. - createDefaultStreamWithDurations(LockupLinear.Durations({ cliff: cliffDuration, total: totalDuration })); - } - function testFuzz_RevertWhen_TotalDurationCalculationOverflows(LockupLinear.Durations memory durations) external whenNotDelegateCalled whenCliffDurationCalculationDoesNotOverflow { uint40 startTime = getBlockTimestamp(); - durations.cliff = boundUint40(durations.cliff, 0, MAX_UINT40 - startTime); + durations.cliff = boundUint40(durations.cliff, 1 seconds, MAX_UINT40 - startTime); durations.total = boundUint40(durations.total, MAX_UINT40 - startTime + 1 seconds, MAX_UINT40); - // Calculate the cliff time and the end time. Needs to be "unchecked" to avoid an overflow. + // Calculate the cliff time and the end time. Needs to be "unchecked" to allow an overflow. uint40 cliffTime; uint40 endTime; unchecked { @@ -81,29 +54,22 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is whenCliffDurationCalculationDoesNotOverflow whenTotalDurationCalculationDoesNotOverflow { - durations.total = boundUint40(durations.total, 0, MAX_UNIX_TIMESTAMP); + durations.total = boundUint40(durations.total, 1 seconds, MAX_UNIX_TIMESTAMP); vm.assume(durations.cliff < durations.total); // Make the Sender the stream's funder (recall that the Sender is the default caller). address funder = users.sender; - // Load the initial protocol revenues. - uint128 initialProtocolRevenues = lockupLinear.protocolRevenues(dai); - // Expect the assets to be transferred from the funder to {SablierV2LockupLinear}. - expectCallToTransferFrom({ - from: funder, - to: address(lockupLinear), - amount: defaults.DEPOSIT_AMOUNT() + defaults.PROTOCOL_FEE_AMOUNT() - }); + expectCallToTransferFrom({ from: funder, to: address(lockupLinear), value: defaults.DEPOSIT_AMOUNT() }); // Expect the broker fee to be paid to the broker. - expectCallToTransferFrom({ from: funder, to: users.broker, amount: defaults.BROKER_FEE_AMOUNT() }); + expectCallToTransferFrom({ from: funder, to: users.broker, value: defaults.BROKER_FEE_AMOUNT() }); - // Create the range struct by calculating the start time, cliff time and the end time. - LockupLinear.Range memory range = LockupLinear.Range({ + // Create the timestamps struct by calculating the start time, cliff time and the end time. + LockupLinear.Timestamps memory timestamps = LockupLinear.Timestamps({ start: getBlockTimestamp(), - cliff: getBlockTimestamp() + durations.cliff, + cliff: durations.cliff == 0 ? 0 : getBlockTimestamp() + durations.cliff, end: getBlockTimestamp() + durations.total }); @@ -118,7 +84,7 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is asset: dai, cancelable: true, transferable: true, - range: range, + timestamps: timestamps, broker: users.broker }); @@ -126,11 +92,11 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is createDefaultStreamWithDurations(durations); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); - expectedStream.cliffTime = range.cliff; - expectedStream.endTime = range.end; - expectedStream.startTime = range.start; + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); + expectedStream.cliffTime = timestamps.cliff; + expectedStream.endTime = timestamps.end; + expectedStream.startTime = timestamps.start; assertEq(actualStream, expectedStream); // Assert that the stream's status is "STREAMING". @@ -138,16 +104,11 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is Lockup.Status expectedStatus = Lockup.Status.STREAMING; assertEq(actualStatus, expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. uint256 actualNextStreamId = lockupLinear.nextStreamId(); uint256 expectedNextStreamId = streamId + 1; assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); - // Assert that the protocol fee has been recorded. - uint128 actualProtocolRevenues = lockupLinear.protocolRevenues(dai); - uint128 expectedProtocolRevenues = initialProtocolRevenues + defaults.PROTOCOL_FEE_AMOUNT(); - assertEq(actualProtocolRevenues, expectedProtocolRevenues, "protocolRevenues"); - // Assert that the NFT has been minted. address actualNFTOwner = lockupLinear.ownerOf({ tokenId: streamId }); address expectedNFTOwner = users.recipient; diff --git a/test/integration/fuzz/lockup-linear/createWithRange.t.sol b/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol similarity index 56% rename from test/integration/fuzz/lockup-linear/createWithRange.t.sol rename to test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol index 3b9cd914b..d59318ec1 100644 --- a/test/integration/fuzz/lockup-linear/createWithRange.t.sol +++ b/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol @@ -1,37 +1,51 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { MAX_UD60x18, UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { MAX_UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { Errors } from "src/libraries/Errors.sol"; import { Broker, Lockup, LockupLinear } from "src/types/DataTypes.sol"; -import { CreateWithRange_Integration_Shared_Test } from "../../shared/lockup-linear/createWithRange.t.sol"; +import { CreateWithTimestamps_Integration_Shared_Test } from "../../shared/lockup/createWithTimestamps.t.sol"; import { LockupLinear_Integration_Fuzz_Test } from "./LockupLinear.t.sol"; -contract CreateWithRange_LockupLinear_Integration_Fuzz_Test is +contract CreateWithTimestamps_LockupLinear_Integration_Fuzz_Test is LockupLinear_Integration_Fuzz_Test, - CreateWithRange_Integration_Shared_Test + CreateWithTimestamps_Integration_Shared_Test { function setUp() public virtual - override(LockupLinear_Integration_Fuzz_Test, CreateWithRange_Integration_Shared_Test) + override(LockupLinear_Integration_Fuzz_Test, CreateWithTimestamps_Integration_Shared_Test) { LockupLinear_Integration_Fuzz_Test.setUp(); - CreateWithRange_Integration_Shared_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); } - function testFuzz_RevertWhen_StartTimeGreaterThanCliffTime(uint40 startTime) + function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero { - startTime = boundUint40(startTime, defaults.CLIFF_TIME() + 1 seconds, MAX_UNIX_TIMESTAMP); + vm.assume(broker.account != address(0)); + broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) + ); + createDefaultStreamWithBroker(broker); + } + + function testFuzz_RevertWhen_StartTimeNotLessThanCliffTime(uint40 startTime) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + { + startTime = boundUint40(startTime, defaults.CLIFF_TIME() + 1 seconds, defaults.END_TIME() - 1 seconds); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, defaults.CLIFF_TIME() + Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime.selector, startTime, defaults.CLIFF_TIME() ) ); createDefaultStreamWithStartTime(startTime); @@ -45,10 +59,9 @@ contract CreateWithRange_LockupLinear_Integration_Fuzz_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime { uint40 startTime = defaults.START_TIME(); - endTime = boundUint40(endTime, startTime, startTime + 2 weeks); + endTime = boundUint40(endTime, startTime + 1 seconds, startTime + 2 weeks); cliffTime = boundUint40(cliffTime, endTime, MAX_UNIX_TIMESTAMP); vm.expectRevert( @@ -56,59 +69,17 @@ contract CreateWithRange_LockupLinear_Integration_Fuzz_Test is Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime ) ); - createDefaultStreamWithRange(LockupLinear.Range({ start: startTime, cliff: cliffTime, end: endTime })); - } - - function testFuzz_RevertWhen_ProtocolFeeTooHigh(UD60x18 protocolFee) - external - whenNotDelegateCalled - whenRecipientNonZeroAddress - whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime - whenCliffTimeLessThanEndTime - whenEndTimeInTheFuture - { - protocolFee = _bound(protocolFee, MAX_FEE + ud(1), MAX_UD60x18); - - // Set the protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); - changePrank({ msgSender: users.sender }); - - // Run the test. - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierV2Lockup_ProtocolFeeTooHigh.selector, protocolFee, MAX_FEE) - ); - createDefaultStream(); - } - - function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) - external - whenNotDelegateCalled - whenRecipientNonZeroAddress - whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime - whenCliffTimeLessThanEndTime - whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh - { - vm.assume(broker.account != address(0)); - broker.fee = _bound(broker.fee, MAX_FEE + ud(1), MAX_UD60x18); - vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, broker.fee, MAX_FEE)); - createDefaultStreamWithBroker(broker); + createDefaultStreamWithTimestamps(LockupLinear.Timestamps({ start: startTime, cliff: cliffTime, end: endTime })); } struct Vars { uint256 actualNextStreamId; address actualNFTOwner; - uint256 actualProtocolRevenues; Lockup.Status actualStatus; Lockup.CreateAmounts createAmounts; uint256 expectedNextStreamId; address expectedNFTOwner; - uint256 expectedProtocolRevenues; Lockup.Status expectedStatus; - uint128 initialProtocolRevenues; } /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: @@ -120,64 +91,63 @@ contract CreateWithRange_LockupLinear_Integration_Fuzz_Test is /// - Start time in the present /// - Start time in the future /// - Start time lower than and equal to cliff time + /// - Cliff time zero and not zero /// - Multiple values for the cliff time and the end time /// - Multiple values for the broker fee, including zero - /// - Multiple values for the protocol fee, including zero - function testFuzz_CreateWithRange( + function testFuzz_CreateWithTimestamps( address funder, - LockupLinear.CreateWithRange memory params, - UD60x18 protocolFee + LockupLinear.CreateWithTimestamps memory params ) external whenNotDelegateCalled whenDepositAmountNotZero - whenStartTimeNotGreaterThanCliffTime + whenStartTimeNotZero whenCliffTimeLessThanEndTime whenEndTimeInTheFuture - givenProtocolFeeNotTooHigh whenBrokerFeeNotTooHigh whenAssetContract whenAssetERC20 { vm.assume(funder != address(0) && params.recipient != address(0) && params.broker.account != address(0)); vm.assume(params.totalAmount != 0); - params.range.start = - boundUint40(params.range.start, defaults.START_TIME(), defaults.START_TIME() + 10_000 seconds); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); - params.range.end = boundUint40(params.range.end, params.range.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - protocolFee = _bound(protocolFee, 0, MAX_FEE); + params.timestamps.start = + boundUint40(params.timestamps.start, defaults.START_TIME(), defaults.START_TIME() + 10_000 seconds); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.transferable = true; + // The cliff time must be either zero or greater than the start time. + if (params.timestamps.cliff > 0) { + params.timestamps.cliff = boundUint40( + params.timestamps.cliff, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks + ); + params.timestamps.end = + boundUint40(params.timestamps.end, params.timestamps.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); + } else { + params.timestamps.end = + boundUint40(params.timestamps.end, params.timestamps.start + 1 seconds, MAX_UNIX_TIMESTAMP); + } + // Calculate the fee amounts and the deposit amount. Vars memory vars; - vars.createAmounts.protocolFee = ud(params.totalAmount).mul(protocolFee).intoUint128(); - vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); - vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.protocolFee - vars.createAmounts.brokerFee; - // Set the fuzzed protocol fee. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: protocolFee }); + vars.createAmounts.brokerFee = ud(params.totalAmount).mul(params.broker.fee).intoUint128(); + vars.createAmounts.deposit = params.totalAmount - vars.createAmounts.brokerFee; // Make the fuzzed funder the caller in this test. - changePrank(funder); + resetPrank(funder); // Mint enough assets to the funder. deal({ token: address(dai), to: funder, give: params.totalAmount }); // Approve {SablierV2LockupLinear} to transfer the assets from the fuzzed funder. - dai.approve({ spender: address(lockupLinear), amount: MAX_UINT256 }); + dai.approve({ spender: address(lockupLinear), value: MAX_UINT256 }); // Expect the assets to be transferred from the funder to {SablierV2LockupLinear}. - expectCallToTransferFrom({ - from: funder, - to: address(lockupLinear), - amount: vars.createAmounts.deposit + vars.createAmounts.protocolFee - }); + expectCallToTransferFrom({ from: funder, to: address(lockupLinear), value: vars.createAmounts.deposit }); // Expect the broker fee to be paid to the broker, if not zero. if (vars.createAmounts.brokerFee > 0) { - expectCallToTransferFrom({ from: funder, to: params.broker.account, amount: vars.createAmounts.brokerFee }); + expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); } // Expect the relevant event to be emitted. @@ -191,53 +161,50 @@ contract CreateWithRange_LockupLinear_Integration_Fuzz_Test is asset: dai, cancelable: params.cancelable, transferable: params.transferable, - range: params.range, + timestamps: params.timestamps, broker: params.broker.account }); // Create the stream. - lockupLinear.createWithRange( - LockupLinear.CreateWithRange({ + lockupLinear.createWithTimestamps( + LockupLinear.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: params.totalAmount, asset: dai, - broker: params.broker, cancelable: params.cancelable, - range: params.range, - recipient: params.recipient, transferable: params.transferable, - sender: params.sender, - totalAmount: params.totalAmount + timestamps: params.timestamps, + broker: params.broker }) ); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); - assertEq(actualStream.cliffTime, params.range.cliff, "cliffTime"); - assertEq(actualStream.endTime, params.range.end, "endTime"); + assertEq(actualStream.cliffTime, params.timestamps.cliff, "cliffTime"); + assertEq(actualStream.endTime, params.timestamps.end, "endTime"); assertEq(actualStream.isCancelable, params.cancelable, "isCancelable"); assertEq(actualStream.isDepleted, false, "isStream"); - assertEq(actualStream.isTransferable, true, "isTransferable"); assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); assertEq(actualStream.sender, params.sender, "sender"); - assertEq(actualStream.startTime, params.range.start, "startTime"); + assertEq(actualStream.startTime, params.timestamps.start, "startTime"); assertEq(actualStream.wasCanceled, false, "wasCanceled"); // Assert that the stream's status is correct. vars.actualStatus = lockupLinear.statusOf(streamId); - vars.expectedStatus = params.range.start > getBlockTimestamp() ? Lockup.Status.PENDING : Lockup.Status.STREAMING; + vars.expectedStatus = + params.timestamps.start > getBlockTimestamp() ? Lockup.Status.PENDING : Lockup.Status.STREAMING; assertEq(vars.actualStatus, vars.expectedStatus); - // Assert that the next stream id has been bumped. + // Assert that the next stream ID has been bumped. vars.actualNextStreamId = lockupLinear.nextStreamId(); vars.expectedNextStreamId = streamId + 1; assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); - // Assert that the protocol fee has been recorded. - vars.actualProtocolRevenues = lockupLinear.protocolRevenues(dai); - vars.expectedProtocolRevenues = vars.createAmounts.protocolFee; - assertEq(vars.actualProtocolRevenues, vars.expectedProtocolRevenues, "protocolRevenues"); - // Assert that the NFT has been minted. vars.actualNFTOwner = lockupLinear.ownerOf({ tokenId: streamId }); vars.expectedNFTOwner = params.recipient; diff --git a/test/integration/fuzz/lockup-linear/streamedAmountOf.t.sol b/test/integration/fuzz/lockup-linear/streamedAmountOf.t.sol index 590400ff4..16fc0b538 100644 --- a/test/integration/fuzz/lockup-linear/streamedAmountOf.t.sol +++ b/test/integration/fuzz/lockup-linear/streamedAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ZERO } from "@prb/math/src/UD60x18.sol"; @@ -21,10 +21,7 @@ contract StreamedAmountOf_LockupLinear_Integration_Fuzz_Test is StreamedAmountOf_Integration_Shared_Test.setUp(); defaultStreamId = createDefaultStream(); - // Disable the protocol fee so that it doesn't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); } function testFuzz_StreamedAmountOf_CliffTimeInTheFuture(uint40 timeJump) @@ -33,7 +30,7 @@ contract StreamedAmountOf_LockupLinear_Integration_Fuzz_Test is givenStreamHasNotBeenCanceled { timeJump = boundUint40(timeJump, 0, defaults.CLIFF_DURATION() - 1); - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(defaultStreamId); uint128 expectedStreamedAmount = 0; assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); @@ -67,18 +64,18 @@ contract StreamedAmountOf_LockupLinear_Integration_Fuzz_Test is deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed deposit amount. - LockupLinear.CreateWithRange memory params = defaults.createWithRange(); + LockupLinear.CreateWithTimestamps memory params = defaults.createWithTimestampsLL(); params.broker = Broker({ account: address(0), fee: ZERO }); params.totalAmount = depositAmount; - uint256 streamId = lockupLinear.createWithRange(params); + uint256 streamId = lockupLinear.createWithTimestamps(params); // Simulate the passage of time. - uint40 currentTime = defaults.START_TIME() + timeJump; - vm.warp({ timestamp: currentTime }); + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); // Run the test. uint128 actualStreamedAmount = lockupLinear.streamedAmountOf(streamId); - uint128 expectedStreamedAmount = calculateStreamedAmount(currentTime, depositAmount); + uint128 expectedStreamedAmount = calculateStreamedAmount(blockTimestamp, depositAmount); assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); } @@ -94,28 +91,28 @@ contract StreamedAmountOf_LockupLinear_Integration_Fuzz_Test is whenCliffTimeNotInTheFuture { vm.assume(depositAmount != 0); - timeWarp0 = boundUint40(timeWarp0, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1); + timeWarp0 = boundUint40(timeWarp0, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); timeWarp1 = boundUint40(timeWarp1, timeWarp0, defaults.TOTAL_DURATION()); // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream with the fuzzed deposit amount. - LockupLinear.CreateWithRange memory params = defaults.createWithRange(); + LockupLinear.CreateWithTimestamps memory params = defaults.createWithTimestampsLL(); params.totalAmount = depositAmount; - uint256 streamId = lockupLinear.createWithRange(params); + uint256 streamId = lockupLinear.createWithTimestamps(params); // Warp to the future for the first time. - vm.warp({ timestamp: defaults.START_TIME() + timeWarp0 }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp0 }); // Calculate the streamed amount at this midpoint in time. uint128 streamedAmount0 = lockupLinear.streamedAmountOf(streamId); // Warp to the future for the second time. - vm.warp({ timestamp: defaults.START_TIME() + timeWarp1 }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp1 }); // Assert that this streamed amount is greater than or equal to the previous streamed amount. uint128 streamedAmount1 = lockupLinear.streamedAmountOf(streamId); - assertGte(streamedAmount1, streamedAmount0, "streamedAmount"); + assertGe(streamedAmount1, streamedAmount0, "streamedAmount"); } } diff --git a/test/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol b/test/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol index 07168646e..7bb8d459b 100644 --- a/test/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol +++ b/test/integration/fuzz/lockup-linear/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ZERO } from "@prb/math/src/UD60x18.sol"; @@ -27,17 +27,14 @@ contract WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test is givenStreamHasNotBeenCanceled { timeJump = boundUint40(timeJump, 0, defaults.CLIFF_DURATION() - 1); - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); uint128 actualWithdrawableAmount = lockupLinear.withdrawableAmountOf(defaultStreamId); uint128 expectedWithdrawableAmount = 0; assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } modifier whenCliffTimeNotInTheFuture() { - // Disable the protocol fee so that it doesn't interfere with the calculations. - changePrank({ msgSender: users.admin }); - comptroller.setProtocolFee({ asset: dai, newProtocolFee: ZERO }); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); _; } @@ -64,18 +61,18 @@ contract WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test is deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. - LockupLinear.CreateWithRange memory params = defaults.createWithRange(); + LockupLinear.CreateWithTimestamps memory params = defaults.createWithTimestampsLL(); params.broker = Broker({ account: address(0), fee: ZERO }); params.totalAmount = depositAmount; - uint256 streamId = lockupLinear.createWithRange(params); + uint256 streamId = lockupLinear.createWithTimestamps(params); // Simulate the passage of time. - uint40 currentTime = defaults.START_TIME() + timeJump; - vm.warp({ timestamp: currentTime }); + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); // Run the test. uint128 actualWithdrawableAmount = lockupLinear.withdrawableAmountOf(streamId); - uint128 expectedWithdrawableAmount = calculateStreamedAmount(currentTime, depositAmount); + uint128 expectedWithdrawableAmount = calculateStreamedAmount(blockTimestamp, depositAmount); assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); } @@ -108,24 +105,24 @@ contract WithdrawableAmountOf_LockupLinear_Integration_Fuzz_Test is timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); depositAmount = boundUint128(depositAmount, 10_000, MAX_UINT128); - // Define the current time. - uint40 currentTime = defaults.START_TIME() + timeJump; + // Define the block timestamp. + uint40 blockTimestamp = defaults.START_TIME() + timeJump; // Bound the withdraw amount. - uint128 streamedAmount = calculateStreamedAmount(currentTime, depositAmount); + uint128 streamedAmount = calculateStreamedAmount(blockTimestamp, depositAmount); withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); // Mint enough assets to the Sender. deal({ token: address(dai), to: users.sender, give: depositAmount }); // Create the stream. The broker fee is disabled so that it doesn't interfere with the calculations. - LockupLinear.CreateWithRange memory params = defaults.createWithRange(); + LockupLinear.CreateWithTimestamps memory params = defaults.createWithTimestampsLL(); params.broker = Broker({ account: address(0), fee: ZERO }); params.totalAmount = depositAmount; - uint256 streamId = lockupLinear.createWithRange(params); + uint256 streamId = lockupLinear.createWithTimestamps(params); // Simulate the passage of time. - vm.warp({ timestamp: currentTime }); + vm.warp({ newTimestamp: blockTimestamp }); // Make the withdrawal. lockupLinear.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); diff --git a/test/integration/fuzz/lockup-tranched/LockupTranched.t.sol b/test/integration/fuzz/lockup-tranched/LockupTranched.t.sol new file mode 100644 index 000000000..a07b20380 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/LockupTranched.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; + +import { LockupTranched_Integration_Shared_Test } from "../../shared/lockup-tranched/LockupTranched.t.sol"; +import { Integration_Test } from "../../Integration.t.sol"; +import { Cancel_Integration_Fuzz_Test } from "../lockup/cancel.t.sol"; +import { CancelMultiple_Integration_Fuzz_Test } from "../lockup/cancelMultiple.t.sol"; +import { GetWithdrawnAmount_Integration_Fuzz_Test } from "../lockup/getWithdrawnAmount.t.sol"; +import { RefundableAmountOf_Integration_Fuzz_Test } from "../lockup/refundableAmountOf.t.sol"; +import { WithdrawMax_Integration_Fuzz_Test } from "../lockup/withdrawMax.t.sol"; +import { WithdrawMaxAndTransfer_Integration_Fuzz_Test } from "../lockup/withdrawMaxAndTransfer.t.sol"; +import { WithdrawMultiple_Integration_Fuzz_Test } from "../lockup/withdrawMultiple.t.sol"; + +/*////////////////////////////////////////////////////////////////////////// + NON-SHARED ABSTRACT TEST +//////////////////////////////////////////////////////////////////////////*/ + +/// @notice Common testing logic needed across {SablierV2LockupTranched} integration fuzz tests. +abstract contract LockupTranched_Integration_Fuzz_Test is Integration_Test, LockupTranched_Integration_Shared_Test { + function setUp() public virtual override(Integration_Test, LockupTranched_Integration_Shared_Test) { + // Both of these contracts inherit from {Base_Test}, which is fine because multiple inheritance is + // allowed in Solidity, and {Base_Test-setUp} will only be called once. + Integration_Test.setUp(); + LockupTranched_Integration_Shared_Test.setUp(); + + // Cast the LockupTranched contract as {ISablierV2Lockup}. + lockup = ISablierV2Lockup(lockupTranched); + } +} + +/*////////////////////////////////////////////////////////////////////////// + SHARED TESTS +//////////////////////////////////////////////////////////////////////////*/ + +contract Cancel_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + Cancel_Integration_Fuzz_Test +{ + function setUp() public virtual override(LockupTranched_Integration_Fuzz_Test, Cancel_Integration_Fuzz_Test) { + LockupTranched_Integration_Fuzz_Test.setUp(); + Cancel_Integration_Fuzz_Test.setUp(); + } +} + +contract CancelMultiple_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + CancelMultiple_Integration_Fuzz_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, CancelMultiple_Integration_Fuzz_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + CancelMultiple_Integration_Fuzz_Test.setUp(); + } +} + +contract RefundableAmountOf_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + RefundableAmountOf_Integration_Fuzz_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, RefundableAmountOf_Integration_Fuzz_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + RefundableAmountOf_Integration_Fuzz_Test.setUp(); + } +} + +contract GetWithdrawnAmount_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + GetWithdrawnAmount_Integration_Fuzz_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, GetWithdrawnAmount_Integration_Fuzz_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + GetWithdrawnAmount_Integration_Fuzz_Test.setUp(); + } +} + +contract WithdrawMax_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + WithdrawMax_Integration_Fuzz_Test +{ + function setUp() public virtual override(LockupTranched_Integration_Fuzz_Test, WithdrawMax_Integration_Fuzz_Test) { + LockupTranched_Integration_Fuzz_Test.setUp(); + WithdrawMax_Integration_Fuzz_Test.setUp(); + } +} + +contract WithdrawMaxAndTransfer_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + WithdrawMaxAndTransfer_Integration_Fuzz_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, WithdrawMaxAndTransfer_Integration_Fuzz_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + WithdrawMaxAndTransfer_Integration_Fuzz_Test.setUp(); + } +} + +contract WithdrawMultiple_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + WithdrawMultiple_Integration_Fuzz_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, WithdrawMultiple_Integration_Fuzz_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + WithdrawMultiple_Integration_Fuzz_Test.setUp(); + } +} diff --git a/test/integration/fuzz/lockup-tranched/createWithDurations.t.sol b/test/integration/fuzz/lockup-tranched/createWithDurations.t.sol new file mode 100644 index 000000000..10bb043d1 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/createWithDurations.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { CreateWithDurations_Integration_Shared_Test } from "../../shared/lockup/createWithDurations.t.sol"; +import { LockupTranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; + +contract CreateWithDurations_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + CreateWithDurations_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, CreateWithDurations_Integration_Shared_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + CreateWithDurations_Integration_Shared_Test.setUp(); + } + + struct Vars { + uint256 actualNextStreamId; + address actualNFTOwner; + Lockup.Status actualStatus; + Lockup.CreateAmounts createAmounts; + uint256 expectedNextStreamId; + address expectedNFTOwner; + Lockup.Status expectedStatus; + address funder; + bool isCancelable; + bool isSettled; + LockupTranched.Tranche[] tranchesWithTimestamps; + uint128 totalAmount; + } + + function testFuzz_CreateWithDurations(LockupTranched.TrancheWithDuration[] memory tranches) + external + whenNotDelegateCalled + whenTrancheCountNotTooHigh + whenDurationsNotZero + whenTimestampsCalculationsDoNotOverflow + { + vm.assume(tranches.length != 0); + + // Fuzz the durations. + Vars memory vars; + fuzzTrancheDurations(tranches); + + // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). + (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts(tranches); + + // Make the Sender the stream's funder (recall that the Sender is the default caller). + vars.funder = users.sender; + + // Mint enough assets to the fuzzed funder. + deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); + + // Expect the assets to be transferred from the funder to {SablierV2LockupTranched}. + expectCallToTransferFrom({ from: vars.funder, to: address(lockupTranched), value: vars.createAmounts.deposit }); + + // Expect the broker fee to be paid to the broker, if not zero. + if (vars.createAmounts.brokerFee > 0) { + expectCallToTransferFrom({ from: vars.funder, to: users.broker, value: vars.createAmounts.brokerFee }); + } + + // Create the timestamps struct. + vars.tranchesWithTimestamps = getTranchesWithTimestamps(tranches); + LockupTranched.Timestamps memory timestamps = LockupTranched.Timestamps({ + start: getBlockTimestamp(), + end: vars.tranchesWithTimestamps[vars.tranchesWithTimestamps.length - 1].timestamp + }); + + // Expect the relevant event to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit CreateLockupTranchedStream({ + streamId: streamId, + funder: vars.funder, + sender: users.sender, + recipient: users.recipient, + amounts: vars.createAmounts, + asset: dai, + cancelable: true, + transferable: true, + tranches: vars.tranchesWithTimestamps, + timestamps: timestamps, + broker: users.broker + }); + + // Create the stream. + LockupTranched.CreateWithDurations memory params = defaults.createWithDurationsLT(); + params.tranches = tranches; + params.totalAmount = vars.totalAmount; + params.transferable = true; + lockupTranched.createWithDurations(params); + + // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation + // because some tranche amounts can be zero. + vars.isSettled = lockupTranched.refundableAmountOf(streamId) == 0; + vars.isCancelable = vars.isSettled ? false : true; + + // Assert that the stream has been created. + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(streamId); + assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); + assertEq(actualStream.asset, dai, "asset"); + assertEq(actualStream.endTime, timestamps.end, "endTime"); + assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); + assertEq(actualStream.isDepleted, false, "isDepleted"); + assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); + assertEq(actualStream.tranches, vars.tranchesWithTimestamps, "tranches"); + assertEq(actualStream.sender, users.sender, "sender"); + assertEq(actualStream.startTime, timestamps.start, "startTime"); + assertEq(actualStream.wasCanceled, false, "wasCanceled"); + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.statusOf(streamId); + vars.expectedStatus = vars.isSettled ? Lockup.Status.SETTLED : Lockup.Status.STREAMING; + assertEq(vars.actualStatus, vars.expectedStatus); + + // Assert that the next stream ID has been bumped. + vars.actualNextStreamId = lockupTranched.nextStreamId(); + vars.expectedNextStreamId = streamId + 1; + assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + vars.actualNFTOwner = lockupTranched.ownerOf({ tokenId: streamId }); + vars.expectedNFTOwner = users.recipient; + assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "NFT owner"); + } +} diff --git a/test/integration/fuzz/lockup-tranched/createWithTimestamps.t.sol b/test/integration/fuzz/lockup-tranched/createWithTimestamps.t.sol new file mode 100644 index 000000000..31d7a2122 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/createWithTimestamps.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { MAX_UD60x18, UD60x18, ud, ZERO } from "@prb/math/src/UD60x18.sol"; +import { stdError } from "forge-std/src/StdError.sol"; + +import { Errors } from "src/libraries/Errors.sol"; +import { Broker, Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { CreateWithTimestamps_Integration_Shared_Test } from "../../shared/lockup/createWithTimestamps.t.sol"; +import { LockupTranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; + +contract CreateWithTimestamps_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + CreateWithTimestamps_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, CreateWithTimestamps_Integration_Shared_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + CreateWithTimestamps_Integration_Shared_Test.setUp(); + } + + function testFuzz_RevertWhen_TrancheCountTooHigh(uint256 trancheCount) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenTrancheCountNotZero + { + uint256 defaultMax = defaults.MAX_TRANCHE_COUNT(); + trancheCount = _bound(trancheCount, defaultMax + 1, defaultMax * 10); + LockupTranched.Tranche[] memory tranches = new LockupTranched.Tranche[](trancheCount); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2LockupTranched_TrancheCountTooHigh.selector, trancheCount) + ); + createDefaultStreamWithTranches(tranches); + } + + function testFuzz_RevertWhen_TrancheAmountsSumOverflows( + uint128 amount0, + uint128 amount1 + ) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + { + amount0 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); + amount1 = boundUint128(amount0, MAX_UINT128 / 2 + 1, MAX_UINT128); + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].amount = amount0; + tranches[1].amount = amount1; + vm.expectRevert(stdError.arithmeticError); + createDefaultStreamWithTranches(tranches); + } + + function testFuzz_RevertWhen_StartTimeNotLessThanFirstTrancheTimestamp(uint40 firstTimestamp) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + { + firstTimestamp = boundUint40(firstTimestamp, 0, defaults.START_TIME()); + + // Change the timestamp of the first tranche. + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + tranches[0].timestamp = firstTimestamp; + + // Expect the relevant error to be thrown. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_StartTimeNotLessThanFirstTrancheTimestamp.selector, + defaults.START_TIME(), + tranches[0].timestamp + ) + ); + + // Create the stream. + createDefaultStreamWithTranches(tranches); + } + + function testFuzz_RevertWhen_DepositAmountNotEqualToTrancheAmountsSum(uint128 depositDiff) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + { + depositDiff = boundUint128(depositDiff, 100, defaults.TOTAL_AMOUNT()); + + UD60x18 brokerFee = ZERO; + resetPrank({ msgSender: users.sender }); + + // Adjust the default deposit amount. + uint128 defaultDepositAmount = defaults.DEPOSIT_AMOUNT(); + uint128 depositAmount = defaultDepositAmount + depositDiff; + + // Prepare the params. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: brokerFee }); + params.totalAmount = depositAmount; + + // Expect the relevant error to be thrown. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupTranched_DepositAmountNotEqualToTrancheAmountsSum.selector, + depositAmount, + defaultDepositAmount + ) + ); + + // Create the stream. + lockupTranched.createWithTimestamps(params); + } + + function testFuzz_RevertWhen_BrokerFeeTooHigh(Broker memory broker) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + { + vm.assume(broker.account != address(0)); + broker.fee = _bound(broker.fee, MAX_BROKER_FEE + ud(1), MAX_UD60x18); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierV2Lockup_BrokerFeeTooHigh.selector, broker.fee, MAX_BROKER_FEE) + ); + createDefaultStreamWithBroker(broker); + } + + struct Vars { + uint256 actualNextStreamId; + address actualNFTOwner; + Lockup.Status actualStatus; + Lockup.CreateAmounts createAmounts; + uint256 expectedNextStreamId; + address expectedNFTOwner; + Lockup.Status expectedStatus; + bool isCancelable; + bool isSettled; + uint128 totalAmount; + } + + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - All possible permutations for the funder, sender, recipient, and broker + /// - Multiple values for the tranche amounts, exponents, and timestamps + /// - Cancelable and not cancelable + /// - Start time in the past + /// - Start time in the present + /// - Start time in the future + /// - Start time equal and not equal to the first tranche timestamp + /// - Multiple values for the broker fee, including zero + function testFuzz_CreateWithTimestamps( + address funder, + LockupTranched.CreateWithTimestamps memory params + ) + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenStartTimeNotZero + whenTrancheCountNotZero + whenTrancheCountNotTooHigh + whenTrancheAmountsSumDoesNotOverflow + whenStartTimeLessThanFirstTrancheTimestamp + whenTrancheTimestampsOrdered + whenEndTimeInTheFuture + whenDepositAmountEqualToTrancheAmountsSum + whenBrokerFeeNotTooHigh + whenAssetContract + whenAssetERC20 + { + vm.assume(funder != address(0) && params.recipient != address(0) && params.broker.account != address(0)); + vm.assume(params.tranches.length != 0); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + + params.startTime = boundUint40(params.startTime, 1, defaults.START_TIME()); + params.transferable = true; + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(params.tranches, params.startTime); + + // Fuzz the tranche amounts and calculate the total and create amounts (deposit and broker fee). + Vars memory vars; + (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts({ + upperBound: MAX_UINT128, + tranches: params.tranches, + brokerFee: params.broker.fee + }); + + // Make the fuzzed funder the caller in the rest of this test. + resetPrank(funder); + + // Mint enough assets to the fuzzed funder. + deal({ token: address(dai), to: funder, give: vars.totalAmount }); + + // Approve {SablierV2LockupTranched} to transfer the assets from the fuzzed funder. + dai.approve({ spender: address(lockupTranched), value: MAX_UINT256 }); + + // Expect the assets to be transferred from the funder to {SablierV2LockupTranched}. + expectCallToTransferFrom({ from: funder, to: address(lockupTranched), value: vars.createAmounts.deposit }); + + // Expect the broker fee to be paid to the broker, if not zero. + if (vars.createAmounts.brokerFee > 0) { + expectCallToTransferFrom({ from: funder, to: params.broker.account, value: vars.createAmounts.brokerFee }); + } + + // Expect the relevant event to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + LockupTranched.Timestamps memory timestamps = LockupTranched.Timestamps({ + start: params.startTime, + end: params.tranches[params.tranches.length - 1].timestamp + }); + emit CreateLockupTranchedStream({ + streamId: streamId, + funder: funder, + sender: params.sender, + recipient: params.recipient, + amounts: vars.createAmounts, + asset: dai, + cancelable: params.cancelable, + transferable: params.transferable, + tranches: params.tranches, + timestamps: timestamps, + broker: params.broker.account + }); + + // Create the stream. + lockupTranched.createWithTimestamps( + LockupTranched.CreateWithTimestamps({ + sender: params.sender, + recipient: params.recipient, + totalAmount: vars.totalAmount, + asset: dai, + cancelable: params.cancelable, + transferable: params.transferable, + startTime: params.startTime, + tranches: params.tranches, + broker: params.broker + }) + ); + + // Check if the stream is settled. It is possible for a Lockup Tranched stream to settle at the time of creation + // because some tranche amounts can be zero. + vars.isSettled = (lockupTranched.getDepositedAmount(streamId) - lockupTranched.streamedAmountOf(streamId)) == 0; + vars.isCancelable = vars.isSettled ? false : params.cancelable; + + // Assert that the stream has been created. + LockupTranched.StreamLT memory actualStream = lockupTranched.getStream(streamId); + assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); + assertEq(actualStream.asset, dai, "asset"); + assertEq(actualStream.endTime, timestamps.end, "endTime"); + assertEq(actualStream.isCancelable, vars.isCancelable, "isCancelable"); + assertEq(actualStream.isDepleted, false, "isStream"); + assertEq(actualStream.isStream, true, "isStream"); + assertEq(actualStream.isTransferable, true, "isTransferable"); + assertEq(actualStream.recipient, params.recipient, "recipient"); + assertEq(actualStream.sender, params.sender, "sender"); + assertEq(actualStream.tranches, params.tranches, "tranches"); + assertEq(actualStream.startTime, timestamps.start, "startTime"); + assertEq(actualStream.wasCanceled, false, "wasCanceled"); + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.statusOf(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); + + // Assert that the next stream ID has been bumped. + vars.actualNextStreamId = lockupTranched.nextStreamId(); + vars.expectedNextStreamId = streamId + 1; + assertEq(vars.actualNextStreamId, vars.expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + vars.actualNFTOwner = lockupTranched.ownerOf({ tokenId: streamId }); + vars.expectedNFTOwner = params.recipient; + assertEq(vars.actualNFTOwner, vars.expectedNFTOwner, "NFT owner"); + } +} diff --git a/test/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol b/test/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol new file mode 100644 index 000000000..29c105b53 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/streamedAmountOf.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ZERO } from "@prb/math/src/UD60x18.sol"; +import { Broker, LockupTranched } from "src/types/DataTypes.sol"; + +import { StreamedAmountOf_Integration_Shared_Test } from "../../shared/lockup/streamedAmountOf.t.sol"; +import { LockupTranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; + +contract StreamedAmountOf_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + StreamedAmountOf_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, StreamedAmountOf_Integration_Shared_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + StreamedAmountOf_Integration_Shared_Test.setUp(); + + resetPrank({ msgSender: users.sender }); + } + + modifier givenMultipleTranches() { + _; + } + + modifier whenCurrentTimestampNot1st() { + _; + } + + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - End time in the past + /// - End time in the present + /// - End time in the future + /// - Multiple deposit amounts + /// - Status streaming + /// - Status settled + function testFuzz_StreamedAmountOf_Calculation( + LockupTranched.Tranche[] memory tranches, + uint40 timeJump + ) + external + givenNotNull + givenStreamHasNotBeenCanceled + whenStartTimeInThePast + givenMultipleTranches + whenCurrentTimestampNot1st + { + vm.assume(tranches.length > 1); + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(tranches, defaults.START_TIME()); + + // Fuzz the tranche amounts. + (uint128 totalAmount,) = + fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: ZERO }); + + // Bound the time jump. + uint40 firstTrancheDuration = tranches[1].timestamp - tranches[0].timestamp; + uint40 totalDuration = tranches[tranches.length - 1].timestamp - defaults.START_TIME(); + timeJump = boundUint40(timeJump, firstTrancheDuration, totalDuration + 100 seconds); + + // Mint enough assets to the Sender. + deal({ token: address(dai), to: users.sender, give: totalAmount }); + + // Create the stream with the fuzzed tranches. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: ZERO }); + params.tranches = tranches; + params.totalAmount = totalAmount; + uint256 streamId = lockupTranched.createWithTimestamps(params); + + // Simulate the passage of time. + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); + + // Run the test. + uint128 actualStreamedAmount = lockupTranched.streamedAmountOf(streamId); + uint128 expectedStreamedAmount = calculateStreamedAmountForTranches(blockTimestamp, tranches, totalAmount); + assertEq(actualStreamedAmount, expectedStreamedAmount, "streamedAmount"); + } + + /// @dev The streamed amount must never go down over time. + function testFuzz_StreamedAmountOf_Monotonicity( + LockupTranched.Tranche[] memory tranches, + uint40 timeWarp0, + uint40 timeWarp1 + ) + external + givenNotNull + givenStreamHasNotBeenCanceled + whenStartTimeInThePast + givenMultipleTranches + whenCurrentTimestampNot1st + { + vm.assume(tranches.length > 1); + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(tranches, defaults.START_TIME()); + + // Fuzz the tranche amounts. + (uint128 totalAmount,) = + fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: ZERO }); + + // Bound the time warps. + uint40 firstTrancheDuration = tranches[1].timestamp - tranches[0].timestamp; + uint40 totalDuration = tranches[tranches.length - 1].timestamp - defaults.START_TIME(); + timeWarp0 = boundUint40(timeWarp0, firstTrancheDuration, totalDuration - 1); + timeWarp1 = boundUint40(timeWarp1, timeWarp0, totalDuration); + + // Mint enough assets to the Sender. + deal({ token: address(dai), to: users.sender, give: totalAmount }); + + // Create the stream with the fuzzed tranches. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: ZERO }); + params.tranches = tranches; + params.totalAmount = totalAmount; + uint256 streamId = lockupTranched.createWithTimestamps(params); + + // Warp to the future for the first time. + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp0 }); + + // Calculate the streamed amount at this midpoint in time. + uint128 streamedAmount0 = lockupTranched.streamedAmountOf(streamId); + + // Warp to the future for the second time. + vm.warp({ newTimestamp: defaults.START_TIME() + timeWarp1 }); + + // Assert that this streamed amount is greater than or equal to the previous streamed amount. + uint128 streamedAmount1 = lockupTranched.streamedAmountOf(streamId); + assertGe(streamedAmount1, streamedAmount0, "streamedAmount"); + } +} diff --git a/test/integration/fuzz/lockup-tranched/withdraw.t.sol b/test/integration/fuzz/lockup-tranched/withdraw.t.sol new file mode 100644 index 000000000..7a73f5df6 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/withdraw.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { Withdraw_Integration_Fuzz_Test } from "../lockup/withdraw.t.sol"; +import { LockupTranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; + +/// @dev This contract complements the tests in {Withdraw_Integration_Fuzz_Test} by testing the withdraw function +/// against +/// streams created with fuzzed tranches. +contract Withdraw_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + Withdraw_Integration_Fuzz_Test +{ + function setUp() public virtual override(LockupTranched_Integration_Fuzz_Test, Withdraw_Integration_Fuzz_Test) { + LockupTranched_Integration_Fuzz_Test.setUp(); + Withdraw_Integration_Fuzz_Test.setUp(); + } + + struct Params { + LockupTranched.Tranche[] tranches; + uint256 timeJump; + address to; + } + + struct Vars { + Lockup.Status actualStatus; + uint256 actualWithdrawnAmount; + Lockup.CreateAmounts createAmounts; + Lockup.Status expectedStatus; + uint256 expectedWithdrawnAmount; + bool isDepleted; + bool isSettled; + address funder; + uint256 streamId; + uint128 totalAmount; + uint40 totalDuration; + uint128 withdrawAmount; + uint128 withdrawableAmount; + } + + function testFuzz_Withdraw_TrancheFuzzing(Params memory params) + external + whenNotDelegateCalled + givenNotNull + whenToNonZeroAddress + whenWithdrawAmountNotZero + whenNoOverdraw + { + vm.assume(params.tranches.length != 0); + vm.assume(params.to != address(0)); + + // Make the Sender the stream's funder (recall that the Sender is the default caller). + Vars memory vars; + vars.funder = users.sender; + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(params.tranches, defaults.START_TIME()); + + // Fuzz the tranche amounts. + (vars.totalAmount, vars.createAmounts) = fuzzTranchedStreamAmounts(params.tranches); + + // Bound the time jump. + vars.totalDuration = params.tranches[params.tranches.length - 1].timestamp - defaults.START_TIME(); + params.timeJump = _bound(params.timeJump, 1 seconds, vars.totalDuration + 100 seconds); + + // Mint enough assets to the funder. + deal({ token: address(dai), to: vars.funder, give: vars.totalAmount }); + + // Make the Sender the caller. + resetPrank({ msgSender: users.sender }); + + // Create the stream with the fuzzed tranches. + LockupTranched.CreateWithTimestamps memory createParams = defaults.createWithTimestampsLT(); + createParams.totalAmount = vars.totalAmount; + createParams.tranches = params.tranches; + + vars.streamId = lockupTranched.createWithTimestamps(createParams); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.START_TIME() + params.timeJump }); + + // Query the withdrawable amount. + vars.withdrawableAmount = lockupTranched.withdrawableAmountOf(vars.streamId); + + // Halt the test if the withdraw amount is zero. + if (vars.withdrawableAmount == 0) { + return; + } + + // Bound the withdraw amount. + vars.withdrawAmount = boundUint128(vars.withdrawAmount, 1, vars.withdrawableAmount); + + // Make the Recipient the caller. + resetPrank({ msgSender: users.recipient }); + + // Expect the assets to be transferred to the fuzzed `to` address. + expectCallToTransfer({ to: params.to, value: vars.withdrawAmount }); + + // Expect the relevant events to be emitted. + vm.expectEmit({ emitter: address(lockupTranched) }); + emit WithdrawFromLockupStream({ streamId: vars.streamId, to: params.to, asset: dai, amount: vars.withdrawAmount }); + vm.expectEmit({ emitter: address(lockupTranched) }); + emit MetadataUpdate({ _tokenId: vars.streamId }); + + // Make the withdrawal. + lockupTranched.withdraw({ streamId: vars.streamId, to: params.to, amount: vars.withdrawAmount }); + + // Check if the stream is depleted or settled. It is possible for the stream to be just settled + // and not depleted because the withdraw amount is fuzzed. + vars.isDepleted = vars.withdrawAmount == vars.createAmounts.deposit; + vars.isSettled = lockupTranched.refundableAmountOf(vars.streamId) == 0; + + // Assert that the stream's status is correct. + vars.actualStatus = lockupTranched.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); + + // Assert that the withdrawn amount has been updated. + vars.actualWithdrawnAmount = lockupTranched.getWithdrawnAmount(vars.streamId); + vars.expectedWithdrawnAmount = vars.withdrawAmount; + assertEq(vars.actualWithdrawnAmount, vars.expectedWithdrawnAmount, "withdrawnAmount"); + } +} diff --git a/test/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol b/test/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol new file mode 100644 index 000000000..407414ba6 --- /dev/null +++ b/test/integration/fuzz/lockup-tranched/withdrawableAmountOf.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ZERO } from "@prb/math/src/UD60x18.sol"; + +import { Broker, LockupTranched } from "src/types/DataTypes.sol"; + +import { LockupTranched_Integration_Fuzz_Test } from "./LockupTranched.t.sol"; +import { WithdrawableAmountOf_Integration_Shared_Test } from "../../shared/lockup/withdrawableAmountOf.t.sol"; + +contract WithdrawableAmountOf_LockupTranched_Integration_Fuzz_Test is + LockupTranched_Integration_Fuzz_Test, + WithdrawableAmountOf_Integration_Shared_Test +{ + function setUp() + public + virtual + override(LockupTranched_Integration_Fuzz_Test, WithdrawableAmountOf_Integration_Shared_Test) + { + LockupTranched_Integration_Fuzz_Test.setUp(); + WithdrawableAmountOf_Integration_Shared_Test.setUp(); + + resetPrank({ msgSender: users.sender }); + } + + modifier whenStartTimeInThePast() { + _; + } + + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - End time in the past + /// - End time in the present + /// - End time in the future + /// - Status streaming + /// - Status settled + function testFuzz_WithdrawableAmountOf_NoPreviousWithdrawals(uint40 timeJump) external whenStartTimeInThePast { + timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + + // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with + // the calculations. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: ZERO }); + params.totalAmount = defaults.DEPOSIT_AMOUNT(); + uint256 streamId = lockupTranched.createWithTimestamps(params); + + // Simulate the passage of time. + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + vm.warp({ newTimestamp: blockTimestamp }); + + // Run the test. + uint128 actualWithdrawableAmount = lockupTranched.withdrawableAmountOf(streamId); + uint128 expectedWithdrawableAmount = + calculateStreamedAmountForTranches(blockTimestamp, defaults.tranches(), defaults.DEPOSIT_AMOUNT()); + assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); + } + + modifier whenWithWithdrawals() { + _; + } + + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - End time in the past + /// - End time in the present + /// - End time in the future + /// - Multiple withdraw amounts + /// - Status streaming + /// - Status settled + /// - Status depleted + /// - Withdraw amount equal to deposited amount and not + function testFuzz_WithdrawableAmountOf( + uint40 timeJump, + uint128 withdrawAmount + ) + external + whenStartTimeInThePast + whenWithWithdrawals + { + timeJump = boundUint40(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); + + // Define the block timestamp. + uint40 blockTimestamp = defaults.START_TIME() + timeJump; + + // Bound the withdraw amount. + uint128 streamedAmount = + calculateStreamedAmountForTranches(blockTimestamp, defaults.tranches(), defaults.DEPOSIT_AMOUNT()); + withdrawAmount = boundUint128(withdrawAmount, 1, streamedAmount); + + // Create the stream with a custom total amount. The broker fee is disabled so that it doesn't interfere with + // the calculations. + LockupTranched.CreateWithTimestamps memory params = defaults.createWithTimestampsLT(); + params.broker = Broker({ account: address(0), fee: ZERO }); + params.totalAmount = defaults.DEPOSIT_AMOUNT(); + uint256 streamId = lockupTranched.createWithTimestamps(params); + + // Simulate the passage of time. + vm.warp({ newTimestamp: blockTimestamp }); + + // Make the withdrawal. + lockupTranched.withdraw({ streamId: streamId, to: users.recipient, amount: withdrawAmount }); + + // Run the test. + uint128 actualWithdrawableAmount = lockupTranched.withdrawableAmountOf(streamId); + uint128 expectedWithdrawableAmount = streamedAmount - withdrawAmount; + assertEq(actualWithdrawableAmount, expectedWithdrawableAmount, "withdrawableAmount"); + } +} diff --git a/test/integration/fuzz/lockup/cancel.t.sol b/test/integration/fuzz/lockup/cancel.t.sol index d59082cb5..532e47b6d 100644 --- a/test/integration/fuzz/lockup/cancel.t.sol +++ b/test/integration/fuzz/lockup/cancel.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; @@ -22,7 +22,7 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test, Cancel_Integ timeJump = _bound(timeJump, 1 seconds, 100 weeks); // Warp to the past. - vm.warp({ timestamp: getBlockTimestamp() - timeJump }); + vm.warp({ newTimestamp: getBlockTimestamp() - timeJump }); // Cancel the stream. lockup.cancel(defaultStreamId); @@ -39,7 +39,7 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test, Cancel_Integ /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - Multiple values for the current time + /// - Multiple values for the block timestamp /// - With and without withdrawals function testFuzz_Cancel( uint256 timeJump, @@ -52,18 +52,23 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test, Cancel_Integ whenCallerAuthorized givenStreamCancelable givenStatusStreaming - givenRecipientContract - givenRecipientImplementsHook - whenRecipientDoesNotRevert - whenNoRecipientReentrancy + givenRecipientAllowedToHook + whenRecipientNotReverting + whenRecipientReturnsSelector + whenRecipientNotReentrant { - timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1); + timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); + + // Allow the recipient to hook. + resetPrank({ msgSender: users.admin }); + lockup.allowToHook(address(recipientGood)); + resetPrank({ msgSender: users.sender }); // Create the stream. - uint256 streamId = createDefaultStreamWithRecipient(address(goodRecipient)); + uint256 streamId = createDefaultStreamWithRecipient(address(recipientGood)); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. uint128 streamedAmount = lockup.streamedAmountOf(streamId); @@ -71,17 +76,17 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test, Cancel_Integ // Make the withdrawal only if the amount is greater than zero. if (withdrawAmount > 0) { - lockup.withdraw({ streamId: streamId, to: address(goodRecipient), amount: withdrawAmount }); + lockup.withdraw({ streamId: streamId, to: address(recipientGood), amount: withdrawAmount }); } // Expect the assets to be refunded to the Sender. uint128 senderAmount = lockup.refundableAmountOf(streamId); - expectCallToTransfer({ to: users.sender, amount: senderAmount }); + expectCallToTransfer({ to: users.sender, value: senderAmount }); // Expect the relevant events to be emitted. uint128 recipientAmount = lockup.withdrawableAmountOf(streamId); vm.expectEmit({ emitter: address(lockup) }); - emit CancelLockupStream(streamId, users.sender, address(goodRecipient), dai, senderAmount, recipientAmount); + emit CancelLockupStream(streamId, users.sender, address(recipientGood), dai, senderAmount, recipientAmount); vm.expectEmit({ emitter: address(lockup) }); emit MetadataUpdate({ _tokenId: streamId }); @@ -99,7 +104,7 @@ abstract contract Cancel_Integration_Fuzz_Test is Integration_Test, Cancel_Integ // Assert that the NFT has not been burned. address actualNFTOwner = lockup.ownerOf({ tokenId: streamId }); - address expectedNFTOwner = address(goodRecipient); + address expectedNFTOwner = address(recipientGood); assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); } } diff --git a/test/integration/fuzz/lockup/cancelMultiple.t.sol b/test/integration/fuzz/lockup/cancelMultiple.t.sol index d9e2b451e..eac94ed5c 100644 --- a/test/integration/fuzz/lockup/cancelMultiple.t.sol +++ b/test/integration/fuzz/lockup/cancelMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Solarray } from "solarray/src/Solarray.sol"; @@ -31,16 +31,16 @@ abstract contract CancelMultiple_Integration_Fuzz_Test is Integration_Test, Canc uint256 streamId = createDefaultStreamWithEndTime(endTime); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); - // Create the stream ids array. + // Create the stream IDs array. uint256[] memory streamIds = Solarray.uint256s(testStreamIds[0], streamId); // Expect the assets to be refunded to the Sender. uint128 senderAmount0 = lockup.refundableAmountOf(streamIds[0]); - expectCallToTransfer({ to: users.sender, amount: senderAmount0 }); + expectCallToTransfer({ to: users.sender, value: senderAmount0 }); uint128 senderAmount1 = lockup.refundableAmountOf(streamIds[1]); - expectCallToTransfer({ to: users.sender, amount: senderAmount1 }); + expectCallToTransfer({ to: users.sender, value: senderAmount1 }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/fuzz/lockup/getWithdrawnAmount.t.sol b/test/integration/fuzz/lockup/getWithdrawnAmount.t.sol index 88e7c4413..71da96105 100644 --- a/test/integration/fuzz/lockup/getWithdrawnAmount.t.sol +++ b/test/integration/fuzz/lockup/getWithdrawnAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { GetWithdrawnAmount_Integration_Shared_Test } from "../../shared/lockup/getWithdrawnAmount.t.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -16,7 +16,7 @@ abstract contract GetWithdrawnAmount_Integration_Fuzz_Test is timeJump = _bound(timeJump, 0 seconds, defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Assert that the withdrawn amount has been updated. uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); @@ -35,7 +35,7 @@ abstract contract GetWithdrawnAmount_Integration_Fuzz_Test is timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. uint128 streamedAmount = lockup.streamedAmountOf(defaultStreamId); diff --git a/test/integration/fuzz/lockup/refundableAmountOf.t.sol b/test/integration/fuzz/lockup/refundableAmountOf.t.sol index 340352881..013548a9d 100644 --- a/test/integration/fuzz/lockup/refundableAmountOf.t.sol +++ b/test/integration/fuzz/lockup/refundableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "../../shared/lockup/Lockup.t.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -19,7 +19,7 @@ abstract contract RefundableAmountOf_Integration_Fuzz_Test is Integration_Test, timeJump = _bound(timeJump, 0 seconds, defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the streamed amount. uint128 streamedAmount = lockup.streamedAmountOf(defaultStreamId); diff --git a/test/integration/fuzz/lockup/withdraw.t.sol b/test/integration/fuzz/lockup/withdraw.t.sol index 3ef2815c4..a5b6d5618 100644 --- a/test/integration/fuzz/lockup/withdraw.t.sol +++ b/test/integration/fuzz/lockup/withdraw.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; @@ -11,6 +11,39 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I Withdraw_Integration_Shared_Test.setUp(); } + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: + /// + /// - Multiple caller addresses. + function testFuzz_Withdraw_UnknownCaller(address caller) + external + whenNotDelegateCalled + givenNotNull + whenToNonZeroAddress + whenWithdrawAmountNotZero + whenNoOverdraw + { + vm.assume(caller != users.sender && caller != users.recipient); + + // Make the fuzzed address the caller in this test. + resetPrank({ msgSender: caller }); + + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); + + // Make the withdrawal. + lockup.withdraw({ streamId: defaultStreamId, to: users.recipient, amount: defaults.WITHDRAW_AMOUNT() }); + + // Assert that the stream's status is still "STREAMING". + Lockup.Status actualStatus = lockup.statusOf(defaultStreamId); + Lockup.Status expectedStatus = Lockup.Status.STREAMING; + assertEq(actualStatus, expectedStatus); + + // Assert that the withdrawn amount has been updated. + uint128 actualWithdrawnAmount = lockup.getWithdrawnAmount(defaultStreamId); + uint128 expectedWithdrawnAmount = defaults.WITHDRAW_AMOUNT(); + assertEq(actualWithdrawnAmount, expectedWithdrawnAmount, "withdrawnAmount"); + } + /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// /// - Multiple values for the withdrawal address. @@ -19,10 +52,9 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I whenNotDelegateCalled givenNotNull givenStreamNotDepleted - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero - whenWithdrawAmountNotGreaterThanWithdrawableAmount + whenNoOverdraw { vm.assume(to != address(0)); @@ -30,10 +62,10 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I lockup.approve({ to: users.operator, tokenId: defaultStreamId }); // Make the operator the caller in this test. - changePrank({ msgSender: users.operator }); + resetPrank({ msgSender: users.operator }); // Simulate the passage of time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); // Make the withdrawal. lockup.withdraw({ streamId: defaultStreamId, to: to, amount: defaults.WITHDRAW_AMOUNT() }); @@ -51,7 +83,7 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I /// @dev Given enough fuzz runs, all of the following scenarios will be fuzzed: /// - /// - Multiple values for the current time. + /// - Multiple values for the block timestamp. /// - Multiple values for the withdrawal address. /// - Multiple withdraw amounts. function testFuzz_Withdraw_StreamHasBeenCanceled( @@ -62,29 +94,28 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I external whenNotDelegateCalled givenNotNull - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero - whenWithdrawAmountNotGreaterThanWithdrawableAmount + whenNoOverdraw whenCallerRecipient { timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); vm.assume(to != address(0)); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Cancel the stream. - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); lockup.cancel({ streamId: defaultStreamId }); - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); // Bound the withdraw amount. uint128 withdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); withdrawAmount = boundUint128(withdrawAmount, 1, withdrawableAmount); // Expect the assets to be transferred to the fuzzed `to` address. - expectCallToTransfer({ to: to, amount: withdrawAmount }); + expectCallToTransfer({ to: to, value: withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -130,25 +161,23 @@ abstract contract Withdraw_Integration_Fuzz_Test is Integration_Test, Withdraw_I external whenNotDelegateCalled givenNotNull - whenCallerAuthorized whenToNonZeroAddress whenWithdrawAmountNotZero - whenWithdrawAmountNotGreaterThanWithdrawableAmount - whenCallerRecipient + whenNoOverdraw whenStreamHasNotBeenCanceled { timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() * 2); vm.assume(to != address(0)); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the withdraw amount. uint128 withdrawableAmount = lockup.withdrawableAmountOf(defaultStreamId); withdrawAmount = boundUint128(withdrawAmount, 1, withdrawableAmount); // Expect the assets to be transferred to the fuzzed `to` address. - expectCallToTransfer({ to: to, amount: withdrawAmount }); + expectCallToTransfer({ to: to, value: withdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/fuzz/lockup/withdrawMax.t.sol b/test/integration/fuzz/lockup/withdrawMax.t.sol index 60b631199..b15c2fb4a 100644 --- a/test/integration/fuzz/lockup/withdrawMax.t.sol +++ b/test/integration/fuzz/lockup/withdrawMax.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; @@ -15,10 +15,10 @@ abstract contract WithdrawMax_Integration_Fuzz_Test is Integration_Test, Withdra timeJump = _bound(timeJump, defaults.TOTAL_DURATION(), defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Expect the ERC-20 assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: defaults.DEPOSIT_AMOUNT() }); + expectCallToTransfer({ to: users.recipient, value: defaults.DEPOSIT_AMOUNT() }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); @@ -56,13 +56,13 @@ abstract contract WithdrawMax_Integration_Fuzz_Test is Integration_Test, Withdra timeJump = _bound(timeJump, defaults.CLIFF_DURATION(), defaults.TOTAL_DURATION() - 1 seconds); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the withdraw amount. uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); // Expect the assets to be transferred to the Recipient. - expectCallToTransfer({ to: users.recipient, amount: withdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: withdrawAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol b/test/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol index 8aef73118..ecde8780a 100644 --- a/test/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol +++ b/test/integration/fuzz/lockup/withdrawMaxAndTransfer.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { WithdrawMaxAndTransfer_Integration_Shared_Test } from "../../shared/lockup/withdrawMaxAndTransfer.t.sol"; import { Integration_Test } from "../../Integration.t.sol"; @@ -31,14 +31,14 @@ abstract contract WithdrawMaxAndTransfer_Integration_Fuzz_Test is timeJump = _bound(timeJump, 0, defaults.TOTAL_DURATION() * 2); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Get the withdraw amount. uint128 withdrawAmount = lockup.withdrawableAmountOf(defaultStreamId); if (withdrawAmount > 0) { // Expect the assets to be transferred to the fuzzed recipient. - expectCallToTransfer({ to: users.recipient, amount: withdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: withdrawAmount }); // Expect the relevant event to be emitted. vm.expectEmit({ emitter: address(lockup) }); diff --git a/test/integration/fuzz/lockup/withdrawMultiple.t.sol b/test/integration/fuzz/lockup/withdrawMultiple.t.sol index e26326d4c..fae6c0e43 100644 --- a/test/integration/fuzz/lockup/withdrawMultiple.t.sol +++ b/test/integration/fuzz/lockup/withdrawMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Solarray } from "solarray/src/Solarray.sol"; @@ -18,7 +18,6 @@ abstract contract WithdrawMultiple_Integration_Fuzz_Test is function testFuzz_WithdrawMultiple( uint256 timeJump, - address to, uint128 ongoingWithdrawAmount ) external @@ -31,16 +30,10 @@ abstract contract WithdrawMultiple_Integration_Fuzz_Test is whenNoAmountZero whenNoAmountOverdraws { - vm.assume(to != address(0)); timeJump = _bound(timeJump, defaults.TOTAL_DURATION(), defaults.TOTAL_DURATION() * 2 - 1 seconds); - // Hard code the withdrawal address if the caller is the stream's sender. - if (caller == users.sender) { - to = users.recipient; - } - // Create a new stream with an end time double that of the default stream. - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); uint40 ongoingEndTime = defaults.END_TIME() + defaults.TOTAL_DURATION(); uint256 ongoingStreamId = createDefaultStreamWithEndTime(ongoingEndTime); @@ -49,29 +42,39 @@ abstract contract WithdrawMultiple_Integration_Fuzz_Test is uint128 settledWithdrawAmount = defaults.DEPOSIT_AMOUNT(); // Run the test with the caller provided in {whenCallerAuthorizedAllStreams}. - changePrank({ msgSender: caller }); + resetPrank({ msgSender: caller }); // Simulate the passage of time. - vm.warp({ timestamp: defaults.START_TIME() + timeJump }); + vm.warp({ newTimestamp: defaults.START_TIME() + timeJump }); // Bound the ongoing withdraw amount. uint128 ongoingWithdrawableAmount = lockup.withdrawableAmountOf(ongoingStreamId); ongoingWithdrawAmount = boundUint128(ongoingWithdrawAmount, 1, ongoingWithdrawableAmount); // Expect the withdrawals to be made. - expectCallToTransfer({ to: to, amount: ongoingWithdrawAmount }); - expectCallToTransfer({ to: to, amount: settledWithdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: ongoingWithdrawAmount }); + expectCallToTransfer({ to: users.recipient, value: settledWithdrawAmount }); // Expect the relevant events to be emitted. vm.expectEmit({ emitter: address(lockup) }); - emit WithdrawFromLockupStream({ streamId: ongoingStreamId, to: to, asset: dai, amount: ongoingWithdrawAmount }); + emit WithdrawFromLockupStream({ + streamId: ongoingStreamId, + to: users.recipient, + asset: dai, + amount: ongoingWithdrawAmount + }); vm.expectEmit({ emitter: address(lockup) }); - emit WithdrawFromLockupStream({ streamId: settledStreamId, to: to, asset: dai, amount: settledWithdrawAmount }); + emit WithdrawFromLockupStream({ + streamId: settledStreamId, + to: users.recipient, + asset: dai, + amount: settledWithdrawAmount + }); // Make the withdrawals. uint256[] memory streamIds = Solarray.uint256s(ongoingStreamId, settledStreamId); uint128[] memory amounts = Solarray.uint128s(ongoingWithdrawAmount, settledWithdrawAmount); - lockup.withdrawMultiple({ streamIds: streamIds, to: to, amounts: amounts }); + lockup.withdrawMultiple(streamIds, amounts); // Assert that the statuses have been updated. assertEq(lockup.statusOf(streamIds[0]), Lockup.Status.STREAMING, "status0"); diff --git a/test/integration/fuzz/nft-descriptor/isAllowedCharacter.t.sol b/test/integration/fuzz/nft-descriptor/isAllowedCharacter.t.sol new file mode 100644 index 000000000..1976e23b9 --- /dev/null +++ b/test/integration/fuzz/nft-descriptor/isAllowedCharacter.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { NFTDescriptor_Integration_Shared_Test } from "../../shared/nft-descriptor/NFTDescriptor.t.sol"; + +contract IsAllowedCharacter_Integration_Fuzz_Test is NFTDescriptor_Integration_Shared_Test { + bytes1 internal constant SPACE = 0x20; // ASCII 32 + bytes1 internal constant DASH = 0x2D; // ASCII 45 + bytes1 internal constant ZERO = 0x30; // ASCII 48 + bytes1 internal constant NINE = 0x39; // ASCII 57 + bytes1 internal constant A = 0x41; // ASCII 65 + bytes1 internal constant Z = 0x5A; // ASCII 90 + bytes1 internal constant a = 0x61; // ASCII 97 + bytes1 internal constant z = 0x7A; // ASCII 122 + + modifier whenNotEmptyString() { + _; + } + + /// @dev Given enough fuzz runs, all the following scenarios will be fuzzed: + /// + /// - String with only alphanumerical characters + /// - String with only non-alphanumerical characters + /// - String with both alphanumerical and non-alphanumerical characters + function testFuzz_IsAllowedCharacter(string memory symbol) external view whenNotEmptyString { + bytes memory b = bytes(symbol); + uint256 length = b.length; + bool expectedResult = true; + for (uint256 i = 0; i < length; ++i) { + bytes1 char = b[i]; + if (!isAlphanumericOrSpaceChar(char)) { + expectedResult = false; + break; + } + } + bool actualResult = nftDescriptorMock.isAllowedCharacter_(symbol); + assertEq(actualResult, expectedResult, "isAllowedCharacter"); + } + + function isAlphanumericOrSpaceChar(bytes1 char) internal pure returns (bool) { + bool isSpace = char == SPACE; + bool isDash = char == DASH; + bool isDigit = char >= ZERO && char <= NINE; + bool isUppercaseLetter = char >= A && char <= Z; + bool isLowercaseLetter = char >= a && char <= z; + return isSpace || isDash || isDigit || isUppercaseLetter || isLowercaseLetter; + } +} diff --git a/test/integration/shared/flash-loan/FlashLoan.t.sol b/test/integration/shared/flash-loan/FlashLoan.t.sol deleted file mode 100644 index e0609c920..000000000 --- a/test/integration/shared/flash-loan/FlashLoan.t.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { SablierV2FlashLoan } from "src/abstracts/SablierV2FlashLoan.sol"; - -import { FlashLoanMock } from "../../../mocks/flash-loan/FlashLoanMock.sol"; -import { Integration_Test } from "../../Integration.t.sol"; - -/// @notice Common testing logic needed by all {SablierV2FlashLoan} integration tests. -abstract contract FlashLoan_Integration_Shared_Test is Integration_Test { - /*////////////////////////////////////////////////////////////////////////// - TEST CONTRACTS - //////////////////////////////////////////////////////////////////////////*/ - - SablierV2FlashLoan internal flashLoan; - - /*////////////////////////////////////////////////////////////////////////// - SET-UP FUNCTION - //////////////////////////////////////////////////////////////////////////*/ - - function setUp() public virtual override { - Integration_Test.setUp(); - - // Deploy the flash loan mock. - flashLoan = new FlashLoanMock(users.admin, comptroller); - - // Set the default flash fee in the comptroller. - comptroller.setFlashFee({ newFlashFee: defaults.FLASH_FEE() }); - } -} diff --git a/test/integration/shared/flash-loan/flashLoanFunction.t.sol b/test/integration/shared/flash-loan/flashLoanFunction.t.sol deleted file mode 100644 index 032b72d97..000000000 --- a/test/integration/shared/flash-loan/flashLoanFunction.t.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; - -import { FlashLoan_Integration_Shared_Test } from "./FlashLoan.t.sol"; - -contract FlashLoanFunction_Integration_Shared_Test is FlashLoan_Integration_Shared_Test { - uint128 internal constant LIQUIDITY_AMOUNT = 8_755_001e18; - - function setUp() public virtual override { - FlashLoan_Integration_Shared_Test.setUp(); - } - - modifier whenNotDelegateCalled() { - _; - } - - modifier whenAmountNotTooHigh() { - _; - } - - modifier givenAssetFlashLoanable() { - if (!comptroller.isFlashAsset(dai)) { - comptroller.toggleFlashAsset(dai); - } - _; - } - - modifier whenCalculatedFeeNotTooHigh() { - _; - } - - modifier whenBorrowDoesNotFail() { - _; - } - - modifier whenNoReentrancy() { - _; - } -} diff --git a/test/integration/shared/lockup-dynamic/LockupDynamic.t.sol b/test/integration/shared/lockup-dynamic/LockupDynamic.t.sol index 314c81aac..941620ada 100644 --- a/test/integration/shared/lockup-dynamic/LockupDynamic.t.sol +++ b/test/integration/shared/lockup-dynamic/LockupDynamic.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -10,8 +10,8 @@ import { Lockup_Integration_Shared_Test } from "../lockup/Lockup.t.sol"; /// @notice Common testing logic needed across {SablierV2LockupDynamic} integration tests. abstract contract LockupDynamic_Integration_Shared_Test is Lockup_Integration_Shared_Test { struct CreateParams { - LockupDynamic.CreateWithDeltas createWithDeltas; - LockupDynamic.CreateWithMilestones createWithMilestones; + LockupDynamic.CreateWithDurations createWithDurations; + LockupDynamic.CreateWithTimestamps createWithTimestamps; } /// @dev These have to be pre-declared so that `vm.expectRevert` does not expect a revert in `defaults`. @@ -21,100 +21,92 @@ abstract contract LockupDynamic_Integration_Shared_Test is Lockup_Integration_Sh function setUp() public virtual override { Lockup_Integration_Shared_Test.setUp(); - _params.createWithDeltas.sender = users.sender; - _params.createWithDeltas.recipient = users.recipient; - _params.createWithDeltas.totalAmount = defaults.TOTAL_AMOUNT(); - _params.createWithDeltas.asset = dai; - _params.createWithDeltas.cancelable = true; - _params.createWithDeltas.transferable = true; - _params.createWithDeltas.broker = defaults.broker(); - - _params.createWithMilestones.sender = users.sender; - _params.createWithMilestones.recipient = users.recipient; - _params.createWithMilestones.totalAmount = defaults.TOTAL_AMOUNT(); - _params.createWithMilestones.asset = dai; - _params.createWithMilestones.cancelable = true; - _params.createWithMilestones.transferable = true; - _params.createWithMilestones.startTime = defaults.START_TIME(); - _params.createWithMilestones.broker = defaults.broker(); + _params.createWithDurations.sender = users.sender; + _params.createWithDurations.recipient = users.recipient; + _params.createWithDurations.totalAmount = defaults.TOTAL_AMOUNT(); + _params.createWithDurations.asset = dai; + _params.createWithDurations.cancelable = true; + _params.createWithDurations.transferable = true; + _params.createWithDurations.broker = defaults.broker(); + + _params.createWithTimestamps.sender = users.sender; + _params.createWithTimestamps.recipient = users.recipient; + _params.createWithTimestamps.totalAmount = defaults.TOTAL_AMOUNT(); + _params.createWithTimestamps.asset = dai; + _params.createWithTimestamps.cancelable = true; + _params.createWithTimestamps.transferable = true; + _params.createWithTimestamps.startTime = defaults.START_TIME(); + _params.createWithTimestamps.broker = defaults.broker(); // See https://github.com/ethereum/solidity/issues/12783 - LockupDynamic.SegmentWithDelta[] memory segmentsWithDeltas = defaults.segmentsWithDeltas(); + LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations = defaults.segmentsWithDurations(); LockupDynamic.Segment[] memory segments = defaults.segments(); for (uint256 i = 0; i < defaults.SEGMENT_COUNT(); ++i) { - _params.createWithDeltas.segments.push(segmentsWithDeltas[i]); - _params.createWithMilestones.segments.push(segments[i]); + _params.createWithDurations.segments.push(segmentsWithDurations[i]); + _params.createWithTimestamps.segments.push(segments[i]); } } - /// @dev Creates the default stream. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStream() internal override returns (uint256 streamId) { - streamId = lockupDynamic.createWithMilestones(_params.createWithMilestones); + streamId = lockupDynamic.createWithTimestamps(_params.createWithTimestamps); } - /// @dev Creates the default stream with the provided asset. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithAsset(IERC20 asset) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.asset = asset; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided broker. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithBroker(Broker memory broker) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.broker = broker; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with deltas. - function createDefaultStreamWithDeltas() internal returns (uint256 streamId) { - streamId = lockupDynamic.createWithDeltas(_params.createWithDeltas); + /// @dev Creates the default stream with durations. + function createDefaultStreamWithDurations() internal returns (uint256 streamId) { + streamId = lockupDynamic.createWithDurations(_params.createWithDurations); } - /// @dev Creates the default stream with the provided deltas. - function createDefaultStreamWithDeltas(LockupDynamic.SegmentWithDelta[] memory segments) + /// @dev Creates the default stream with the provided durations. + function createDefaultStreamWithDurations(LockupDynamic.SegmentWithDuration[] memory segments) internal returns (uint256 streamId) { - LockupDynamic.CreateWithDeltas memory params = _params.createWithDeltas; + LockupDynamic.CreateWithDurations memory params = _params.createWithDurations; params.segments = segments; - streamId = lockupDynamic.createWithDeltas(params); + streamId = lockupDynamic.createWithDurations(params); } - /// @dev Creates the default stream with the provided end time. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithEndTime(uint40 endTime) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; - params.segments[1].milestone = endTime; - streamId = lockupDynamic.createWithMilestones(params); + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.segments[1].timestamp = endTime; + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates a stream that will not be cancelable. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamNotCancelable() internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.cancelable = false; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the NFT transfer disabled. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamNotTransferable() internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.transferable = false; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided range. - function createDefaultStreamWithRange(LockupDynamic.Range memory range) internal returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; - params.startTime = range.start; - params.segments[1].milestone = range.end; - streamId = lockupDynamic.createWithMilestones(params); - } - - /// @dev Creates the default stream with the provided recipient. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithRecipient(address recipient) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.recipient = recipient; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } /// @dev Creates the default stream with the provided segments. @@ -122,29 +114,55 @@ abstract contract LockupDynamic_Integration_Shared_Test is Lockup_Integration_Sh internal returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.segments = segments; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided sender. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithSender(address sender) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.sender = sender; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided start time.. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithStartTime(uint40 startTime) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.startTime = startTime; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); + } + + /// @dev Creates the default stream with the provided timestamps. + function createDefaultStreamWithTimestamps(LockupDynamic.Timestamps memory timestamps) + internal + returns (uint256 streamId) + { + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.startTime = timestamps.start; + params.segments[1].timestamp = timestamps.end; + streamId = lockupDynamic.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided total amount. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithTotalAmount(uint128 totalAmount) internal override returns (uint256 streamId) { - LockupDynamic.CreateWithMilestones memory params = _params.createWithMilestones; + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; params.totalAmount = totalAmount; - streamId = lockupDynamic.createWithMilestones(params); + streamId = lockupDynamic.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithUsers( + address recipient, + address sender + ) + internal + override + returns (uint256 streamId) + { + LockupDynamic.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.recipient = recipient; + params.sender = sender; + streamId = lockupDynamic.createWithTimestamps(params); } } diff --git a/test/integration/shared/lockup-dynamic/createWithDeltas.t.sol b/test/integration/shared/lockup-dynamic/createWithDeltas.t.sol deleted file mode 100644 index e5c4e51dc..000000000 --- a/test/integration/shared/lockup-dynamic/createWithDeltas.t.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { LockupDynamic_Integration_Shared_Test } from "./LockupDynamic.t.sol"; - -contract CreateWithDeltas_Integration_Shared_Test is LockupDynamic_Integration_Shared_Test { - uint256 internal streamId; - - function setUp() public virtual override { - streamId = lockupDynamic.nextStreamId(); - } - - modifier whenNotDelegateCalled() { - _; - } - - modifier whenLoopCalculationsDoNotOverflowBlockGasLimit() { - _; - } - - modifier whenDeltasNotZero() { - _; - } - - modifier whenMilestonesCalculationsDoNotOverflow() { - _; - } -} diff --git a/test/integration/shared/lockup-dynamic/createWithMilestones.t.sol b/test/integration/shared/lockup-dynamic/createWithMilestones.t.sol deleted file mode 100644 index a26a14c23..000000000 --- a/test/integration/shared/lockup-dynamic/createWithMilestones.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { LockupDynamic_Integration_Shared_Test } from "./LockupDynamic.t.sol"; - -contract CreateWithMilestones_Integration_Shared_Test is LockupDynamic_Integration_Shared_Test { - uint256 internal streamId; - - function setUp() public virtual override { - streamId = lockupDynamic.nextStreamId(); - } - - modifier whenNotDelegateCalled() { - _; - } - - modifier whenRecipientNonZeroAddress() { - _; - } - - modifier whenDepositAmountNotZero() { - _; - } - - modifier whenSegmentCountNotZero() { - _; - } - - modifier whenSegmentCountNotTooHigh() { - _; - } - - modifier whenSegmentAmountsSumDoesNotOverflow() { - _; - } - - modifier whenStartTimeLessThanFirstSegmentMilestone() { - _; - } - - modifier whenSegmentMilestonesOrdered() { - _; - } - - modifier whenEndTimeInTheFuture() { - _; - } - - modifier whenDepositAmountEqualToSegmentAmountsSum() { - _; - } - - modifier givenProtocolFeeNotTooHigh() { - _; - } - - modifier whenBrokerFeeNotTooHigh() { - _; - } - - modifier whenAssetContract() { - _; - } - - modifier whenAssetERC20() { - _; - } -} diff --git a/test/integration/shared/lockup-linear/LockupLinear.t.sol b/test/integration/shared/lockup-linear/LockupLinear.t.sol index 2a909fa0d..c7344ac45 100644 --- a/test/integration/shared/lockup-linear/LockupLinear.t.sol +++ b/test/integration/shared/lockup-linear/LockupLinear.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -11,7 +11,7 @@ import { Lockup_Integration_Shared_Test } from "../lockup/Lockup.t.sol"; abstract contract LockupLinear_Integration_Shared_Test is Lockup_Integration_Shared_Test { struct Params { LockupLinear.CreateWithDurations createWithDurations; - LockupLinear.CreateWithRange createWithRange; + LockupLinear.CreateWithTimestamps createWithTimestamps; } /// @dev These have to be pre-declared so that `vm.expectRevert` does not expect a revert in `defaults`. @@ -20,27 +20,27 @@ abstract contract LockupLinear_Integration_Shared_Test is Lockup_Integration_Sha function setUp() public virtual override { Lockup_Integration_Shared_Test.setUp(); - _params.createWithDurations = defaults.createWithDurations(); - _params.createWithRange = defaults.createWithRange(); + _params.createWithDurations = defaults.createWithDurationsLL(); + _params.createWithTimestamps = defaults.createWithTimestampsLL(); } - /// @dev Creates the default stream. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStream() internal override returns (uint256 streamId) { - streamId = lockupLinear.createWithRange(_params.createWithRange); + streamId = lockupLinear.createWithTimestamps(_params.createWithTimestamps); } - /// @dev Creates the default stream with the provided address. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithAsset(IERC20 asset) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.asset = asset; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided broker. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithBroker(Broker memory broker) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.broker = broker; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } /// @dev Creates the default stream with durations. @@ -58,62 +58,77 @@ abstract contract LockupLinear_Integration_Shared_Test is Lockup_Integration_Sha streamId = lockupLinear.createWithDurations(params); } - /// @dev Creates the default stream that is not cancelable. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamNotCancelable() internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.cancelable = false; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the NFT transfer disabled. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamNotTransferable() internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.transferable = false; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided end time. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithEndTime(uint40 endTime) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; - params.range.end = endTime; - streamId = lockupLinear.createWithRange(params); + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.timestamps.end = endTime; + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided createWithRange. - function createDefaultStreamWithRange(LockupLinear.Range memory createWithRange) - internal - returns (uint256 streamId) - { - LockupLinear.CreateWithRange memory params = _params.createWithRange; - params.range = createWithRange; - streamId = lockupLinear.createWithRange(params); - } - - /// @dev Creates the default stream with the provided recipient. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithRecipient(address recipient) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.recipient = recipient; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided sender. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithSender(address sender) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.sender = sender; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided start time. + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithStartTime(uint40 startTime) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; - params.range.start = startTime; - streamId = lockupLinear.createWithRange(params); + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.timestamps.start = startTime; + streamId = lockupLinear.createWithTimestamps(params); } - /// @dev Creates the default stream with the provided total amount. + /// @dev Creates the default stream with the provided timestamps. + function createDefaultStreamWithTimestamps(LockupLinear.Timestamps memory timestamps) + internal + returns (uint256 streamId) + { + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.timestamps = timestamps; + streamId = lockupLinear.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test function createDefaultStreamWithTotalAmount(uint128 totalAmount) internal override returns (uint256 streamId) { - LockupLinear.CreateWithRange memory params = _params.createWithRange; + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; params.totalAmount = totalAmount; - streamId = lockupLinear.createWithRange(params); + streamId = lockupLinear.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithUsers( + address recipient, + address sender + ) + internal + override + returns (uint256 streamId) + { + LockupLinear.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.recipient = recipient; + params.sender = sender; + streamId = lockupLinear.createWithTimestamps(params); } } diff --git a/test/integration/shared/lockup-linear/createWithDurations.t.sol b/test/integration/shared/lockup-linear/createWithDurations.t.sol deleted file mode 100644 index e5aac174b..000000000 --- a/test/integration/shared/lockup-linear/createWithDurations.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { LockupLinear_Integration_Shared_Test } from "./LockupLinear.t.sol"; - -contract CreateWithDurations_Integration_Shared_Test is LockupLinear_Integration_Shared_Test { - uint256 internal streamId; - - function setUp() public virtual override { - streamId = lockupLinear.nextStreamId(); - } - - modifier whenNotDelegateCalled() { - _; - } - - modifier whenCliffDurationCalculationDoesNotOverflow() { - _; - } - - modifier whenTotalDurationCalculationDoesNotOverflow() { - _; - } -} diff --git a/test/integration/shared/lockup-linear/createWithRange.t.sol b/test/integration/shared/lockup-linear/createWithRange.t.sol deleted file mode 100644 index 7a7ff6780..000000000 --- a/test/integration/shared/lockup-linear/createWithRange.t.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { LockupLinear_Integration_Shared_Test } from "./LockupLinear.t.sol"; - -abstract contract CreateWithRange_Integration_Shared_Test is LockupLinear_Integration_Shared_Test { - uint256 internal streamId; - - function setUp() public virtual override { - streamId = lockupLinear.nextStreamId(); - } - - modifier whenNotDelegateCalled() { - _; - } - - modifier whenRecipientNonZeroAddress() { - _; - } - - modifier whenDepositAmountNotZero() { - _; - } - - modifier whenStartTimeNotGreaterThanCliffTime() { - _; - } - - modifier whenCliffTimeLessThanEndTime() { - _; - } - - modifier whenEndTimeInTheFuture() { - _; - } - - modifier givenProtocolFeeNotTooHigh() { - _; - } - - modifier whenBrokerFeeNotTooHigh() { - _; - } - - modifier whenAssetContract() { - _; - } - - modifier whenAssetERC20() { - _; - } -} diff --git a/test/integration/shared/lockup-tranched/LockupTranched.t.sol b/test/integration/shared/lockup-tranched/LockupTranched.t.sol new file mode 100644 index 000000000..97a39415f --- /dev/null +++ b/test/integration/shared/lockup-tranched/LockupTranched.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Broker, LockupTranched } from "src/types/DataTypes.sol"; + +import { Lockup_Integration_Shared_Test } from "../lockup/Lockup.t.sol"; + +/// @notice Common testing logic needed across {SablierV2LockupTranched} integration tests. +abstract contract LockupTranched_Integration_Shared_Test is Lockup_Integration_Shared_Test { + struct CreateParams { + LockupTranched.CreateWithDurations createWithDurations; + LockupTranched.CreateWithTimestamps createWithTimestamps; + } + + /// @dev These have to be pre-declared so that `vm.expectRevert` does not expect a revert in `defaults`. + /// See https://github.com/foundry-rs/foundry/issues/4762. + CreateParams private _params; + + function setUp() public virtual override { + Lockup_Integration_Shared_Test.setUp(); + + _params.createWithDurations.sender = users.sender; + _params.createWithDurations.recipient = users.recipient; + _params.createWithDurations.totalAmount = defaults.TOTAL_AMOUNT(); + _params.createWithDurations.asset = dai; + _params.createWithDurations.cancelable = true; + _params.createWithDurations.transferable = true; + _params.createWithDurations.broker = defaults.broker(); + + _params.createWithTimestamps.sender = users.sender; + _params.createWithTimestamps.recipient = users.recipient; + _params.createWithTimestamps.totalAmount = defaults.TOTAL_AMOUNT(); + _params.createWithTimestamps.asset = dai; + _params.createWithTimestamps.cancelable = true; + _params.createWithTimestamps.transferable = true; + _params.createWithTimestamps.startTime = defaults.START_TIME(); + _params.createWithTimestamps.broker = defaults.broker(); + + // See https://github.com/ethereum/solidity/issues/12783 + LockupTranched.TrancheWithDuration[] memory tranchesWithDurations = defaults.tranchesWithDurations(); + LockupTranched.Tranche[] memory tranches = defaults.tranches(); + for (uint256 i = 0; i < defaults.TRANCHE_COUNT(); ++i) { + _params.createWithDurations.tranches.push(tranchesWithDurations[i]); + _params.createWithTimestamps.tranches.push(tranches[i]); + } + } + + /// @dev Creates the default stream. + function createDefaultStream() internal override returns (uint256 streamId) { + streamId = lockupTranched.createWithTimestamps(_params.createWithTimestamps); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithAsset(IERC20 asset) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.asset = asset; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithBroker(Broker memory broker) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.broker = broker; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @dev Creates the default stream with durations. + function createDefaultStreamWithDurations() internal returns (uint256 streamId) { + streamId = lockupTranched.createWithDurations(_params.createWithDurations); + } + + /// @dev Creates the default stream with the provided durations. + function createDefaultStreamWithDurations(LockupTranched.TrancheWithDuration[] memory tranches) + internal + returns (uint256 streamId) + { + LockupTranched.CreateWithDurations memory params = _params.createWithDurations; + params.tranches = tranches; + streamId = lockupTranched.createWithDurations(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithEndTime(uint40 endTime) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.tranches[2].timestamp = endTime; + + // Ensure the timestamps are arranged in ascending order. + if (params.tranches[2].timestamp <= params.tranches[1].timestamp) { + params.tranches[1].timestamp = params.tranches[2].timestamp - 1; + } + if (params.tranches[1].timestamp <= params.tranches[0].timestamp) { + params.tranches[0].timestamp = params.tranches[1].timestamp - 1; + } + + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamNotCancelable() internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.cancelable = false; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamNotTransferable() internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.transferable = false; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @dev Creates the default stream with the provided timestamps. + function createDefaultStreamWithTimestamps(LockupTranched.Timestamps memory timestamps) + internal + returns (uint256 streamId) + { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.startTime = timestamps.start; + params.tranches[1].timestamp = timestamps.end; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithRecipient(address recipient) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.recipient = recipient; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @dev Creates the default stream with the provided tranches. + function createDefaultStreamWithTranches(LockupTranched.Tranche[] memory tranches) + internal + returns (uint256 streamId) + { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.tranches = tranches; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithSender(address sender) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.sender = sender; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithStartTime(uint40 startTime) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.startTime = startTime; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithTotalAmount(uint128 totalAmount) internal override returns (uint256 streamId) { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.totalAmount = totalAmount; + streamId = lockupTranched.createWithTimestamps(params); + } + + /// @inheritdoc Lockup_Integration_Shared_Test + function createDefaultStreamWithUsers( + address recipient, + address sender + ) + internal + override + returns (uint256 streamId) + { + LockupTranched.CreateWithTimestamps memory params = _params.createWithTimestamps; + params.recipient = recipient; + params.sender = sender; + streamId = lockupTranched.createWithTimestamps(params); + } +} diff --git a/test/integration/shared/lockup/Lockup.t.sol b/test/integration/shared/lockup/Lockup.t.sol index 7974e50d6..b7da0ea18 100644 --- a/test/integration/shared/lockup/Lockup.t.sol +++ b/test/integration/shared/lockup/Lockup.t.sol @@ -1,25 +1,20 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2Base } from "src/interfaces/ISablierV2Base.sol"; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { Broker } from "src/types/DataTypes.sol"; import { Base_Test } from "test/Base.t.sol"; /// @dev This contracts avoids duplicating test logic for {SablierV2LockupLinear} and {SablierV2LockupDynamic}; -/// both of these contracts inherit from {SablierV2Base} and {SablierV2Lockup}. +/// both of these contracts inherit from {SablierV2Lockup}. abstract contract Lockup_Integration_Shared_Test is Base_Test { /*////////////////////////////////////////////////////////////////////////// TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - /// @dev A test contract that is meant to be overridden by the implementing contract, which will be - /// either {SablierV2LockupLinear} or {SablierV2LockupDynamic}. - ISablierV2Base internal base; - /// @dev A test contract that is meant to be overridden by the implementing contract, which will be /// either {SablierV2LockupLinear} or {SablierV2LockupDynamic}. ISablierV2Lockup internal lockup; @@ -29,12 +24,8 @@ abstract contract Lockup_Integration_Shared_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function setUp() public virtual override { - // Set the default protocol fee. - comptroller.setProtocolFee({ asset: dai, newProtocolFee: defaults.PROTOCOL_FEE() }); - comptroller.setProtocolFee({ asset: IERC20(address(usdt)), newProtocolFee: defaults.PROTOCOL_FEE() }); - // Make the Sender the default caller in this test suite. - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); } /*////////////////////////////////////////////////////////////////////////// @@ -59,6 +50,11 @@ abstract contract Lockup_Integration_Shared_Test is Base_Test { /// @dev Creates the default stream with the provided end time. function createDefaultStreamWithEndTime(uint40 endTime) internal virtual returns (uint256 streamId); + /// @dev Creates the default stream with the provided user as the recipient and the sender. + function createDefaultStreamWithIdenticalUsers(address user) internal virtual returns (uint256 streamId) { + return createDefaultStreamWithUsers({ recipient: user, sender: user }); + } + /// @dev Creates the default stream with the provided recipient. function createDefaultStreamWithRecipient(address recipient) internal virtual returns (uint256 streamId); @@ -70,4 +66,13 @@ abstract contract Lockup_Integration_Shared_Test is Base_Test { /// @dev Creates the default stream with the provided total amount. function createDefaultStreamWithTotalAmount(uint128 totalAmount) internal virtual returns (uint256 streamId); + + /// @dev Creates the default stream with the provided sender and recipient. + function createDefaultStreamWithUsers( + address recipient, + address sender + ) + internal + virtual + returns (uint256 streamId); } diff --git a/test/integration/shared/lockup/cancel.t.sol b/test/integration/shared/lockup/cancel.t.sol index eba14f6ac..da2a1563e 100644 --- a/test/integration/shared/lockup/cancel.t.sol +++ b/test/integration/shared/lockup/cancel.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -8,58 +8,58 @@ abstract contract Cancel_Integration_Shared_Test is Lockup_Integration_Shared_Te function setUp() public virtual override { defaultStreamId = createDefaultStream(); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); } - modifier whenNotDelegateCalled() { + modifier givenNotNull() { _; } - modifier givenNotNull() { + modifier givenRecipientAllowedToHook() { _; } - modifier givenStreamCold() { + /// @dev In LockupLinear, the streaming starts after the cliff time, whereas in LockupDynamic, the streaming starts + /// after the start time. + modifier givenStatusStreaming() { + // Warp to the future, after the stream's start time but before the stream's end time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); _; } - modifier givenStreamWarm() { + modifier givenStreamCancelable() { _; } - modifier whenCallerUnauthorized() { + modifier givenStreamCold() { _; } - modifier whenCallerAuthorized() { + modifier givenStreamWarm() { _; } - modifier givenStreamCancelable() { + modifier whenCallerAuthorized() { _; } - /// @dev In the LockupLinear contract, the streaming starts after the cliff time, whereas in the LockupDynamic - /// contract, the streaming starts after the start time. - modifier givenStatusStreaming() { - // Warp to the future, after the stream's start time but before the stream's end time. - vm.warp({ timestamp: defaults.WARP_26_PERCENT() }); + modifier whenCallerUnauthorized() { _; } - modifier givenRecipientContract() { + modifier whenNotDelegateCalled() { _; } - modifier givenRecipientImplementsHook() { + modifier whenRecipientNotReentrant() { _; } - modifier whenRecipientDoesNotRevert() { + modifier whenRecipientNotReverting() { _; } - modifier whenNoRecipientReentrancy() { + modifier whenRecipientReturnsSelector() { _; } } diff --git a/test/integration/shared/lockup/cancelMultiple.t.sol b/test/integration/shared/lockup/cancelMultiple.t.sol index f5f366c8b..1da602606 100644 --- a/test/integration/shared/lockup/cancelMultiple.t.sol +++ b/test/integration/shared/lockup/cancelMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -15,7 +15,7 @@ abstract contract CancelMultiple_Integration_Shared_Test is Lockup_Integration_S /// @dev Creates the default streams used throughout the tests. function createTestStreams() internal { // Warp back to the original timestamp. - vm.warp({ timestamp: originalTime }); + vm.warp({ newTimestamp: originalTime }); // Create the test streams. testStreamIds = new uint256[](2); @@ -24,11 +24,11 @@ abstract contract CancelMultiple_Integration_Shared_Test is Lockup_Integration_S testStreamIds[1] = createDefaultStreamWithEndTime(defaults.END_TIME() + defaults.TOTAL_DURATION()); } - modifier whenNotDelegateCalled() { + modifier givenAllStreamsCancelable() { _; } - modifier whenArrayCountNotZero() { + modifier givenAllStreamsWarm() { _; } @@ -36,23 +36,23 @@ abstract contract CancelMultiple_Integration_Shared_Test is Lockup_Integration_S _; } - modifier givenAllStreamsWarm() { - _; - } - - modifier whenCallerUnauthorized() { + modifier whenArrayCountNotZero() { _; } modifier whenCallerAuthorizedAllStreams() { _; - vm.warp({ timestamp: originalTime }); + vm.warp({ newTimestamp: originalTime }); createTestStreams(); - changePrank({ msgSender: users.sender }); + resetPrank({ msgSender: users.sender }); _; } - modifier givenAllStreamsCancelable() { + modifier whenCallerUnauthorized() { + _; + } + + modifier whenNotDelegateCalled() { _; } } diff --git a/test/integration/shared/lockup/createWithDurations.t.sol b/test/integration/shared/lockup/createWithDurations.t.sol new file mode 100644 index 000000000..8a2acdc20 --- /dev/null +++ b/test/integration/shared/lockup/createWithDurations.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; + +abstract contract CreateWithDurations_Integration_Shared_Test is Lockup_Integration_Shared_Test { + uint256 internal streamId; + + function setUp() public virtual override { + streamId = lockup.nextStreamId(); + } + + modifier whenCliffDurationCalculationDoesNotOverflow() { + _; + } + + modifier whenDurationsNotZero() { + _; + } + + modifier whenNotDelegateCalled() { + _; + } + + modifier whenSegmentCountNotTooHigh() { + _; + } + + modifier whenTimestampsCalculationsDoNotOverflow() { + _; + } + + modifier whenTotalDurationCalculationDoesNotOverflow() { + _; + } + + modifier whenTrancheCountNotTooHigh() { + _; + } +} diff --git a/test/integration/shared/lockup/createWithTimestamps.t.sol b/test/integration/shared/lockup/createWithTimestamps.t.sol new file mode 100644 index 000000000..252104613 --- /dev/null +++ b/test/integration/shared/lockup/createWithTimestamps.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; + +abstract contract CreateWithTimestamps_Integration_Shared_Test is Lockup_Integration_Shared_Test { + uint256 internal streamId; + + function setUp() public virtual override { + streamId = lockup.nextStreamId(); + } + + modifier whenAssetContract() { + _; + } + + modifier whenAssetERC20() { + _; + } + + modifier whenBrokerFeeNotTooHigh() { + _; + } + + modifier whenCliffTimeGreaterThanZero() { + _; + } + + modifier whenCliffTimeLessThanEndTime() { + _; + } + + modifier whenCliffTimeZero() { + _; + } + + modifier whenDepositAmountEqualToSegmentAmountsSum() { + _; + } + + modifier whenDepositAmountEqualToTrancheAmountsSum() { + _; + } + + modifier whenDepositAmountNotZero() { + _; + } + + modifier whenEndTimeInTheFuture() { + _; + } + + modifier whenNotDelegateCalled() { + _; + } + + modifier whenRecipientNonZeroAddress() { + _; + } + + modifier whenSegmentAmountsSumDoesNotOverflow() { + _; + } + + modifier whenSegmentCountNotTooHigh() { + _; + } + + modifier whenSegmentCountNotZero() { + _; + } + + modifier whenSegmentTimestampsOrdered() { + _; + } + + modifier whenStartTimeLessThanEndTime() { + _; + } + + modifier whenStartTimeLessThanFirstSegmentTimestamp() { + _; + } + + modifier whenStartTimeLessThanFirstTrancheTimestamp() { + _; + } + + modifier whenStartTimeNotZero() { + _; + } + + modifier whenTrancheAmountsSumDoesNotOverflow() { + _; + } + + modifier whenTrancheCountNotTooHigh() { + _; + } + + modifier whenTrancheCountNotZero() { + _; + } + + modifier whenTrancheTimestampsOrdered() { + _; + } +} diff --git a/test/integration/shared/lockup/getWithdrawnAmount.t.sol b/test/integration/shared/lockup/getWithdrawnAmount.t.sol index b88b483d7..e7a3985ca 100644 --- a/test/integration/shared/lockup/getWithdrawnAmount.t.sol +++ b/test/integration/shared/lockup/getWithdrawnAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -7,7 +7,7 @@ abstract contract GetWithdrawnAmount_Integration_Shared_Test is Lockup_Integrati uint256 internal defaultStreamId; function setUp() public virtual override { - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } modifier givenNotNull() { diff --git a/test/integration/shared/lockup/streamedAmountOf.t.sol b/test/integration/shared/lockup/streamedAmountOf.t.sol index da3f03619..bcacb2edf 100644 --- a/test/integration/shared/lockup/streamedAmountOf.t.sol +++ b/test/integration/shared/lockup/streamedAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -14,15 +14,15 @@ abstract contract StreamedAmountOf_Integration_Shared_Test is Lockup_Integration _; } - modifier givenStreamHasBeenCanceled() { + modifier givenStatusStreaming() { _; } - modifier givenStreamHasNotBeenCanceled() { + modifier givenStreamHasBeenCanceled() { _; } - modifier givenStatusStreaming() { + modifier givenStreamHasNotBeenCanceled() { _; } diff --git a/test/integration/shared/lockup/withdraw.t.sol b/test/integration/shared/lockup/withdraw.t.sol index 9b5d21454..75fe7fa5e 100644 --- a/test/integration/shared/lockup/withdraw.t.sol +++ b/test/integration/shared/lockup/withdraw.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -8,10 +8,12 @@ abstract contract Withdraw_Integration_Shared_Test is Lockup_Integration_Shared_ function setUp() public virtual override { defaultStreamId = createDefaultStream(); - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } - modifier whenNotDelegateCalled() { + modifier givenEndTimeInTheFuture() { + // Simulate the passage of time. + vm.warp({ newTimestamp: defaults.WARP_26_PERCENT() }); _; } @@ -19,36 +21,61 @@ abstract contract Withdraw_Integration_Shared_Test is Lockup_Integration_Shared_ _; } + modifier givenRecipientAllowedToHook() { + _; + } + modifier givenStreamNotDepleted() { - vm.warp({ timestamp: defaults.START_TIME() }); + vm.warp({ newTimestamp: defaults.START_TIME() }); _; } - modifier whenCallerUnauthorized() { + modifier whenCallerRecipient() { _; } - modifier whenCallerAuthorized() { + modifier whenCallerSender() { + resetPrank({ msgSender: users.sender }); _; } - modifier whenToNonZeroAddress() { + modifier whenNoOverdraw() { _; } - modifier whenWithdrawAmountNotZero() { + modifier whenNotDelegateCalled() { _; } - modifier whenWithdrawAmountNotGreaterThanWithdrawableAmount() { + modifier whenRecipientNotReentrant() { _; } - modifier whenCallerRecipient() { + modifier whenRecipientNotReverting() { + _; + } + + modifier whenRecipientReturnsSelector() { _; } modifier whenStreamHasNotBeenCanceled() { _; } + + modifier whenToNonZeroAddress() { + _; + } + + modifier whenWithdrawalAddressIsRecipient() { + _; + } + + modifier whenWithdrawalAddressNotRecipient() { + _; + } + + modifier whenWithdrawAmountNotZero() { + _; + } } diff --git a/test/integration/shared/lockup/withdrawMax.t.sol b/test/integration/shared/lockup/withdrawMax.t.sol index a60582981..f19c3323c 100644 --- a/test/integration/shared/lockup/withdrawMax.t.sol +++ b/test/integration/shared/lockup/withdrawMax.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -8,7 +8,7 @@ abstract contract WithdrawMax_Integration_Shared_Test is Lockup_Integration_Shar function setUp() public virtual override { defaultStreamId = createDefaultStream(); - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } modifier givenEndTimeInTheFuture() { diff --git a/test/integration/shared/lockup/withdrawMaxAndTransfer.t.sol b/test/integration/shared/lockup/withdrawMaxAndTransfer.t.sol index 5f6fcf64f..222cb78eb 100644 --- a/test/integration/shared/lockup/withdrawMaxAndTransfer.t.sol +++ b/test/integration/shared/lockup/withdrawMaxAndTransfer.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -8,10 +8,10 @@ abstract contract WithdrawMaxAndTransfer_Integration_Shared_Test is Lockup_Integ function setUp() public virtual override { defaultStreamId = createDefaultStream(); - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); } - modifier whenNotDelegateCalled() { + modifier givenNFTNotBurned() { _; } @@ -19,19 +19,19 @@ abstract contract WithdrawMaxAndTransfer_Integration_Shared_Test is Lockup_Integ _; } - modifier whenCallerCurrentRecipient() { + modifier givenStreamTransferable() { _; } - modifier givenStreamTransferable() { + modifier givenWithdrawableAmountNotZero() { _; } - modifier givenNFTNotBurned() { + modifier whenCallerCurrentRecipient() { _; } - modifier givenWithdrawableAmountNotZero() { + modifier whenNotDelegateCalled() { _; } } diff --git a/test/integration/shared/lockup/withdrawMultiple.t.sol b/test/integration/shared/lockup/withdrawMultiple.t.sol index 2896f77c7..ca236ecff 100644 --- a/test/integration/shared/lockup/withdrawMultiple.t.sol +++ b/test/integration/shared/lockup/withdrawMultiple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -19,7 +19,7 @@ abstract contract WithdrawMultiple_Integration_Shared_Test is Lockup_Integration /// @dev Creates the default streams used throughout the tests. function createTestStreams() internal { // Warp back to the original timestamp. - vm.warp({ timestamp: originalTime }); + vm.warp({ newTimestamp: originalTime }); // Define the default amounts. testAmounts = new uint128[](3); @@ -37,15 +37,8 @@ abstract contract WithdrawMultiple_Integration_Shared_Test is Lockup_Integration testStreamIds[2] = createDefaultStream(); } - modifier whenNotDelegateCalled() { - _; - } - - modifier whenToNonZeroAddress() { - _; - } - - modifier whenArraysEqual() { + modifier givenNoDepletedStream() { + vm.warp({ newTimestamp: defaults.START_TIME() }); _; } @@ -53,12 +46,7 @@ abstract contract WithdrawMultiple_Integration_Shared_Test is Lockup_Integration _; } - modifier givenNoDepletedStream() { - vm.warp({ timestamp: defaults.START_TIME() }); - _; - } - - modifier whenCallerUnauthorized() { + modifier whenArraysEqual() { _; } @@ -71,22 +59,34 @@ abstract contract WithdrawMultiple_Integration_Shared_Test is Lockup_Integration _; createTestStreams(); caller = users.recipient; - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); _; createTestStreams(); caller = users.operator; - changePrank({ msgSender: users.recipient }); + resetPrank({ msgSender: users.recipient }); lockup.setApprovalForAll({ operator: users.operator, approved: true }); caller = users.operator; - changePrank({ msgSender: users.operator }); + resetPrank({ msgSender: users.operator }); _; } - modifier whenNoAmountZero() { + modifier whenCallerUnauthorized() { _; } modifier whenNoAmountOverdraws() { _; } + + modifier whenNoAmountZero() { + _; + } + + modifier whenNotDelegateCalled() { + _; + } + + modifier whenToNonZeroAddress() { + _; + } } diff --git a/test/integration/shared/lockup/withdrawableAmountOf.t.sol b/test/integration/shared/lockup/withdrawableAmountOf.t.sol index 609a9a2dd..624075f1d 100644 --- a/test/integration/shared/lockup/withdrawableAmountOf.t.sol +++ b/test/integration/shared/lockup/withdrawableAmountOf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup_Integration_Shared_Test } from "./Lockup.t.sol"; @@ -13,15 +13,15 @@ abstract contract WithdrawableAmountOf_Integration_Shared_Test is Lockup_Integra _; } - modifier givenStreamHasBeenCanceled() { + modifier givenStatusStreaming() { _; } - modifier givenStreamHasNotBeenCanceled() { + modifier givenStreamHasBeenCanceled() { _; } - modifier givenStatusStreaming() { + modifier givenStreamHasNotBeenCanceled() { _; } } diff --git a/test/integration/concrete/nft-descriptor/NFTDescriptor.t.sol b/test/integration/shared/nft-descriptor/NFTDescriptor.t.sol similarity index 88% rename from test/integration/concrete/nft-descriptor/NFTDescriptor.t.sol rename to test/integration/shared/nft-descriptor/NFTDescriptor.t.sol index 4bd60f05e..d7dd5099f 100644 --- a/test/integration/concrete/nft-descriptor/NFTDescriptor.t.sol +++ b/test/integration/shared/nft-descriptor/NFTDescriptor.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Integration_Test } from "../../Integration.t.sol"; import { NFTDescriptorMock } from "../../../mocks/NFTDescriptorMock.sol"; -abstract contract NFTDescriptor_Integration_Concrete_Test is Integration_Test { +abstract contract NFTDescriptor_Integration_Shared_Test is Integration_Test { NFTDescriptorMock internal nftDescriptorMock; function setUp() public virtual override { diff --git a/test/invariant/Invariant.t.sol b/test/invariant/Invariant.t.sol index 8e563b6c5..df5b9fe67 100644 --- a/test/invariant/Invariant.t.sol +++ b/test/invariant/Invariant.t.sol @@ -1,30 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { StdInvariant } from "forge-std/src/StdInvariant.sol"; import { Base_Test } from "../Base.t.sol"; -import { ComptrollerHandler } from "./handlers/ComptrollerHandler.sol"; -import { TimestampStore } from "./stores/TimestampStore.sol"; /// @notice Common logic needed by all invariant tests. abstract contract Invariant_Test is Base_Test, StdInvariant { - /*////////////////////////////////////////////////////////////////////////// - TEST CONTRACTS - //////////////////////////////////////////////////////////////////////////*/ - - ComptrollerHandler internal comptrollerHandler; - TimestampStore internal timestampStore; - - /*////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////*/ - - modifier useCurrentTimestamp() { - vm.warp(timestampStore.currentTimestamp()); - _; - } - /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION //////////////////////////////////////////////////////////////////////////*/ @@ -32,28 +14,8 @@ abstract contract Invariant_Test is Base_Test, StdInvariant { function setUp() public virtual override { Base_Test.setUp(); - // Deploy V2 Core. - deployCoreConditionally(); - - // Deploy the handlers. - timestampStore = new TimestampStore(); - comptrollerHandler = - new ComptrollerHandler({ asset_: dai, comptroller_: comptroller, timestampStore_: timestampStore }); - vm.prank({ msgSender: users.admin }); - comptroller.transferAdmin(address(comptrollerHandler)); - - // Label the handlers. - vm.label({ account: address(comptrollerHandler), newLabel: "ComptrollerHandler" }); - vm.label({ account: address(timestampStore), newLabel: "TimestampStore" }); - - // Target only the handlers for invariant testing (to avoid getting reverts). - targetContract(address(comptrollerHandler)); - // Prevent these contracts from being fuzzed as `msg.sender`. - excludeSender(address(comptroller)); - excludeSender(address(comptrollerHandler)); excludeSender(address(lockupDynamic)); excludeSender(address(lockupLinear)); - excludeSender(address(timestampStore)); } } diff --git a/test/invariant/Lockup.t.sol b/test/invariant/Lockup.t.sol index f6b4c7922..bccc95c92 100644 --- a/test/invariant/Lockup.t.sol +++ b/test/invariant/Lockup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { Lockup } from "src/types/DataTypes.sol"; @@ -38,9 +38,8 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { //////////////////////////////////////////////////////////////////////////*/ // solhint-disable max-line-length - function invariant_ContractBalance() external useCurrentTimestamp { + function invariant_ContractBalance() external view { uint256 contractBalance = dai.balanceOf(address(lockup)); - uint256 protocolRevenues = lockup.protocolRevenues(dai); uint256 lastStreamId = lockupStore.lastStreamId(); uint256 depositedAmountsSum; @@ -53,18 +52,18 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { withdrawnAmountsSum += uint256(lockup.getWithdrawnAmount(streamId)); } - assertGte( + assertGe( contractBalance, - depositedAmountsSum + protocolRevenues - refundedAmountsSum - withdrawnAmountsSum, - unicode"Invariant violation: contract balances < Σ deposited amounts + protocol revenues - Σ refunded amounts - Σ withdrawn amounts" + depositedAmountsSum - refundedAmountsSum - withdrawnAmountsSum, + unicode"Invariant violation: contract balances < Σ deposited amounts - Σ refunded amounts - Σ withdrawn amounts" ); } - function invariant_DepositedAmountGteStreamedAmount() external useCurrentTimestamp { + function invariant_DepositedAmountGteStreamedAmount() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( + assertGe( lockup.getDepositedAmount(streamId), lockup.streamedAmountOf(streamId), "Invariant violation: deposited amount < streamed amount" @@ -72,11 +71,11 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_DepositedAmountGteWithdrawableAmount() external useCurrentTimestamp { + function invariant_DepositedAmountGteWithdrawableAmount() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( + assertGe( lockup.getDepositedAmount(streamId), lockup.withdrawableAmountOf(streamId), "Invariant violation: deposited amount < withdrawable amount" @@ -84,11 +83,11 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_DepositedAmountGteWithdrawnAmount() external useCurrentTimestamp { + function invariant_DepositedAmountGteWithdrawnAmount() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( + assertGe( lockup.getDepositedAmount(streamId), lockup.getWithdrawnAmount(streamId), "Invariant violation: deposited amount < withdrawn amount" @@ -96,7 +95,16 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_EndTimeGtStartTime() external useCurrentTimestamp { + function invariant_DepositedAmountNotZero() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 streamId = lockupStore.streamIds(i); + uint128 depositAmount = lockup.getDepositedAmount(streamId); + assertNotEq(depositAmount, 0, "Invariant violated: stream non-null, deposited amount zero"); + } + } + + function invariant_EndTimeGtStartTime() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -108,15 +116,24 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_NextStreamId() external useCurrentTimestamp { + function invariant_NextStreamId() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 nextStreamId = lockup.nextStreamId(); - assertEq(nextStreamId, lastStreamId + 1, "Invariant violation: next stream id not incremented"); + assertEq(nextStreamId, lastStreamId + 1, "Invariant violation: next stream ID not incremented"); + } + } + + function invariant_StartTimeNotZero() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 streamId = lockupStore.streamIds(i); + uint40 startTime = lockup.getStartTime(streamId); + assertGt(startTime, 0, "Invariant violated: start time zero"); } } - function invariant_StatusCanceled() external useCurrentTimestamp { + function invariant_StatusCanceled() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -141,7 +158,7 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StatusDepleted() external useCurrentTimestamp { + function invariant_StatusDepleted() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -166,7 +183,7 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StatusPending() external useCurrentTimestamp { + function invariant_StatusPending() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -200,7 +217,7 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StatusSettled() external useCurrentTimestamp { + function invariant_StatusSettled() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -225,7 +242,7 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StatusStreaming() external useCurrentTimestamp { + function invariant_StatusStreaming() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -245,7 +262,7 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } /// @dev See diagram at https://i.postimg.cc/sfHsBkWB/mermaid-diagram-2023-04-25-190035.png. - function invariant_StatusTransitions() external useCurrentTimestamp { + function invariant_StatusTransitions() external { uint256 lastStreamId = lockupStore.lastStreamId(); if (lastStreamId == 0) { return; @@ -294,11 +311,11 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StreamedAmountGteWithdrawableAmount() external useCurrentTimestamp { + function invariant_StreamedAmountGteWithdrawableAmount() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( + assertGe( lockup.streamedAmountOf(streamId), lockup.withdrawableAmountOf(streamId), "Invariant violation: streamed amount < withdrawable amount" @@ -306,11 +323,11 @@ abstract contract Lockup_Invariant_Test is Invariant_Test { } } - function invariant_StreamedAmountGteWithdrawnAmount() external useCurrentTimestamp { + function invariant_StreamedAmountGteWithdrawnAmount() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( + assertGe( lockup.streamedAmountOf(streamId), lockup.getWithdrawnAmount(streamId), "Invariant violation: streamed amount < withdrawn amount" diff --git a/test/invariant/LockupDynamic.t.sol b/test/invariant/LockupDynamic.t.sol index 2fd242720..60c7d2989 100644 --- a/test/invariant/LockupDynamic.t.sol +++ b/test/invariant/LockupDynamic.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup, LockupDynamic } from "src/types/DataTypes.sol"; @@ -24,19 +24,10 @@ contract LockupDynamic_Invariant_Test is Lockup_Invariant_Test { Lockup_Invariant_Test.setUp(); // Deploy the LockupDynamic handlers. - dynamicHandler = new LockupDynamicHandler({ - asset_: dai, - timestampStore_: timestampStore, - lockupStore_: lockupStore, - lockupDynamic_: lockupDynamic - }); - dynamicCreateHandler = new LockupDynamicCreateHandler({ - asset_: dai, - timestampStore_: timestampStore, - lockupStore_: lockupStore, - comptroller_: comptroller, - lockupDynamic_: lockupDynamic - }); + dynamicHandler = + new LockupDynamicHandler({ asset_: dai, lockupStore_: lockupStore, lockupDynamic_: lockupDynamic }); + dynamicCreateHandler = + new LockupDynamicCreateHandler({ asset_: dai, lockupStore_: lockupStore, lockupDynamic_: lockupDynamic }); // Label the contracts. vm.label({ account: address(dynamicHandler), newLabel: "LockupDynamicHandler" }); @@ -59,42 +50,22 @@ contract LockupDynamic_Invariant_Test is Lockup_Invariant_Test { INVARIANTS //////////////////////////////////////////////////////////////////////////*/ - /// @dev The deposited amount must not be zero. - function invariant_DepositedAmountNotZero() external useCurrentTimestamp { - uint256 lastStreamId = lockupStore.lastStreamId(); - for (uint256 i = 0; i < lastStreamId; ++i) { - uint256 streamId = lockupStore.streamIds(i); - LockupDynamic.Stream memory stream = lockupDynamic.getStream(streamId); - assertNotEq(stream.amounts.deposited, 0, "Invariant violated: stream non-null, deposited amount zero"); - } - } - - /// @dev The end time cannot be zero because it must be greater than the start time (which can be zero). - function invariant_EndTimeNotZero() external useCurrentTimestamp { - uint256 lastStreamId = lockupStore.lastStreamId(); - for (uint256 i = 0; i < lastStreamId; ++i) { - uint256 streamId = lockupStore.streamIds(i); - LockupDynamic.Stream memory stream = lockupDynamic.getStream(streamId); - assertNotEq(stream.endTime, 0, "Invariant violated: end time zero"); - } - } - - /// @dev Unordered segment milestones are not allowed. - function invariant_SegmentMilestonesOrdered() external useCurrentTimestamp { + /// @dev Unordered segment timestamps are not allowed. + function invariant_SegmentTimestampsOrdered() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); LockupDynamic.Segment[] memory segments = lockupDynamic.getSegments(streamId); - uint40 previousMilestone = segments[0].milestone; + uint40 previousTimestamp = segments[0].timestamp; for (uint256 j = 1; j < segments.length; ++j) { - assertGt(segments[j].milestone, previousMilestone, "Invariant violated: segment milestones not ordered"); - previousMilestone = segments[j].milestone; + assertGt(segments[j].timestamp, previousTimestamp, "Invariant violated: segment timestamps not ordered"); + previousTimestamp = segments[j].timestamp; } } } /// @dev Settled streams must not appear as cancelable in {SablierV2LockupDynamic.getStream}. - function invariant_StatusSettled_GetStream() external { + function invariant_StatusSettled_GetStream() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); diff --git a/test/invariant/LockupLinear.t.sol b/test/invariant/LockupLinear.t.sol index b0af03760..d1060ef50 100644 --- a/test/invariant/LockupLinear.t.sol +++ b/test/invariant/LockupLinear.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; -import { Lockup, LockupLinear } from "src/types/DataTypes.sol"; +import { Lockup } from "src/types/DataTypes.sol"; import { Lockup_Invariant_Test } from "./Lockup.t.sol"; import { LockupLinearHandler } from "./handlers/LockupLinearHandler.sol"; @@ -24,18 +24,10 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { Lockup_Invariant_Test.setUp(); // Deploy the lockupLinear contract handlers. - lockupLinearHandler = new LockupLinearHandler({ - asset_: dai, - timestampStore_: timestampStore, - lockupStore_: lockupStore, - lockupLinear_: lockupLinear - }); - lockupLinearCreateHandler = new LockupLinearCreateHandler({ - asset_: dai, - timestampStore_: timestampStore, - lockupStore_: lockupStore, - lockupLinear_: lockupLinear - }); + lockupLinearHandler = + new LockupLinearHandler({ asset_: dai, lockupStore_: lockupStore, lockupLinear_: lockupLinear }); + lockupLinearCreateHandler = + new LockupLinearCreateHandler({ asset_: dai, lockupStore_: lockupStore, lockupLinear_: lockupLinear }); // Label the handler contracts. vm.label({ account: address(lockupLinearHandler), newLabel: "LockupLinearHandler" }); @@ -58,31 +50,23 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { INVARIANTS //////////////////////////////////////////////////////////////////////////*/ - /// @dev The cliff time must not be less than the start time. - function invariant_CliffTimeGteStartTime() external useCurrentTimestamp { + /// @dev If it is not zero, the cliff time must be strictly greater than the start time. + function invariant_CliffTimeGtStartTimeOrZero() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( - lockupLinear.getCliffTime(streamId), - lockupLinear.getStartTime(streamId), - "Invariant violated: cliff time < start time" - ); - } - } - - /// @dev The deposited amount must not be zero. - function invariant_DepositedAmountNotZero() external useCurrentTimestamp { - uint256 lastStreamId = lockupStore.lastStreamId(); - for (uint256 i = 0; i < lastStreamId; ++i) { - uint256 streamId = lockupStore.streamIds(i); - LockupLinear.Stream memory stream = lockupLinear.getStream(streamId); - assertNotEq(stream.amounts.deposited, 0, "Invariant violated: stream non-null, deposited amount zero"); + if (lockupLinear.getCliffTime(streamId) > 0) { + assertGt( + lockupLinear.getCliffTime(streamId), + lockupLinear.getStartTime(streamId), + "Invariant violated: cliff time <= start time" + ); + } } } /// @dev The end time must not be less than or equal to the cliff time. - function invariant_EndTimeGtCliffTime() external useCurrentTimestamp { + function invariant_EndTimeGtCliffTime() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); @@ -94,18 +78,8 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { } } - /// @dev The end time must not be zero because it must be greater than the start time (which can be zero). - function invariant_EndTimeNotZero() external useCurrentTimestamp { - uint256 lastStreamId = lockupStore.lastStreamId(); - for (uint256 i = 0; i < lastStreamId; ++i) { - uint256 streamId = lockupStore.streamIds(i); - LockupLinear.Stream memory stream = lockupLinear.getStream(streamId); - assertNotEq(stream.endTime, 0, "Invariant violated: stream non-null, end time zero"); - } - } - /// @dev Settled streams must not appear as cancelable in {SablierV2LockupLinear.getStream}. - function invariant_StatusSettled_GetStream() external { + function invariant_StatusSettled_GetStream() external view { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); diff --git a/test/invariant/LockupTranched.t.sol b/test/invariant/LockupTranched.t.sol new file mode 100644 index 000000000..f60414f99 --- /dev/null +++ b/test/invariant/LockupTranched.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Lockup, LockupTranched } from "src/types/DataTypes.sol"; + +import { Lockup_Invariant_Test } from "./Lockup.t.sol"; +import { LockupTranchedCreateHandler } from "./handlers/LockupTranchedCreateHandler.sol"; +import { LockupTranchedHandler } from "./handlers/LockupTranchedHandler.sol"; + +/// @dev Invariant tests for {SablierV2LockupTranched}. +contract LockupTranched_Invariant_Test is Lockup_Invariant_Test { + /*////////////////////////////////////////////////////////////////////////// + TEST CONTRACTS + //////////////////////////////////////////////////////////////////////////*/ + + LockupTranchedHandler internal tranchedHandler; + LockupTranchedCreateHandler internal tranchedCreateHandler; + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + Lockup_Invariant_Test.setUp(); + + // Deploy the LockupTranched handlers. + tranchedHandler = + new LockupTranchedHandler({ asset_: dai, lockupStore_: lockupStore, lockupTranched_: lockupTranched }); + tranchedCreateHandler = + new LockupTranchedCreateHandler({ asset_: dai, lockupStore_: lockupStore, lockupTranched_: lockupTranched }); + + // Label the contracts. + vm.label({ account: address(tranchedHandler), newLabel: "LockupTranchedHandler" }); + vm.label({ account: address(tranchedCreateHandler), newLabel: "LockupTranchedCreateHandler" }); + + // Cast the LockupTranched contract and handler. + lockup = lockupTranched; + lockupHandler = tranchedHandler; + + // Target the LockupTranched handlers for invariant testing. + targetContract(address(tranchedHandler)); + targetContract(address(tranchedCreateHandler)); + + // Prevent these contracts from being fuzzed as `msg.sender`. + excludeSender(address(tranchedHandler)); + excludeSender(address(tranchedCreateHandler)); + } + + /*////////////////////////////////////////////////////////////////////////// + INVARIANTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Settled streams must not appear as cancelable in {SablierV2LockupTranched.getStream}. + function invariant_StatusSettled_GetStream() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 streamId = lockupStore.streamIds(i); + if (lockupTranched.statusOf(streamId) == Lockup.Status.SETTLED) { + assertFalse( + lockupTranched.getStream(streamId).isCancelable, + "Invariant violation: stream returned by getStream() is cancelable" + ); + } + } + } + + /// @dev Unordered tranche timestamps are not allowed. + function invariant_TrancheTimestampsOrdered() external view { + uint256 lastStreamId = lockupStore.lastStreamId(); + for (uint256 i = 0; i < lastStreamId; ++i) { + uint256 streamId = lockupStore.streamIds(i); + LockupTranched.Tranche[] memory tranches = lockupTranched.getTranches(streamId); + uint40 previousTimestamp = tranches[0].timestamp; + for (uint256 j = 1; j < tranches.length; ++j) { + assertGt(tranches[j].timestamp, previousTimestamp, "Invariant violated: tranche timestamps not ordered"); + previousTimestamp = tranches[j].timestamp; + } + } + } +} diff --git a/test/invariant/handlers/BaseHandler.sol b/test/invariant/handlers/BaseHandler.sol index 75523ea07..fdd804cdb 100644 --- a/test/invariant/handlers/BaseHandler.sol +++ b/test/invariant/handlers/BaseHandler.sol @@ -1,30 +1,21 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Vm } from "@prb/test/src/PRBTest.sol"; import { StdCheats } from "forge-std/src/StdCheats.sol"; import { Constants } from "../../utils/Constants.sol"; import { Fuzzers } from "../../utils/Fuzzers.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; /// @notice Base contract with common logic needed by all handler contracts. abstract contract BaseHandler is Constants, Fuzzers, StdCheats { /*////////////////////////////////////////////////////////////////////////// - CONSTANTS + STATE-VARIABLES //////////////////////////////////////////////////////////////////////////*/ /// @dev Maximum number of streams that can be created during an invariant campaign. uint256 internal constant MAX_STREAM_COUNT = 100; - /// @dev The virtual address of the Foundry VM. - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - - /*////////////////////////////////////////////////////////////////////////// - VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - /// @dev Maps function names to the number of times they have been called. mapping(string func => uint256 calls) public calls; @@ -38,19 +29,12 @@ abstract contract BaseHandler is Constants, Fuzzers, StdCheats { /// @dev Default ERC-20 asset used for testing. IERC20 public asset; - /// @dev Reference to the timestamp store, which is needed for simulating the passage of time. - TimestampStore public timestampStore; - - /// @dev An instance of the Foundry VM, which contains cheatcodes for testing. - Vm internal constant vm = Vm(VM_ADDRESS); - /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor(IERC20 asset_, TimestampStore timestampStore_) { + constructor(IERC20 asset_) { asset = asset_; - timestampStore = timestampStore_; } /*////////////////////////////////////////////////////////////////////////// @@ -58,12 +42,10 @@ abstract contract BaseHandler is Constants, Fuzzers, StdCheats { //////////////////////////////////////////////////////////////////////////*/ /// @dev Simulates the passage of time. The time jump is upper bounded so that streams don't settle too quickly. - /// See https://github.com/foundry-rs/foundry/issues/4994. /// @param timeJumpSeed A fuzzed value needed for generating random time warps. modifier adjustTimestamp(uint256 timeJumpSeed) { uint256 timeJump = _bound(timeJumpSeed, 2 minutes, 40 days); - timestampStore.increaseCurrentTimestamp(timeJump); - vm.warp(timestampStore.currentTimestamp()); + vm.warp(block.timestamp + timeJump); _; } @@ -91,8 +73,7 @@ abstract contract BaseHandler is Constants, Fuzzers, StdCheats { /// @dev Makes the provided sender the caller. modifier useNewSender(address sender) { - vm.startPrank(sender); + resetPrank(sender); _; - vm.stopPrank(); } } diff --git a/test/invariant/handlers/ComptrollerHandler.sol b/test/invariant/handlers/ComptrollerHandler.sol deleted file mode 100644 index c5d9f5e6c..000000000 --- a/test/invariant/handlers/ComptrollerHandler.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18, UNIT } from "@prb/math/src/UD60x18.sol"; - -import { ISablierV2Comptroller } from "src/interfaces/ISablierV2Comptroller.sol"; - -import { TimestampStore } from "../stores/TimestampStore.sol"; -import { BaseHandler } from "./BaseHandler.sol"; - -/// @dev This contract and not {SablierV2Comptroller} is exposed to Foundry for invariant testing. The point is -/// to bound and restrict the inputs that get passed to the real-world contract to avoid getting reverts. -contract ComptrollerHandler is BaseHandler { - /*////////////////////////////////////////////////////////////////////////// - TEST CONTRACTS - //////////////////////////////////////////////////////////////////////////*/ - - ISablierV2Comptroller public comptroller; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - constructor( - IERC20 asset_, - TimestampStore timestampStore_, - ISablierV2Comptroller comptroller_ - ) - BaseHandler(asset_, timestampStore_) - { - comptroller = comptroller_; - } - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-COMPTROLLER - //////////////////////////////////////////////////////////////////////////*/ - - function setFlashFee( - uint256 timeJumpSeed, - UD60x18 newFlashFee - ) - external - instrument("setFlashFee") - adjustTimestamp(timeJumpSeed) - { - newFlashFee = _bound(newFlashFee, 0, UNIT); - comptroller.setFlashFee(newFlashFee); - } - - function setProtocolFee( - uint256 timeJumpSeed, - UD60x18 newProtocolFee - ) - external - instrument("setProtocolFee") - adjustTimestamp(timeJumpSeed) - { - newProtocolFee = _bound(newProtocolFee, 0, MAX_FEE); - comptroller.setProtocolFee(asset, newProtocolFee); - } - - function toggleFlashAsset(uint256 timeJumpSeed) - external - instrument("toggleFlashAsset") - adjustTimestamp(timeJumpSeed) - { - comptroller.toggleFlashAsset(asset); - } -} diff --git a/test/invariant/handlers/FlashLoanHandler.sol b/test/invariant/handlers/FlashLoanHandler.sol deleted file mode 100644 index 8165e152f..000000000 --- a/test/invariant/handlers/FlashLoanHandler.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; - -import { SablierV2FlashLoan } from "src/abstracts/SablierV2FlashLoan.sol"; -import { IERC3156FlashBorrower } from "src/interfaces/erc3156/IERC3156FlashBorrower.sol"; -import { ISablierV2Comptroller } from "src/interfaces/ISablierV2Comptroller.sol"; - -import { TimestampStore } from "../stores/TimestampStore.sol"; -import { BaseHandler } from "./BaseHandler.sol"; - -/// @dev This contract and not {SablierV2FlashLoan} is exposed to Foundry for invariant testing. The point is -/// to bound and restrict the inputs that get passed to the real-world contract to avoid getting reverts. -contract FlashLoanHandler is BaseHandler { - /*////////////////////////////////////////////////////////////////////////// - TEST CONTRACTS - //////////////////////////////////////////////////////////////////////////*/ - - ISablierV2Comptroller public comptroller; - SablierV2FlashLoan public flashLoanContract; - IERC3156FlashBorrower internal receiver; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - constructor( - IERC20 asset_, - TimestampStore timestampStore_, - ISablierV2Comptroller comptroller_, - SablierV2FlashLoan flashLoanContract_, - IERC3156FlashBorrower receiver_ - ) - BaseHandler(asset_, timestampStore_) - { - comptroller = comptroller_; - flashLoanContract = flashLoanContract_; - receiver = receiver_; - } - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-FLASH-LOAN - //////////////////////////////////////////////////////////////////////////*/ - - function flashLoan( - uint256 timeJumpSeed, - uint128 amount - ) - external - instrument("flashLoan") - adjustTimestamp(timeJumpSeed) - { - // Only up to `MAX_UINT128` assets can be flash loaned. - uint256 balance = asset.balanceOf(address(this)); - uint128 upperBound = uint128(Math.min(balance, MAX_UINT128)); - amount = boundUint128(amount, 0, upperBound); - - // Only supported assets can be flash loaned. - bool isFlashAsset = comptroller.isFlashAsset(asset); - if (!isFlashAsset) { - return; - } - - // The flash fee must be less than or equal to `MAX_UINT128`. - uint256 fee = flashLoanContract.flashFee(address(asset), amount); - if (fee > type(uint128).max) { - return; - } - - // Mint the flash fee to the receiver so that they can repay the flash loan. - deal({ token: address(asset), to: address(receiver), give: fee }); - - // Some contracts do not inherit from {SablierV2FlashLoan}. - (bool success,) = address(flashLoanContract).staticcall( - abi.encodeWithSelector( - SablierV2FlashLoan.flashLoan.selector, receiver, address(asset), amount, bytes("Some Data") - ) - ); - if (!success) { - return; - } - - // Execute the flash loan. - flashLoanContract.flashLoan({ - receiver: receiver, - asset: address(asset), - amount: amount, - data: bytes("Some Data") - }); - } -} diff --git a/test/invariant/handlers/LockupDynamicCreateHandler.sol b/test/invariant/handlers/LockupDynamicCreateHandler.sol index 025cf43fd..02f39c2ac 100644 --- a/test/invariant/handlers/LockupDynamicCreateHandler.sol +++ b/test/invariant/handlers/LockupDynamicCreateHandler.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ISablierV2Comptroller } from "src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; import { LockupDynamic } from "src/types/DataTypes.sol"; import { LockupStore } from "../stores/LockupStore.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; /// @dev This contract is a complement of {LockupDynamicHandler}. The goal is to bias the invariant calls @@ -19,7 +17,6 @@ contract LockupDynamicCreateHandler is BaseHandler { TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - ISablierV2Comptroller public comptroller; ISablierV2LockupDynamic public lockupDynamic; LockupStore public lockupStore; @@ -27,17 +24,8 @@ contract LockupDynamicCreateHandler is BaseHandler { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor( - IERC20 asset_, - TimestampStore timestampStore_, - LockupStore lockupStore_, - ISablierV2Comptroller comptroller_, - ISablierV2LockupDynamic lockupDynamic_ - ) - BaseHandler(asset_, timestampStore_) - { + constructor(IERC20 asset_, LockupStore lockupStore_, ISablierV2LockupDynamic lockupDynamic_) BaseHandler(asset_) { lockupStore = lockupStore_; - comptroller = comptroller_; lockupDynamic = lockupDynamic_; } @@ -45,12 +33,12 @@ contract LockupDynamicCreateHandler is BaseHandler { HANDLER FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - function createWithDeltas( + function createWithDurations( uint256 timeJumpSeed, - LockupDynamic.CreateWithDeltas memory params + LockupDynamic.CreateWithDurations memory params ) public - instrument("createWithDeltas") + instrument("createWithDurations") adjustTimestamp(timeJumpSeed) checkUsers(params.sender, params.recipient, params.broker.account) useNewSender(params.sender) @@ -66,16 +54,15 @@ contract LockupDynamicCreateHandler is BaseHandler { } // Bound the broker fee. - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); - // Fuzz the deltas. - fuzzSegmentDeltas(params.segments); + // Fuzz the durations. + fuzzSegmentDurations(params.segments); - // Fuzz the segment amounts and calculate the create amounts (total, deposit, protocol fee, and broker fee). + // Fuzz the segment amounts and calculate the total amount. (params.totalAmount,) = fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: params.segments, - protocolFee: comptroller.protocolFees(asset), brokerFee: params.broker.fee }); @@ -83,22 +70,22 @@ contract LockupDynamicCreateHandler is BaseHandler { deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); // Approve {SablierV2LockupDynamic} to spend the assets. - asset.approve({ spender: address(lockupDynamic), amount: params.totalAmount }); + asset.approve({ spender: address(lockupDynamic), value: params.totalAmount }); // Create the stream. params.asset = asset; - uint256 streamId = lockupDynamic.createWithDeltas(params); + uint256 streamId = lockupDynamic.createWithDurations(params); - // Store the stream id. + // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); } - function createWithMilestones( + function createWithTimestamps( uint256 timeJumpSeed, - LockupDynamic.CreateWithMilestones memory params + LockupDynamic.CreateWithTimestamps memory params ) public - instrument("createWithMilestones") + instrument("createWithTimestamps") adjustTimestamp(timeJumpSeed) checkUsers(params.sender, params.recipient, params.broker.account) useNewSender(params.sender) @@ -113,17 +100,16 @@ contract LockupDynamicCreateHandler is BaseHandler { return; } - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - params.startTime = boundUint40(params.startTime, 0, getBlockTimestamp()); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp()); - // Fuzz the segment milestones. - fuzzSegmentMilestones(params.segments, params.startTime); + // Fuzz the segment timestamps. + fuzzSegmentTimestamps(params.segments, params.startTime); - // Fuzz the segment amounts and calculate the create amounts (total, deposit, protocol fee, and broker fee). + // Fuzz the segment amounts and calculate the total amount. (params.totalAmount,) = fuzzDynamicStreamAmounts({ upperBound: 1_000_000_000e18, segments: params.segments, - protocolFee: comptroller.protocolFees(asset), brokerFee: params.broker.fee }); @@ -131,13 +117,13 @@ contract LockupDynamicCreateHandler is BaseHandler { deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); // Approve {SablierV2LockupDynamic} to spend the assets. - asset.approve({ spender: address(lockupDynamic), amount: params.totalAmount }); + asset.approve({ spender: address(lockupDynamic), value: params.totalAmount }); // Create the stream. params.asset = asset; - uint256 streamId = lockupDynamic.createWithMilestones(params); + uint256 streamId = lockupDynamic.createWithTimestamps(params); - // Store the stream id. + // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); } } diff --git a/test/invariant/handlers/LockupDynamicHandler.sol b/test/invariant/handlers/LockupDynamicHandler.sol index 9a33711a3..34bbf0a4e 100644 --- a/test/invariant/handlers/LockupDynamicHandler.sol +++ b/test/invariant/handlers/LockupDynamicHandler.sol @@ -1,23 +1,21 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; import { LockupStore } from "../stores/LockupStore.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; import { LockupHandler } from "./LockupHandler.sol"; -/// @dev This contract and not {SablierV2LockupDynamic} is exposed to Foundry for invariant testing. The point is +/// @dev This contract and not {SablierV2LockupDynamic} is exposed to Foundry for invariant testing. The goal is /// to bound and restrict the inputs that get passed to the real-world contract to avoid getting reverts. contract LockupDynamicHandler is LockupHandler { constructor( IERC20 asset_, - TimestampStore timestampStore_, LockupStore lockupStore_, ISablierV2LockupDynamic lockupDynamic_ ) - LockupHandler(asset_, timestampStore_, lockupStore_, lockupDynamic_) + LockupHandler(asset_, lockupStore_, lockupDynamic_) { } } diff --git a/test/invariant/handlers/LockupHandler.sol b/test/invariant/handlers/LockupHandler.sol index 1a93ffc24..5637bd7a5 100644 --- a/test/invariant/handlers/LockupHandler.sol +++ b/test/invariant/handlers/LockupHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -7,7 +7,6 @@ import { ISablierV2Lockup } from "src/interfaces/ISablierV2Lockup.sol"; import { Lockup } from "src/types/DataTypes.sol"; import { LockupStore } from "../stores/LockupStore.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; /// @dev Common handler logic between {LockupLinearHandler} and {LockupDynamicHandler}. @@ -31,14 +30,7 @@ abstract contract LockupHandler is BaseHandler { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor( - IERC20 asset_, - TimestampStore timestampStore_, - LockupStore lockupStore_, - ISablierV2Lockup lockup_ - ) - BaseHandler(asset_, timestampStore_) - { + constructor(IERC20 asset_, LockupStore lockupStore_, ISablierV2Lockup lockup_) BaseHandler(asset_) { lockupStore = lockupStore_; lockup = lockup_; } @@ -49,9 +41,8 @@ abstract contract LockupHandler is BaseHandler { modifier useAdmin() { address admin = lockup.admin(); - vm.startPrank(admin); + resetPrank(admin); _; - vm.stopPrank(); } /// @dev Picks a random stream from the store. @@ -69,17 +60,15 @@ abstract contract LockupHandler is BaseHandler { modifier useFuzzedStreamRecipient() { uint256 lastStreamId = lockupStore.lastStreamId(); currentRecipient = lockupStore.recipients(currentStreamId); - vm.startPrank(currentRecipient); + resetPrank(currentRecipient); _; - vm.stopPrank(); } modifier useFuzzedStreamSender() { uint256 lastStreamId = lockupStore.lastStreamId(); currentSender = lockupStore.senders(currentStreamId); - vm.startPrank(currentSender); + resetPrank(currentSender); _; - vm.stopPrank(); } /*////////////////////////////////////////////////////////////////////////// @@ -138,22 +127,6 @@ abstract contract LockupHandler is BaseHandler { lockup.cancel(currentStreamId); } - function claimProtocolRevenues(uint256 timeJumpSeed) - external - instrument("claimProtocolRevenues") - adjustTimestamp(timeJumpSeed) - useAdmin - { - // Can claim revenues only if the protocol has revenues. - uint128 protocolRevenues = lockup.protocolRevenues(asset); - if (protocolRevenues == 0) { - return; - } - - // Claim the protocol revenues. - lockup.claimProtocolRevenues(asset); - } - function renounce( uint256 timeJumpSeed, uint256 streamIndexSeed @@ -302,7 +275,7 @@ abstract contract LockupHandler is BaseHandler { // Make the max withdrawal and transfer the NFT. lockup.withdrawMaxAndTransfer({ streamId: currentStreamId, newRecipient: newRecipient }); - // Update the recipient associated with this stream id. + // Update the recipient associated with this stream ID. lockupStore.updateRecipient(currentStreamId, newRecipient); } @@ -339,7 +312,7 @@ abstract contract LockupHandler is BaseHandler { // Transfer the NFT to the new recipient. lockup.transferFrom({ from: currentRecipient, to: newRecipient, tokenId: currentStreamId }); - // Update the recipient associated with this stream id. + // Update the recipient associated with this stream ID. lockupStore.updateRecipient(currentStreamId, newRecipient); } } diff --git a/test/invariant/handlers/LockupLinearCreateHandler.sol b/test/invariant/handlers/LockupLinearCreateHandler.sol index a6c2c82d8..fce35e868 100644 --- a/test/invariant/handlers/LockupLinearCreateHandler.sol +++ b/test/invariant/handlers/LockupLinearCreateHandler.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.sol"; import { LockupLinear } from "src/types/DataTypes.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; import { LockupStore } from "../stores/LockupStore.sol"; import { BaseHandler } from "./BaseHandler.sol"; @@ -25,14 +24,7 @@ contract LockupLinearCreateHandler is BaseHandler { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - constructor( - IERC20 asset_, - TimestampStore timestampStore_, - LockupStore lockupStore_, - ISablierV2LockupLinear lockupLinear_ - ) - BaseHandler(asset_, timestampStore_) - { + constructor(IERC20 asset_, LockupStore lockupStore_, ISablierV2LockupLinear lockupLinear_) BaseHandler(asset_) { lockupStore = lockupStore_; lockupLinear = lockupLinear_; } @@ -57,7 +49,7 @@ contract LockupLinearCreateHandler is BaseHandler { } // Bound the stream parameters. - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); params.durations.cliff = boundUint40(params.durations.cliff, 1 seconds, 2500 seconds); params.durations.total = boundUint40(params.durations.total, params.durations.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); @@ -67,22 +59,22 @@ contract LockupLinearCreateHandler is BaseHandler { deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); // Approve {SablierV2LockupLinear} to spend the assets. - asset.approve({ spender: address(lockupLinear), amount: params.totalAmount }); + asset.approve({ spender: address(lockupLinear), value: params.totalAmount }); // Create the stream. params.asset = asset; uint256 streamId = lockupLinear.createWithDurations(params); - // Store the stream id. + // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); } - function createWithRange( + function createWithTimestamps( uint256 timeJumpSeed, - LockupLinear.CreateWithRange memory params + LockupLinear.CreateWithTimestamps memory params ) public - instrument("createWithRange") + instrument("createWithTimestamps") adjustTimestamp(timeJumpSeed) checkUsers(params.sender, params.recipient, params.broker.account) useNewSender(params.sender) @@ -92,31 +84,33 @@ contract LockupLinearCreateHandler is BaseHandler { return; } - uint40 currentTime = getBlockTimestamp(); - params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - params.range.start = boundUint40(params.range.start, 0, currentTime); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); + uint40 blockTimestamp = getBlockTimestamp(); + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.timestamps.start = boundUint40(params.timestamps.start, 1 seconds, blockTimestamp); params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); - // Bound the end time so that it is always greater than both the current time and the cliff time (this is - // a requirement of the protocol). - params.range.end = boundUint40( - params.range.end, - (params.range.cliff <= currentTime ? currentTime : params.range.cliff) + 1 seconds, - MAX_UNIX_TIMESTAMP - ); + // The cliff time must be either zero or greater than the start time. + if (params.timestamps.cliff > 0) { + params.timestamps.cliff = boundUint40( + params.timestamps.cliff, params.timestamps.start + 1 seconds, params.timestamps.start + 52 weeks + ); + } + + // Bound the end time so that it is always greater than the start time, the cliff time, and the block timestamp. + uint40 endTimeLowerBound = maxOfThree(params.timestamps.start, params.timestamps.cliff, blockTimestamp); + params.timestamps.end = boundUint40(params.timestamps.end, endTimeLowerBound + 1 seconds, MAX_UNIX_TIMESTAMP); // Mint enough assets to the Sender. deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); // Approve {SablierV2LockupLinear} to spend the assets. - asset.approve({ spender: address(lockupLinear), amount: params.totalAmount }); + asset.approve({ spender: address(lockupLinear), value: params.totalAmount }); // Create the stream. params.asset = asset; - uint256 streamId = lockupLinear.createWithRange(params); + uint256 streamId = lockupLinear.createWithTimestamps(params); - // Store the stream id. + // Store the stream ID. lockupStore.pushStreamId(streamId, params.sender, params.recipient); } } diff --git a/test/invariant/handlers/LockupLinearHandler.sol b/test/invariant/handlers/LockupLinearHandler.sol index 0c5bca009..9e2451660 100644 --- a/test/invariant/handlers/LockupLinearHandler.sol +++ b/test/invariant/handlers/LockupLinearHandler.sol @@ -1,23 +1,21 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.sol"; import { LockupStore } from "../stores/LockupStore.sol"; -import { TimestampStore } from "../stores/TimestampStore.sol"; import { LockupHandler } from "./LockupHandler.sol"; -/// @dev This contract and not {SablierV2LockupLinear} is exposed to Foundry for invariant testing. The point is +/// @dev This contract and not {SablierV2LockupLinear} is exposed to Foundry for invariant testing. The goal is /// to bound and restrict the inputs that get passed to the real-world contract to avoid getting reverts. contract LockupLinearHandler is LockupHandler { constructor( IERC20 asset_, - TimestampStore timestampStore_, LockupStore lockupStore_, ISablierV2LockupLinear lockupLinear_ ) - LockupHandler(asset_, timestampStore_, lockupStore_, lockupLinear_) + LockupHandler(asset_, lockupStore_, lockupLinear_) { } } diff --git a/test/invariant/handlers/LockupTranchedCreateHandler.sol b/test/invariant/handlers/LockupTranchedCreateHandler.sol new file mode 100644 index 000000000..f4de81f15 --- /dev/null +++ b/test/invariant/handlers/LockupTranchedCreateHandler.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISablierV2LockupTranched } from "src/interfaces/ISablierV2LockupTranched.sol"; +import { LockupTranched } from "src/types/DataTypes.sol"; + +import { LockupStore } from "../stores/LockupStore.sol"; +import { BaseHandler } from "./BaseHandler.sol"; + +/// @dev This contract is a complement of {LockupTranchedHandler}. The goal is to bias the invariant calls +/// toward the lockup functions (especially the create stream functions) by creating multiple handlers for +/// the lockup contracts. +contract LockupTranchedCreateHandler is BaseHandler { + /*////////////////////////////////////////////////////////////////////////// + TEST CONTRACTS + //////////////////////////////////////////////////////////////////////////*/ + + LockupStore public lockupStore; + ISablierV2LockupTranched public lockupTranched; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor( + IERC20 asset_, + LockupStore lockupStore_, + ISablierV2LockupTranched lockupTranched_ + ) + BaseHandler(asset_) + { + lockupStore = lockupStore_; + lockupTranched = lockupTranched_; + } + + /*////////////////////////////////////////////////////////////////////////// + HANDLER FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function createWithDurations( + uint256 timeJumpSeed, + LockupTranched.CreateWithDurations memory params + ) + public + instrument("createWithDurations") + adjustTimestamp(timeJumpSeed) + checkUsers(params.sender, params.recipient, params.broker.account) + useNewSender(params.sender) + { + // We don't want to create more than a certain number of streams. + if (lockupStore.lastStreamId() > MAX_STREAM_COUNT) { + return; + } + + // The protocol doesn't allow empty tranche arrays. + if (params.tranches.length == 0) { + return; + } + + // Bound the broker fee. + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + + // Fuzz the durations. + fuzzTrancheDurations(params.tranches); + + // Fuzz the tranche amounts and calculate the total amount. + (params.totalAmount,) = fuzzTranchedStreamAmounts({ + upperBound: 1_000_000_000e18, + tranches: params.tranches, + brokerFee: params.broker.fee + }); + + // Mint enough assets to the Sender. + deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); + + // Approve {SablierV2LockupTranched} to spend the assets. + asset.approve({ spender: address(lockupTranched), value: params.totalAmount }); + + // Create the stream. + params.asset = asset; + uint256 streamId = lockupTranched.createWithDurations(params); + + // Store the stream ID. + lockupStore.pushStreamId(streamId, params.sender, params.recipient); + } + + function createWithTimestamps( + uint256 timeJumpSeed, + LockupTranched.CreateWithTimestamps memory params + ) + public + instrument("createWithTimestamps") + adjustTimestamp(timeJumpSeed) + checkUsers(params.sender, params.recipient, params.broker.account) + useNewSender(params.sender) + { + // We don't want to create more than a certain number of streams. + if (lockupStore.lastStreamId() >= MAX_STREAM_COUNT) { + return; + } + + // The protocol doesn't allow empty tranche arrays. + if (params.tranches.length == 0) { + return; + } + + params.broker.fee = _bound(params.broker.fee, 0, MAX_BROKER_FEE); + params.startTime = boundUint40(params.startTime, 1, getBlockTimestamp()); + + // Fuzz the tranche timestamps. + fuzzTrancheTimestamps(params.tranches, params.startTime); + + // Fuzz the tranche amounts and calculate the total amount. + (params.totalAmount,) = fuzzTranchedStreamAmounts({ + upperBound: 1_000_000_000e18, + tranches: params.tranches, + brokerFee: params.broker.fee + }); + + // Mint enough assets to the Sender. + deal({ token: address(asset), to: params.sender, give: asset.balanceOf(params.sender) + params.totalAmount }); + + // Approve {SablierV2LockupTranched} to spend the assets. + asset.approve({ spender: address(lockupTranched), value: params.totalAmount }); + + // Create the stream. + params.asset = asset; + uint256 streamId = lockupTranched.createWithTimestamps(params); + + // Store the stream ID. + lockupStore.pushStreamId(streamId, params.sender, params.recipient); + } +} diff --git a/test/invariant/handlers/LockupTranchedHandler.sol b/test/invariant/handlers/LockupTranchedHandler.sol new file mode 100644 index 000000000..25ec40b98 --- /dev/null +++ b/test/invariant/handlers/LockupTranchedHandler.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISablierV2LockupTranched } from "src/interfaces/ISablierV2LockupTranched.sol"; + +import { LockupStore } from "../stores/LockupStore.sol"; +import { LockupHandler } from "./LockupHandler.sol"; + +/// @dev This contract and not {SablierV2LockupTranched} is exposed to Foundry for invariant testing. The goal is +/// to bound and restrict the inputs that get passed to the real-world contract to avoid getting reverts. +contract LockupTranchedHandler is LockupHandler { + constructor( + IERC20 asset_, + LockupStore lockupStore_, + ISablierV2LockupTranched lockupTranched_ + ) + LockupHandler(asset_, lockupStore_, lockupTranched_) + { } +} diff --git a/test/invariant/stores/LockupStore.sol b/test/invariant/stores/LockupStore.sol index 1106cf0f6..0314ac96a 100644 --- a/test/invariant/stores/LockupStore.sol +++ b/test/invariant/stores/LockupStore.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; @@ -21,12 +21,12 @@ contract LockupStore { //////////////////////////////////////////////////////////////////////////*/ function pushStreamId(uint256 streamId, address sender, address recipient) external { - // Store the stream ids, the senders, and the recipients. + // Store the stream IDs, the senders, and the recipients. streamIds.push(streamId); senders[streamId] = sender; recipients[streamId] = recipient; - // Update the last stream id. + // Update the last stream ID. lastStreamId = streamId; } diff --git a/test/invariant/stores/TimestampStore.sol b/test/invariant/stores/TimestampStore.sol deleted file mode 100644 index d214e0726..000000000 --- a/test/invariant/stores/TimestampStore.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -/// @dev Because Foundry does not commit the state changes between invariant runs, we need to -/// save the current timestamp in a contract with persistent storage. -contract TimestampStore { - uint256 public currentTimestamp; - - constructor() { - currentTimestamp = block.timestamp; - } - - function increaseCurrentTimestamp(uint256 timeJump) external { - currentTimestamp += timeJump; - } -} diff --git a/test/mocks/AdminableMock.sol b/test/mocks/AdminableMock.sol index 3b358a5ac..0ea10b4b6 100644 --- a/test/mocks/AdminableMock.sol +++ b/test/mocks/AdminableMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { Adminable } from "../../src/abstracts/Adminable.sol"; import { IAdminable } from "../../src/interfaces/IAdminable.sol"; diff --git a/test/mocks/Hooks.sol b/test/mocks/Hooks.sol new file mode 100644 index 000000000..99d3a7c00 --- /dev/null +++ b/test/mocks/Hooks.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC165, ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import { ISablierLockupRecipient } from "../../src/interfaces/ISablierLockupRecipient.sol"; +import { ISablierV2Lockup } from "../../src/interfaces/ISablierV2Lockup.sol"; + +contract RecipientGood is ISablierLockupRecipient, ERC165 { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(ISablierLockupRecipient).interfaceId; + } + + function onSablierLockupCancel( + uint256 streamId, + address sender, + uint128 senderAmount, + uint128 recipientAmount + ) + external + pure + override + returns (bytes4) + { + streamId; + sender; + senderAmount; + recipientAmount; + + return ISablierLockupRecipient.onSablierLockupCancel.selector; + } + + function onSablierLockupWithdraw( + uint256 streamId, + address caller, + address to, + uint128 amount + ) + external + pure + override + returns (bytes4) + { + streamId; + caller; + to; + amount; + + return ISablierLockupRecipient.onSablierLockupWithdraw.selector; + } +} + +contract RecipientInterfaceIDIncorrect is ISablierLockupRecipient, ERC165 { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == 0xffffffff; + } + + function onSablierLockupCancel(uint256, address, uint128, uint128) external pure override returns (bytes4) { + return ISablierLockupRecipient.onSablierLockupCancel.selector; + } + + function onSablierLockupWithdraw(uint256, address, address, uint128) external pure override returns (bytes4) { + return ISablierLockupRecipient.onSablierLockupWithdraw.selector; + } +} + +contract RecipientInterfaceIDMissing { + function onSablierLockupCancel(uint256, address, uint128, uint128) external pure returns (bytes4) { + return ISablierLockupRecipient.onSablierLockupCancel.selector; + } + + function onSablierLockupWithdraw(uint256, address, address, uint128) external pure returns (bytes4) { + return ISablierLockupRecipient.onSablierLockupWithdraw.selector; + } +} + +contract RecipientInvalidSelector is ISablierLockupRecipient, ERC165 { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(ISablierLockupRecipient).interfaceId; + } + + function onSablierLockupCancel( + uint256 streamId, + address sender, + uint128 senderAmount, + uint128 recipientAmount + ) + external + pure + override + returns (bytes4) + { + streamId; + sender; + senderAmount; + recipientAmount; + + return 0x10000000; + } + + function onSablierLockupWithdraw( + uint256 streamId, + address caller, + address to, + uint128 amount + ) + external + pure + override + returns (bytes4) + { + streamId; + caller; + to; + amount; + + return 0x12345678; + } +} + +contract RecipientReentrant is ISablierLockupRecipient, ERC165 { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(ISablierLockupRecipient).interfaceId; + } + + function onSablierLockupCancel( + uint256 streamId, + address sender, + uint128 senderAmount, + uint128 recipientAmount + ) + external + override + returns (bytes4) + { + streamId; + sender; + senderAmount; + recipientAmount; + + ISablierV2Lockup(msg.sender).withdraw(streamId, address(this), recipientAmount); + + return ISablierLockupRecipient.onSablierLockupCancel.selector; + } + + function onSablierLockupWithdraw( + uint256 streamId, + address caller, + address to, + uint128 amount + ) + external + override + returns (bytes4) + { + streamId; + caller; + to; + amount; + + ISablierV2Lockup(msg.sender).withdraw(streamId, address(this), amount); + + return ISablierLockupRecipient.onSablierLockupWithdraw.selector; + } +} + +contract RecipientReverting is ISablierLockupRecipient, ERC165 { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + return interfaceId == type(ISablierLockupRecipient).interfaceId; + } + + function onSablierLockupCancel( + uint256 streamId, + address sender, + uint128 senderAmount, + uint128 recipientAmount + ) + external + pure + override + returns (bytes4) + { + streamId; + sender; + senderAmount; + recipientAmount; + revert("You shall not pass"); + } + + function onSablierLockupWithdraw( + uint256 streamId, + address caller, + address to, + uint128 amount + ) + external + pure + override + returns (bytes4) + { + streamId; + caller; + to; + amount; + revert("You shall not pass"); + } +} diff --git a/test/mocks/NFTDescriptorMock.sol b/test/mocks/NFTDescriptorMock.sol index b9b22f1a4..f0315e156 100644 --- a/test/mocks/NFTDescriptorMock.sol +++ b/test/mocks/NFTDescriptorMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; @@ -52,28 +52,22 @@ contract NFTDescriptorMock is SablierV2NFTDescriptor { } function generateDescription_( - string memory streamingModel, + string memory sablierModel, string memory assetSymbol, - string memory streamId, string memory sablierAddress, - string memory assetAddress + string memory assetAddress, + string memory streamId, + bool isTransferable ) external pure returns (string memory) { - return generateDescription(streamingModel, assetSymbol, streamId, sablierAddress, assetAddress); + return generateDescription(sablierModel, assetSymbol, sablierAddress, assetAddress, streamId, isTransferable); } - function generateName_( - string memory streamingModel, - string memory streamId - ) - external - pure - returns (string memory) - { - return generateName(streamingModel, streamId); + function generateName_(string memory sablierModel, string memory streamId) external pure returns (string memory) { + return generateName(sablierModel, streamId); } function generateSVG_(NFTSVG.SVGParams memory params) external pure returns (string memory) { @@ -84,6 +78,10 @@ contract NFTDescriptorMock is SablierV2NFTDescriptor { return SVGElements.hourglass(status); } + function isAllowedCharacter_(string memory symbol) external pure returns (bool) { + return isAllowedCharacter(symbol); + } + function mapSymbol_(IERC721Metadata nft) external view returns (string memory) { return mapSymbol(nft); } diff --git a/test/mocks/Noop.sol b/test/mocks/Noop.sol index eb9ce1363..8f441ef3e 100644 --- a/test/mocks/Noop.sol +++ b/test/mocks/Noop.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; /// @dev This contract does nothing (no-op = no operation). contract Noop { } diff --git a/test/mocks/erc20/ERC20Bytes32.sol b/test/mocks/erc20/ERC20Bytes32.sol index fc2480d33..50b065903 100644 --- a/test/mocks/erc20/ERC20Bytes32.sol +++ b/test/mocks/erc20/ERC20Bytes32.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; contract ERC20Bytes32 { function symbol() external pure returns (bytes32) { diff --git a/test/mocks/erc20/ERC20MissingReturn.sol b/test/mocks/erc20/ERC20MissingReturn.sol index 3e4d47143..0f9d548e7 100644 --- a/test/mocks/erc20/ERC20MissingReturn.sol +++ b/test/mocks/erc20/ERC20MissingReturn.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; /// @notice An implementation of ERC-20 that does not return a boolean in {transfer} and {transferFrom}. /// @dev See https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca/. diff --git a/test/mocks/erc20/ERC20Mock.sol b/test/mocks/erc20/ERC20Mock.sol new file mode 100644 index 000000000..8cf2389dd --- /dev/null +++ b/test/mocks/erc20/ERC20Mock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) { } +} diff --git a/test/mocks/flash-loan/FaultyFlashLoanReceiver.sol b/test/mocks/flash-loan/FaultyFlashLoanReceiver.sol deleted file mode 100644 index 98c18a999..000000000 --- a/test/mocks/flash-loan/FaultyFlashLoanReceiver.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { IERC3156FlashBorrower } from "../../../src/interfaces/erc3156/IERC3156FlashBorrower.sol"; - -contract FaultyFlashLoanReceiver is IERC3156FlashBorrower { - bytes32 internal constant FAULTY_RESPONSE = keccak256("This is a faulty response"); - - function onFlashLoan( - address initiator, - address asset, - uint256 amount, - uint256 fee, - bytes calldata data - ) - external - returns (bytes32 response) - { - initiator; - data; - IERC20(asset).approve({ spender: msg.sender, amount: amount + fee }); - response = FAULTY_RESPONSE; - } -} diff --git a/test/mocks/flash-loan/FlashLoanMock.sol b/test/mocks/flash-loan/FlashLoanMock.sol deleted file mode 100644 index 232c06cb1..000000000 --- a/test/mocks/flash-loan/FlashLoanMock.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { SablierV2Base } from "../../../src/abstracts/SablierV2Base.sol"; -import { ISablierV2Comptroller } from "../../../src/interfaces/ISablierV2Comptroller.sol"; -import { SablierV2FlashLoan } from "../../../src/abstracts/SablierV2FlashLoan.sol"; - -/// @dev Needed to test the {SablierV2FlashLoan} abstract. -contract FlashLoanMock is SablierV2FlashLoan { - constructor( - address initialAdmin, - ISablierV2Comptroller initialComptroller - ) - SablierV2Base(initialAdmin, initialComptroller) - { } -} diff --git a/test/mocks/flash-loan/GoodFlashLoanReceiver.sol b/test/mocks/flash-loan/GoodFlashLoanReceiver.sol deleted file mode 100644 index 85c9794dc..000000000 --- a/test/mocks/flash-loan/GoodFlashLoanReceiver.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { IERC3156FlashBorrower } from "../../../src/interfaces/erc3156/IERC3156FlashBorrower.sol"; - -import { Constants } from "../../utils/Constants.sol"; - -contract GoodFlashLoanReceiver is Constants, IERC3156FlashBorrower { - function onFlashLoan( - address initiator, - address asset, - uint256 amount, - uint256 fee, - bytes calldata data - ) - external - returns (bytes32 response) - { - initiator; - data; - IERC20(asset).approve({ spender: msg.sender, amount: amount + fee }); - response = FLASH_LOAN_CALLBACK_SUCCESS; - } -} diff --git a/test/mocks/flash-loan/ReentrantFlashLoanReceiver.sol b/test/mocks/flash-loan/ReentrantFlashLoanReceiver.sol deleted file mode 100644 index e0fbc12a8..000000000 --- a/test/mocks/flash-loan/ReentrantFlashLoanReceiver.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { IERC3156FlashBorrower } from "../../../src/interfaces/erc3156/IERC3156FlashBorrower.sol"; -import { IERC3156FlashLender } from "../../../src/interfaces/erc3156/IERC3156FlashLender.sol"; - -import { Constants } from "../../utils/Constants.sol"; - -contract ReentrantFlashLoanReceiver is Constants, IERC3156FlashBorrower { - function onFlashLoan( - address initiator, - address asset, - uint256 amount, - uint256 fee, - bytes calldata data - ) - external - returns (bytes32 response) - { - initiator; - IERC20(asset).approve({ spender: msg.sender, amount: amount + fee }); - IERC3156FlashLender(msg.sender).flashLoan({ receiver: this, asset: asset, amount: amount, data: data }); - response = FLASH_LOAN_CALLBACK_SUCCESS; - } -} diff --git a/test/mocks/hooks/GoodRecipient.sol b/test/mocks/hooks/GoodRecipient.sol deleted file mode 100644 index e4f8aecf3..000000000 --- a/test/mocks/hooks/GoodRecipient.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { ISablierV2LockupRecipient } from "../../../src/interfaces/hooks/ISablierV2LockupRecipient.sol"; - -contract GoodRecipient is ISablierV2LockupRecipient { - function onStreamCanceled( - uint256 streamId, - address sender, - uint128 senderAmount, - uint128 recipientAmount - ) - external - pure - { - streamId; - sender; - senderAmount; - recipientAmount; - } - - function onStreamRenounced(uint256 streamId) external pure { - streamId; - } - - function onStreamWithdrawn(uint256 streamId, address caller, address to, uint128 amount) external pure { - streamId; - caller; - to; - amount; - } -} diff --git a/test/mocks/hooks/ReentrantRecipient.sol b/test/mocks/hooks/ReentrantRecipient.sol deleted file mode 100644 index dfe7bcfb3..000000000 --- a/test/mocks/hooks/ReentrantRecipient.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { ISablierV2Lockup } from "../../../src/interfaces/ISablierV2Lockup.sol"; -import { ISablierV2LockupRecipient } from "../../../src/interfaces/hooks/ISablierV2LockupRecipient.sol"; - -contract ReentrantRecipient is ISablierV2LockupRecipient { - function onStreamCanceled( - uint256 streamId, - address sender, - uint128 senderAmount, - uint128 recipientAmount - ) - external - { - streamId; - senderAmount; - sender; - recipientAmount; - ISablierV2Lockup(msg.sender).cancel(streamId); - } - - function onStreamRenounced(uint256 streamId) external { - ISablierV2Lockup(msg.sender).renounce(streamId); - } - - function onStreamWithdrawn(uint256 streamId, address caller, address to, uint128 amount) external { - streamId; - caller; - to; - amount; - ISablierV2Lockup(msg.sender).withdraw(streamId, address(this), amount); - } -} diff --git a/test/mocks/hooks/RevertingRecipient.sol b/test/mocks/hooks/RevertingRecipient.sol deleted file mode 100644 index 0593e08fb..000000000 --- a/test/mocks/hooks/RevertingRecipient.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; - -import { ISablierV2LockupRecipient } from "../../../src/interfaces/hooks/ISablierV2LockupRecipient.sol"; - -contract RevertingRecipient is ISablierV2LockupRecipient { - function onStreamCanceled( - uint256 streamId, - address sender, - uint128 senderAmount, - uint128 recipientAmount - ) - external - pure - { - streamId; - sender; - senderAmount; - recipientAmount; - revert("You shall not pass"); - } - - function onStreamRenounced(uint256 streamId) external pure { - streamId; - revert("You shall not pass"); - } - - function onStreamWithdrawn(uint256 streamId, address caller, address to, uint128 amount) external pure { - streamId; - caller; - to; - amount; - revert("You shall not pass"); - } -} diff --git a/test/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol b/test/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol index 16bb5e4de..253915466 100644 --- a/test/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol +++ b/test/unit/concrete/adminable/transfer-admin/transferAdmin.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -8,7 +8,7 @@ import { Adminable_Unit_Shared_Test } from "../../../shared/Adminable.t.sol"; contract TransferAdmin_Unit_Concrete_Test is Adminable_Unit_Shared_Test { function test_RevertWhen_CallerNotAdmin() external { // Make Eve the caller in this test. - changePrank(users.eve); + resetPrank(users.eve); // Run the test. vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); diff --git a/test/unit/concrete/comptroller/Comptroller.t.sol b/test/unit/concrete/comptroller/Comptroller.t.sol deleted file mode 100644 index 635946627..000000000 --- a/test/unit/concrete/comptroller/Comptroller.t.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { SablierV2Comptroller } from "../../../../src/SablierV2Comptroller.sol"; - -import { Base_Test } from "../../../Base.t.sol"; - -contract Comptroller_Unit_Concrete_Test is Base_Test { - function setUp() public virtual override { - Base_Test.setUp(); - deployConditionally(); - } - - /// @dev Conditionally deploys {SablierV2Comptroller} normally or from a source precompiled with `--via-ir`. - function deployConditionally() internal { - if (!isTestOptimizedProfile()) { - comptroller = new SablierV2Comptroller(users.admin); - } else { - comptroller = deployOptimizedComptroller(users.admin); - } - vm.label({ account: address(comptroller), newLabel: "SablierV2Comptroller" }); - } -} diff --git a/test/unit/concrete/comptroller/constructor.t.sol b/test/unit/concrete/comptroller/constructor.t.sol deleted file mode 100644 index 6c03fe5f5..000000000 --- a/test/unit/concrete/comptroller/constructor.t.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { SablierV2Comptroller } from "src/SablierV2Comptroller.sol"; - -import { Base_Test } from "../../../Base.t.sol"; - -contract Constructor_Comptroller_Unit_Concrete_Test is Base_Test { - function test_Constructor() external { - // Expect the relevant event to be emitted. - vm.expectEmit(); - emit TransferAdmin({ oldAdmin: address(0), newAdmin: users.admin }); - - // Construct the contract. - SablierV2Comptroller constructedComptroller = new SablierV2Comptroller({ initialAdmin: users.admin }); - - // Assert that the admin has been initialized. - address actualAdmin = constructedComptroller.admin(); - address expectedAdmin = users.admin; - assertEq(actualAdmin, expectedAdmin, "admin"); - } -} diff --git a/test/unit/concrete/comptroller/flash-fee/flashFee.t.sol b/test/unit/concrete/comptroller/flash-fee/flashFee.t.sol deleted file mode 100644 index 9fe6fb139..000000000 --- a/test/unit/concrete/comptroller/flash-fee/flashFee.t.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Comptroller_Unit_Concrete_Test } from "../Comptroller.t.sol"; - -contract FlashFee_Unit_Concrete_Test is Comptroller_Unit_Concrete_Test { - function setUp() public virtual override { - Comptroller_Unit_Concrete_Test.setUp(); - // Make the Admin the default caller in this test suite. - vm.startPrank({ msgSender: users.admin }); - } - - function test_FlashFee_Zero() external { - UD60x18 actualFlashFee = comptroller.flashFee(); - UD60x18 expectedFlashFee = ZERO; - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } - - function test_FlashFee() external { - comptroller.setFlashFee(defaults.FLASH_FEE()); - UD60x18 actualFlashFee = comptroller.flashFee(); - UD60x18 expectedFlashFee = defaults.FLASH_FEE(); - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } -} diff --git a/test/unit/concrete/comptroller/flash-fee/flashFee.tree b/test/unit/concrete/comptroller/flash-fee/flashFee.tree deleted file mode 100644 index 04e7932b7..000000000 --- a/test/unit/concrete/comptroller/flash-fee/flashFee.tree +++ /dev/null @@ -1,5 +0,0 @@ -flashFee.t.sol -├── given the flash fee has not been set -│ └── it should return zero -└── given the flash fee has been set - └── it should return the correct flash fee diff --git a/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.t.sol b/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.t.sol deleted file mode 100644 index 2efb39738..000000000 --- a/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.t.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; - -import { UD60x18, ZERO } from "@prb/math/src/UD60x18.sol"; - -import { Errors } from "src/libraries/Errors.sol"; - -import { Comptroller_Unit_Concrete_Test } from "../Comptroller.t.sol"; - -contract SetFlashFee_Unit_Concrete_Test is Comptroller_Unit_Concrete_Test { - function setUp() public virtual override { - Comptroller_Unit_Concrete_Test.setUp(); - // Make the Admin the default caller in this test suite. - vm.startPrank({ msgSender: users.admin }); - } - - function test_RevertWhen_CallerNotAdmin() external { - // Make Eve the caller in this test. - changePrank({ msgSender: users.eve }); - - // Run the test. - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - comptroller.setFlashFee({ newFlashFee: MAX_FEE }); - } - - /// @dev The admin is the default caller in the comptroller tests. - modifier whenCallerAdmin() { - _; - } - - function test_SetFlashFee_SameFee() external whenCallerAdmin { - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetFlashFee({ admin: users.admin, oldFlashFee: ZERO, newFlashFee: ZERO }); - comptroller.setFlashFee({ newFlashFee: ZERO }); - - // She the same flash fee. - comptroller.setFlashFee({ newFlashFee: ZERO }); - - // Assert that the flash fee has not changed. - UD60x18 actualFlashFee = comptroller.flashFee(); - UD60x18 expectedFlashFee = ZERO; - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } - - modifier whenNewFee() { - _; - } - - function test_SetFlashFee() external whenCallerAdmin whenNewFee { - UD60x18 newFlashFee = defaults.FLASH_FEE(); - - // Expect the relevant event to be emitted. - vm.expectEmit({ emitter: address(comptroller) }); - emit SetFlashFee({ admin: users.admin, oldFlashFee: ZERO, newFlashFee: newFlashFee }); - - // She the new flash fee. - comptroller.setFlashFee(newFlashFee); - - // Assert that the flash fee has been updated. - UD60x18 actualFlashFee = comptroller.flashFee(); - UD60x18 expectedFlashFee = newFlashFee; - assertEq(actualFlashFee, expectedFlashFee, "flashFee"); - } -} diff --git a/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.tree b/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.tree deleted file mode 100644 index 9f97bbefd..000000000 --- a/test/unit/concrete/comptroller/set-flash-fee/setFlashFee.tree +++ /dev/null @@ -1,10 +0,0 @@ -setFlashFee.t.sol -├── when the caller is not the admin -│ └── it should revert -└── when the caller is the admin - ├── when the new flash fee is the same as the current flash fee - │ ├── it should re-set the flash fee - │ └── it should emit a {SetFlashFee} event - └── when the new flash fee is not the same as the current flash fee - ├── it should set the new flash fee - └── it should emit a {SetFlashFee} event diff --git a/test/unit/concrete/nft-descriptor/NFTDescriptor.t.sol b/test/unit/concrete/nft-descriptor/NFTDescriptor.t.sol index 8fe60913d..f2c26b5ee 100644 --- a/test/unit/concrete/nft-descriptor/NFTDescriptor.t.sol +++ b/test/unit/concrete/nft-descriptor/NFTDescriptor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SablierV2NFTDescriptor } from "src/SablierV2NFTDescriptor.sol"; diff --git a/test/unit/concrete/nft-descriptor/abbreviateAmount.t.sol b/test/unit/concrete/nft-descriptor/abbreviateAmount.t.sol index 6a9d927ca..369c95d25 100644 --- a/test/unit/concrete/nft-descriptor/abbreviateAmount.t.sol +++ b/test/unit/concrete/nft-descriptor/abbreviateAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SVGElements } from "src/libraries/SVGElements.sol"; @@ -14,7 +14,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test return string.concat(SVGElements.SIGN_GE, " ", abbreviation); } - function test_AbbreviateAmount_Zero() external { + function test_AbbreviateAmount_Zero() external view { string memory expectedAbbreviation = "0"; assertEq(aa({ amount: 0, decimals: 0 }), expectedAbbreviation, "abbreviation"); assertEq(aa({ amount: 0, decimals: 1 }), expectedAbbreviation, "abbreviation"); @@ -22,7 +22,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 0, decimals: 18 }), expectedAbbreviation, "abbreviation"); } - function test_AbbreviateAmount_Tiny() external { + function test_AbbreviateAmount_Tiny() external view { string memory expectedAbbreviation = string.concat(SVGElements.SIGN_LT, " 1"); assertEq(aa({ amount: 5, decimals: 1 }), expectedAbbreviation, "abbreviation"); assertEq(aa({ amount: 9, decimals: 1 }), expectedAbbreviation, "abbreviation"); @@ -32,7 +32,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 1e18 - 1, decimals: 18 }), expectedAbbreviation, "abbreviation"); } - function test_AbbreviateAmount_Zillions() external { + function test_AbbreviateAmount_Zillions() external view { string memory expectedAbbreviation = string.concat(SVGElements.SIGN_GT, " 999.99T"); assertEq(aa({ amount: 1e15, decimals: 0 }), expectedAbbreviation, "abbreviation"); assertEq(aa({ amount: 1e16, decimals: 1 }), expectedAbbreviation, "abbreviation"); @@ -44,7 +44,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: MAX_UINT128, decimals: 18 }), expectedAbbreviation, "abbreviation"); } - function test_AbbreviateAmount_NoSuffix() external { + function test_AbbreviateAmount_NoSuffix() external view { assertEq(aa({ amount: 1, decimals: 0 }), ge("1"), "abbreviation"); assertEq(aa({ amount: 5, decimals: 0 }), ge("5"), "abbreviation"); assertEq(aa({ amount: 121, decimals: 1 }), ge("12"), "abbreviation"); @@ -53,7 +53,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 988e18, decimals: 18 }), ge("988"), "abbreviation"); } - function test_AbbreviateAmount_Thousands() external { + function test_AbbreviateAmount_Thousands() external view { assertEq(aa({ amount: 1337, decimals: 0 }), ge("1.33K"), "abbreviation"); assertEq(aa({ amount: 1080, decimals: 0 }), ge("1.08K"), "abbreviation"); assertEq(aa({ amount: 1800, decimals: 0 }), ge("1.80K"), "abbreviation"); @@ -64,7 +64,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 201_287e18, decimals: 18 }), ge("201.28K"), "abbreviation"); } - function test_AbbreviateAmount_Millions() external { + function test_AbbreviateAmount_Millions() external view { assertEq(aa({ amount: 1_337_081, decimals: 0 }), ge("1.33M"), "abbreviation"); assertEq(aa({ amount: 2_194_000, decimals: 0 }), ge("2.19M"), "abbreviation"); assertEq(aa({ amount: 30_448_842, decimals: 1 }), ge("3.04M"), "abbreviation"); @@ -74,7 +74,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 577_308_003e18, decimals: 18 }), ge("577.30M"), "abbreviation"); } - function test_AbbreviateAmount_Billions() external { + function test_AbbreviateAmount_Billions() external view { assertEq(aa({ amount: 1_337_081_132, decimals: 0 }), ge("1.33B"), "abbreviation"); assertEq(aa({ amount: 2_763_455_030, decimals: 0 }), ge("2.76B"), "abbreviation"); assertEq(aa({ amount: 30_008_011_215, decimals: 1 }), ge("3B"), "abbreviation"); @@ -84,7 +84,7 @@ contract AbbreviateAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test assertEq(aa({ amount: 699_881_672_021e18, decimals: 18 }), ge("699.88B"), "abbreviation"); } - function test_AbbreviateAmount_Trillions() external { + function test_AbbreviateAmount_Trillions() external view { assertEq(aa({ amount: 2_578_924_152_034, decimals: 0 }), ge("2.57T"), "abbreviation"); assertEq(aa({ amount: 3_931_548_209_201, decimals: 0 }), ge("3.93T"), "abbreviation"); assertEq(aa({ amount: 60_008_233_054_613, decimals: 1 }), ge("6T"), "abbreviation"); diff --git a/test/unit/concrete/nft-descriptor/calculateDurationInDays.t.sol b/test/unit/concrete/nft-descriptor/calculateDurationInDays.t.sol index 886735e00..62cae342b 100644 --- a/test/unit/concrete/nft-descriptor/calculateDurationInDays.t.sol +++ b/test/unit/concrete/nft-descriptor/calculateDurationInDays.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SVGElements } from "src/libraries/SVGElements.sol"; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function test_CalculateDurationInDays_Zero() external { + function test_CalculateDurationInDays_Zero() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime + 1 days - 1 seconds; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); @@ -14,7 +14,7 @@ contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concre assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); } - function test_CalculateDurationInDays_One() external { + function test_CalculateDurationInDays_One() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime + 1 days; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); @@ -22,7 +22,7 @@ contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concre assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); } - function test_CalculateDurationInDays_FortyTwo() external { + function test_CalculateDurationInDays_FortyTwo() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime + 42 days; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); @@ -30,7 +30,7 @@ contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concre assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); } - function test_CalculateDurationInDays_Leet() external { + function test_CalculateDurationInDays_Leet() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime + 1337 days; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); @@ -38,7 +38,7 @@ contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concre assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); } - function test_CalculateDurationInDays_TenThousand() external { + function test_CalculateDurationInDays_TenThousand() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime + 10_000 days; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); @@ -46,7 +46,7 @@ contract CalculateDurationInDays_Unit_Concrete_Test is NFTDescriptor_Unit_Concre assertEq(actualDurationInDays, expectedDurationInDays, "durationInDays"); } - function test_CalculateDurationInDays_Overflow() external { + function test_CalculateDurationInDays_Overflow() external view { uint256 startTime = block.timestamp; uint256 endTime = startTime - 1 seconds; string memory actualDurationInDays = nftDescriptorMock.calculateDurationInDays_(startTime, endTime); diff --git a/test/unit/concrete/nft-descriptor/calculatePixelWidth.t.sol b/test/unit/concrete/nft-descriptor/calculatePixelWidth.t.sol index a8c73c30b..a01fd49ea 100644 --- a/test/unit/concrete/nft-descriptor/calculatePixelWidth.t.sol +++ b/test/unit/concrete/nft-descriptor/calculatePixelWidth.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; @@ -19,13 +19,13 @@ contract CalculatePixelWidth_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T return bytes(text).length * CHAR_WIDTH_SMALL; } - function test_CalculatePixelWidth_EmptyString() external { + function test_CalculatePixelWidth_EmptyString() external view { uint256 actualWidth = cpw({ text: "", largeFont: false }); uint256 expectedWidth = 0; assertEq(actualWidth, expectedWidth, "width"); } - function test_CalculatePixelWidth_Caption() external { + function test_CalculatePixelWidth_Caption() external view { bool largeFont = false; assertEq(cpw("Progress", largeFont), small("Progress"), "pixel width"); assertEq(cpw("Status", largeFont), small("Status"), "pixel width"); @@ -33,7 +33,7 @@ contract CalculatePixelWidth_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T assertEq(cpw("Duration", largeFont), small("Duration"), "pixel width"); } - function test_CalculatePixelWidth_Progress() external { + function test_CalculatePixelWidth_Progress() external view { bool largeFont = true; assertEq(cpw("0%", largeFont), large("0%"), "pixel width"); assertEq(cpw("0.01%", largeFont), large("0.01%"), "pixel width"); @@ -45,7 +45,7 @@ contract CalculatePixelWidth_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T assertEq(cpw("100%", largeFont), large("100%"), "pixel width"); } - function test_CalculatePixelWidth_Status() external { + function test_CalculatePixelWidth_Status() external view { bool largeFont = true; assertEq(cpw("Depleted", largeFont), large("Depleted"), "pixel width"); assertEq(cpw("Canceled", largeFont), large("Canceled"), "pixel width"); @@ -54,7 +54,7 @@ contract CalculatePixelWidth_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T assertEq(cpw("Pending", largeFont), large("Pending"), "pixel width"); } - function test_CalculatePixelWidth_Streamed() external { + function test_CalculatePixelWidth_Streamed() external view { bool largeFont = true; assertEq(cpw("< 1", largeFont), large("< 1"), "pixel width"); assertEq(cpw("≥ 42.73K", largeFont), large(" 42.73K") + CHAR_WIDTH_LARGE, "pixel width"); @@ -64,7 +64,7 @@ contract CalculatePixelWidth_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T assertEq(cpw("≥ 999.99T", largeFont), large(" 999.99T") + CHAR_WIDTH_LARGE, "pixel width"); } - function test_CalculatePixelWidth_Duration() external { + function test_CalculatePixelWidth_Duration() external view { bool largeFont = true; assertEq(cpw("< 1 Day", largeFont), large("< 1 Day"), "pixel width"); assertEq(cpw("1 Day", largeFont), large("1 Day"), "pixel width"); diff --git a/test/unit/concrete/nft-descriptor/calculateStreamedPercentage.t.sol b/test/unit/concrete/nft-descriptor/calculateStreamedPercentage.t.sol index a8b642e71..1345780df 100644 --- a/test/unit/concrete/nft-descriptor/calculateStreamedPercentage.t.sol +++ b/test/unit/concrete/nft-descriptor/calculateStreamedPercentage.t.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract CalculateStreamedPercentage_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function test_CalculateStreamedPercentage_Zero() external { + function test_CalculateStreamedPercentage_Zero() external view { uint256 actualStreamedPercentage = nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 0, depositedAmount: 1337e18 }); uint256 expectedStreamedPercentage = 0; assertEq(actualStreamedPercentage, expectedStreamedPercentage, "streamedPercentage"); } - function test_CalculateStreamedPercentage_Streaming() external { + function test_CalculateStreamedPercentage_Streaming() external view { uint256 actualStreamedPercentage = nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 100e18, depositedAmount: 400e18 }); uint256 expectedStreamedPercentage = 2500; assertEq(actualStreamedPercentage, expectedStreamedPercentage, "streamedPercentage"); } - function test_CalculateStreamedPercentage_Settled() external { + function test_CalculateStreamedPercentage_Settled() external view { uint256 actualStreamedPercentage = nftDescriptorMock.calculateStreamedPercentage_({ streamedAmount: 1337e18, depositedAmount: 1337e18 }); uint256 expectedStreamedPercentage = 10_000; diff --git a/test/unit/concrete/nft-descriptor/generateAttributes.t.sol b/test/unit/concrete/nft-descriptor/generateAttributes.t.sol index 68b490107..1c5f9403d 100644 --- a/test/unit/concrete/nft-descriptor/generateAttributes.t.sol +++ b/test/unit/concrete/nft-descriptor/generateAttributes.t.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-line-length,quotes -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract GenerateAttributes_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function test_GenerateAttributes_Empty() external { + function test_GenerateAttributes_Empty() external view { string memory actualAttributes = nftDescriptorMock.generateAttributes_("", "", ""); string memory expectedAttributes = '[{"trait_type":"Asset","value":""},{"trait_type":"Sender","value":""},{"trait_type":"Status","value":""}]'; assertEq(actualAttributes, expectedAttributes, "metadata attributes"); } - function test_GenerateAttributes() external { + function test_GenerateAttributes() external view { string memory actualAttributes = nftDescriptorMock.generateAttributes_("DAI", "0x50725493D337CdC4e381f658e10d29d128BD6927", "Streaming"); string memory expectedAttributes = diff --git a/test/unit/concrete/nft-descriptor/generateDescription.t.sol b/test/unit/concrete/nft-descriptor/generateDescription.t.sol index aacded97a..cb5f6ecbf 100644 --- a/test/unit/concrete/nft-descriptor/generateDescription.t.sol +++ b/test/unit/concrete/nft-descriptor/generateDescription.t.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-line-length,quotes -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract GenerateDescription_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - string internal constant DISCLAIMER = + string internal constant INFO_NON_TRANSFERABLE = + unicode"❕INFO: This NFT is non-transferable. It cannot be sold or transferred to another account."; + string internal constant INFO_TRANSFERABLE = unicode"⚠️ WARNING: Transferring the NFT makes the new owner the recipient of the stream. The funds are not automatically withdrawn for the previous recipient."; - function test_GenerateDescription_Empty() external { - string memory actualDescription = nftDescriptorMock.generateDescription_("", "", "", "", ""); + function test_GenerateDescription_Empty() external view { + string memory actualDescription = nftDescriptorMock.generateDescription_("", "", "", "", "", true); string memory expectedDescription = string.concat( "This NFT represents a payment stream in a Sablier V2 ", " contract. The owner of this NFT can withdraw the streamed assets, which are denominated in ", @@ -20,18 +22,50 @@ contract GenerateDescription_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T "\\n- ", " Address: ", "\\n\\n", - DISCLAIMER + INFO_TRANSFERABLE ); assertEq(actualDescription, expectedDescription, "metadata description"); } - function test_GenerateDescription() external { + function test_GenerateDescription_NonTransferable() external view { string memory actualDescription = nftDescriptorMock.generateDescription_( "Lockup Linear", dai.symbol(), + "0x78B190C1E493752f85E02b00a0C98851A5638A30", + "0xFEbD67A34821d1607a57DD31aae5f246D7dE2ca2", "42", + false + ); + string memory expectedDescription = string.concat( + "This NFT represents a payment stream in a Sablier V2 ", + "Lockup Linear", + " contract. The owner of this NFT can withdraw the streamed assets, which are denominated in ", + dai.symbol(), + ".\\n\\n", + "- Stream ID: ", + "42", + "\\n- ", + "Lockup Linear", + " Address: ", "0x78B190C1E493752f85E02b00a0C98851A5638A30", - "0xFEbD67A34821d1607a57DD31aae5f246D7dE2ca2" + "\\n- ", + "DAI", + " Address: ", + "0xFEbD67A34821d1607a57DD31aae5f246D7dE2ca2", + "\\n\\n", + INFO_NON_TRANSFERABLE + ); + assertEq(actualDescription, expectedDescription, "metadata description"); + } + + function test_GenerateDescription() external view { + string memory actualDescription = nftDescriptorMock.generateDescription_( + "Lockup Linear", + dai.symbol(), + "0x78B190C1E493752f85E02b00a0C98851A5638A30", + "0xFEbD67A34821d1607a57DD31aae5f246D7dE2ca2", + "42", + true ); string memory expectedDescription = string.concat( "This NFT represents a payment stream in a Sablier V2 ", @@ -50,7 +84,7 @@ contract GenerateDescription_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T " Address: ", "0xFEbD67A34821d1607a57DD31aae5f246D7dE2ca2", "\\n\\n", - DISCLAIMER + INFO_TRANSFERABLE ); assertEq(actualDescription, expectedDescription, "metadata description"); } diff --git a/test/unit/concrete/nft-descriptor/generateName.t.sol b/test/unit/concrete/nft-descriptor/generateName.t.sol index 4f1d98231..3efedc0e5 100644 --- a/test/unit/concrete/nft-descriptor/generateName.t.sol +++ b/test/unit/concrete/nft-descriptor/generateName.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract GenerateName_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function gn(string memory streamingModel, string memory streamId) internal view returns (string memory) { - return nftDescriptorMock.generateName_(streamingModel, streamId); + function gn(string memory sablierModel, string memory streamId) internal view returns (string memory) { + return nftDescriptorMock.generateName_(sablierModel, streamId); } function dyn(string memory streamId) internal pure returns (string memory) { @@ -16,13 +16,13 @@ contract GenerateName_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { return string.concat("Sablier V2 Lockup Linear #", streamId); } - function test_GenerateName_Empty() external { + function test_GenerateName_Empty() external view { assertEq(gn("", ""), "Sablier V2 #", "metadata name"); assertEq(gn("A", ""), "Sablier V2 A #", "metadata name"); assertEq(gn("", "1"), "Sablier V2 #1", "metadata name"); } - function test_GenerateName() external { + function test_GenerateName() external view { assertEq(gn("Lockup Dynamic", "1"), dyn("1"), "metadata name"); assertEq(gn("Lockup Dynamic", "42"), dyn("42"), "metadata name"); assertEq(gn("Lockup Dynamic", "1337"), dyn("1337"), "metadata name"); diff --git a/test/unit/concrete/nft-descriptor/generateSVG.t.sol b/test/unit/concrete/nft-descriptor/generateSVG.t.sol index 8230c5970..290a4c802 100644 --- a/test/unit/concrete/nft-descriptor/generateSVG.t.sol +++ b/test/unit/concrete/nft-descriptor/generateSVG.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // solhint-disable max-line-length -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTSVG } from "src/libraries/NFTSVG.sol"; import { SVGElements } from "src/libraries/SVGElements.sol"; @@ -11,7 +11,7 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { /// @dev If you need to update the hard-coded token URI: /// 1. Use "vm.writeFile" to log the strings to a file. /// 2. Remember to escape 'Courier New' with \'Courier New\'. - function test_GenerateSVG_Pending() external { + function test_GenerateSVG_Pending() external view { string memory actualSVG = nftDescriptorMock.generateSVG_( NFTSVG.SVGParams({ accentColor: "hsl(155,18%,30%)", @@ -22,8 +22,8 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { progress: "0%", progressNumerical: 0, sablierAddress: "0xf3a045dc986015be9ae43bb3462ae5981b0816e0", - status: "Pending", - streamingModel: "Lockup Linear" + sablierModel: "Lockup Linear", + status: "Pending" }) ); string memory expectedSVG = @@ -31,7 +31,7 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { assertEq(actualSVG, expectedSVG, "SVG mismatch"); } - function test_GenerateSVG_Streaming() external { + function test_GenerateSVG_Streaming() external view { string memory actualSVG = nftDescriptorMock.generateSVG_( NFTSVG.SVGParams({ accentColor: "hsl(114,3%,53%)", @@ -42,8 +42,8 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { progress: "42.35%", progressNumerical: 4235, sablierAddress: "0xf3a045dc986015be9ae43bb3462ae5981b0816e0", - status: "Streaming", - streamingModel: "Lockup Linear" + sablierModel: "Lockup Linear", + status: "Streaming" }) ); string memory expectedSVG = @@ -51,7 +51,7 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { assertEq(actualSVG, expectedSVG, "SVG mismatch"); } - function test_GenerateSVG_Depleted() external { + function test_GenerateSVG_Depleted() external view { string memory actualSVG = nftDescriptorMock.generateSVG_( NFTSVG.SVGParams({ accentColor: "hsl(123,25%,44%)", @@ -62,8 +62,8 @@ contract GenerateSVG_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { progress: "100%", progressNumerical: 100, sablierAddress: "0xf3a045dc986015be9ae43bb3462ae5981b0816e0", - status: "Depleted", - streamingModel: "Lockup Linear" + sablierModel: "Lockup Linear", + status: "Depleted" }) ); string memory expectedSVG = diff --git a/test/unit/concrete/nft-descriptor/hourglass.t.sol b/test/unit/concrete/nft-descriptor/hourglass.t.sol index 4c67580de..9841de5a4 100644 --- a/test/unit/concrete/nft-descriptor/hourglass.t.sol +++ b/test/unit/concrete/nft-descriptor/hourglass.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LibString } from "solady/src/utils/LibString.sol"; @@ -10,31 +10,31 @@ import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract Hourglass_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { using LibString for string; - function test_Hourglass_Pending() external { + function test_Hourglass_Pending() external view { string memory hourglass = nftDescriptorMock.hourglass_("pending"); uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); } - function test_Hourglass_Streaming() external { + function test_Hourglass_Streaming() external view { string memory hourglass = nftDescriptorMock.hourglass_("Streaming"); uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); } - function test_Hourglass_Settled() external { + function test_Hourglass_Settled() external view { string memory hourglass = nftDescriptorMock.hourglass_("Settled"); uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); assertEq(index, LibString.NOT_FOUND, "hourglass upper bulb should NOT be present"); } - function test_Hourglass_Canceled() external { + function test_Hourglass_Canceled() external view { string memory hourglass = nftDescriptorMock.hourglass_("Canceled"); uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); assertNotEq(index, LibString.NOT_FOUND, "hourglass upper bulb should be present"); } - function test_Hourglass_Depleted() external { + function test_Hourglass_Depleted() external view { string memory hourglass = nftDescriptorMock.hourglass_("Depleted"); uint256 index = hourglass.indexOf(SVGElements.HOURGLASS_UPPER_BULB); assertEq(index, LibString.NOT_FOUND, "hourglass upper bulb should NOT be present"); diff --git a/test/unit/concrete/nft-descriptor/stringifyCardType.t.sol b/test/unit/concrete/nft-descriptor/stringifyCardType.t.sol index 9f5797882..109d87399 100644 --- a/test/unit/concrete/nft-descriptor/stringifyCardType.t.sol +++ b/test/unit/concrete/nft-descriptor/stringifyCardType.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { SVGElements } from "src/libraries/SVGElements.sol"; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract StringifyCardType_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function test_StringifyCardType() external { + function test_StringifyCardType() external view { assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.PROGRESS), "Progress"); assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.STATUS), "Status"); assertEq(nftDescriptorMock.stringifyCardType_(SVGElements.CardType.AMOUNT), "Amount"); diff --git a/test/unit/concrete/nft-descriptor/stringifyFractionalAmount.t.sol b/test/unit/concrete/nft-descriptor/stringifyFractionalAmount.t.sol index ee248d1cd..4e5862a32 100644 --- a/test/unit/concrete/nft-descriptor/stringifyFractionalAmount.t.sol +++ b/test/unit/concrete/nft-descriptor/stringifyFractionalAmount.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; @@ -8,17 +8,17 @@ contract StringifyFractionalAmount_Unit_Concrete_Test is NFTDescriptor_Unit_Conc return nftDescriptorMock.stringifyFractionalAmount_(amount); } - function test_FractionalAmount_Zero() external { + function test_FractionalAmount_Zero() external view { assertEq(sfa(0), "", "fractional part mismatch"); } - function test_FractionalAmount_LeadingZero() external { + function test_FractionalAmount_LeadingZero() external view { assertEq(sfa(1), ".01", "fractional part mismatch"); assertEq(sfa(5), ".05", "fractional part mismatch"); assertEq(sfa(9), ".09", "fractional part mismatch"); } - function test_FractionalAmount_NoLeadingZero() external { + function test_FractionalAmount_NoLeadingZero() external view { assertEq(sfa(10), ".10", "fractional part mismatch"); assertEq(sfa(12), ".12", "fractional part mismatch"); assertEq(sfa(33), ".33", "fractional part mismatch"); diff --git a/test/unit/concrete/nft-descriptor/stringifyPercentage.t.sol b/test/unit/concrete/nft-descriptor/stringifyPercentage.t.sol index f60682af6..d0d72913d 100644 --- a/test/unit/concrete/nft-descriptor/stringifyPercentage.t.sol +++ b/test/unit/concrete/nft-descriptor/stringifyPercentage.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; @@ -8,7 +8,7 @@ contract StringifyPercentage_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T return nftDescriptorMock.stringifyPercentage_(percentage); } - function test_StringifyPercentage_NoFractionalPart() external { + function test_StringifyPercentage_NoFractionalPart() external view { assertEq(sp(0), "0%", "percentage mismatch"); assertEq(sp(100), "1%", "percentage mismatch"); assertEq(sp(300), "3%", "percentage mismatch"); @@ -17,7 +17,7 @@ contract StringifyPercentage_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_T assertEq(sp(10_000), "100%", "percentage mismatch"); } - function test_StringifyPercentage_FractionalPart() external { + function test_StringifyPercentage_FractionalPart() external view { assertEq(sp(1), "0.01%", "percentage mismatch"); assertEq(sp(42), "0.42%", "percentage mismatch"); assertEq(sp(314), "3.14%", "percentage mismatch"); diff --git a/test/unit/concrete/nft-descriptor/stringifyStatus.t.sol b/test/unit/concrete/nft-descriptor/stringifyStatus.t.sol index e7404f7d2..a18b115f7 100644 --- a/test/unit/concrete/nft-descriptor/stringifyStatus.t.sol +++ b/test/unit/concrete/nft-descriptor/stringifyStatus.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Lockup } from "src/types/DataTypes.sol"; import { NFTDescriptor_Unit_Concrete_Test } from "./NFTDescriptor.t.sol"; contract StringifyStatus_Unit_Concrete_Test is NFTDescriptor_Unit_Concrete_Test { - function test_StringifyStatus() external { + function test_StringifyStatus() external view { assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.DEPLETED), "Depleted", "depleted status mismatch"); assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.CANCELED), "Canceled", "canceled status mismatch"); assertEq(nftDescriptorMock.stringifyStatus_(Lockup.Status.STREAMING), "Streaming", "streaming status mismatch"); diff --git a/test/unit/fuzz/transferAdmin.t.sol b/test/unit/fuzz/transferAdmin.t.sol index b762991fc..dd4412cbc 100644 --- a/test/unit/fuzz/transferAdmin.t.sol +++ b/test/unit/fuzz/transferAdmin.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Errors } from "src/libraries/Errors.sol"; @@ -8,10 +8,10 @@ import { Adminable_Unit_Shared_Test } from "../shared/Adminable.t.sol"; contract TransferAdmin_Unit_Fuzz_Test is Adminable_Unit_Shared_Test { function testFuzz_RevertWhen_CallerNotAdmin(address eve) external { vm.assume(eve != address(0) && eve != users.admin); - assumeNoPrecompiles(eve); + assumeNotPrecompile(eve); // Make Eve the caller in this test. - changePrank(eve); + resetPrank(eve); // Run the test. vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, eve)); diff --git a/test/unit/shared/Adminable.t.sol b/test/unit/shared/Adminable.t.sol index a2e3b6ae9..9a0ffe2b8 100644 --- a/test/unit/shared/Adminable.t.sol +++ b/test/unit/shared/Adminable.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { Base_Test } from "../../Base.t.sol"; import { AdminableMock } from "../../mocks/AdminableMock.sol"; @@ -10,7 +10,7 @@ abstract contract Adminable_Unit_Shared_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); deployConditionally(); - vm.startPrank({ msgSender: users.admin }); + resetPrank({ msgSender: users.admin }); } /// @dev Conditionally deploys {AdminableMock} normally or from a source precompiled with `--via-ir`. diff --git a/test/utils/.npmignore b/test/utils/.npmignore new file mode 100644 index 000000000..c607d373a --- /dev/null +++ b/test/utils/.npmignore @@ -0,0 +1 @@ +*.t.sol diff --git a/test/utils/Assertions.sol b/test/utils/Assertions.sol index f4f86cd5c..492f2f678 100644 --- a/test/utils/Assertions.sol +++ b/test/utils/Assertions.sol @@ -1,22 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +// solhint-disable event-name-camelcase +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { PRBMathAssertions } from "@prb/math/test/utils/Assertions.sol"; -import { PRBTest } from "@prb/test/src/PRBTest.sol"; -import { Lockup, LockupDynamic, LockupLinear } from "../../src/types/DataTypes.sol"; +import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; -abstract contract Assertions is PRBTest, PRBMathAssertions { +abstract contract Assertions is PRBMathAssertions { /*////////////////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////////////////*/ - event LogNamedArray(string key, LockupDynamic.Segment[] segments); + event log_named_array(string key, LockupDynamic.Segment[] segments); - event LogNamedUint128(string key, uint128 value); + event log_named_array(string key, LockupTranched.Tranche[] tranches); - event LogNamedUint40(string key, uint40 value); + event log_named_uint128(string key, uint128 value); + + event log_named_uint40(string key, uint40 value); /*////////////////////////////////////////////////////////////////////////// ASSERTIONS @@ -30,91 +32,138 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { } /// @dev Compares two {IERC20} values. - function assertEq(IERC20 a, IERC20 b) internal { + function assertEq(IERC20 a, IERC20 b) internal pure { assertEq(address(a), address(b)); } /// @dev Compares two {IERC20} values. - function assertEq(IERC20 a, IERC20 b, string memory err) internal { + function assertEq(IERC20 a, IERC20 b, string memory err) internal pure { assertEq(address(a), address(b), err); } + /// @dev Compares two {LockupDynamic.Stream} struct entities. + function assertEq(LockupDynamic.StreamLD memory a, LockupDynamic.StreamLD memory b) internal { + assertEq(a.asset, b.asset, "asset"); + assertEq(a.endTime, b.endTime, "endTime"); + assertEq(a.isCancelable, b.isCancelable, "isCancelable"); + assertEq(a.isDepleted, b.isDepleted, "isDepleted"); + assertEq(a.isStream, b.isStream, "isStream"); + assertEq(a.isTransferable, b.isTransferable, "isTransferable"); + assertEq(a.recipient, b.recipient, "recipient"); + assertEq(a.segments, b.segments, "segments"); + assertEq(a.sender, b.sender, "sender"); + assertEq(a.startTime, b.startTime, "startTime"); + assertEq(a.wasCanceled, b.wasCanceled, "wasCanceled"); + } + /// @dev Compares two {LockupLinear.Stream} struct entities. - function assertEq(LockupLinear.Stream memory a, LockupLinear.Stream memory b) internal { + function assertEq(LockupLinear.StreamLL memory a, LockupLinear.StreamLL memory b) internal { assertEq(a.amounts, b.amounts); assertEq(a.asset, b.asset, "asset"); assertEq(a.cliffTime, b.cliffTime, "cliffTime"); assertEq(a.endTime, b.endTime, "endTime"); assertEq(a.isCancelable, b.isCancelable, "isCancelable"); assertEq(a.isDepleted, b.isDepleted, "isDepleted"); - assertEq(a.isTransferable, b.isTransferable, "isTransferable"); assertEq(a.isStream, b.isStream, "isStream"); + assertEq(a.isTransferable, b.isTransferable, "isTransferable"); + assertEq(a.recipient, b.recipient, "recipient"); assertEq(a.sender, b.sender, "sender"); assertEq(a.startTime, b.startTime, "startTime"); assertEq(a.wasCanceled, b.wasCanceled, "wasCanceled"); } - /// @dev Compares two {LockupDynamic.Stream} struct entities. - function assertEq(LockupDynamic.Stream memory a, LockupDynamic.Stream memory b) internal { + /// @dev Compares two {LockupTranched.Stream} struct entities. + function assertEq(LockupTranched.StreamLT memory a, LockupTranched.StreamLT memory b) internal { assertEq(a.asset, b.asset, "asset"); assertEq(a.endTime, b.endTime, "endTime"); assertEq(a.isCancelable, b.isCancelable, "isCancelable"); assertEq(a.isDepleted, b.isDepleted, "isDepleted"); - assertEq(a.isTransferable, b.isTransferable, "isTransferable"); assertEq(a.isStream, b.isStream, "isStream"); - assertEq(a.segments, b.segments, "segments"); + assertEq(a.isTransferable, b.isTransferable, "isTransferable"); + assertEq(a.recipient, b.recipient, "recipient"); assertEq(a.sender, b.sender, "sender"); assertEq(a.startTime, b.startTime, "startTime"); + assertEq(a.tranches, b.tranches, "tranches"); assertEq(a.wasCanceled, b.wasCanceled, "wasCanceled"); } - /// @dev Compares two {LockupLinear.Range} struct entities. - function assertEq(LockupLinear.Range memory a, LockupLinear.Range memory b) internal { - assertEqUint40(a.cliff, b.cliff, "range.cliff"); - assertEqUint40(a.end, b.end, "range.end"); - assertEqUint40(a.start, b.start, "range.start"); + /// @dev Compares two {LockupDynamic.Timestamps} struct entities. + function assertEq(LockupDynamic.Timestamps memory a, LockupDynamic.Timestamps memory b) internal { + assertEqUint40(a.end, b.end, "timestamps.end"); + assertEqUint40(a.start, b.start, "timestamps.start"); } - /// @dev Compares two {LockupDynamic.Range} struct entities. - function assertEq(LockupDynamic.Range memory a, LockupDynamic.Range memory b) internal { - assertEqUint40(a.end, b.end, "range.end"); - assertEqUint40(a.start, b.start, "range.start"); + /// @dev Compares two {LockupLinear.Timestamps} struct entities. + function assertEq(LockupLinear.Timestamps memory a, LockupLinear.Timestamps memory b) internal { + assertEqUint40(a.cliff, b.cliff, "timestamps.cliff"); + assertEqUint40(a.end, b.end, "timestamps.end"); + assertEqUint40(a.start, b.start, "timestamps.start"); } - /// @dev Compares two {LockupDynamic.Segment[]} arrays. + /// @dev Compares two {LockupTranched.Timestamps} struct entities. + function assertEq(LockupTranched.Timestamps memory a, LockupTranched.Timestamps memory b) internal { + assertEqUint40(a.end, b.end, "timestamps.end"); + assertEqUint40(a.start, b.start, "timestamps.start"); + } + + /// @dev Compares two {LockupDynamic.Segment} arrays. function assertEq(LockupDynamic.Segment[] memory a, LockupDynamic.Segment[] memory b) internal { if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit Log("Error: a == b not satisfied [LockupDynamic.Segment[]]"); - emit LogNamedArray(" Left", b); - emit LogNamedArray(" Right", a); + emit log("Error: a == b not satisfied [LockupDynamic.Segment[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); fail(); } } - /// @dev Compares two `LockupDynamic.Segment[]` arrays. + /// @dev Compares two {LockupDynamic.Segment} arrays. function assertEq(LockupDynamic.Segment[] memory a, LockupDynamic.Segment[] memory b, string memory err) internal { if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { - emit LogNamedString("Error", err); + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + /// @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))) { + emit log("Error: a == b not satisfied [LockupTranched.Tranche[]]"); + emit log_named_array(" Left", a); + emit log_named_array(" Right", b); + fail(); + } + } + + /// @dev Compares two {LockupTranched.Tranche} arrays. + function assertEq( + LockupTranched.Tranche[] memory a, + LockupTranched.Tranche[] memory b, + string memory err + ) + internal + { + if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { + emit log_named_string("Error", err); assertEq(a, b); } } /// @dev Compares two {Lockup.Status} enum values. - function assertEq(Lockup.Status a, Lockup.Status b) internal { + function assertEq(Lockup.Status a, Lockup.Status b) internal pure { assertEq(uint256(a), uint256(b), "status"); } /// @dev Compares two {Lockup.Status} enum values. - function assertEq(Lockup.Status a, Lockup.Status b, string memory err) internal { + function assertEq(Lockup.Status a, Lockup.Status b, string memory err) internal pure { assertEq(uint256(a), uint256(b), err); } /// @dev Compares two `uint128` numbers. function assertEqUint128(uint128 a, uint128 b) internal { if (a != b) { - emit Log("Error: a == b not satisfied [uint128]"); - emit LogNamedUint128(" Left", b); - emit LogNamedUint128(" Right", a); + emit log("Error: a == b not satisfied [uint128]"); + emit log_named_uint128(" Left", a); + emit log_named_uint128(" Right", b); fail(); } } @@ -122,7 +171,7 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { /// @dev Compares two `uint128` numbers. function assertEqUint128(uint128 a, uint128 b, string memory err) internal { if (a != b) { - emit LogNamedString("Error", err); + emit log_named_string("Error", err); assertEqUint128(a, b); } } @@ -130,9 +179,9 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { /// @dev Compares two `uint40` numbers. function assertEqUint40(uint40 a, uint40 b) internal { if (a != b) { - emit Log("Error: a == b not satisfied [uint40]"); - emit LogNamedUint40(" Left", b); - emit LogNamedUint40(" Right", a); + emit log("Error: a == b not satisfied [uint40]"); + emit log_named_uint40(" Left", a); + emit log_named_uint40(" Right", b); fail(); } } @@ -140,18 +189,18 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { /// @dev Compares two `uint40` numbers. function assertEqUint40(uint40 a, uint40 b, string memory err) internal { if (a != b) { - emit LogNamedString("Error", err); + emit log_named_string("Error", err); assertEqUint40(a, b); } } /// @dev Compares two {Lockup.Status} enum values. - function assertNotEq(Lockup.Status a, Lockup.Status b) internal { + function assertNotEq(Lockup.Status a, Lockup.Status b) internal pure { assertNotEq(uint256(a), uint256(b), "status"); } /// @dev Compares two {Lockup.Status} enum values. - function assertNotEq(Lockup.Status a, Lockup.Status b, string memory err) internal { + function assertNotEq(Lockup.Status a, Lockup.Status b, string memory err) internal pure { assertNotEq(uint256(a), uint256(b), err); } } diff --git a/test/utils/BaseScript.t.sol b/test/utils/BaseScript.t.sol new file mode 100644 index 000000000..764cb1eef --- /dev/null +++ b/test/utils/BaseScript.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { StdAssertions } from "forge-std/src/StdAssertions.sol"; + +import { BaseScript } from "script/Base.s.sol"; + +contract BaseScript_Test is StdAssertions { + using Strings for uint256; + + BaseScript internal baseScript; + + function setUp() public { + baseScript = new BaseScript(); + } + + function test_ConstructCreate2Salt() public view { + string memory chainId = block.chainid.toString(); + string memory version = "1.2.0"; + string memory salt = string.concat("ChainID ", chainId, ", Version ", version); + + bytes32 actualSalt = baseScript.constructCreate2Salt(); + bytes32 expectedSalt = bytes32(abi.encodePacked(salt)); + assertEq(actualSalt, expectedSalt, "CREATE2 salt mismatch"); + } +} diff --git a/test/utils/Calculations.sol b/test/utils/Calculations.sol index 1a867d7bb..1290e64bf 100644 --- a/test/utils/Calculations.sol +++ b/test/utils/Calculations.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/Uint128.sol"; import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uint40.sol"; import { SD59x18 } from "@prb/math/src/SD59x18.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; -import { LockupDynamic } from "../../src/types/DataTypes.sol"; +import { LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; import { Defaults } from "./Defaults.sol"; @@ -16,38 +16,28 @@ abstract contract Calculations { Defaults private defaults = new Defaults(); - /// @dev Calculates the deposit amount by calculating and subtracting the protocol fee amount and the - /// broker fee amount from the total amount. - function calculateDepositAmount( - uint128 totalAmount, - UD60x18 protocolFee, - UD60x18 brokerFee - ) - internal - pure - returns (uint128) - { - uint128 protocolFeeAmount = ud(totalAmount).mul(protocolFee).intoUint128(); + /// @dev Calculates the deposit amount by calculating and subtracting the broker fee amount from the total amount. + function calculateDepositAmount(uint128 totalAmount, UD60x18 brokerFee) internal pure returns (uint128) { uint128 brokerFeeAmount = ud(totalAmount).mul(brokerFee).intoUint128(); - return totalAmount - protocolFeeAmount - brokerFeeAmount; + return totalAmount - brokerFeeAmount; } /// @dev Helper function that replicates the logic of {SablierV2LockupLinear.streamedAmountOf}. - function calculateStreamedAmount(uint40 currentTime, uint128 depositAmount) internal view returns (uint128) { - if (currentTime > defaults.END_TIME()) { + function calculateStreamedAmount(uint40 blockTimestamp, uint128 depositAmount) internal view returns (uint128) { + if (blockTimestamp > defaults.END_TIME()) { return depositAmount; } unchecked { - UD60x18 elapsedTime = ud(currentTime - defaults.START_TIME()); - UD60x18 totalTime = ud(defaults.TOTAL_DURATION()); - UD60x18 elapsedTimePercentage = elapsedTime.div(totalTime); + UD60x18 elapsedTime = ud(blockTimestamp - defaults.START_TIME()); + UD60x18 totalDuration = ud(defaults.TOTAL_DURATION()); + UD60x18 elapsedTimePercentage = elapsedTime.div(totalDuration); return elapsedTimePercentage.mul(ud(depositAmount)).intoUint128(); } } /// @dev Replicates the logic of {SablierV2LockupDynamic._calculateStreamedAmountForMultipleSegments}. function calculateStreamedAmountForMultipleSegments( - uint40 currentTime, + uint40 blockTimestamp, LockupDynamic.Segment[] memory segments, uint128 depositAmount ) @@ -55,36 +45,36 @@ abstract contract Calculations { view returns (uint128) { - if (currentTime >= segments[segments.length - 1].milestone) { + if (blockTimestamp >= segments[segments.length - 1].timestamp) { return depositAmount; } unchecked { uint128 previousSegmentAmounts; - uint40 currentSegmentMilestone = segments[0].milestone; + uint40 currentSegmentTimestamp = segments[0].timestamp; uint256 index = 0; - while (currentSegmentMilestone < currentTime) { + while (currentSegmentTimestamp < blockTimestamp) { previousSegmentAmounts += segments[index].amount; index += 1; - currentSegmentMilestone = segments[index].milestone; + currentSegmentTimestamp = segments[index].timestamp; } SD59x18 currentSegmentAmount = segments[index].amount.intoSD59x18(); SD59x18 currentSegmentExponent = segments[index].exponent.intoSD59x18(); - currentSegmentMilestone = segments[index].milestone; + currentSegmentTimestamp = segments[index].timestamp; - uint40 previousMilestone; + uint40 previousTimestamp; if (index > 0) { - previousMilestone = segments[index - 1].milestone; + previousTimestamp = segments[index - 1].timestamp; } else { - previousMilestone = defaults.START_TIME(); + previousTimestamp = defaults.START_TIME(); } - SD59x18 elapsedSegmentTime = (currentTime - previousMilestone).intoSD59x18(); - SD59x18 totalSegmentTime = (currentSegmentMilestone - previousMilestone).intoSD59x18(); + SD59x18 elapsedTime = (blockTimestamp - previousTimestamp).intoSD59x18(); + SD59x18 segmentDuration = (currentSegmentTimestamp - previousTimestamp).intoSD59x18(); - SD59x18 elapsedSegmentTimePercentage = elapsedSegmentTime.div(totalSegmentTime); - SD59x18 multiplier = elapsedSegmentTimePercentage.pow(currentSegmentExponent); + SD59x18 elapsedTimePercentage = elapsedTime.div(segmentDuration); + SD59x18 multiplier = elapsedTimePercentage.pow(currentSegmentExponent); SD59x18 segmentStreamedAmount = multiplier.mul(currentSegmentAmount); return previousSegmentAmounts + uint128(segmentStreamedAmount.intoUint256()); } @@ -92,24 +82,55 @@ abstract contract Calculations { /// @dev Replicates the logic of {SablierV2LockupDynamic._calculateStreamedAmountForOneSegment}. function calculateStreamedAmountForOneSegment( - uint40 currentTime, + uint40 blockTimestamp, LockupDynamic.Segment memory segment ) internal view returns (uint128) { - if (currentTime >= segment.milestone) { + if (blockTimestamp >= segment.timestamp) { return segment.amount; } unchecked { - SD59x18 elapsedTime = (currentTime - defaults.START_TIME()).intoSD59x18(); - SD59x18 totalTime = (segment.milestone - defaults.START_TIME()).intoSD59x18(); + SD59x18 elapsedTime = (blockTimestamp - defaults.START_TIME()).intoSD59x18(); + SD59x18 totalDuration = (segment.timestamp - defaults.START_TIME()).intoSD59x18(); - SD59x18 elapsedTimePercentage = elapsedTime.div(totalTime); + SD59x18 elapsedTimePercentage = elapsedTime.div(totalDuration); SD59x18 multiplier = elapsedTimePercentage.pow(segment.exponent.intoSD59x18()); - SD59x18 streamedAmountSd = multiplier.mul(segment.amount.intoSD59x18()); - return uint128(streamedAmountSd.intoUint256()); + SD59x18 streamedAmount = multiplier.mul(segment.amount.intoSD59x18()); + return uint128(streamedAmount.intoUint256()); + } + } + + /// @dev Helper function that replicates the logic of {SablierV2LockupTranched._calculateStreamedAmount}. + function calculateStreamedAmountForTranches( + uint40 blockTimestamp, + LockupTranched.Tranche[] memory tranches, + uint128 depositAmount + ) + internal + pure + returns (uint128) + { + if (blockTimestamp >= tranches[tranches.length - 1].timestamp) { + return depositAmount; } + + // Sum the amounts in all tranches that precede the block timestamp. + uint128 streamedAmount = tranches[0].amount; + uint40 currentTrancheTimestamp = tranches[1].timestamp; + uint256 index = 1; + + // Using unchecked arithmetic is safe because the tranches amounts sum equal to total amount at this point. + unchecked { + while (currentTrancheTimestamp <= blockTimestamp) { + streamedAmount += tranches[index].amount; + index += 1; + currentTrancheTimestamp = tranches[index].timestamp; + } + } + + return streamedAmount; } } diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 996dad865..e9d11f6eb 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; abstract contract Constants { - bytes32 internal constant FLASH_LOAN_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - uint40 internal constant MAY_1_2023 = 1_682_899_200; - UD60x18 internal constant MAX_FEE = UD60x18.wrap(0.1e18); // 10% - uint40 internal constant MAX_UNIX_TIMESTAMP = 2_147_483_647; // 2^31 - 1 + UD60x18 internal constant MAX_BROKER_FEE = UD60x18.wrap(0.1e18); // 10% uint128 internal constant MAX_UINT128 = type(uint128).max; + uint256 internal constant MAX_UINT256 = type(uint256).max; uint40 internal constant MAX_UINT40 = type(uint40).max; + uint40 internal constant MAX_UNIX_TIMESTAMP = 2_147_483_647; // 2^31 - 1 + uint40 internal constant MAY_1_2024 = 1_714_518_000; } diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index 3c5d16699..279eda4f6 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -1,11 +1,11 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19; +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD2x18, ud2x18 } from "@prb/math/src/UD2x18.sol"; +import { ud2x18 } from "@prb/math/src/UD2x18.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { Broker, Lockup, LockupDynamic, LockupLinear } from "../../src/types/DataTypes.sol"; +import { Broker, Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; import { Constants } from "./Constants.sol"; import { Users } from "./Types.sol"; @@ -13,33 +13,28 @@ import { Users } from "./Types.sol"; /// @notice Contract with default values used throughout the tests. contract Defaults is Constants { /*////////////////////////////////////////////////////////////////////////// - CONSTANTS + STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ UD60x18 public constant BROKER_FEE = UD60x18.wrap(0.003e18); // 0.3% - uint128 public constant BROKER_FEE_AMOUNT = 30.120481927710843373e18; // 0.3% of total amount + uint128 public constant BROKER_FEE_AMOUNT = 30.090270812437311935e18; // 0.3% of total amount uint128 public constant CLIFF_AMOUNT = 2500e18; uint40 public immutable CLIFF_TIME; uint40 public constant CLIFF_DURATION = 2500 seconds; uint128 public constant DEPOSIT_AMOUNT = 10_000e18; uint40 public immutable END_TIME; - UD60x18 public constant FLASH_FEE = UD60x18.wrap(0.0005e18); // 0.05% - uint256 public constant MAX_SEGMENT_COUNT = 300; + uint256 public constant MAX_SEGMENT_COUNT = 10_000; uint40 public immutable MAX_SEGMENT_DURATION; - UD60x18 public constant PROTOCOL_FEE = UD60x18.wrap(0.001e18); // 0.1% - uint128 public constant PROTOCOL_FEE_AMOUNT = 10.040160642570281124e18; // 0.1% of total amount + uint256 public constant MAX_TRANCHE_COUNT = 10_000; uint128 public constant REFUND_AMOUNT = DEPOSIT_AMOUNT - CLIFF_AMOUNT; uint256 public SEGMENT_COUNT; uint40 public immutable START_TIME; - uint128 public constant TOTAL_AMOUNT = 10_040.160642570281124497e18; // deposit / (1 - fee) + uint128 public constant TOTAL_AMOUNT = 10_030.090270812437311935e18; // deposit + broker fee uint40 public constant TOTAL_DURATION = 10_000 seconds; + uint256 public TRANCHE_COUNT; uint128 public constant WITHDRAW_AMOUNT = 2600e18; uint40 public immutable WARP_26_PERCENT; // 26% of the way through the stream - /*////////////////////////////////////////////////////////////////////////// - VARIABLES - //////////////////////////////////////////////////////////////////////////*/ - IERC20 private asset; Users private users; @@ -48,11 +43,12 @@ contract Defaults is Constants { //////////////////////////////////////////////////////////////////////////*/ constructor() { - START_TIME = uint40(MAY_1_2023) + 2 days; + START_TIME = uint40(MAY_1_2024) + 2 days; CLIFF_TIME = START_TIME + CLIFF_DURATION; END_TIME = START_TIME + TOTAL_DURATION; MAX_SEGMENT_DURATION = TOTAL_DURATION / uint40(MAX_SEGMENT_COUNT); SEGMENT_COUNT = 2; + TRANCHE_COUNT = 3; WARP_26_PERCENT = START_TIME + CLIFF_DURATION + 100 seconds; } @@ -85,19 +81,11 @@ contract Defaults is Constants { } function lockupCreateAmounts() public pure returns (Lockup.CreateAmounts memory) { - return Lockup.CreateAmounts({ - deposit: DEPOSIT_AMOUNT, - protocolFee: PROTOCOL_FEE_AMOUNT, - brokerFee: BROKER_FEE_AMOUNT - }); + return Lockup.CreateAmounts({ deposit: DEPOSIT_AMOUNT, brokerFee: BROKER_FEE_AMOUNT }); } - function lockupDynamicRange() public view returns (LockupDynamic.Range memory) { - return LockupDynamic.Range({ start: START_TIME, end: END_TIME }); - } - - function lockupDynamicStream() public view returns (LockupDynamic.Stream memory) { - return LockupDynamic.Stream({ + function lockupDynamicStream() public view returns (LockupDynamic.StreamLD memory) { + return LockupDynamic.StreamLD({ amounts: lockupAmounts(), asset: asset, endTime: END_TIME, @@ -105,6 +93,7 @@ contract Defaults is Constants { isDepleted: false, isStream: true, isTransferable: true, + recipient: users.recipient, segments: segments(), sender: users.sender, startTime: START_TIME, @@ -112,12 +101,12 @@ contract Defaults is Constants { }); } - function lockupLinearRange() public view returns (LockupLinear.Range memory) { - return LockupLinear.Range({ start: START_TIME, cliff: CLIFF_TIME, end: END_TIME }); + function lockupDynamicTimestamps() public view returns (LockupDynamic.Timestamps memory) { + return LockupDynamic.Timestamps({ start: START_TIME, end: END_TIME }); } - function lockupLinearStream() public view returns (LockupLinear.Stream memory) { - return LockupLinear.Stream({ + function lockupLinearStream() public view returns (LockupLinear.StreamLL memory) { + return LockupLinear.StreamLL({ amounts: lockupAmounts(), asset: asset, cliffTime: CLIFF_TIME, @@ -126,112 +115,170 @@ contract Defaults is Constants { isTransferable: true, isDepleted: false, isStream: true, + recipient: users.recipient, sender: users.sender, startTime: START_TIME, wasCanceled: false }); } - function maxSegments() public view returns (LockupDynamic.Segment[] memory maxSegments_) { - uint128 amount = DEPOSIT_AMOUNT / uint128(MAX_SEGMENT_COUNT); - UD2x18 exponent = ud2x18(2.71e18); + function lockupLinearTimestamps() public view returns (LockupLinear.Timestamps memory) { + return LockupLinear.Timestamps({ start: START_TIME, cliff: CLIFF_TIME, end: END_TIME }); + } - // Generate a bunch of segments with the same amount, same exponent, and with milestones evenly spread apart. - maxSegments_ = new LockupDynamic.Segment[](MAX_SEGMENT_COUNT); - for (uint40 i = 0; i < MAX_SEGMENT_COUNT; ++i) { - maxSegments_[i] = ( - LockupDynamic.Segment({ - amount: amount, - exponent: exponent, - milestone: START_TIME + MAX_SEGMENT_DURATION * (i + 1) - }) - ); - } + function lockupTranchedStream() public view returns (LockupTranched.StreamLT memory) { + return LockupTranched.StreamLT({ + amounts: lockupAmounts(), + asset: asset, + endTime: END_TIME, + isCancelable: true, + isDepleted: false, + isStream: true, + isTransferable: true, + recipient: users.recipient, + sender: users.sender, + startTime: START_TIME, + tranches: tranches(), + wasCanceled: false + }); + } + + function lockupTranchedTimestamps() public view returns (LockupTranched.Timestamps memory) { + return LockupTranched.Timestamps({ start: START_TIME, end: END_TIME }); } function segments() public view returns (LockupDynamic.Segment[] memory segments_) { segments_ = new LockupDynamic.Segment[](2); segments_[0] = ( - LockupDynamic.Segment({ amount: 2500e18, exponent: ud2x18(3.14e18), milestone: START_TIME + CLIFF_DURATION }) + LockupDynamic.Segment({ amount: 2500e18, exponent: ud2x18(3.14e18), timestamp: START_TIME + CLIFF_DURATION }) ); segments_[1] = ( - LockupDynamic.Segment({ amount: 7500e18, exponent: ud2x18(0.5e18), milestone: START_TIME + TOTAL_DURATION }) + LockupDynamic.Segment({ amount: 7500e18, exponent: ud2x18(0.5e18), timestamp: START_TIME + TOTAL_DURATION }) ); } - function segmentsWithDeltas() public view returns (LockupDynamic.SegmentWithDelta[] memory segmentsWithDeltas_) { + function segmentsWithDurations() + public + view + returns (LockupDynamic.SegmentWithDuration[] memory segmentsWithDurations_) + { LockupDynamic.Segment[] memory segments_ = segments(); - segmentsWithDeltas_ = new LockupDynamic.SegmentWithDelta[](2); - segmentsWithDeltas_[0] = ( - LockupDynamic.SegmentWithDelta({ + segmentsWithDurations_ = new LockupDynamic.SegmentWithDuration[](2); + segmentsWithDurations_[0] = ( + LockupDynamic.SegmentWithDuration({ amount: segments_[0].amount, exponent: segments_[0].exponent, - delta: 2500 seconds + duration: 2500 seconds }) ); - segmentsWithDeltas_[1] = ( - LockupDynamic.SegmentWithDelta({ + segmentsWithDurations_[1] = ( + LockupDynamic.SegmentWithDuration({ amount: segments_[1].amount, exponent: segments_[1].exponent, - delta: 7500 seconds + duration: 7500 seconds }) ); } + function tranches() public view returns (LockupTranched.Tranche[] memory tranches_) { + tranches_ = new LockupTranched.Tranche[](3); + tranches_[0] = LockupTranched.Tranche({ amount: 2500e18, timestamp: START_TIME + CLIFF_DURATION }); + tranches_[1] = LockupTranched.Tranche({ amount: 100e18, timestamp: WARP_26_PERCENT }); + tranches_[2] = LockupTranched.Tranche({ amount: 7400e18, timestamp: START_TIME + TOTAL_DURATION }); + } + + function tranchesWithDurations() + public + pure + returns (LockupTranched.TrancheWithDuration[] memory tranchesWithDurations_) + { + tranchesWithDurations_ = new LockupTranched.TrancheWithDuration[](3); + tranchesWithDurations_[0] = LockupTranched.TrancheWithDuration({ amount: 2500e18, duration: 2500 seconds }); + tranchesWithDurations_[1] = LockupTranched.TrancheWithDuration({ amount: 100e18, duration: 100 seconds }); + tranchesWithDurations_[2] = LockupTranched.TrancheWithDuration({ amount: 7400e18, duration: 7400 seconds }); + } + /*////////////////////////////////////////////////////////////////////////// PARAMS //////////////////////////////////////////////////////////////////////////*/ - function createWithDeltas() public view returns (LockupDynamic.CreateWithDeltas memory) { - return LockupDynamic.CreateWithDeltas({ + function createWithDurationsLD() public view returns (LockupDynamic.CreateWithDurations memory) { + return LockupDynamic.CreateWithDurations({ + sender: users.sender, + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, asset: asset, - broker: broker(), cancelable: true, transferable: true, - recipient: users.recipient, - segments: segmentsWithDeltas(), - sender: users.sender, - totalAmount: TOTAL_AMOUNT + segments: segmentsWithDurations(), + broker: broker() }); } - function createWithDurations() public view returns (LockupLinear.CreateWithDurations memory) { + function createWithDurationsLL() public view returns (LockupLinear.CreateWithDurations memory) { return LockupLinear.CreateWithDurations({ + sender: users.sender, + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, asset: asset, - broker: broker(), cancelable: true, transferable: true, durations: durations(), - recipient: users.recipient, - sender: users.sender, - totalAmount: TOTAL_AMOUNT + broker: broker() }); } - function createWithMilestones() public view returns (LockupDynamic.CreateWithMilestones memory) { - return LockupDynamic.CreateWithMilestones({ + function createWithDurationsLT() public view returns (LockupTranched.CreateWithDurations memory) { + return LockupTranched.CreateWithDurations({ + sender: users.sender, + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, asset: asset, - broker: broker(), cancelable: true, transferable: true, - recipient: users.recipient, - segments: segments(), + tranches: tranchesWithDurations(), + broker: broker() + }); + } + + function createWithTimestampsLD() public view returns (LockupDynamic.CreateWithTimestamps memory) { + return LockupDynamic.CreateWithTimestamps({ sender: users.sender, + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, + asset: asset, + cancelable: true, + transferable: true, startTime: START_TIME, - totalAmount: TOTAL_AMOUNT + segments: segments(), + broker: broker() }); } - function createWithRange() public view returns (LockupLinear.CreateWithRange memory) { - return LockupLinear.CreateWithRange({ + function createWithTimestampsLL() public view returns (LockupLinear.CreateWithTimestamps memory) { + return LockupLinear.CreateWithTimestamps({ + sender: users.sender, + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, asset: asset, - broker: broker(), cancelable: true, transferable: true, - range: lockupLinearRange(), - recipient: users.recipient, + timestamps: lockupLinearTimestamps(), + broker: broker() + }); + } + + function createWithTimestampsLT() public view returns (LockupTranched.CreateWithTimestamps memory) { + return LockupTranched.CreateWithTimestamps({ sender: users.sender, - totalAmount: TOTAL_AMOUNT + recipient: users.recipient, + totalAmount: TOTAL_AMOUNT, + asset: asset, + cancelable: true, + transferable: true, + startTime: START_TIME, + tranches: tranches(), + broker: broker() }); } } diff --git a/test/utils/DeployOptimized.sol b/test/utils/DeployOptimized.sol index 6891b70cf..226ac9af6 100644 --- a/test/utils/DeployOptimized.sol +++ b/test/utils/DeployOptimized.sol @@ -1,25 +1,17 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { StdCheats } from "forge-std/src/StdCheats.sol"; -import { ISablierV2Comptroller } from "../../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2LockupDynamic } from "../../src/interfaces/ISablierV2LockupDynamic.sol"; import { ISablierV2LockupLinear } from "../../src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "../../src/interfaces/ISablierV2LockupTranched.sol"; import { ISablierV2NFTDescriptor } from "../../src/interfaces/ISablierV2NFTDescriptor.sol"; abstract contract DeployOptimized is StdCheats { - /// @dev Deploys {SablierV2Comptroller} from an optimized source compiled with `--via-ir`. - function deployOptimizedComptroller(address initialAdmin) internal returns (ISablierV2Comptroller) { - return ISablierV2Comptroller( - deployCode("out-optimized/SablierV2Comptroller.sol/SablierV2Comptroller.json", abi.encode(initialAdmin)) - ); - } - /// @dev Deploys {SablierV2LockupDynamic} from an optimized source compiled with `--via-ir`. function deployOptimizedLockupDynamic( address initialAdmin, - ISablierV2Comptroller comptroller_, ISablierV2NFTDescriptor nftDescriptor_, uint256 maxSegmentCount ) @@ -29,7 +21,7 @@ abstract contract DeployOptimized is StdCheats { return ISablierV2LockupDynamic( deployCode( "out-optimized/SablierV2LockupDynamic.sol/SablierV2LockupDynamic.json", - abi.encode(initialAdmin, address(comptroller_), address(nftDescriptor_), maxSegmentCount) + abi.encode(initialAdmin, address(nftDescriptor_), maxSegmentCount) ) ); } @@ -37,7 +29,6 @@ abstract contract DeployOptimized is StdCheats { /// @dev Deploys {SablierV2LockupLinear} from an optimized source compiled with `--via-ir`. function deployOptimizedLockupLinear( address initialAdmin, - ISablierV2Comptroller comptroller_, ISablierV2NFTDescriptor nftDescriptor_ ) internal @@ -46,7 +37,24 @@ abstract contract DeployOptimized is StdCheats { return ISablierV2LockupLinear( deployCode( "out-optimized/SablierV2LockupLinear.sol/SablierV2LockupLinear.json", - abi.encode(initialAdmin, address(comptroller_), address(nftDescriptor_)) + abi.encode(initialAdmin, address(nftDescriptor_)) + ) + ); + } + + /// @dev Deploys {SablierV2LockupTranched} from an optimized source compiled with `--via-ir`. + function deployOptimizedLockupTranched( + address initialAdmin, + ISablierV2NFTDescriptor nftDescriptor_, + uint256 maxTrancheCount + ) + internal + returns (ISablierV2LockupTranched) + { + return ISablierV2LockupTranched( + deployCode( + "out-optimized/SablierV2LockupTranched.sol/SablierV2LockupTranched.json", + abi.encode(initialAdmin, address(nftDescriptor_), maxTrancheCount) ) ); } @@ -59,19 +67,20 @@ abstract contract DeployOptimized is StdCheats { function deployOptimizedCore( address initialAdmin, - uint256 maxSegmentCount + uint256 maxSegmentCount, + uint256 maxTrancheCount ) internal returns ( - ISablierV2Comptroller comptroller_, ISablierV2LockupDynamic lockupDynamic_, ISablierV2LockupLinear lockupLinear_, + ISablierV2LockupTranched lockupTranched_, ISablierV2NFTDescriptor nftDescriptor_ ) { - comptroller_ = deployOptimizedComptroller(initialAdmin); nftDescriptor_ = deployOptimizedNFTDescriptor(); - lockupDynamic_ = deployOptimizedLockupDynamic(initialAdmin, comptroller_, nftDescriptor_, maxSegmentCount); - lockupLinear_ = deployOptimizedLockupLinear(initialAdmin, comptroller_, nftDescriptor_); + lockupDynamic_ = deployOptimizedLockupDynamic(initialAdmin, nftDescriptor_, maxSegmentCount); + lockupLinear_ = deployOptimizedLockupLinear(initialAdmin, nftDescriptor_); + lockupTranched_ = deployOptimizedLockupTranched(initialAdmin, nftDescriptor_, maxTrancheCount); } } diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 13192c7a9..05fa6890c 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { IERC3156FlashBorrower } from "../../src/interfaces/erc3156/IERC3156FlashBorrower.sol"; -import { ISablierV2Comptroller } from "../../src/interfaces/ISablierV2Comptroller.sol"; import { ISablierV2NFTDescriptor } from "../../src/interfaces/ISablierV2NFTDescriptor.sol"; -import { Lockup, LockupDynamic, LockupLinear } from "../../src/types/DataTypes.sol"; +import { Lockup, LockupDynamic, LockupLinear, LockupTranched } from "../../src/types/DataTypes.sol"; /// @notice Abstract contract containing all the events emitted by the protocol. abstract contract Events { @@ -31,43 +28,12 @@ abstract contract Events { event TransferAdmin(address indexed oldAdmin, address indexed newAdmin); - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-BASE - //////////////////////////////////////////////////////////////////////////*/ - - event ClaimProtocolRevenues(address indexed admin, IERC20 indexed asset, uint128 protocolRevenues); - - event SetComptroller( - address indexed admin, ISablierV2Comptroller oldComptroller, ISablierV2Comptroller newComptroller - ); - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-COMPTROLLER - //////////////////////////////////////////////////////////////////////////*/ - - event SetFlashFee(address indexed admin, UD60x18 oldFlashFee, UD60x18 newFlashFee); - - event SetProtocolFee(address indexed admin, IERC20 indexed asset, UD60x18 oldProtocolFee, UD60x18 newProtocolFee); - - event ToggleFlashAsset(address indexed admin, IERC20 indexed asset, bool newFlag); - - /*////////////////////////////////////////////////////////////////////////// - SABLIER-V2-FLASH-LOAN - //////////////////////////////////////////////////////////////////////////*/ - - event FlashLoan( - address indexed initiator, - IERC3156FlashBorrower indexed receiver, - IERC20 indexed asset, - uint256 amount, - uint256 feeAmount, - bytes data - ); - /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-LOCKUP //////////////////////////////////////////////////////////////////////////*/ + event AllowToHook(address indexed admin, address recipient); + event CancelLockupStream( uint256 streamId, address indexed sender, @@ -99,7 +65,7 @@ abstract contract Events { bool cancelable, bool transferable, LockupDynamic.Segment[] segments, - LockupDynamic.Range range, + LockupDynamic.Timestamps timestamps, address broker ); @@ -116,7 +82,25 @@ abstract contract Events { IERC20 indexed asset, bool cancelable, bool transferable, - LockupLinear.Range range, + LockupLinear.Timestamps timestamps, + address broker + ); + + /*////////////////////////////////////////////////////////////////////////// + SABLIER-V2-LOCKUP-TRANCHED + //////////////////////////////////////////////////////////////////////////*/ + + event CreateLockupTranchedStream( + uint256 streamId, + address funder, + address indexed sender, + address indexed recipient, + Lockup.CreateAmounts amounts, + IERC20 indexed asset, + bool cancelable, + bool transferable, + LockupTranched.Tranche[] tranches, + LockupTranched.Timestamps timestamps, address broker ); } diff --git a/test/utils/Fuzzers.sol b/test/utils/Fuzzers.sol index b9a8f6a26..8496d4299 100644 --- a/test/utils/Fuzzers.sol +++ b/test/utils/Fuzzers.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/Uint128.sol"; import { UD60x18, ud, uUNIT } from "@prb/math/src/UD60x18.sol"; -import { Lockup, LockupDynamic } from "../../src/types/DataTypes.sol"; +import { Lockup, LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; import { Constants } from "./Constants.sol"; import { Defaults } from "./Defaults.sol"; @@ -15,64 +15,58 @@ abstract contract Fuzzers is Constants, Utils { Defaults private defaults = new Defaults(); + /*////////////////////////////////////////////////////////////////////////// + LOCKUP-DYNAMIC + //////////////////////////////////////////////////////////////////////////*/ + /// @dev Just like {fuzzDynamicStreamAmounts} but with defaults. function fuzzDynamicStreamAmounts(LockupDynamic.Segment[] memory segments) internal view returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) { - (totalAmount, createAmounts) = fuzzDynamicStreamAmounts({ - upperBound: MAX_UINT128, - segments: segments, - protocolFee: defaults.PROTOCOL_FEE(), - brokerFee: defaults.BROKER_FEE() - }); + (totalAmount, createAmounts) = + fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, segments: segments, brokerFee: defaults.BROKER_FEE() }); } /// @dev Just like {fuzzDynamicStreamAmounts} but with defaults. - function fuzzDynamicStreamAmounts(LockupDynamic.SegmentWithDelta[] memory segments) + function fuzzDynamicStreamAmounts(LockupDynamic.SegmentWithDuration[] memory segments) internal view returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) { - LockupDynamic.Segment[] memory segmentsWithMilestones = getSegmentsWithMilestones(segments); + LockupDynamic.Segment[] memory segmentsWithTimestamps = getSegmentsWithTimestamps(segments); (totalAmount, createAmounts) = fuzzDynamicStreamAmounts({ upperBound: MAX_UINT128, - segments: segmentsWithMilestones, - protocolFee: defaults.PROTOCOL_FEE(), + segments: segmentsWithTimestamps, brokerFee: defaults.BROKER_FEE() }); - for (uint256 i = 0; i < segmentsWithMilestones.length; ++i) { - segments[i].amount = segmentsWithMilestones[i].amount; + for (uint256 i = 0; i < segmentsWithTimestamps.length; ++i) { + segments[i].amount = segmentsWithTimestamps[i].amount; } } - /// @dev Fuzzes the segment amounts and calculates the create amounts (total, deposit, protocol fee, and broker - /// fee). + /// @dev Fuzzes the segment amounts and calculate the total and create amounts (deposit and broker fee). function fuzzDynamicStreamAmounts( uint128 upperBound, - LockupDynamic.SegmentWithDelta[] memory segments, - UD60x18 protocolFee, + LockupDynamic.SegmentWithDuration[] memory segments, UD60x18 brokerFee ) internal view returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) { - LockupDynamic.Segment[] memory segmentsWithMilestones = getSegmentsWithMilestones(segments); - (totalAmount, createAmounts) = - fuzzDynamicStreamAmounts(upperBound, segmentsWithMilestones, protocolFee, brokerFee); - for (uint256 i = 0; i < segmentsWithMilestones.length; ++i) { - segments[i].amount = segmentsWithMilestones[i].amount; + LockupDynamic.Segment[] memory segmentsWithTimestamps = getSegmentsWithTimestamps(segments); + (totalAmount, createAmounts) = fuzzDynamicStreamAmounts(upperBound, segmentsWithTimestamps, brokerFee); + for (uint256 i = 0; i < segmentsWithTimestamps.length; ++i) { + segments[i].amount = segmentsWithTimestamps[i].amount; } } - /// @dev Fuzzes the segment amounts and calculates the create amounts (total, deposit, protocol fee and broker - /// fee). + /// @dev Fuzzes the segment amounts and calculate the total and create amounts (deposit and broker fee). function fuzzDynamicStreamAmounts( uint128 upperBound, LockupDynamic.Segment[] memory segments, - UD60x18 protocolFee, UD60x18 brokerFee ) internal @@ -89,9 +83,8 @@ abstract contract Fuzzers is Constants, Utils { // Fuzz the other segment amounts by bounding from 0. unchecked { for (uint256 i = 1; i < segmentCount; ++i) { - uint128 segmentAmount = boundUint128(segments[i].amount, 0, maxSegmentAmount); - segments[i].amount = segmentAmount; - estimatedDepositAmount += segmentAmount; + segments[i].amount = boundUint128(segments[i].amount, 0, maxSegmentAmount); + estimatedDepositAmount += segments[i].amount; } } @@ -99,63 +92,196 @@ abstract contract Fuzzers is Constants, Utils { // must equal the deposit amount) using this formula: // // $$ - // total = \frac{deposit}{1e18 - protocolFee - brokerFee} + // total = \frac{deposit}{1e18 - brokerFee} // $$ - totalAmount = ud(estimatedDepositAmount).div(ud(uUNIT - protocolFee.intoUint256() - brokerFee.intoUint256())) - .intoUint128(); + totalAmount = ud(estimatedDepositAmount).div(ud(uUNIT - brokerFee.intoUint256())).intoUint128(); - // Calculate the fee amounts. - createAmounts.protocolFee = ud(totalAmount).mul(protocolFee).intoUint128(); + // Calculate the broker fee amount. createAmounts.brokerFee = ud(totalAmount).mul(brokerFee).intoUint128(); // Here, we account for rounding errors and adjust the estimated deposit amount and the segments. We know // that the estimated deposit amount is not greater than the adjusted deposit amount below, because the inverse - // of {Helpers.checkAndCalculateFees} over-expresses the weight of the fees. - createAmounts.deposit = totalAmount - createAmounts.protocolFee - createAmounts.brokerFee; + // of {Helpers.checkAndCalculateBrokerFee} over-expresses the weight of the broker fee. + createAmounts.deposit = totalAmount - createAmounts.brokerFee; segments[segments.length - 1].amount += (createAmounts.deposit - estimatedDepositAmount); } - /// @dev Fuzzes the deltas. - function fuzzSegmentDeltas(LockupDynamic.SegmentWithDelta[] memory segments) internal pure { + /// @dev Fuzzes the segment durations. + function fuzzSegmentDurations(LockupDynamic.SegmentWithDuration[] memory segments) internal view { unchecked { - // Precompute the first segment delta. - segments[0].delta = uint40(_bound(segments[0].delta, 1, 100)); - - // Bound the deltas so that none is zero and the calculations don't overflow. - uint256 deltaCount = segments.length; - uint40 maxDelta = (MAX_UNIX_TIMESTAMP - segments[0].delta) / uint40(deltaCount); - for (uint256 i = 1; i < deltaCount; ++i) { - segments[i].delta = boundUint40(segments[i].delta, 1, maxDelta); + // Precompute the first segment duration. + segments[0].duration = uint40(_bound(segments[0].duration, 1, 100)); + + // Bound the durations so that none is zero and the calculations don't overflow. + uint256 durationCount = segments.length; + uint40 maxDuration = (MAX_UNIX_TIMESTAMP - getBlockTimestamp()) / uint40(durationCount); + for (uint256 i = 1; i < durationCount; ++i) { + segments[i].duration = boundUint40(segments[i].duration, 1, maxDuration); } } } - /// @dev Fuzzes the segment milestones. - function fuzzSegmentMilestones(LockupDynamic.Segment[] memory segments, uint40 startTime) internal view { + /// @dev Fuzzes the segment timestamps. + function fuzzSegmentTimestamps(LockupDynamic.Segment[] memory segments, uint40 startTime) internal view { // Return here if there's only one segment to not run into division by zero. uint40 segmentCount = uint40(segments.length); if (segmentCount == 1) { // The end time must be in the future. - uint40 currentTime = getBlockTimestamp(); - segments[0].milestone = (startTime < currentTime ? currentTime : startTime) + 2 days; + uint40 blockTimestamp = getBlockTimestamp(); + segments[0].timestamp = (startTime < blockTimestamp ? blockTimestamp : startTime) + 2 days; return; } - // The first milestones is precomputed to avoid an underflow in the first loop iteration. We have to - // add 1 because the first milestone must be greater than the start time. - segments[0].milestone = startTime + 1 seconds; + // The first timestamps is precomputed to avoid an underflow in the first loop iteration. We have to + // add 1 because the first timestamp must be greater than the start time. + segments[0].timestamp = startTime + 1 seconds; - // Fuzz the milestones while preserving their order in the array. For each milestone, set its initial guess - // as the sum of the starting milestone and the step size multiplied by the current index. This ensures that - // the initial guesses are evenly spaced. Next, we bound the milestone within a range of half the step size + // Fuzz the timestamps while preserving their order in the array. For each timestamp, set its initial guess + // as the sum of the starting timestamp and the step size multiplied by the current index. This ensures that + // the initial guesses are evenly spaced. Next, we bound the timestamp within a range of half the step size // around the initial guess. - uint256 start = segments[0].milestone; - uint40 step = (MAX_UNIX_TIMESTAMP - segments[0].milestone) / (segmentCount - 1); + uint256 start = segments[0].timestamp; + uint40 step = (MAX_UNIX_TIMESTAMP - segments[0].timestamp) / (segmentCount - 1); uint40 halfStep = step / 2; for (uint256 i = 1; i < segmentCount; ++i) { - uint256 milestone = start + i * step; - milestone = _bound(milestone, milestone - halfStep, milestone + halfStep); - segments[i].milestone = uint40(milestone); + uint256 timestamp = start + i * step; + timestamp = _bound(timestamp, timestamp - halfStep, timestamp + halfStep); + segments[i].timestamp = uint40(timestamp); + } + } + + /*////////////////////////////////////////////////////////////////////////// + LOCKUP-TRANCHED + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Fuzzes the tranche durations. + function fuzzTrancheDurations(LockupTranched.TrancheWithDuration[] memory tranches) internal view { + unchecked { + // Precompute the first tranche duration. + tranches[0].duration = uint40(_bound(tranches[0].duration, 1, 100)); + + // Bound the durations so that none is zero and the calculations don't overflow. + uint256 durationCount = tranches.length; + uint40 maxDuration = (MAX_UNIX_TIMESTAMP - getBlockTimestamp()) / uint40(durationCount); + for (uint256 i = 1; i < durationCount; ++i) { + tranches[i].duration = boundUint40(tranches[i].duration, 1, maxDuration); + } + } + } + + /// @dev Fuzzes the tranche timestamps. + function fuzzTrancheTimestamps(LockupTranched.Tranche[] memory tranches, uint40 startTime) internal view { + // Return here if there's only one tranche to not run into division by zero. + uint40 trancheCount = uint40(tranches.length); + if (trancheCount == 1) { + // The end time must be in the future. + uint40 blockTimestamp = getBlockTimestamp(); + tranches[0].timestamp = (startTime < blockTimestamp ? blockTimestamp : startTime) + 2 days; + return; + } + + // The first timestamps is precomputed to avoid an underflow in the first loop iteration. We have to + // add 1 because the first timestamp must be greater than the start time. + tranches[0].timestamp = startTime + 1 seconds; + + // Fuzz the timestamps while preserving their order in the array. For each timestamp, set its initial guess + // as the sum of the starting timestamp and the step size multiplied by the current index. This ensures that + // the initial guesses are evenly spaced. Next, we bound the timestamp within a range of half the step size + // around the initial guess. + uint256 start = tranches[0].timestamp; + uint40 step = (MAX_UNIX_TIMESTAMP - tranches[0].timestamp) / (trancheCount - 1); + uint40 halfStep = step / 2; + for (uint256 i = 1; i < trancheCount; ++i) { + uint256 timestamp = start + i * step; + timestamp = _bound(timestamp, timestamp - halfStep, timestamp + halfStep); + tranches[i].timestamp = uint40(timestamp); } } + + /// @dev Just like {fuzzTranchedStreamAmounts} but with defaults. + function fuzzTranchedStreamAmounts(LockupTranched.Tranche[] memory tranches) + internal + view + returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + { + (totalAmount, createAmounts) = + fuzzTranchedStreamAmounts({ upperBound: MAX_UINT128, tranches: tranches, brokerFee: defaults.BROKER_FEE() }); + } + + /// @dev Just like {fuzzTranchedStreamAmounts} but with defaults. + function fuzzTranchedStreamAmounts(LockupTranched.TrancheWithDuration[] memory tranches) + internal + view + returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + { + LockupTranched.Tranche[] memory tranchesWithTimestamps = getTranchesWithTimestamps(tranches); + (totalAmount, createAmounts) = fuzzTranchedStreamAmounts({ + upperBound: MAX_UINT128, + tranches: tranchesWithTimestamps, + brokerFee: defaults.BROKER_FEE() + }); + for (uint256 i = 0; i < tranchesWithTimestamps.length; ++i) { + tranches[i].amount = tranchesWithTimestamps[i].amount; + } + } + + /// @dev Fuzzes the tranche amounts and calculates the total and create amounts (deposit and broker fee). + function fuzzTranchedStreamAmounts( + uint128 upperBound, + LockupTranched.TrancheWithDuration[] memory tranches, + UD60x18 brokerFee + ) + internal + view + returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + { + LockupTranched.Tranche[] memory tranchesWithTimestamps = getTranchesWithTimestamps(tranches); + (totalAmount, createAmounts) = fuzzTranchedStreamAmounts(upperBound, tranchesWithTimestamps, brokerFee); + for (uint256 i = 0; i < tranchesWithTimestamps.length; ++i) { + tranches[i].amount = tranchesWithTimestamps[i].amount; + } + } + + /// @dev Fuzzes the tranche amounts and calculates the total and create amounts (deposit and broker fee). + function fuzzTranchedStreamAmounts( + uint128 upperBound, + LockupTranched.Tranche[] memory tranches, + UD60x18 brokerFee + ) + internal + pure + returns (uint128 totalAmount, Lockup.CreateAmounts memory createAmounts) + { + uint256 trancheCount = tranches.length; + uint128 maxTrancheAmount = upperBound / uint128(trancheCount * 2); + + // Precompute the first tranche amount to prevent zero deposit amounts. + tranches[0].amount = boundUint128(tranches[0].amount, 100, maxTrancheAmount); + uint128 estimatedDepositAmount = tranches[0].amount; + + // Fuzz the other tranche amounts by bounding from 0. + unchecked { + for (uint256 i = 1; i < trancheCount; ++i) { + tranches[i].amount = boundUint128(tranches[i].amount, 0, maxTrancheAmount); + estimatedDepositAmount += tranches[i].amount; + } + } + + // Calculate the total amount from the approximated deposit amount (recall that the sum of all tranche amounts + // must equal the deposit amount) using this formula: + // + // $$ + // total = \frac{deposit}{1e18 - brokerFee} + // $$ + totalAmount = ud(estimatedDepositAmount).div(ud(uUNIT - brokerFee.intoUint256())).intoUint128(); + + // Calculate the broker fee amount. + createAmounts.brokerFee = ud(totalAmount).mul(brokerFee).intoUint128(); + + // Here, we account for rounding errors and adjust the estimated deposit amount and the tranches. We know + // that the estimated deposit amount is not greater than the adjusted deposit amount below, because the inverse + // of {Helpers.checkAndCalculateBrokerFee} over-expresses the weight of the broker fee. + createAmounts.deposit = totalAmount - createAmounts.brokerFee; + tranches[tranches.length - 1].amount += (createAmounts.deposit - estimatedDepositAmount); + } } diff --git a/test/utils/Precompiles.sol b/test/utils/Precompiles.sol deleted file mode 100644 index a107d8013..000000000 --- a/test/utils/Precompiles.sol +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// solhint-disable max-line-length,no-inline-assembly,reason-string -pragma solidity >=0.8.19; - -import { ISablierV2Comptroller } from "../../src/interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2LockupDynamic } from "../../src/interfaces/ISablierV2LockupDynamic.sol"; -import { ISablierV2LockupLinear } from "../../src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2NFTDescriptor } from "../../src/interfaces/ISablierV2NFTDescriptor.sol"; -import { SablierV2NFTDescriptor } from "../../src/SablierV2NFTDescriptor.sol"; - -/// @notice This is useful for external integrations seeking to test against the exact deployed bytecode, as recompiling -/// with via IR enabled would be time-consuming. -/// -/// The BUSL-1.1 license permits non-production usage of this file. This prohibits running the code on mainnet, -/// but allows for execution in test environments, such as a local development network or a testnet. -contract Precompiles { - /*////////////////////////////////////////////////////////////////////////// - CONSTANTS - //////////////////////////////////////////////////////////////////////////*/ - - uint256 internal constant MAX_SEGMENT_COUNT = 300; - - /*////////////////////////////////////////////////////////////////////////// - BYTECODES - //////////////////////////////////////////////////////////////////////////*/ - - bytes public constant BYTECODE_COMPTROLLER = - hex"60803461009857601f6104b338819003918201601f19168301916001600160401b0383118484101761009d5780849260209460405283398101031261009857516001600160a01b0381169081900361009857600080546001600160a01b0319168217815560405191907fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a36103ff90816100b48239f35b600080fd5b634e487b7160e01b600052604160045260246000fdfe608060408181526004918236101561001657600080fd5b600092833560e01c9182634d81e51d1461039d5750816375829def146102e5578163907a267b14610253578163b5b3ca2c146101ab578163cb01e30e146100f957508063dcf844a7146100c3578063e07df5b4146100a55763f851a4401461007d57600080fd5b346100a157816003193601126100a1576001600160a01b0360209254169051908152f35b5080fd5b50346100a157816003193601126100a1576020906001549051908152f35b50346100a15760203660031901126100a157806020926001600160a01b036100e96103d7565b1681526003845220549051908152f35b9050346101a75760203660031901126101a7576101146103d7565b6001600160a01b03918285541633810361017a575050169081835260026020528083209081549160ff8316159260ff84169060ff1916179055519081527f8cd3a7bc46b26a3b0c07a05a47af78abcaa647626f631d92ea64f8867b23bbec60203392a380f35b84516331b339a960e21b81526001600160a01b039091169181019182523360208301529081906040010390fd5b8280fd5b9050346101a757816003193601126101a7576101c56103d7565b90602435916001600160a01b039182865416338103610226575050907f371789a3d97098f3070492613273a065a7e8a19e009fd1ae92a4b4d4c71ed62d9116928385526003602052808520928084549455815193845260208401523392a380f35b85516331b339a960e21b81526001600160a01b039091169181019182523360208301529081906040010390fd5b919050346101a75760203660031901126101a7578135916001600160a01b038454163381036102b85750507fc059ba3e07a1c4d1fa8845bdb2af2dd85e844684e0a59e6073499e4338788465906001549280600155815193845260208401523392a280f35b82516331b339a960e21b81526001600160a01b039091169181019182523360208301529081906040010390fd5b919050346101a75760203660031901126101a7578135916001600160a01b03918284168094036103995784549283169033820361036d575050507fffffffffffffffffffffffff00000000000000000000000000000000000000001681178255337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808380a380f35b516331b339a960e21b81526001600160a01b039091169181019182523360208301529081906040010390fd5b8480fd5b849084346101a75760203660031901126101a75760ff906020936001600160a01b036103c76103d7565b1681526002855220541615158152f35b600435906001600160a01b03821682036103ed57565b600080fdfea164736f6c6343000817000a"; - bytes public constant BYTECODE_LOCKUP_DYNAMIC = - hex"60c0346200046e57601f62005be438819003918201601f19168301916001600160401b038311848410176200032b578084926080946040528339810103126200046e5780516001600160a01b038082169290918390036200046e5760208101518281168091036200046e5760408201519183831683036200046e5760600151936200008962000473565b90601d82527f5361626c696572205632204c6f636b75702044796e616d6963204e46540000006020830152620000be62000473565b601181527029a0a116ab1916a627a1a5aaa816a22ca760791b602082015230608052600080546001600160a01b03199081168417825560018054909116909517909455927fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a38051906001600160401b0382116200032b5760035490600182811c9216801562000463575b60208310146200044d5781601f849311620003d8575b50602090601f83116001146200034d5760009262000341575b50508160011b916000199060031b1c1916176003555b80516001600160401b0381116200032b576004918254600181811c9116801562000320575b60208210146200030b579081601f849311620002b3575b50602090601f831160011462000248576000926200023c575b50508160011b916000199060031b1c19161790555b1660018060a01b0319600a541617600a5560a05260016009556040516157509081620004948239608051816132f7015260a051818181610f7a01526134180152f35b015190503880620001e5565b6000858152602081209350601f198516905b8181106200029a575090846001959493921062000280575b505050811b019055620001fa565b015160001960f88460031b161c1916905538808062000272565b929360206001819287860151815501950193016200025a565b909150836000526020600020601f840160051c8101916020851062000300575b90601f859493920160051c01905b818110620002f05750620001cc565b60008155849350600101620002e1565b9091508190620002d3565b602284634e487b7160e01b6000525260246000fd5b90607f1690620001b5565b634e487b7160e01b600052604160045260246000fd5b0151905038806200017a565b600360009081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b9350601f198516905b818110620003bf5750908460019594939210620003a5575b505050811b0160035562000190565b015160001960f88460031b161c1916905538808062000396565b929360206001819287860151815501950193016200037e565b60036000529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c8101916020851062000442575b90601f859493920160051c01905b81811062000432575062000161565b6000815584935060010162000423565b909150819062000415565b634e487b7160e01b600052602260045260246000fd5b91607f16916200014b565b600080fd5b60408051919082016001600160401b038111838210176200032b5760405256fe608080604052600436101561001357600080fd5b60003560e01c90816301ffc9a7146126f55750806306fdde0314612630578063081812fc14612612578063095ea7b31461247e5780631400ecec146123d957806316844456146121155780631c1cdd4c146120af5780631e99d5691461209157806323b872dd1461206857806339a73c031461202557806340e58ee514611dd1578063425d30dd14611db357806342842e0e14611d6357806342966c6814611bd35780634857501f14611b5d5780634869e12d14611b215780635fe3b56714611afa5780636352211e14611acb5780636d0cee7514611a7357806370a08231146119c957806375829def146119375780637cad6cd1146118655780637de6b1db146116555780638659c2701461135c578063894e9a0d146110ef5780638bad38dd146110735780638f69b99314610ff05780639067b67714610f9d5780639188ec8414610f6257806395d89b4114610e52578063a22cb46514610d81578063a2ffb89714610c9f578063a6202bf214610b96578063a80fc07114610b41578063ad35efd414610ade578063b256456914610ac0578063b637b86514610a60578063b88d4fde146109d7578063b8a3be66146109a0578063b971302a1461094e578063bc063e1a1461092b578063bc2be1be146108d8578063c156a11d14610820578063c33cd35e146106c1578063c87b56dd1461058e578063cc364f48146104f4578063d4dbd20b1461049f578063d511609f14610450578063d975dfed14610403578063e985e9c5146103ac578063ea5ead191461037e578063eac8f5b814610312578063f590c176146102ea578063f851a440146102c35763fdd46d601461027c57600080fd5b346102be5760603660031901126102be57610295612822565b6044356001600160801b03811681036102be576102bc916102b46132ed565b6004356131da565b005b600080fd5b346102be5760003660031901126102be5760206001600160a01b0360005416604051908152f35b346102be5760203660031901126102be576020610308600435612e21565b6040519015158152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260206001600160a01b0360016040600020015416604051908152f35b6024906040519062b8e7e760e51b82526004820152fd5b346102be5760403660031901126102be576102bc60043561039d612822565b6103a6826140c2565b91612e52565b346102be5760403660031901126102be576103c561280c565b6103cd612822565b906001600160a01b03809116600052600860205260406000209116600052602052602060ff604060002054166040519015158152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c16156103675761043f6020916140c2565b6001600160801b0360405191168152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b602052602060026040600020015460801c604051908152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260206001600160801b0360036040600020015416604051908152f35b346102be5760203660031901126102be57600435600060206040516105188161295c565b828152015280600052600b60205260ff60016040600020015460a81c161561036757600052600b6020526040806000205464ffffffffff82519161055b8361295c565b818160a01c16835260c81c16602082015261058c825180926020908164ffffffffff91828151168552015116910152565bf35b346102be576020806003193601126102be57600435906105cc6105c78360005260056020526001600160a01b0360406000205416151590565b612bdb565b60006001600160a01b03600a5416926044604051809581937fe9dc637500000000000000000000000000000000000000000000000000000000835230600484015260248301525afa9182156106b55760009261063c575b506106386040519282849384528301906127e7565b0390f35b9091503d806000833e61064f81836129a9565b81019082818303126102be5780519067ffffffffffffffff82116102be570181601f820112156102be578051610684816129cb565b9261069260405194856129a9565b8184528482840101116102be576106ae918480850191016127c4565b9082610623565b6040513d6000823e3d90fd5b346102be57600319602036820181136102be5760043567ffffffffffffffff928382116102be576101409082360301126102be576106fd6132ed565b6040519261070a8461293f565b61071682600401612838565b845261072460248301612a69565b602085015261073560448301612916565b604085015261074660648301612916565b91606092606086015261075b60848201612838565b608086015261076c60a482016129e7565b60a086015261077d60c48201612838565b60c086015261078f3660e48301612b04565b60e08601526101248101359182116102be5701366023820112156102be576004810135926107bc84612a51565b936107ca60405195866129a9565b80855260246060602087019202840101923684116102be57602401905b838210610808576020610800888861010082015261336a565b604051908152f35b8285916108153685612a7b565b8152019101906107e7565b346102be5760403660031901126102be5760043561083c612822565b906108456132ed565b80600052600b60205260ff60016040600020015460a81c1615610367578060005260056020526001600160a01b0360406000205416918233036108b5576102bc9261088f836140c2565b6001600160801b0381166108a4575b50613ddb565b6108af908285612e52565b8461089e565b60405163216caf0d60e01b815260048101839052336024820152604490fd5b0390fd5b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b602052602064ffffffffff60406000205460a01c16604051908152f35b346102be5760003660031901126102be57602060405167016345785d8a00008152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260206001600160a01b0360406000205416604051908152f35b346102be5760203660031901126102be57600435600052600b602052602060ff60016040600020015460a81c166040519015158152f35b346102be5760803660031901126102be576109f061280c565b6109f8612822565b6064359167ffffffffffffffff83116102be57366023840112156102be57826004013591610a25836129cb565b92610a3360405194856129a9565b80845236602482870101116102be5760208160009260246102bc9801838801378501015260443591612d8b565b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b602052610638610aac6004604060002001612cc4565b6040519182916020835260208301906128b2565b346102be5760203660031901126102be576020610308600435612d54565b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757610b1890613c75565b6040516005821015610b2b576020918152f35b634e487b7160e01b600052602160045260246000fd5b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260206001600160801b0360026040600020015416604051908152f35b346102be5760203660031901126102be57610baf61280c565b6001600160a01b038060005416338103610c7657508116908160005260026020526001600160801b0360406000205416908115610c455781610c179184600052600260205260406000206fffffffffffffffffffffffffffffffff198154169055339061405a565b6040519081527fca7a4a65a94ed2f37538814e00e1cd4c41a78261561e3f3794592f11409cf5af60203392a3005b602483604051907f8410168c0000000000000000000000000000000000000000000000000000000082526004820152fd5b6040516331b339a960e21b81526001600160a01b03919091166004820152336024820152604490fd5b346102be5760603660031901126102be5767ffffffffffffffff6004358181116102be57610cd1903690600401612881565b9190610cdb612822565b916044359081116102be57610cf4903690600401612881565b92610cfd6132ed565b838503610d4a5760005b858110610d1057005b80610d44610d216001938988612c4b565b3584610d36610d31858b8a612c4b565b612af0565b91610d3f6132ed565b6131da565b01610d07565b60448585604051917faec9344000000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b346102be5760403660031901126102be57610d9a61280c565b602435908115158092036102be576001600160a01b031690813314610e0e57336000526008602052604060002082600052602052604060002060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b606460405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152fd5b346102be5760003660031901126102be5760405160006004549060018260011c9160018416918215610f58575b6020948585108414610f42578587948686529182600014610f22575050600114610ec5575b50610eb1925003836129a9565b6106386040519282849384528301906127e7565b84915060046000527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b906000915b858310610f0a575050610eb1935082010185610ea4565b80548389018501528794508693909201918101610ef3565b60ff191685820152610eb195151560051b8501019250879150610ea49050565b634e487b7160e01b600052602260045260246000fd5b92607f1692610e7f565b346102be5760003660031901126102be5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c161561036757600052600b602052602064ffffffffff60406000205460c81c16604051908152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c16156103675761102a90613c75565b600581101580610b2b5760028214908115611066575b8115611054575b6020826040519015158152f35b9050610b2b5760046020911482611047565b5050600381146000611040565b346102be5760203660031901126102be576004356001600160a01b03908181168091036102be578160005416338103610c76575060015491816001600160a01b03198416176001556040519216825260208201527fdcb09aef4bf01068924ccce937981cbe59d25ba08380cf941aaaea4e4bd3960d60403392a2005b346102be5760203660031901126102be5760606101406040516111118161298c565b60008152600060208201526000604082015260008382015260006080820152600060a0820152600060c0820152600060e08201526000610100820152611155612c71565b6101208201520152600435600052600b60205260ff60016040600020015460a81c161561134457600435600052600b602052604060002061123860046040519261119e8461298c565b80546001600160a01b038116855264ffffffffff8160a01c16602086015264ffffffffff8160c81c16604086015260ff8160f01c161515606086015260f81c1515608085015260ff60018201546001600160a01b03811660a0870152818160a01c16151560c0870152818160a81c16151560e087015260b01c16151561010085015261122c60028201612c90565b61012085015201612cc4565b610140820152611249600435613c75565b906005821015610b2b5760026101409214611338575b610638604051928392602084526001600160a01b03815116602085015264ffffffffff602082015116604085015264ffffffffff60408201511660608501526060810151151560808501526080810151151560a08501526001600160a01b0360a08201511660c085015260c0810151151560e085015260e08101511515610100850152610100810151151561012085015261132461012082015183860190604090816001600160801b0391828151168552826020820151166020860152015116910152565b01516101a0808401526101c08301906128b2565b6000606082015261125f565b602460405162b8e7e760e51b81526004356004820152fd5b346102be576020806003193601126102be5760043567ffffffffffffffff81116102be5761138e903690600401612881565b906113976132ed565b6000915b8083106113a457005b6113af838284612c4b565b35926113b96132ed565b6113c284612ba4565b156113df5760248460405190634a5541ef60e01b82526004820152fd5b6113eb84929394612e21565b61163d5761140f82600052600b6020526001600160a01b0360406000205416331490565b156108b55761141d82613282565b82600052600b8087526114366002604060002001612c90565b906001600160801b0392838351168482161015611625578560005281895260ff60406000205460f01c161561160d57906114a482858b61149a7f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa509683895116612a38565b9601511690612a38565b9580600052818a526040600020938a855498600160f81b7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8b1617875560038882169788156115f3575b0197831697886fffffffffffffffffffffffffffffffff198254161790556001600160a01b03809a16958691600584528b604060002054169687945260019b8c6040600020015416946115408b858861405a565b604080518881526001600160801b0392831660208201529290911690820152606090a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce78a604051838152a1813b6115a0575b505050505001919061139b565b813b156102be576000608492819560405197889687956372eba20360e01b875260048701526024860152604485015260648401525af16115e4575b80808080611593565b6115ed90612978565b856115db565b60018101600160a01b60ff60a01b198254161790556114ec565b602486604051906339c6dc7360e21b82526004820152fd5b602486604051906322cad1af60e11b82526004820152fd5b6024826040519063fe19f19f60e01b82526004820152fd5b346102be576020806003193601126102be57600435906116736132ed565b81600052600b815260ff60016040600020015460a81c161561184e5761169882613c75565b6005811015610b2b57600481036116c15760248360405190634a5541ef60e01b82526004820152fd5b600381036116e1576024836040519063fe19f19f60e01b82526004820152fd5b6002146118365761170882600052600b6020526001600160a01b0360406000205416331490565b156108b55781600052600b815260ff60406000205460f01c161561181e5781600052600b8152604060002060ff60f01b19815416905560405191807f0eb069207093cd3e51cd1370d2d369770057fbe29947e577e5fb428c6c6fc78f600080a2600582526001600160a01b036040600020541692833b6117af575b7ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce78383604051908152a1005b833b156102be57600081602481837ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7987f341a0bd90000000000000000000000000000000000000000000000000000000083528760048401525af1156117835761181890612978565b83611783565b602482604051906339c6dc7360e21b82526004820152fd5b602482604051906322cad1af60e11b82526004820152fd5b6024826040519062b8e7e760e51b82526004820152fd5b346102be5760203660031901126102be576004356001600160a01b03908181168091036102be578160005416338103610c765750600a5491816001600160a01b0319841617600a556040519216825260208201527fa2548bd4b805e907c1558a47b5858324fe8bb4a2e1ddfca647eecbf65610eebc60403392a260095460001981019081116119215760407f6bd5c950a8d8df17f772f5af37cb3655737899cbf903264b9795592da439661c91815190600182526020820152a1005b634e487b7160e01b600052601160045260246000fd5b346102be5760203660031901126102be5761195061280c565b6000546001600160a01b03808216923384036119a2576001600160a01b03199350169182911617600055337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf80600080a3005b6040516331b339a960e21b81526001600160a01b0385166004820152336024820152604490fd5b346102be5760203660031901126102be576001600160a01b036119ea61280c565b168015611a095760005260066020526020604060002054604051908152f35b608460405162461bcd60e51b815260206004820152602960248201527f4552433732313a2061646472657373207a65726f206973206e6f74206120766160448201527f6c6964206f776e657200000000000000000000000000000000000000000000006064820152fd5b346102be5760203660031901126102be57600435611aaa6105c78260005260056020526001600160a01b0360406000205416151590565b600052600560205260206001600160a01b0360406000205416604051908152f35b346102be5760203660031901126102be576020611ae9600435612c26565b6001600160a01b0360405191168152f35b346102be5760003660031901126102be5760206001600160a01b0360015416604051908152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c16156103675761043f602091613fdf565b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c1615610367576000611b9982613c75565b6005811015610b2b57600203611bb7575b6020906040519015158152f35b50600052600b602052602060ff60406000205460f01c16611baa565b346102be5760203660031901126102be57600435611bef6132ed565b611bf881612ba4565b15611d3257611c0681613f76565b15611d1257611c1481612c26565b611c1d82612d54565b159081611d09575b81611cf6575b50611cde57602081611c5d7ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce793612c26565b9080600052600783526001600160a01b036040600020926001600160a01b031993848154169055169182600052600684526040600020600019815401905581600052600584526040600020908154169055806000604051937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8280a48152a1005b60249060405190630da9b01360e01b82526004820152fd5b6001600160a01b03915016151582611c2b565b60009150611c25565b60405163216caf0d60e01b81526004810191909152336024820152604490fd5b602490604051907f817cd6390000000000000000000000000000000000000000000000000000000082526004820152fd5b346102be57611d713661284c565b60405191602083019383851067ffffffffffffffff861117611d9d576102bc9460405260008452612d8b565b634e487b7160e01b600052604160045260246000fd5b346102be5760203660031901126102be576020610308600435612ba4565b346102be576020806003193601126102be5760043590611def6132ed565b611df882612ba4565b15611e155760248260405190634a5541ef60e01b82526004820152fd5b611e1e82612e21565b61163d57611e4282600052600b6020526001600160a01b0360406000205416331490565b156108b557611e5082613282565b9180600052600b8252611e696002604060002001612c90565b906001600160801b03938483511685821610156118365781600052600b845260ff60406000205460f01c161561181e57808585611eac611eb69483885116612a38565b9501511690612a38565b9080600052600b84527ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7604060002094855494600160f81b7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8716178755600388861697881561200b575b0197811697886fffffffffffffffffffffffffffffffff198254161790556001600160a01b038096169560058352867f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa508260406000205416978893600b87526001604060002001541694611f948d858861405a565b604080518a81526001600160801b0392831660208201529290911690820152606090a4604051838152a1813b611fc657005b813b156102be576000608492819560405197889687956372eba20360e01b875260048701526024860152604485015260648401525af161200257005b6102bc90612978565b60018101600160a01b60ff60a01b19825416179055611f1f565b346102be5760203660031901126102be576001600160a01b0361204661280c565b16600052600260205260206001600160801b0360406000205416604051908152f35b346102be576102bc6120793661284c565b9161208c6120878433613cfc565b612b33565b613ddb565b346102be5760003660031901126102be576020600954604051908152f35b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c1615610367576120e990613c75565b6005811015610b2b57806020911590811561210a575b506040519015158152f35b6001915014826120ff565b346102be57602060031981813601126102be576004359067ffffffffffffffff908183116102be57610120833603918201126102be576121536132ed565b61010483013590602219018112156102be578201916004830135928284116102be57602481016060916060860280360383136102be5760249061219588612a51565b976121a3604051998a6129a9565b8852888801920101913683116102be57905b878383106123c2578787878251906121cc82612a51565b916121da60405193846129a9565b808352601f196121e982612a51565b018660005b8281106123ac5750505064ffffffffff90814216946001600160801b03968761221682613349565b515116828a61222484613349565b510151168580604061223586613349565b510151168a0116906040519261224a84612923565b83528b830152604082015261225e87613349565b5261226886613349565b506001938660015b8a8c87831061232b57908b846001600160a01b038c60a4810135828116908190036102be57610800956122eb9561231b946122ad60248601612acf565b6122b960448701612acf565b6122c560648801612adc565b916122d288600401612adc565b94846122e060848b01612af0565b966040519d8e61293f565b168c528d8c0152151560408b0152151560608a01521660808801521660a086015260c085015260c4369101612b04565b60e083015261010082015261336a565b88938580604061235f8b8661234f8a8e9a612346828d613356565b5151169a613356565b5101511694600019890190613356565b51015116816040612370888c613356565b510151160116916040519361238485612923565b84528301526040820152612398828b613356565b526123a3818a613356565b50018790612270565b6123b4612c71565b8282880101520187906121ee565b84916123ce3685612a7b565b8152019101906121b5565b346102be5760203660031901126102be5760043580600052600b60205260ff60016040600020015460a81c16156103675760209060009080600052600b8352604060002060ff815460f01c168061246c575b612443575b50506001600160801b0360405191168152f35b61246592506001600160801b03600261245f9201541691613282565b90612a38565b8280612430565b5060ff600182015460a01c161561242b565b346102be5760403660031901126102be5761249761280c565b602435906001600160a01b0380806124ae85612c26565b169216918083146125a857803314908115612583575b5015612519578260005260076020526040600020826001600160a01b03198254161790556124f183612c26565b167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600080a4005b608460405162461bcd60e51b815260206004820152603d60248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f7420746f60448201527f6b656e206f776e6572206f7220617070726f76656420666f7220616c6c0000006064820152fd5b9050600052600860205260406000203360005260205260ff60406000205416846124c4565b608460405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e6560448201527f72000000000000000000000000000000000000000000000000000000000000006064820152fd5b346102be5760203660031901126102be576020611ae96004356129fb565b346102be5760003660031901126102be5760405160006003549060018260011c91600184169182156126eb575b6020948585108414610f42578587948686529182600014610f2257505060011461268e5750610eb1925003836129a9565b84915060036000527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b906000915b8583106126d3575050610eb1935082010185610ea4565b805483890185015287945086939092019181016126bc565b92607f169261265d565b346102be5760203660031901126102be57600435907fffffffff0000000000000000000000000000000000000000000000000000000082168092036102be57817f80ac58cd000000000000000000000000000000000000000000000000000000006020931490811561279a575b8115612770575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483612769565b7f5b5e139f0000000000000000000000000000000000000000000000000000000081149150612762565b60005b8381106127d75750506000910152565b81810151838201526020016127c7565b90602091612800815180928185528580860191016127c4565b601f01601f1916010190565b600435906001600160a01b03821682036102be57565b602435906001600160a01b03821682036102be57565b35906001600160a01b03821682036102be57565b60609060031901126102be576001600160a01b039060043582811681036102be579160243590811681036102be579060443590565b9181601f840112156102be5782359167ffffffffffffffff83116102be576020808501948460051b0101116102be57565b90815180825260208080930193019160005b8281106128d2575050505090565b835180516001600160801b031686528083015167ffffffffffffffff168684015260409081015164ffffffffff1690860152606090940193928101926001016128c4565b359081151582036102be57565b6060810190811067ffffffffffffffff821117611d9d57604052565b610120810190811067ffffffffffffffff821117611d9d57604052565b6040810190811067ffffffffffffffff821117611d9d57604052565b67ffffffffffffffff8111611d9d57604052565b610160810190811067ffffffffffffffff821117611d9d57604052565b90601f8019910116810190811067ffffffffffffffff821117611d9d57604052565b67ffffffffffffffff8111611d9d57601f01601f191660200190565b35906001600160801b03821682036102be57565b612a1e6105c78260005260056020526001600160a01b0360406000205416151590565b60005260076020526001600160a01b036040600020541690565b6001600160801b03918216908216039190821161192157565b67ffffffffffffffff8111611d9d5760051b60200190565b359064ffffffffff821682036102be57565b91908260609103126102be57604051612a9381612923565b8092612a9e816129e7565b825260208101359067ffffffffffffffff821682036102be576040612aca918193602086015201612a69565b910152565b3580151581036102be5790565b356001600160a01b03811681036102be5790565b356001600160801b03811681036102be5790565b91908260409103126102be57604051612b1c8161295c565b6020808294612b2a81612838565b84520135910152565b15612b3a57565b608460405162461bcd60e51b815260206004820152602d60248201527f4552433732313a2063616c6c6572206973206e6f7420746f6b656e206f776e6560448201527f72206f7220617070726f766564000000000000000000000000000000000000006064820152fd5b80600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260ff60016040600020015460a01c1690565b15612be257565b606460405162461bcd60e51b815260206004820152601860248201527f4552433732313a20696e76616c696420746f6b656e20494400000000000000006044820152fd5b60005260056020526001600160a01b0360406000205416612c48811515612bdb565b90565b9190811015612c5b5760051b0190565b634e487b7160e01b600052603260045260246000fd5b60405190612c7e82612923565b60006040838281528260208201520152565b90604051612c9d81612923565b6040819360018154916001600160801b0392838116865260801c6020860152015416910152565b908154612cd081612a51565b92604093612ce160405191826129a9565b82815280946020809201926000526020600020906000935b858510612d0857505050505050565b60018481928451612d1881612923565b64ffffffffff87546001600160801b038116835267ffffffffffffffff8160801c168584015260c01c1686820152815201930194019391612cf9565b80600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260ff60016040600020015460b01c1690565b90612daf939291612d9f6120878433613cfc565b612daa838383613ddb565b6146f5565b15612db657565b60405162461bcd60e51b815260206004820152603260248201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560448201527f63656976657220696d706c656d656e74657200000000000000000000000000006064820152608490fd5b80600052600b60205260ff60016040600020015460a81c161561036757600052600b60205260406000205460f81c90565b929192612e5d6132ed565b612e6681612ba4565b6131c257612e8a81600052600b6020526001600160a01b0360406000205416331490565b918215806131b2575b6108b557600092828452602093600585526001600160a01b039660409388858420541693806131a6575b6131685788811698891561313f576001600160801b0380841693841561310f57612f00612ee98a613fdf565b8a8852600b8c5260028a8920015460801c90612a38565b82811686116130c75750918491612f5f612f2d612f9895600b8e8e8c525260028c8b20015460801c6140ea565b8b8952600b8d5260028b8a200190836fffffffffffffffffffffffffffffffff1983549260801b169116178155612c90565b90612f7a818d8401511692828c818351169201511690612a38565b161115613098575b888652600b8a526001888720015416928361405a565b88867f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d898851868152a4823314158061308e575b612fff575b5050507ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce793945051908152a1565b823b1561308a5760847ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7979883865195869485937f13375c3b0000000000000000000000000000000000000000000000000000000085528a6004860152336024860152604485015260648401525af161307b575b859481612fd1565b61308490612978565b38613073565b5080fd5b50823b1515612fcc565b888652600b8a5287862060018101600160a01b60ff60a01b1982541617905560ff60f01b198154169055612f82565b88517fa1fb2bbc000000000000000000000000000000000000000000000000000000008152600481018b90526001600160801b03928316602482015291166044820152606490fd5b6024898951907fd2aabcd90000000000000000000000000000000000000000000000000000000082526004820152fd5b600486517fc61a0e9e000000000000000000000000000000000000000000000000000000008152fd5b85896064928751927f5b97ed720000000000000000000000000000000000000000000000000000000084526004840152336024840152166044820152fd5b50838982161415612ebd565b506131bc82613f76565b15612e93565b60249060405190634a5541ef60e01b82526004820152fd5b9291926131e681612ba4565b6131c25761320a81600052600b6020526001600160a01b0360406000205416331490565b91821580613272575b6108b557600092828452602093600585526001600160a01b03966040938885842054169380613266575b6131685788811698891561313f576001600160801b0380841693841561310f57612f00896140c2565b5083898216141561323d565b5061327c82613f76565b15613213565b64ffffffffff80421682600052600b602052604060002091825482828260a01c1610156132e35760c81c1611156132d15760040154600110156132c857612c48906141d9565b612c4890614105565b6001600160801b039150600201541690565b5050505050600090565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016300361331f57565b60046040517fa1c0d6e5000000000000000000000000000000000000000000000000000000008152fd5b805115612c5b5760200190565b8051821015612c5b5760209160051b010190565b906001600160a01b036001541660206001600160a01b0360c0850151166024604051809481937fdcf844a700000000000000000000000000000000000000000000000000000000835260048301525afa80156106b557600090613c41575b6133eb91506001600160801b0360a08501511690602060e08601510151916143c4565b6001600160801b0381511661010084015164ffffffffff6020860151168215613c175781518015613bed577f00000000000000000000000000000000000000000000000000000000000000008111613bbc575064ffffffffff604061344f84613349565b51015116811015613b655750600090819082815184905b808210613ad2575050505064ffffffffff421664ffffffffff8216811015613a925750506001600160801b0316808203613a5b5750506009549283600052600b6020526040600020916001600160801b0381511660028401906fffffffffffffffffffffffffffffffff198254161790556001600160a01b0360c083015116600184015490750100000000000000000000000000000000000000000060408501511515928654927fffffffffffffffffff0000ff000000000000000000000000000000000000000076ff000000000000000000000000000000000000000000006060890151151560b01b16921617171760018601556001600160a01b0384511691610100850151926040613581855195600019870190613356565b510151927fff000000000000000000000000000000000000000000000000000000000000007eff0000000000000000000000000000000000000000000000000000000000007dffffffffff0000000000000000000000000000000000000000000000000078ffffffffff000000000000000000000000000000000000000060208b015160a01b169660c81b169460f01b16911617171717845560005b81811061398b575050600185016009556001600160a01b0360c08301511660005260026020526001600160801b0380604060002054168160208401511601166001600160a01b0360c0840151166000526040600020906fffffffffffffffffffffffffffffffff198254161790556001600160a01b036080830151168015613947576136c86136c28760005260056020526001600160a01b0360406000205416151590565b15614503565b6136d186612d54565b158061393e575b80613936575b61391e5760207ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7916137296136c28960005260056020526001600160a01b0360406000205416151590565b806000526006825260406000206001815401905587600052600582526040600020816001600160a01b0319825416179055876040519160007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8180a4878152a16137b96001600160a01b0360c0840151166001600160801b0380845116816020860151160116903090339061454e565b6001600160801b03604082015116806138ef575b507fef3d668acee46576ad5d407c42ab4d0cde13f3cd70b28f09a0fb9e3bf5bf09cb6138ac6001600160a01b03845116926001600160a01b03608086015116946001600160a01b0360c082015116966138e46138c460408401511515928c606086015115156001600160a01b0360e061010089015194549864ffffffffff6040519a6138588c61295c565b818160a01c168c5260c81c1660208b01520151511695604051998a99610160948b523360208c015260408b0190604090816001600160801b0391828151168552826020820151166020860152015116910152565b60a089015260c08801528060e08801528601906128b2565b926101008501906020908164ffffffffff91828151168552015116910152565b6101408301520390a4565b613918906001600160a01b0360c0850151166001600160a01b0360e0860151511690339061454e565b386137cd565b60248660405190630da9b01360e01b82526004820152fd5b5060006136de565b508015156136d8565b606460405162461bcd60e51b815260206004820152602060248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152fd5b61399a81610100860151613356565b519060048601549168010000000000000000831015611d9d5760018301806004890155831015612c5b5760019260048801600052602060002001906001600160801b03815116908254917fffffff00000000000000000000000000000000000000000000000000000000007cffffffffff000000000000000000000000000000000000000000000000604077ffffffffffffffff00000000000000000000000000000000602086015160801b1694015160c01b16931617171790550161361d565b60449250604051917fd90b7e3900000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b6040517f210aec0e00000000000000000000000000000000000000000000000000000000815264ffffffffff918216600482015291166024820152604490fd5b9193509193613af6906001600160801b03613aed8588613356565b515116906140ea565b9364ffffffffff806040613b0a8685613356565b51015116941680851115613b28575060018493019192919092613466565b8385606492604051927f7b0bada8000000000000000000000000000000000000000000000000000000008452600484015260248301526044820152fd5b64ffffffffff6040613b7684613349565b5101516040517fb4c9e52c00000000000000000000000000000000000000000000000000000000815264ffffffffff938416600482015291169091166024820152604490fd5b602490604051907f4757689b0000000000000000000000000000000000000000000000000000000082526004820152fd5b60046040517f3952c64e000000000000000000000000000000000000000000000000000000008152fd5b60046040517f6095d3bc000000000000000000000000000000000000000000000000000000008152fd5b506020813d602011613c6d575b81613c5b602093836129a9565b810103126102be576133eb90516133c8565b3d9150613c4e565b80600052600b602052604060002060ff600182015460a01c16600014613c9c575050600490565b805460f81c613cf5575460a01c64ffffffffff164210613cef57613cbf81613282565b90600052600b6020526001600160801b038060026040600020015416911610600014613cea57600190565b600290565b50600090565b5050600390565b906001600160a01b038080613d1084612c26565b16931691838314938415613d43575b508315613d2d575b50505090565b613d39919293506129fb565b1614388080613d27565b909350600052600860205260406000208260005260205260ff604060002054169238613d1f565b15613d7157565b608460405162461bcd60e51b815260206004820152602560248201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060448201527f6f776e65720000000000000000000000000000000000000000000000000000006064820152fd5b90613e049291613dea83612c26565b916001600160a01b03948593848094169687911614613d6a565b1690811580613f0d57613e1684612d54565b159081613f04575b5080613efb575b613ee35791808492613e657ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce796602096613e5e85612c26565b1614613d6a565b60009382855260078652604085206001600160a01b031990818154169055818652600687526040862060001981540190558286526040862060018154019055838652600587528260408720918254161790557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6040519580a48152a1565b60248360405190630da9b01360e01b82526004820152fd5b50831515613e25565b90501538613e1e565b608460405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f2061646460448201527f72657373000000000000000000000000000000000000000000000000000000006064820152fd5b60009080825260056020526001600160a01b038060408420541692833314938415613fbb575b50508215613fa957505090565b909150613fb633926129fb565b161490565b60ff9294509060409181526008602052818120338252602052205416913880613f9c565b80600052600b602052613ff86002604060002001612c90565b81600052600b602052604060002060ff600182015460a01c1660001461402b57506001600160801b039150602001511690565b5460f81c61403d5750612c4890613282565b612c4891506001600160801b036040818351169201511690612a38565b916001600160a01b03604051927fa9059cbb000000000000000000000000000000000000000000000000000000006020850152166024830152604482015260448152608081019181831067ffffffffffffffff841117611d9d576140c0926040526145b9565b565b612c48906140cf81613fdf565b90600052600b60205260026040600020015460801c90612a38565b9190916001600160801b038080941691160191821161192157565b64ffffffffff61413a600091838352600b60205280806040852054818160a01c1693849160c81c160316918142160316614888565b91808252600b602052600460408320018054156141c55790829167ffffffffffffffff93526141976020832054828452600b6020526141926001600160801b03968760026040882001541696879360801c1690614978565b6149e6565b9283136141ad5750506141a990614ad0565b1690565b60029350604092508152600b60205220015460801c90565b602483634e487b7160e01b81526032600452fd5b64ffffffffff90814216906000908152600b6020526040908181208251936142008561298c565b8154956001600160a01b039182881687526020870197828160a01c168952828160c81c168789015260ff8160f01c161515606089015260f81c1515608088015260ff600193600186015490811660a08a0152818160a01c16151560c08a0152818160a81c16151560e08a015260b01c16151561010088015261014061429b600461428c60028801612c90565b966101208b0197885201612cc4565b97019187835280876142ad889a613349565b5101511693828288965b161061438c5750916143416141929284888161434698976001600160801b039e8f6142e38b8a51613356565b5151169d8a8f9b602061430067ffffffffffffffff928d51613356565b51015116998483614312848451613356565b51015116965081156143805761433092935051906000190190613356565b5101511680925b0316920316614888565b614978565b92831361435f5750506143598391614ad0565b16011690565b51602001519293928316928416831015915061437b9050575090565b905090565b50505051168092614337565b8094986001600160801b0390816143a48c8851613356565b51511601169801938282808a6143bb898951613356565b510151166142b7565b9092916143cf612c71565b936001600160801b03928381169182156144db5767016345785d8a00008082116144a45780851161446d57506144198561440a8193866155fc565b169460208901958652846155fc565b1691846144306040890194808652828751166140ea565b1610156144575761444984918261445295511690612a38565b91511690612a38565b168252565b634e487b7160e01b600052600160045260246000fd5b84604491604051917f4fea5c1a00000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b60449250604051917f47152d6700000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b505050505090506040516144ee81612923565b60008152600060208201526000604082015290565b1561450a57565b606460405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152fd5b9290604051927f23b872dd0000000000000000000000000000000000000000000000000000000060208501526001600160a01b03809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff841117611d9d576140c0926040525b6001600160a01b0316906146196040516145d28161295c565b6020938482527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564858301526000808587829751910182855af16146136146c5565b916156ab565b80519182159184831561469e575b5050509050156146345750565b6084906040519062461bcd60e51b82526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f742073756363656564000000000000000000000000000000000000000000006064820152fd5b91938180945001031261308a578201519081151582036146c2575080388084614627565b80fd5b3d156146f0573d906146d6826129cb565b916146e460405193846129a9565b82523d6000602084013e565b606090565b9290803b1561487f5761475f916020916001600160a01b0394604051809581948293897f150b7a02000000000000000000000000000000000000000000000000000000009b8c865233600487015216602485015260448401526080606484015260848301906127e7565b03916000968791165af19082908261481e575b50506147f8576147806146c5565b805190816147f35760405162461bcd60e51b815260206004820152603260248201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560448201527f63656976657220696d706c656d656e74657200000000000000000000000000006064820152608490fd5b602001fd5b7fffffffff00000000000000000000000000000000000000000000000000000000161490565b909192506020813d602011614877575b8161483b602093836129a9565b8101031261308a5751907fffffffff00000000000000000000000000000000000000000000000000000000821682036146c25750903880614772565b3d915061482e565b50505050600190565b600160ff1b80821490811561496e575b5061494457600081121561493b576148c1816000035b6000841215614934578360000390614b0c565b917f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83116148fd57600019911813156148f75790565b60000390565b60449250604051917fd49c26b300000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b8390614b0c565b6148c1816148ae565b60046040517f9fe2b450000000000000000000000000000000000000000000000000000000008152fd5b9050821438614898565b80614993575061498e57670de0b6b3a764000090565b600090565b90670de0b6b3a76400008083146149e05750806149b8575050670de0b6b3a764000090565b670de0b6b3a764000081146149dc576149d790614192612c4893614c06565b614d48565b5090565b91505090565b600160ff1b808214908115614ac6575b50614a9c576000811215614a9357614a1f816000035b6000841215614a8c5783600003906155fc565b917f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8311614a5557600019911813156148f75790565b60449250604051917f120b5b4300000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b83906155fc565b614a1f81614a0c565b60046040517fa6070c25000000000000000000000000000000000000000000000000000000008152fd5b90508214386149f6565b60008112614adb5790565b602490604051907f2463f3d50000000000000000000000000000000000000000000000000000000082526004820152fd5b670de0b6b3a7640000916000198383099280830292838086109503948086039514614bc85782851015614b8c57908291096001821901821680920460028082600302188083028203028083028203028083028203028083028203028083028203028092029003029360018380600003040190848311900302920304170290565b82606492604051927f63a05778000000000000000000000000000000000000000000000000000000008452600484015260248301526044820152fd5b505080925015614bd6570490565b634e487b7160e01b600052601260045260246000fd5b8015614bd6576ec097ce7bc90715b34b9f10000000000590565b80600080831315614d1757670de0b6b3a764000092838112614cf457506001925b808305906001600160801b03821160071b91821c9167ffffffffffffffff831160061b92831c63ffffffff811160051b90811c61ffff811160041b90811c60ff811160031b90811c91600f831160021b92831c93600197600160038711811b96871c11961717171717171781810294811d90828214614ce857506706f05b59d3b20000905b848213614cbc5750505050500290565b808391020590671bc16d674ec80000821215614cdb575b831d90614cac565b8091950194831d90614cd3565b93505093925050020290565b6000199392508015614bd6576ec097ce7bc90715b34b9f10000000000591614c27565b602483604051907f059b101b0000000000000000000000000000000000000000000000000000000082526004820152fd5b6000811215614d775768033dd1780914b97114198112613cef57614d6e90600003614d48565b612c4890614bec565b680a688906bd8affffff81136155cb57670de0b6b3a764000080604092831b05907780000000000000000000000000000000000000000000000067ff0000000000000083166154ae575b66ff00000000000083166153a6575b65ff000000000083166152a6575b64ff0000000083166151ae575b63ff00000083166150be575b62ff00008316614fd6575b61ff008316614ef6575b60ff8316614e1f575b02911c60bf031c90565b60808316614ee4575b838316614ed2575b60208316614ec0575b60108316614eae575b60088316614e9c575b60048316614e8a575b60028316614e78575b6001831615614e15576801000000000000000102831c614e15565b6801000000000000000102831c614e5d565b6801000000000000000302831c614e54565b6801000000000000000602831c614e4b565b6801000000000000000b02831c614e42565b6801000000000000001602831c614e39565b6801000000000000002c02831c614e30565b6801000000000000005902831c614e28565b6180008316614fc4575b6140008316614fb2575b6120008316614fa0575b6110008316614f8e575b6108008316614f7c575b6104008316614f6a575b6102008316614f58575b610100831615614e0c57680100000000000000b102831c614e0c565b6801000000000000016302831c614f3c565b680100000000000002c602831c614f32565b6801000000000000058c02831c614f28565b68010000000000000b1702831c614f1e565b6801000000000000162e02831c614f14565b68010000000000002c5d02831c614f0a565b680100000000000058b902831c614f00565b6280000083166150ac575b62400000831661509a575b622000008316615088575b621000008316615076575b620800008316615064575b620400008316615052575b620200008316615040575b62010000831615614e02576801000000000000b17202831c614e02565b680100000000000162e402831c615023565b6801000000000002c5c802831c615018565b68010000000000058b9102831c61500d565b680100000000000b172102831c615002565b68010000000000162e4302831c614ff7565b680100000000002c5c8602831c614fec565b6801000000000058b90c02831c614fe1565b6380000000831661519c575b6340000000831661518a575b63200000008316615178575b63100000008316615166575b63080000008316615154575b63040000008316615142575b63020000008316615130575b6301000000831615614df75768010000000000b1721802831c614df7565b6801000000000162e43002831c615112565b68010000000002c5c86002831c615106565b680100000000058b90c002831c6150fa565b6801000000000b17217f02831c6150ee565b680100000000162e42ff02831c6150e2565b6801000000002c5c85fe02831c6150d6565b68010000000058b90bfc02831c6150ca565b6480000000008316615294575b6440000000008316615282575b6420000000008316615270575b641000000000831661525e575b640800000000831661524c575b640400000000831661523a575b6402000000008316615228575b640100000000831615614deb57680100000000b17217f802831c614deb565b68010000000162e42ff102831c615209565b680100000002c5c85fe302831c6151fc565b6801000000058b90bfce02831c6151ef565b68010000000b17217fbb02831c6151e2565b6801000000162e42fff002831c6151d5565b68010000002c5c8601cc02831c6151c8565b680100000058b90c0b4902831c6151bb565b658000000000008316615394575b654000000000008316615382575b652000000000008316615370575b65100000000000831661535e575b65080000000000831661534c575b65040000000000831661533a575b650200000000008316615328575b65010000000000831615614dde576801000000b17218355102831c614dde565b680100000162e430e5a202831c615308565b6801000002c5c863b73f02831c6152fa565b68010000058b90cf1e6e02831c6152ec565b680100000b1721bcfc9a02831c6152de565b68010000162e43f4f83102831c6152d0565b680100002c5c89d5ec6d02831c6152c2565b6801000058b91b5bc9ae02831c6152b4565b6680000000000000831661549c575b6640000000000000831661548a575b66200000000000008316615478575b66100000000000008316615466575b66080000000000008316615454575b66040000000000008316615442575b66020000000000008316615430575b6601000000000000831615614dd05768010000b17255775c0402831c614dd0565b6801000162e525ee054702831c61540f565b68010002c5cc37da949202831c615400565b680100058ba01fb9f96d02831c6153f1565b6801000b175effdc76ba02831c6153e2565b680100162f3904051fa102831c6153d3565b6801002c605e2e8cec5002831c6153c4565b68010058c86da1c09ea202831c6153b5565b67800000000000000083166155ac575b674000000000000000831661559a575b6720000000000000008316615588575b6710000000000000008316615576575b6708000000000000008316615564575b6704000000000000008316615552575b6702000000000000008316615540575b670100000000000000831615614dc157680100b1afa5abcbed6102831c614dc1565b68010163da9fb33356d802831c61551e565b680102c9a3e778060ee702831c61550e565b6801059b0d31585743ae02831c6154fe565b68010b5586cf9890f62a02831c6154ee565b6801172b83c7d517adce02831c6154de565b6801306fe0a31b7152df02831c6154ce565b5077b504f333f9de6484800000000000000000000000000000006154be565b602490604051907f0360d0280000000000000000000000000000000000000000000000000000000082526004820152fd5b9091906000198382098382029182808310920391808303921461569a57670de0b6b3a7640000908183101561566357947faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac1066994950990828211900360ee1b910360121c170290565b60449086604051917f5173648d00000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b5050670de0b6b3a764000090049150565b9192901561570c57508151156156bf575090565b3b156156c85790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b82519091501561571f5750805190602001fd5b6108d49060405191829162461bcd60e51b83526020600484015260248301906127e756fea164736f6c6343000817000a"; - bytes public constant BYTECODE_LOCKUP_LINEAR = - hex"60a034620003e757601f196001600160401b03601f62004c533881900382810185168601919084831187841017620003ec57808792606094604052833981010312620003e75783516001600160a01b03928382169291839003620003e7576020918287015196858816809803620003e75760400151948516809503620003e7576200008962000402565b90601c82527f5361626c696572205632204c6f636b7570204c696e656172204e46540000000084830152620000bd62000402565b601181527029a0a116ab1916a627a1a5aaa816a624a760791b8582015230608052600080546001600160a01b031990811688178255600180548216909b178b5596817fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808180a38351858111620003d35760039485548c81811c91168015620003c8575b89821014620003b45790818684931162000361575b508890868311600114620002f8578492620002ec575b505060001982871b1c1916908b1b1784555b8151948511620002d8576004958654998b8b811c9b168015620002cd575b828c1014620002ba57848b1162000271575b869798999a50819487116001146200020a57505093620001fe575b505082871b92600019911b1c19161790555b600a541617600a556009556040516148309081620004238239608051816137190152f35b015191503880620001c8565b8883528183208c9890969594939116915b8282106200025757505085116200023c575b50505050811b019055620001da565b01519060f884600019921b161c19169055388080806200022d565b8484015187558c989096019593840193908101906200021b565b87835281832085880160051c81019b838910620002af575b860160051c019a8c905b8c8110620002a3575050620001ad565b848155018c9062000293565b909b508b9062000289565b634e487b7160e01b835260228852602483fd5b9a607f169a6200019b565b634e487b7160e01b81526041600452602490fd5b0151905038806200016b565b908c8e9416918886528a862092865b8c82821062000341575050841162000328575b505050811b0184556200017d565b015160001983891b60f8161c191690553880806200031a565b91929395968291958786015181550195019301908f959493929162000307565b9091508684528884208680850160051c8201928b8610620003aa575b918f91869594930160051c01915b8281106200039b57505062000155565b8681558594508f91016200038b565b925081926200037d565b634e487b7160e01b84526022600452602484fd5b90607f169062000140565b634e487b7160e01b82526041600452602482fd5b600080fd5b634e487b7160e01b600052604160045260246000fd5b60408051919082016001600160401b03811183821017620003ec5760405256fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a714612dd85750806306fdde0314612d14578063081812fc14612cf5578063095ea7b314612b665780631400ecec14612ac65780631c1cdd4c14612a615780631e99d56914612a4357806323b872dd14612a1957806339a73c03146129d857806340e58ee51461273a578063425d30dd1461271b57806342842e0e146126cb57806342966c68146125415780634857501f146124b75780634869e12d1461247c5780635fe3b567146124555780636352211e146124255780636d0cee75146123cf57806370a082311461232657806375829def14612293578063780a82c8146122435780637cad6cd1146121725780637de6b1db14611f925780638659c27014611c71578063894e9a0d14611a1d5780638bad38dd146119a05780638f69b993146119045780639067b677146118b157806395d89b41146117a257806396ce143114611683578063a22cb465146115b2578063a2ffb897146111c5578063a6202bf2146110c8578063a80fc07114611076578063ab167ccc14610f3d578063ad35efd414610edb578063b256456914610ebc578063b88d4fde14610e32578063b8a3be6614610dfd578063b971302a14610dae578063bc063e1a14610d8b578063bc2be1be14610d3b578063c156a11d146109c1578063c87b56dd14610887578063cc364f48146107d9578063d4dbd20b14610787578063d511609f1461073b578063d975dfed146106ef578063e985e9c51461069a578063ea5ead1914610674578063eac8f5b81461060b578063f590c176146105e2578063f851a440146105bc5763fdd46d601461027357600080fd5b346105b95760603660031901126105b95760043561028f612f07565b610297613047565b906102a061370f565b6102a98361313a565b6105a1576102cd83600052600b6020526001600160a01b0360406000205416331490565b90811580610591575b61057257838552602092600584526001600160a01b0391826040882054169380610566575b61054057828116928315610516576001600160801b038084169384156104fe57610324896140f8565b82811686116104ca5750938093926103ca9261038f6103578d9a99988d8c52600b8d52600260408d20015460801c614120565b8c8b52600b8c5261038a600260408d20019182906001600160801b036001600160801b031983549260801b169116179055565b613226565b906103ab818c840151169282604081835116920151169061309a565b16111561049a575b898852600b89526001604089200154169283614090565b82877f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d88604051868152a48233141580610490575b610432575b837ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce78688604051908152a180f35b823b1561048c57608484928360405195869485936313375c3b60e01b85528b6004860152336024860152604485015260648401525af1610474575b8080610404565b61047d90612f83565b61048857823861046d565b8280fd5b8380fd5b50823b15156103ff565b898852600b89526040882060018101600160c81b60ff60c81b1982541617905560ff60f01b1981541690556103b3565b60405163287ecaef60e21b8152600481018b90526001600160801b03928316602482015291166044820152606490fd5b0390fd5b6024896040519063d2aabcd960e01b82526004820152fd5b60046040517fc61a0e9e000000000000000000000000000000000000000000000000000000008152fd5b858360649260405192632dcbf6b960e11b84526004840152336024840152166044820152fd5b508383821614156102fb565b60405163216caf0d60e01b815260048101859052336024820152604490fd5b5061059b8461376b565b156102d6565b60248360405190634a5541ef60e01b82526004820152fd5b80fd5b50346105b957806003193601126105b9576001600160a01b036020915416604051908152f35b50346105b95760203660031901126105b9576020610601600435613327565b6040519015158152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760016040836001600160a01b039360209552600b855220015416604051908152f35b6024906040519062b8e7e760e51b82526004820152fd5b50346105b95760403660031901126105b957600435610691612f07565b610297826140f8565b50346105b95760403660031901126105b9576106b4612ef1565b60406106be612f07565b926001600160a01b0380931681526008602052209116600052602052602060ff604060002054166040519015158152f35b50346105b95760203660031901126105b95760ff6001604060043593848152600b60205220015460d01c161561065d5761072a6020916140f8565b6001600160801b0360405191168152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760408260029260209452600b845220015460801c604051908152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760036040836001600160801b039360209552600b855220015416604051908152f35b50346105b95760203660031901126105b9576004356107f6613207565b50808252600b60205260ff600160408420015460d01c161561065d578160409160609352600b60205220600181549164ffffffffff918291015460a01c16906040519261084284612fd1565b818160a01c16845260c81c166020830152604082015261088560405180926040908164ffffffffff91828151168552826020820151166020860152015116910152565bf35b50346105b9576020806003193601126109b1576004356108c56108c08260005260056020526001600160a01b0360406000205416151590565b613171565b826001600160a01b03600a5416916044604051809481937fe9dc637500000000000000000000000000000000000000000000000000000000835230600484015260248301525afa9283156109b5578093610934575b5050610930604051928284938452830190612ecc565b0390f35b909192503d8082843e6109478184613009565b82019183818403126109b15780519067ffffffffffffffff8211610488570182601f820112156109b15780519161097d8361302b565b9361098b6040519586613009565b8385528584840101116105b95750906109a991848085019101612ea9565b90388061091a565b5080fd5b604051903d90823e3d90fd5b50346105b95760403660031901126105b9576004356109de612f07565b906109e761370f565b808352602091600b835260ff600160408620015460d01c1615610d2457818452600583526001600160a01b03806040862054169081330361057257610a2b846140f8565b906001600160801b0390818316918215938415610a52575b89610a4f898989613574565b80f35b610a5a61370f565b610a638861313a565b610d0c57610a8788600052600b6020526001600160a01b0360406000205416331490565b94851580610cfc575b610cdd57888b5260058a528360408c2054169580610cd3575b610caf57861561051657610c9757610ac0886140f8565b8281168511610c67575090610b20610aed8b969594938a8852600b8c52600260408920015460801c614120565b898752600b8b5261038a600260408920019182906001600160801b036001600160801b031983549260801b169116179055565b90610b3c818b840151169282604081835116920151169061309a565b161115610c37575b868452600b8852600160408520015416610b5f828683614090565b84877f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d8a604051868152a48133141580610c2d575b610bd2575b5050507ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7610a4f94604051858152a13880808080610a43565b813b156104885782916084839260405194859384926313375c3b60e01b84528b600485015233602485015289604485015260648401525af1610c15575b80610b99565b610c1e90612f83565b610c29578438610c0f565b8480fd5b50813b1515610b94565b868452600b88526040842060018101600160c81b60ff60c81b1982541617905560ff60f01b198154169055610b44565b60405163287ecaef60e21b8152600481018a90526001600160801b03928316602482015291166044820152606490fd5b6024886040519063d2aabcd960e01b82526004820152fd5b6064898860405191632dcbf6b960e11b835260048301523360248301526044820152fd5b5085871415610aa9565b60405163216caf0d60e01b8152600481018a9052336024820152604490fd5b50610d068961376b565b15610a90565b60248860405190634a5541ef60e01b82526004820152fd5b6024826040519062b8e7e760e51b82526004820152fd5b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760408264ffffffffff9260209452600b8452205460a01c16604051908152f35b50346105b957806003193601126105b957602060405167016345785d8a00008152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d576040826001600160a01b039260209452600b8452205416604051908152f35b50346105b95760203660031901126105b95760ff600160406020936004358152600b855220015460d01c166040519015158152f35b50346105b95760803660031901126105b957610e4c612ef1565b610e54612f07565b906064359067ffffffffffffffff821161048c573660238301121561048c5781600401359284610e838561302b565b93610e916040519586613009565b85855236602487830101116109b15785610a4f96602460209301838801378501015260443591613291565b50346105b95760203660031901126105b957602061060160043561325a565b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d57610f149061340e565b604051906005811015610f2957602092508152f35b602483634e487b7160e01b81526021600452fd5b50346105b9576101403660031901126105b957610f5861370f565b610f60613207565b9064ffffffffff80421680845260c43582811681036110715781018216602085015260e4359081831682036110715701166040830152606435916001600160a01b03918284168094036105b957506084358015158091036110715760a435908115158092036110715760243594848616809603611071576004359585871680970361107157604435906001600160801b038216809203611071576040519761100789612fb4565b8852602088015260408701526060860152608085015260a084015260c0830152604061010319360112611071576040519161104183612fed565b61010435918216820361107157826110699260209452610124358482015260e082015261384f565b604051908152f35b600080fd5b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760026040836001600160801b039360209552600b855220015416604051908152f35b50346105b95760203660031901126105b9576110e2612ef1565b6001600160a01b038083541633810361119c575081169081835260026020526001600160801b0360408420541690811561116b578161113c918486526002602052604086206001600160801b031981541690553390614090565b6040519081527fca7a4a65a94ed2f37538814e00e1cd4c41a78261561e3f3794592f11409cf5af60203392a380f35b602483604051907f8410168c0000000000000000000000000000000000000000000000000000000082526004820152fd5b6040516331b339a960e21b81526001600160a01b03919091166004820152336024820152604490fd5b50346105b95760603660031901126105b95767ffffffffffffffff600435818111610488576111f8903690600401612f52565b90611201612f07565b92604435908111610c295761121a903690600401612f52565b61122594919461370f565b80840361157b5791926001600160a01b038216159290865b818110611248578780f35b6112538183886131e1565b359061126081858a6131e1565b356001600160801b03811681036110715761127961370f565b6112828361313a565b6105a1576112a683600052600b6020526001600160a01b0360406000205416331490565b80158061156b575b61057257838b5260056020526001600160a01b0360408c2054169080611558575b61152b5787610516576001600160801b03821615611513576112f0846140f8565b6001600160801b0381166001600160801b038416116114e15750908a91848352600b80602052611360600261038a611331868360408a20015460801c614120565b918988528460205260408820019182906001600160801b036001600160801b031983549260801b169116179055565b6001600160801b03611384816020840151169282604081835116920151169061309a565b1611156114b1575b8584526020526001600160a01b036001604085200154166113b76001600160801b0384168a83614090565b6040516001600160801b0384168152867f40b88e5c41c5a97ffb7b6ef88a0a2d505aa0c634cf8a0275cb236ea7dd87ed4d60206001600160a01b038d1693a480331415806114a7575b61143b575b5050507ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce76020600193604051908152a10161123d565b803b15610488576001600160a01b036084898580946001600160801b0360405197889687956313375c3b60e01b87528d60048801523360248801521660448601521660648401525af161148f575b80611405565b61149890612f83565b6114a3578838611489565b8880fd5b50803b1515611400565b858452806020526040842060018101600160c81b60ff60c81b1982541617905560ff60f01b19815416905561138c565b60405163287ecaef60e21b8152600481018690526001600160801b038481166024830152919091166044820152606490fd5b6024846040519063d2aabcd960e01b82526004820152fd5b6064846001600160a01b038960405192632dcbf6b960e11b84526004840152336024840152166044820152fd5b50806001600160a01b03881614156112cf565b506115758461376b565b156112ae565b83604491604051917faec9344000000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b50346105b95760403660031901126105b9576115cc612ef1565b60243590811515809203611071576001600160a01b03169081331461163f5733835260086020526040832082600052602052604060002060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a380f35b606460405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152fd5b50346105b9576101603660031901126105b95761169e61370f565b604051906116ab82612fb4565b6116b3612ef1565b82526116bd612f07565b60208301526116ca613047565b60408301526001600160a01b03906064358281168103611071576060840152608435801515810361107157608084015260a43580151581036110715760a084015260603660c31901126105b9575060405161172481612fd1565b64ffffffffff60c435818116810361107157825260e435818116810361107157602083015261010435908116810361107157604082015260c0830152604061012319360112611071576040519161177a83612fed565b61012435918216820361107157826110699260209452610144358482015260e082015261384f565b50346105b957806003193601126105b95760405190806004549160018360011c92600185169485156118a7575b602095868610811461189357858852879493929187908215611871575050600114611817575b505061180392500383613009565b610930604051928284938452830190612ecc565b90859250600482527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b858310611859575050611803935082010138806117f5565b80548389018501528794508693909201918101611841565b925093505061180394915060ff191682840152151560051b82010138806117f5565b602483634e487b7160e01b81526022600452fd5b93607f16936117cf565b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d57600160408364ffffffffff9360209552600b855220015460a01c16604051908152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5761193d9061340e565b906005821015908161197e5760028314918215611992575b8215611969575b6020836040519015158152f35b90915061197e5750600460209114388061195c565b80634e487b7160e01b602492526021600452fd5b506003831491506000611955565b50346105b95760203660031901126105b9576004356001600160a01b0390818116809103610488578183541633810361119c575060015491816001600160a01b03198416176001556040519216825260208201527fdcb09aef4bf01068924ccce937981cbe59d25ba08380cf941aaaea4e4bd3960d60403392a280f35b50346105b95760203660031901126105b957604051611a3b81612f97565b8181528160208201528160408201528160608201528160808201528160a08201528160c08201528160e08201528161010082015281610120820152610140611a81613207565b9101526004358152600b60205260ff600160408320015460d01c1615611c59576004358152600b60205260408120611b5a600260405192611ac184612f97565b80546001600160a01b038116855264ffffffffff8160a01c16602086015264ffffffffff8160c81c16604086015260ff8160f01c161515606086015260f81c1515608085015260ff60018201546001600160a01b03811660a087015264ffffffffff8160a01c1660c0870152818160c81c16151560e0870152818160d01c16151561010087015260d81c16151561012085015201613226565b610140820152611b6b60043561340e565b6005811015610f29579160026101a09314611c4e575b50610885610140604051926001600160a01b03815116845264ffffffffff602082015116602085015264ffffffffff60408201511660408501526060810151151560608501526080810151151560808501526001600160a01b0360a08201511660a085015264ffffffffff60c08201511660c085015260e0810151151560e0850152610100810151151561010085015261012081015115156101208501520151610140830190604090816001600160801b0391828151168552826020820151166020860152015116910152565b606082015238611b81565b602460405162b8e7e760e51b81526004356004820152fd5b50346105b957602090816003193601126105b95760043567ffffffffffffffff81116109b157611ca683913690600401612f52565b9190611cb061370f565b83925b808410611cbe578480f35b611ccd848284979596976131e1565b3594611cd761370f565b611ce08661313a565b15611cfd5760248660405190634a5541ef60e01b82526004820152fd5b611d0686613327565b611f7a57611d2a86600052600b6020526001600160a01b0360406000205416331490565b15611f5b57611d3886613358565b95808552600b90818752611d5160026040882001613226565b906001600160801b039283835116848b161015611f435781885280895260ff604089205460f01c1615611f2b57611da18a858b611d9760409a9b9c9d9e8389511661309a565b960151169061309a565b92828a52818b52868a20908b8b7f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50845497600160f81b7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8a1617865560038a8216968715611f11575b01998516998a6001600160801b03198254161790556001600160a01b0380991698899360058652818e822054169889965260019d8e912001541694611e4e8b8588614090565b604080518a81526001600160801b0392831660208201529290911690820152606090a47ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce78b604051858152a1813b611eb2575b505050505001919093919293611cb3565b813b15611f0d57899493919285809460849360405197889687956372eba20360e01b875260048701526024860152604485015260648401525af1611ef9575b808080611ea1565b611f0290612f83565b610c29578487611ef1565b8980fd5b60018101600160c81b60ff60c81b19825416179055611e08565b602482604051906339c6dc7360e21b82526004820152fd5b602482604051906322cad1af60e11b82526004820152fd5b60405163216caf0d60e01b815260048101879052336024820152604490fd5b6024866040519063fe19f19f60e01b82526004820152fd5b50346105b9576020806003193601126109b15760043590611fb161370f565b818352600b815260ff600160408520015460d01c1615610d2457611fd48261340e565b600581101561215e5760048103611ffd5760248360405190634a5541ef60e01b82526004820152fd5b6003810361201d576024836040519063fe19f19f60e01b82526004820152fd5b600214611f435761204482600052600b6020526001600160a01b0360406000205416331490565b1561213f57818352600b815260ff604084205460f01c1615611f2b57818352600b81526040832060ff60f01b19815416905582604051837f0eb069207093cd3e51cd1370d2d369770057fbe29947e577e5fb428c6c6fc78f8380a2600583526001600160a01b03604083205416803b6120e7575b5050507ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce791604051908152a180f35b803b1561048857816024818580947f341a0bd90000000000000000000000000000000000000000000000000000000083528960048401525af161212b575b806120b8565b61213490612f83565b610488578238612125565b60405163216caf0d60e01b815260048101839052336024820152604490fd5b602484634e487b7160e01b81526021600452fd5b50346105b95760203660031901126105b9576004356001600160a01b0390818116809103610488578183541633810361119c5750600a5491816001600160a01b0319841617600a556040519216825260208201527fa2548bd4b805e907c1558a47b5858324fe8bb4a2e1ddfca647eecbf65610eebc60403392a2600954600019810190811161222f5760407f6bd5c950a8d8df17f772f5af37cb3655737899cbf903264b9795592da439661c91815190600182526020820152a180f35b602482634e487b7160e01b81526011600452fd5b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d5760408264ffffffffff9260209452600b8452205460c81c16604051908152f35b50346105b95760203660031901126105b9576122ad612ef1565b9080546001600160a01b03808216933385036122ff576001600160a01b03199394501691829116178255337fbdd36143ee09de60bdefca70680e0f71189b2ed7acee364b53917ad433fdaf808380a380f35b6040516331b339a960e21b81526001600160a01b0386166004820152336024820152604490fd5b50346105b95760203660031901126105b9576001600160a01b03612348612ef1565b168015612365578160409160209352600683522054604051908152f35b608460405162461bcd60e51b815260206004820152602960248201527f4552433732313a2061646472657373207a65726f206973206e6f74206120766160448201527f6c6964206f776e657200000000000000000000000000000000000000000000006064820152fd5b50346105b95760203660031901126105b9576001600160a01b0360406020926004356124146108c08260005260056020526001600160a01b0360406000205416151590565b815260058452205416604051908152f35b50346105b95760203660031901126105b95760206124446004356131bc565b6001600160a01b0360405191168152f35b50346105b957806003193601126105b95760206001600160a01b0360015416604051908152f35b50346105b95760203660031901126105b95760ff6001604060043593848152600b60205220015460d01c161561065d5761072a6020916137d4565b50346105b95760203660031901126105b95760043590818152600b60205260ff600160408320015460d01c1615610d2457806124f28361340e565b92600584101561252d57600260209403612513575b50506040519015158152f35b8152600b8352604090205460f01c60ff1690503880612507565b602482634e487b7160e01b81526021600452fd5b50346105b95760203660031901126105b95760043561255e61370f565b6125678161313a565b1561269a576125758161376b565b1561267a57612583816131bc565b61258c8261325a565b159081612672575b8161265f575b50612647576020816125cc7ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7936131bc565b90808552600783526001600160a01b0360408620926001600160a01b03199384815416905516918286526006845260408620600019815401905581865260058452604086209081541690558085604051937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8280a48152a180f35b60249060405190630da9b01360e01b82526004820152fd5b6001600160a01b0391501615153861259a565b839150612594565b60405163216caf0d60e01b81526004810191909152336024820152604490fd5b602490604051907f817cd6390000000000000000000000000000000000000000000000000000000082526004820152fd5b50346105b9576126da36612f1d565b60405191602083019383851067ffffffffffffffff86111761270557610a4f94604052858452613291565b634e487b7160e01b600052604160045260246000fd5b50346105b95760203660031901126105b957602061060160043561313a565b50346105b9576020806003193601126109b1576004359061275961370f565b6127628261313a565b1561277f5760248260405190634a5541ef60e01b82526004820152fd5b9061278981613327565b6129c0576127ad81600052600b6020526001600160a01b0360406000205416331490565b1561267a576127bb81613358565b818452600b83526127d160026040862001613226565b926001600160801b03918285511683821610156129a857838652600b825260ff604087205460f01c16156129905792827ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce783612846878460409761283c8d9b6128f19b8e511661309a565b9b0151169061309a565b92848852600b825287868120947f5edb27d6c1a327513b90a792050debf074b7194444885e3144d4decc5caaaa50865491600160f81b7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84161788556003858216988915612976575b01948d169c858e6001600160801b0319819854161790556001600160a01b038094169b8c94600589526001818e892054169d8e98600b8c5220015416968588614090565b604080518b81526001600160801b0392831660208201529290911690820152606090a4604051848152a1823b612925578480f35b823b15610c2957608492859160405197889687956372eba20360e01b875260048701526024860152604485015260648401525af1612967575b81818080808480f35b61297090612f83565b3861295e565b60018101600160c81b60ff60c81b198254161790556128ad565b602484604051906339c6dc7360e21b82526004820152fd5b602484604051906322cad1af60e11b82526004820152fd5b6024906040519063fe19f19f60e01b82526004820152fd5b50346105b95760203660031901126105b9576001600160801b0360406020926001600160a01b03612a07612ef1565b16815260028452205416604051908152f35b50346105b957610a4f612a2b36612f1d565b91612a3e612a398433613495565b6130c9565b613574565b50346105b957806003193601126105b9576020600954604051908152f35b50346105b95760203660031901126105b957600435808252600b60205260ff600160408420015460d01c161561065d57612a9a9061340e565b90600582101561197e5760208215838115612abb575b506040519015158152f35b600191501482612ab0565b50346105b95760203660031901126105b95760043590818152600b60205260ff600160408320015460d01c1615610d2457602091604082828152600b85522060ff815460f01c1680612b54575b612b2b575b50506001600160801b0360405191168152f35b612b4d92506001600160801b036002612b479201541691613358565b9061309a565b3880612b18565b5060ff600182015460c81c1615612b13565b50346105b95760403660031901126105b957612b80612ef1565b602435906001600160a01b038080612b97856131bc565b16921691808314612c8b57803314908115612c6a575b5015612c0057828452600760205260408420826001600160a01b0319825416179055612bd8836131bc565b167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258480a480f35b608460405162461bcd60e51b815260206004820152603d60248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f7420746f60448201527f6b656e206f776e6572206f7220617070726f76656420666f7220616c6c0000006064820152fd5b9050845260086020526040842033855260205260ff60408520541638612bad565b608460405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e6560448201527f72000000000000000000000000000000000000000000000000000000000000006064820152fd5b50346105b95760203660031901126105b957602061244460043561305d565b50346105b957806003193601126105b95760405190806003549160018360011c9260018516948515612dce575b602095868610811461189357858852879493929187908215611871575050600114612d7457505061180392500383613009565b90859250600382527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b858310612db6575050611803935082010138806117f5565b80548389018501528794508693909201918101612d9e565b93607f1693612d41565b9050346109b15760203660031901126109b1576004357fffffffff00000000000000000000000000000000000000000000000000000000811680910361048857602092507f80ac58cd000000000000000000000000000000000000000000000000000000008114908115612e7f575b8115612e55575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501438612e4e565b7f5b5e139f0000000000000000000000000000000000000000000000000000000081149150612e47565b60005b838110612ebc5750506000910152565b8181015183820152602001612eac565b90602091612ee581518092818552858086019101612ea9565b601f01601f1916010190565b600435906001600160a01b038216820361107157565b602435906001600160a01b038216820361107157565b6060906003190112611071576001600160a01b0390600435828116810361107157916024359081168103611071579060443590565b9181601f840112156110715782359167ffffffffffffffff8311611071576020808501948460051b01011161107157565b67ffffffffffffffff811161270557604052565b610160810190811067ffffffffffffffff82111761270557604052565b610100810190811067ffffffffffffffff82111761270557604052565b6060810190811067ffffffffffffffff82111761270557604052565b6040810190811067ffffffffffffffff82111761270557604052565b90601f8019910116810190811067ffffffffffffffff82111761270557604052565b67ffffffffffffffff811161270557601f01601f191660200190565b604435906001600160801b038216820361107157565b6130806108c08260005260056020526001600160a01b0360406000205416151590565b60005260076020526001600160a01b036040600020541690565b6001600160801b0391821690821603919082116130b357565b634e487b7160e01b600052601160045260246000fd5b156130d057565b608460405162461bcd60e51b815260206004820152602d60248201527f4552433732313a2063616c6c6572206973206e6f7420746f6b656e206f776e6560448201527f72206f7220617070726f766564000000000000000000000000000000000000006064820152fd5b80600052600b60205260ff60016040600020015460d01c161561065d57600052600b60205260ff60016040600020015460c81c1690565b1561317857565b606460405162461bcd60e51b815260206004820152601860248201527f4552433732313a20696e76616c696420746f6b656e20494400000000000000006044820152fd5b60005260056020526001600160a01b03604060002054166131de811515613171565b90565b91908110156131f15760051b0190565b634e487b7160e01b600052603260045260246000fd5b6040519061321482612fd1565b60006040838281528260208201520152565b9060405161323381612fd1565b6040819360018154916001600160801b0392838116865260801c6020860152015416910152565b80600052600b60205260ff60016040600020015460d01c161561065d57600052600b60205260ff60016040600020015460d81c1690565b906132b59392916132a5612a398433613495565b6132b0838383613574565b614469565b156132bc57565b60405162461bcd60e51b815260206004820152603260248201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560448201527f63656976657220696d706c656d656e74657200000000000000000000000000006064820152608490fd5b80600052600b60205260ff60016040600020015460d01c161561065d57600052600b60205260406000205460f81c90565b600090808252600b6020526040822091825464ffffffffff42818360c81c16116134065780600186015460a01c1691824210156133f0576133a59394955060a01c168091039042036145fc565b90828152600b6020526001600160801b03926133cb8460026040852001541680946146dc565b9283116133d85750501690565b60029350604092508152600b60205220015460801c90565b505050505060026001600160801b039101541690565b505091505090565b80600052600b602052604060002060ff600182015460c81c16600014613435575050600490565b805460f81c61348e575460a01c64ffffffffff1642106134885761345881613358565b90600052600b6020526001600160801b03806002604060002001541691161060001461348357600190565b600290565b50600090565b5050600390565b906001600160a01b0380806134a9846131bc565b169316918383149384156134dc575b5083156134c6575b50505090565b6134d29192935061305d565b16143880806134c0565b909350600052600860205260406000208260005260205260ff6040600020541692386134b8565b1561350a57565b608460405162461bcd60e51b815260206004820152602560248201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060448201527f6f776e65720000000000000000000000000000000000000000000000000000006064820152fd5b9061359d9291613583836131bc565b916001600160a01b03948593848094169687911614613503565b16908115806136a6576135af8461325a565b15908161369d575b5080613694575b61367c57918084926135fe7ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7966020966135f7856131bc565b1614613503565b60009382855260078652604085206001600160a01b031990818154169055818652600687526040862060001981540190558286526040862060018154019055838652600587528260408720918254161790557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6040519580a48152a1565b60248360405190630da9b01360e01b82526004820152fd5b508315156135be565b905015386135b7565b608460405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f2061646460448201527f72657373000000000000000000000000000000000000000000000000000000006064820152fd5b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016300361374157565b60046040517fa1c0d6e5000000000000000000000000000000000000000000000000000000008152fd5b60009080825260056020526001600160a01b0380604084205416928333149384156137b0575b5050821561379e57505090565b9091506137ab339261305d565b161490565b60ff9294509060409181526008602052818120338252602052205416913880613791565b80600052600b6020526137ed6002604060002001613226565b81600052600b602052604060002060ff600182015460c81c1660001461382057506001600160801b039150602001511690565b5460f81c61383257506131de90613358565b6131de91506001600160801b03604081835116920151169061309a565b906001600160a01b036001541660206001600160a01b036060850151166024604051809481937fdcf844a700000000000000000000000000000000000000000000000000000000835260048301525afa801561408457600090614050575b6138d091506001600160801b0360408501511690602060e086015101519161413b565b916001600160801b0383511660c082015190156140265764ffffffffff815116602082019064ffffffffff82511690818111613fe657505064ffffffffff604091511691019064ffffffffff8251169081811015613fa657505064ffffffffff8042169151169081811015613f66575050600954926001600160801b038151166040519061395d82612fd1565b815260006020820152600060408201526001600160a01b036060840151169060c08401519164ffffffffff6020840151169064ffffffffff604085015116906080870151151560a088015115159364ffffffffff6001600160a01b038a5116975116604051976139cc89612f97565b88526020880152604087015260608601526000608086015260a085015260c0840152600060e0840152600161010084015261012083015261014082015284600052600b60205260406000206001600160a01b038251166001600160a01b0319825416178155613a6364ffffffffff602084015116829064ffffffffff60a01b1964ffffffffff60a01b83549260a01b169116179055565b604082015181547eff0000000000000000000000000000000000000000000000000000000000006060850151151560f01b169078ffffffffffffffffffffffffffffffffffffffffffffffffff7dffffffffff000000000000000000000000000000000000000000000000007fff000000000000000000000000000000000000000000000000000000000000006080880151151560f81b169460c81b1691161717178155600181016001600160a01b0360a0840151166001600160a01b0319825416178155613b5a64ffffffffff60c085015116829064ffffffffff60a01b1964ffffffffff60a01b83549260a01b169116179055565b60e083015181546101008501516101208601517fffffffff000000ffffffffffffffffffffffffffffffffffffffffffffffffff90921692151560c81b79ff00000000000000000000000000000000000000000000000000169290921791151560d01b7aff0000000000000000000000000000000000000000000000000000169190911790151560d81b7bff00000000000000000000000000000000000000000000000000000016179055610140909101518051602082015160801b6001600160801b03199081166001600160801b03928316176002850155926040906003019201511682825416179055600185016009556001600160a01b0360608401511660005260026020526001600160801b0380604060002054168160208501511601166001600160a01b036060850151166000526040600020918254161790556001600160a01b036020830151168015613f2257613cd5613ccf8660005260056020526001600160a01b0360406000205416151590565b1561427a565b613cde8561325a565b1580613f19575b80613f11575b613ef95760207ff8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce791613d36613ccf8860005260056020526001600160a01b0360406000205416151590565b806000526006825260406000206001815401905586600052600582526040600020816001600160a01b0319825416179055866040519160007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8180a4868152a1613dc66001600160a01b036060840151166001600160801b038084511681602086015116011690309033906142c5565b6001600160801b0360408201511680613eca575b506001600160a01b038251167f075861cbceafeb777e8f15f357121b08f6f3adba387d599bb7b5278ca6192df5610160866001600160a01b0360208701511694613ec16001600160a01b03606089015116976080810151151560a0820151151590613e8b6001600160a01b0360e060c08601519501515116956040519788523360208901526040880190604090816001600160801b0391828151168552826020820151166020860152015116910152565b60a086015260c0850152805164ffffffffff90811660e08601526020820151811661010086015260409091015116610120840152565b610140820152a4565b613ef3906001600160a01b036060850151166001600160a01b0360e086015151169033906142c5565b38613dda565b60248560405190630da9b01360e01b82526004820152fd5b506000613ceb565b50801515613ce5565b606460405162461bcd60e51b815260206004820152602060248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152fd5b6040517f210aec0e00000000000000000000000000000000000000000000000000000000815264ffffffffff918216600482015291166024820152604490fd5b6040517f9fee269100000000000000000000000000000000000000000000000000000000815264ffffffffff918216600482015291166024820152604490fd5b6040517f4c23297000000000000000000000000000000000000000000000000000000000815264ffffffffff918216600482015291166024820152604490fd5b60046040517f6095d3bc000000000000000000000000000000000000000000000000000000008152fd5b506020813d60201161407c575b8161406a60209383613009565b81010312611071576138d090516138ad565b3d915061405d565b6040513d6000823e3d90fd5b916001600160a01b03604051927fa9059cbb000000000000000000000000000000000000000000000000000000006020850152166024830152604482015260448152608081019181831067ffffffffffffffff841117612705576140f692604052614330565b565b6131de90614105816137d4565b90600052600b60205260026040600020015460801c9061309a565b9190916001600160801b03808094169116019182116130b357565b909291614146613207565b936001600160801b03928381169182156142525767016345785d8a000080821161421b578085116141e45750614190856141818193866146dc565b169460208901958652846146dc565b1691846141a7604089019480865282875116614120565b1610156141ce576141c08491826141c99551169061309a565b9151169061309a565b168252565b634e487b7160e01b600052600160045260246000fd5b84604491604051917f4fea5c1a00000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b60449250604051917f47152d6700000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b5050505050905060405161426581612fd1565b60008152600060208201526000604082015290565b1561428157565b606460405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152fd5b9290604051927f23b872dd0000000000000000000000000000000000000000000000000000000060208501526001600160a01b03809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff841117612705576140f6926040525b6001600160a01b03169061439060405161434981612fed565b6020938482527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564858301526000808587829751910182855af161438a614439565b9161478b565b805191821591848315614415575b5050509050156143ab5750565b6084906040519062461bcd60e51b82526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f742073756363656564000000000000000000000000000000000000000000006064820152fd5b9193818094500103126109b1578201519081151582036105b957508038808461439e565b3d15614464573d9061444a8261302b565b916144586040519384613009565b82523d6000602084013e565b606090565b9290803b156145f3576144d3916020916001600160a01b0394604051809581948293897f150b7a02000000000000000000000000000000000000000000000000000000009b8c86523360048701521660248501526044840152608060648401526084830190612ecc565b03916000968791165af190829082614592575b505061456c576144f4614439565b805190816145675760405162461bcd60e51b815260206004820152603260248201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560448201527f63656976657220696d706c656d656e74657200000000000000000000000000006064820152608490fd5b602001fd5b7fffffffff00000000000000000000000000000000000000000000000000000000161490565b909192506020813d6020116145eb575b816145af60209383613009565b810103126109b15751907fffffffff00000000000000000000000000000000000000000000000000000000821682036105b957509038806144e6565b3d91506145a2565b50505050600190565b670de0b6b3a76400009160001983830992808302928380861095039480860395146146b8578285101561467c57908291096001821901821680920460028082600302188083028203028083028203028083028203028083028203028083028203028092029003029360018380600003040190848311900302920304170290565b82606492604051927f63a05778000000000000000000000000000000000000000000000000000000008452600484015260248301526044820152fd5b5050809250156146c6570490565b634e487b7160e01b600052601260045260246000fd5b9091906000198382098382029182808310920391808303921461477a57670de0b6b3a7640000908183101561474357947faccb18165bd6fe31ae1cf318dc5b51eee0e1ba569b88cd74c1773b91fac1066994950990828211900360ee1b910360121c170290565b60449086604051917f5173648d00000000000000000000000000000000000000000000000000000000835260048301526024820152fd5b5050670de0b6b3a764000090049150565b919290156147ec575081511561479f575090565b3b156147a85790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b8251909150156147ff5750805190602001fd5b6104fa9060405191829162461bcd60e51b8352602060048401526024830190612ecc56fea164736f6c6343000817000a"; - bytes public constant BYTECODE_NFT_DESCRIPTOR = - hex"6080806040523461001757615dcc90816200001d8239f35b600080fdfe6080604052600436101561001257600080fd5b60003560e01c63e9dc63751461002757600080fd5b346142f85760403660031901126142f8576001600160a01b0360043516600435036142f857610056608061486e565b60006080819052606060a081905260c082905260e0819052610120819052610140819052610160819052610180919091526101a0526004356001600160a01b03166101008190526100a690614946565b61012052610100516040517feac8f5b80000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa908115614305576000916147dd575b506001600160a01b03610117911680608052614b39565b60a052610100516040517fa80fc0710000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa8015614305576fffffffffffffffffffffffffffffffff916000916147be575b501660c052610100516040517fad35efd40000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa801561430557600090614781575b6101e59150614c86565b61014052610100516040517f4869e12d0000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa90811561430557600091614752575b5060c0516fffffffffffffffffffffffffffffffff16801561473c576fffffffffffffffffffffffffffffffff61271081930216041661010060800152610287600435614d82565b610120608001526040514660208201526bffffffffffffffffffffffff1960043560601b16604082015260243560548201526054815280608081011067ffffffffffffffff60808301111761431157608081016040526020815191012061041a602963ffffffff61032e6103078261016861ffff8860101c1606166155e2565b91601e604660ff6103248460146050848d60081c160601166155e2565b98160601166155e2565b6040519485927f68736c2800000000000000000000000000000000000000000000000000000000602085015261036e815180926020602488019101614826565b83017f2c0000000000000000000000000000000000000000000000000000000000000060248201526103aa825180936020602585019101614826565b017f252c00000000000000000000000000000000000000000000000000000000000060258201526103e5825180936020602785019101614826565b017f252900000000000000000000000000000000000000000000000000000000000060278201520360098101845201826148df565b6104526fffffffffffffffffffffffffffffffff6040608001511660ff61044b6001600160a01b0360805116614f69565b16906150d2565b6104666001600160a01b0360805116614946565b60a051610100516040517fbc2be1be0000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa80156143055760249160009161471d575b5060206001600160a01b03608080015116604051938480927f9067b677000000000000000000000000000000000000000000000000000000008252823560048301525afa801561430557610528926000916146ee575b5064ffffffffff809116911661541d565b61012051610180519092916105b2602161054f60646105488187066158c3565b95046155e2565b6040519481610568879351809260208087019101614826565b820161057d8251809360208085019101614826565b017f250000000000000000000000000000000000000000000000000000000000000060208201520360018101855201836148df565b610100608001519260c060800151956101206080015197604051996105d68b61486e565b8a5260208a015260408901526060880152608087015260a086015260c085015260e0840152610100830152610120820152604051806101c081011067ffffffffffffffff6101c083011117614311576101c0810160405260608152600060208201526000604082015260608082015260006080820152606060a0820152600060c0820152600060e08201526060610100820152600061012082015260006101408201526060610160820152600061018082015260006101a082015260a08201516106a660c08401518451906159cf565b906109b361015c604051926106ba846148c3565b600884527f50726f677265737300000000000000000000000000000000000000000000000060208501526107236040516106f38161488b565b60009052855160208701207fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4701490565b156146e6576090945b610735866155e2565b916040519586938493661e339034b21e9160c91b60208601526109818351958692610767846027840160208901614826565b6d11103334b6361e9111b33333111f60911b602785840101526c1e3932b1ba103bb4b23a341e9160991b603585840101526107ae8551809660206042888701019101614826565b7f22206865696768743d22313030222066696c6c2d6f7061636974793d222e3033828501860160428101919091527f222072783d223135222072793d22313522207374726f6b653d2223666666222060628201527f7374726f6b652d6f7061636974793d222e3122207374726f6b652d77696474686082820152651e911a11179f60d11b60a28201527f3c7465787420783d2232302220793d2233342220666f6e742d66616d696c793d60a88201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f7370616365222060c8820152703337b73a16b9b4bd329e911919383c111f60791b60e88201528651966108b491889160f990910190602001614826565b661e17ba32bc3a1f60c91b8285018601870160f98101919091527f3c7465787420783d2232302220793d2237322220666f6e742d66616d696c793d6101008201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f73706163652220610120820152703337b73a16b9b4bd329e91191b383c111f60791b61014082015287519761094f91899161015190910190602001614826565b661e17ba32bc3a1f60c91b6101518888888887010101010152602061015888888885519c8d9701010101019101614826565b631e17b39f60e11b90860190910190910190910190910161015881019190915281900361013c810190915201826148df565b6101008301526101208201526028610100830151604051906109d48261488b565b60008252610c7a61015c604051926109eb846148c3565b600684527f53746174757300000000000000000000000000000000000000000000000000006020850152610a1e84615ccb565b610a2782615d49565b808211156146de5750945b610a3d8787016155e2565b91604051958693661e339034b21e9160c91b60208601528151610a67816027880160208601614826565b85016d11103334b6361e9111b33333111f60911b60278201526c1e3932b1ba103bb4b23a341e9160991b6035820152610aaa825180936020604285019101614826565b017f22206865696768743d22313030222066696c6c2d6f7061636974793d222e303360428201527f222072783d223135222072793d22313522207374726f6b653d2223666666222060628201527f7374726f6b652d6f7061636974793d222e3122207374726f6b652d77696474686082820152651e911a11179f60d11b60a28201527f3c7465787420783d2232302220793d2233342220666f6e742d66616d696c793d60a88201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f7370616365222060c8820152703337b73a16b9b4bd329e911919383c111f60791b60e8820152610ba682518093602060f985019101614826565b01661e17ba32bc3a1f60c91b60f98201527f3c7465787420783d2232302220793d2237322220666f6e742d66616d696c793d6101008201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f73706163652220610120820152703337b73a16b9b4bd329e91191b383c111f60791b610140820152610c3582518093602061015185019101614826565b01661e17ba32bc3a1f60c91b610151820152610c5c82518093602061015885019101614826565b01631e17b39f60e11b6101588201520361013c8101845201826148df565b610160840152016101808201526028602083015160405190610c9b8261488b565b60008252610ce561015c60405192610cb2846148c3565b600684527f416d6f756e7400000000000000000000000000000000000000000000000000006020850152610a1e84615ccb565b835201602082015261102060808301516030604051610d038161488b565b60008152610faa61015c60405194610d1a866148c3565b600886527f4475726174696f6e0000000000000000000000000000000000000000000000006020870152610d4d86615ccb565b610d5682615d49565b808211156146d65750935b610d6d602886016155e2565b91604051978893661e339034b21e9160c91b60208601528151610d97816027880160208601614826565b85016d11103334b6361e9111b33333111f60911b60278201526c1e3932b1ba103bb4b23a341e9160991b6035820152610dda825180936020604285019101614826565b017f22206865696768743d22313030222066696c6c2d6f7061636974793d222e303360428201527f222072783d223135222072793d22313522207374726f6b653d2223666666222060628201527f7374726f6b652d6f7061636974793d222e3122207374726f6b652d77696474686082820152651e911a11179f60d11b60a28201527f3c7465787420783d2232302220793d2233342220666f6e742d66616d696c793d60a88201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f7370616365222060c8820152703337b73a16b9b4bd329e911919383c111f60791b60e8820152610ed682518093602060f985019101614826565b01661e17ba32bc3a1f60c91b60f98201527f3c7465787420783d2232302220793d2237322220666f6e742d66616d696c793d6101008201527f2227436f7572696572204e6577272c417269616c2c6d6f6e6f73706163652220610120820152703337b73a16b9b4bd329e91191b383c111f60791b610140820152610f6582518093602061015185019101614826565b01661e17ba32bc3a1f60c91b610151820152610f8c82518093602061015885019101614826565b01631e17b39f60e11b6101588201520361013c8101865201846148df565b8260a08601526028810160c0860152602085015190610120860151809161018088015192839185010101605881016080890152605719906103e8030160011c8061014089015201601081016101a088015201602081016040870152010160e0840152610100830151610160840151845191615068565b6060820152604051908161010081011067ffffffffffffffff6101008401111761431157610100820160405260c782527f3c726563742077696474683d223130302522206865696768743d22313030252260208301527f2066696c7465723d2275726c28234e6f69736529222f3e3c7265637420783d2260408301527f37302220793d223730222077696474683d2238363022206865696768743d223860608301527f3630222066696c6c3d2223666666222066696c6c2d6f7061636974793d222e3060808301527f33222072783d223435222072793d22343522207374726f6b653d22236666662260a08301527f207374726f6b652d6f7061636974793d222e3122207374726f6b652d7769647460c08301527f683d2234222f3e0000000000000000000000000000000000000000000000000060e083015282519161010084015191606081015194604051611176816148a7565b603381527f3c636972636c652069643d22476c6f772220723d22353030222066696c6c3d2260208201527f75726c282352616469616c476c6f7729222f3e000000000000000000000000006040820152604051966111d38861486e565b61011c88527f3c66696c7465722069643d224e6f697365223e3c6665466c6f6f6420783d223060208901527f2220793d2230222077696474683d223130302522206865696768743d2231303060408901527f252220666c6f6f642d636f6c6f723d2268736c283233302c3231252c3131252960608901527f2220666c6f6f642d6f7061636974793d22312220726573756c743d22666c6f6f60808901527f6446696c6c222f3e3c666554757262756c656e6365206261736546726571756560a08901527f6e63793d222e3422206e756d4f6374617665733d22332220726573756c743d2260c08901527f4e6f6973652220747970653d226672616374616c4e6f697365222f3e3c66654260e08901527f6c656e6420696e3d224e6f6973652220696e323d22666c6f6f6446696c6c22206101008901527f6d6f64653d22736f66742d6c69676874222f3e3c2f66696c7465723e0000000061012089015260405197886103a081011067ffffffffffffffff6103a08b011117614311576103a0890160405261037b89527f3c706174682069643d224c6f676f222066696c6c3d2223666666222066696c6c60208a01527f2d6f7061636974793d222e312220643d226d3133332e3535392c3132342e303360408a01527f34632d2e3031332c322e3431322d312e3035392c342e3834382d322e3932332c60608a01527f362e3430322d322e3535382c312e3831392d352e3136382c332e3433392d372e60808a01527f3838382c342e3939362d31342e34342c382e3236322d33312e3034372c31322e60a08a01527f3536352d34372e3637342c31322e3536392d382e3835382e3033362d31372e3860c08a01527f33382d312e3237322d32362e3332382d332e3636332d392e3830362d322e373660e08a01527f362d31392e3038372d372e3131332d32372e3536322d31322e3737382d31332e6101008a01527f3834322d382e3032352c392e3436382d32382e3630362c31362e3135332d33356101208a01527f2e323635683063322e3033352d312e3833382c342e3235322d332e3534362c366101408a01527f2e3436332d352e323234683063362e3432392d352e3635352c31362e3231382d6101608a01527f322e3833352c32302e3335382c342e31372c342e3134332c352e3035372c382e6101808a01527f3831362c392e3634392c31332e39322c31332e373334682e30333763352e37336101a08a01527f362c362e3436312c31352e3335372d322e3235332c392e33382d382e34382c306101c08a01527f2c302d332e3531352d332e3531352d332e3531352d332e3531352d31312e34396101e08a01527f2d31312e3437382d35322e3635362d35322e3636342d36342e3833372d36342e6102008a01527f3833376c2e3034392d2e303337632d312e3732352d312e3630362d322e3731396102208a01527f2d332e3834372d322e3735312d362e3230346830632d2e3034362d322e3337356102408a01527f2c312e3036322d342e3538322c322e3732362d362e32323968306c2e3138352d6102608a01527f2e3134386830632e3039392d2e3036322c2e3232322d2e3134382c2e33372d2e6102808a01527f323539683063322e30362d312e3336322c332e3935312d322e3632312c362e306102a08a01527f34342d332e3834324335372e3736332d332e3437332c39372e37362d322e33346102c08a01527f312c3132382e3633372c31382e3333326331362e3637312c392e3934362d32366102e08a01527f2e3334342c35342e3831332d33382e3635312c34302e3139392d362e3239392d6103008a01527f362e3039362d31382e3036332d31372e3734332d31392e3636382d31382e38316103208a01527f312d362e3031362d342e3034372d31332e3036312c342e3737362d372e3735326103408a01527f2c392e3735316c36382e3235342c36382e33373163312e3732342c312e3630316103608a01527f2c322e3731342c332e38342c322e3733382c362e3139325a222f3e00000000006103808a0152604051978860a081011067ffffffffffffffff60a08b01111761431157611cb1611d129160a08b0160405260758b527f3c706174682069643d22466c6f6174696e6754657874222066696c6c3d226e6f60208c01527f6e652220643d224d31323520343568373530733830203020383020383076373560408c01527f307330203830202d3830203830682d373530732d38302030202d3830202d383060608c01527f762d3735307330202d3830203830202d3830222f3e000000000000000000000060808c0152611868615996565b906040517f3c72616469616c4772616469656e742069643d2252616469616c476c6f77223e6020820152611d0d60d87f3c73746f70206f66667365743d223025222073746f702d636f6c6f723d22000093846040850152805161199a60b88660208501936118da81605e840187614826565b8101997f222073746f702d6f7061636974793d222e36222f3e0000000000000000000000605e8c01527f3c73746f70206f66667365743d2231303025222073746f702d636f6c6f723d229a8b607382015261193f825180936020609385019101614826565b017f222073746f702d6f7061636974793d2230222f3e00000000000000000000000060938201527f3c2f72616469616c4772616469656e743e00000000000000000000000000000060a78201520360988101885201866148df565b6119a2615996565b90604051967f3c6c696e6561724772616469656e742069643d2253616e64546f70222078313d60208901527f223025222079313d223025223e000000000000000000000000000000000000006040890152604d8801528251611a0881606b8a0184614826565b8701917f222f3e00000000000000000000000000000000000000000000000000000000009283606b82015289606e820152611a4d825180936020608e85019101614826565b019082608e830152611a9160a2897f3c2f6c696e6561724772616469656e743e0000000000000000000000000000009485609182015203608281018b5201896148df565b611bd7610108611a9f615996565b6040519b8c917f3c6c696e6561724772616469656e742069643d2253616e64426f74746f6d222060208401527f78313d2231303025222079313d2231303025223e00000000000000000000000060408401527f3c73746f70206f66667365743d22313025222073746f702d636f6c6f723d22006054840152611b2b815180926020607387019101614826565b8201908760738301526076820152875190611b4a826096830188614826565b018660968201527f3c616e696d617465206174747269627574654e616d653d22783122206475723d60998201527f2236732220726570656174436f756e743d22696e646566696e6974652220766160b98201527f6c7565733d223330253b3630253b313230253b3630253b3330253b222f3e000060d98201528560f78201520360e881018c52018a6148df565b611bdf615996565b906040519a8b957f3c6c696e6561724772616469656e742069643d22486f7572676c61737353747260208801527f6f6b6522206772616469656e745472616e73666f726d3d22726f74617465283960408801527f302922206772616469656e74556e6974733d227573657253706163654f6e557360608801527f65223e000000000000000000000000000000000000000000000000000000000060808801527f3c73746f70206f66667365743d22353025222073746f702d636f6c6f723d2200608388015251809260a2880190614826565b84018360a28201527f3c73746f70206f66667365743d22383025222073746f702d636f6c6f723d220060a5820152611cf382518093602060c485019101614826565b019160c483015260c78201520360b88101875201856148df565b615068565b92611d32611d1e614c14565b896020815191012090602081519101201490565b9788156146ad575b506040518060c081011067ffffffffffffffff60c0830111176143115760c08101604052609081527f3c7061746820643d224d2035302c3336302061203330302c333030203020312c60208201527f31203630302c302061203330302c333030203020312c31202d3630302c30222060408201527f66696c6c3d2223666666222066696c6c2d6f7061636974793d222e303222207360608201527f74726f6b653d2275726c2823486f7572676c6173735374726f6b65292220737460808201527f726f6b652d77696474683d2234222f3e0000000000000000000000000000000060a082015260405193846102c081011067ffffffffffffffff6102c087011117614311576102c0850160405261029885527f3c7061746820643d226d3536362c3136312e323031762d35332e39323463302d60208601527f31392e3338322d32322e3531332d33372e3536332d36332e3339382d35312e3160408601527f39382d34302e3735362d31332e3539322d39342e3934362d32312e3037392d3160608601527f35322e3538372d32312e303739732d3131312e3833382c372e3438372d31353260808601527f2e3630322c32312e303739632d34302e3839332c31332e3633362d36332e343160a08601527f332c33312e3831362d36332e3431332c35312e3139387635332e39323463302c60c08601527f31372e3138312c31372e3730342c33332e3432372c35302e3232332c34362e3360e08601527f3934763238342e383039632d33322e3531392c31322e39362d35302e3232332c6101008601527f32392e3230362d35302e3232332c34362e3339347635332e39323463302c31396101208601527f2e3338322c32322e35322c33372e3536332c36332e3431332c35312e3139382c6101408601527f34302e3736332c31332e3539322c39342e3935342c32312e3037392c3135322e6101608601527f3630322c32312e303739733131312e3833312d372e3438372c3135322e3538376101808601527f2d32312e3037396334302e3838362d31332e3633362c36332e3339382d33312e6101a08601527f3831362c36332e3339382d35312e313938762d35332e39323463302d31372e316101c08601527f39362d31372e3730342d33332e3433352d35302e3232332d34362e34303156326101e08601527f30372e3630336333322e3531392d31322e3936372c35302e3232332d32392e326102008601527f30362c35302e3232332d34362e3430315a6d2d3334372e3436322c35372e37396102208601527f336c3133302e3935392c3133312e3032372d3133302e3935392c3133312e30316102408601527f33563231382e3939345a6d3236322e3932342e303232763236322e3031386c2d6102608601527f3133302e3933372d3133312e3030362c3133302e3933372d3133312e3031335a6102808601527f222066696c6c3d2223313631383232223e3c2f706174683e00000000000000006102a0860152896000146144885760405161218c8161488b565b60008152995b1561432757604051806101e081011067ffffffffffffffff6101e083011117614311576101e081016040526101b181527f3c7061746820643d226d3438312e34362c3438312e35347638312e3031632d3260208201527f2e33352e37372d342e38322c312e35312d372e33392c322e32332d33302e332c60408201527f382e35342d37342e36352c31332e39322d3132342e30362c31332e39322d353360608201527f2e362c302d3130312e32342d362e33332d3133312e34372d31362e3136762d3860808201527f316c34362e332d34362e3331683137302e33336c34362e32392c34362e33315a60a08201527f222066696c6c3d2275726c282353616e64426f74746f6d29222f3e3c7061746860c08201527f20643d226d3433352e31372c3433352e323363302c312e31372d2e34362c322e60e08201527f33322d312e33332c332e34342d372e31312c392e30382d34312e39332c31352e6101008201527f39382d38332e38312c31352e3938732d37362e372d362e392d38332e38322d316101208201527f352e3938632d2e38372d312e31322d312e33332d322e32372d312e33332d332e6101408201527f3434762d2e30346c382e33342d382e33352e30312d2e30316331332e37322d366101608201527f2e35312c34322e39352d31312e30322c37362e382d31312e30327336322e39376101808201527f2c342e34392c37362e37322c31316c382e34322c382e34325a222066696c6c3d6101a08201527f2275726c282353616e64546f7029222f3e0000000000000000000000000000006101c0820152995b60405196876107e081011067ffffffffffffffff6107e08a01111761431157613b9f9c612e5a6036602d9960819f97631e17b39f60e11b8d7f3c2f646566733e000000000000000000000000000000000000000000000000009a612f2b9f6107e0016040526107a782527f3c672066696c6c3d226e6f6e6522207374726f6b653d2275726c2823486f757260208301527f676c6173735374726f6b652922207374726f6b652d6c696e656361703d22726f60408301527f756e6422207374726f6b652d6d697465726c696d69743d22313022207374726f60608301527f6b652d77696474683d2234223e3c7061746820643d226d3536352e3634312c3160808301527f30372e323863302c392e3533372d352e35362c31382e3632392d31352e36373660a08301527f2c32362e393733682d2e303233632d392e3230342c372e3539362d32322e313960c08301527f342c31342e3536322d33382e3139372c32302e3539322d33392e3530342c313460e08301527f2e3933362d39372e3332352c32342e3335352d3136312e3733332c32342e33356101008301527f352d39302e34382c302d3136372e3934382d31382e3538322d3139392e3935336101208301527f2d34342e393438682d2e303233632d31302e3131352d382e3334342d31352e366101408301527f37362d31372e3433372d31352e3637362d32362e3937332c302d33392e3733356101608301527f2c39362e3535342d37312e3932312c3231352e3635322d37312e3932317332316101808301527f352e3632392c33322e3138352c3231352e3632392c37312e3932315a222f3e3c6101a08301527f7061746820643d226d3133342e33362c3136312e32303363302c33392e3733356101c08301527f2c39362e3535342c37312e3932312c3231352e3635322c37312e3932317332316101e08301527f352e3632392d33322e3138362c3231352e3632392d37312e393231222f3e3c6c6102008301527f696e652078313d223133342e3336222079313d223136312e323033222078323d6102208301527f223133342e3336222079323d223130372e3238222f3e3c6c696e652078313d226102408301527f3536352e3634222079313d223136312e323033222078323d223536352e3634226102608301527f2079323d223130372e3238222f3e3c6c696e652078313d223138342e353834226102808301527f2079313d223230362e383233222078323d223138342e353835222079323d22356102a08301527f33372e353739222f3e3c6c696e652078313d223231382e313831222079313d226102c08301527f3231382e313138222078323d223231382e313831222079323d223536322e35336102e08301527f37222f3e3c6c696e652078313d223438312e383138222079313d223231382e316103008301527f3432222078323d223438312e383139222079323d223536322e343238222f3e3c6103208301527f6c696e652078313d223531352e343135222079313d223230372e3335322220786103408301527f323d223531352e343136222079323d223533372e353739222f3e3c70617468206103608301527f643d226d3138342e35382c3533372e353863302c352e34352c342e32372c31306103808301527f2e36352c31322e30332c31352e3432682e303263352e35312c332e33392c31326103a08301527f2e37392c362e35352c32312e35352c392e34322c33302e32312c392e392c37386103c08301527f2e30322c31362e32382c3133312e38332c31362e32382c34392e34312c302c396103e08301527f332e37362d352e33382c3132342e30362d31332e39322c322e372d2e37362c356104008301527f2e32392d312e35342c372e37352d322e33352c382e37372d322e38372c31362e6104208301527f30352d362e30342c32312e35362d392e3433683063372e37362d342e37372c316104408301527f322e30342d392e39372c31322e30342d31352e3432222f3e3c7061746820643d6104608301527f226d3138342e3538322c3439322e363536632d33312e3335342c31322e3438356104808301527f2d35302e3232332c32382e35382d35302e3232332c34362e3134322c302c392e6104a08301527f3533362c352e3536342c31382e3632372c31352e3637372c32362e393639682e6104c08301527f30323263382e3530332c372e3030352c32302e3231332c31332e3436332c33346104e08301527f2e3532342c31392e3135392c392e3939392c332e3939312c32312e3236392c376105008301527f2e3630392c33332e3539372c31302e3738382c33362e34352c392e3430372c386105208301527f322e3138312c31352e3030322c3133312e3833352c31352e3030327339352e336105408301527f36332d352e3539352c3133312e3830372d31352e3030326331302e3834372d326105608301527f2e37392c32302e3836372d352e3932362c32392e3932342d392e3334392c312e6105808301527f3234342d2e3436372c322e3437332d2e3934322c332e3637332d312e3432342c6105a08301527f31342e3332362d352e3639362c32362e3033352d31322e3136312c33342e35326105c08301527f342d31392e313733682e3032326331302e3131342d382e3334322c31352e36376105e08301527f372d31372e3433332c31352e3637372d32362e3936392c302d31372e3536322d6106008301527f31382e3836392d33332e3636352d35302e3232332d34362e3135222f3e3c70616106208301527f746820643d226d3133342e33362c3539322e373263302c33392e3733352c39366106408301527f2e3535342c37312e3932312c3231352e3635322c37312e393231733231352e366106608301527f32392d33322e3138362c3231352e3632392d37312e393231222f3e3c6c696e656106808301527f2078313d223133342e3336222079313d223539322e3732222078323d223133346106a08301527f2e3336222079323d223533382e373937222f3e3c6c696e652078313d223536356106c08301527f2e3634222079313d223539322e3732222078323d223536352e3634222079323d6106e08301527f223533382e373937222f3e3c706f6c796c696e6520706f696e74733d223438316107008301527f2e383232203438312e393031203438312e373938203438312e383737203438316107208301527f2e373735203438312e383534203335302e303135203335302e303236203231386107408301527f2e313835203231382e313239222f3e3c706f6c796c696e6520706f696e74733d6107608301527f223231382e313835203438312e393031203231382e323331203438312e3835346107808301527f203335302e303135203335302e303236203438312e383232203231382e3135326107a08301527f222f3e3c2f673e000000000000000000000000000000000000000000000000006107c0830152604051998a957f3c672069643d22486f7572676c617373223e00000000000000000000000000006020880152603295612df68151809260208a8c019101614826565b8701612e0b8251809360208a85019101614826565b01612e1f8251809360208985019101614826565b01612e338251809360208885019101614826565b01612e478251809360208785019101614826565b01918201520360168101865201846148df565b6040519e8f9788977f3c646566733e000000000000000000000000000000000000000000000000000060208a0152612e9f6026998260208c9451948593019101614826565b8901612eb48251809360208c85019101614826565b01612ec88251809360208b85019101614826565b01612edc8251809360208a85019101614826565b01612ef08251809360208985019101614826565b01612f048251809360208885019101614826565b01612f188251809360208785019101614826565b019182015203600d8101895201876148df565b6137be604c60e08301516101208401519361351a61314d6060604084015193015196612f578186615c0f565b9461314861012b604051612f6a816148c3565b600581527f2d3130302500000000000000000000000000000000000000000000000000000060208201526040519889917f3c74657874506174682073746172744f66667365743d220000000000000000006020840152612fd4815180926020603787019101614826565b7f2220687265663d2223466c6f6174696e6754657874222066696c6c3d2223666683820160378101919091527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e00000000000000000061010982015282519261311891849161012090910190602001614826565b6a1e17ba32bc3a2830ba341f60a91b90830190910161012081019190915281900361010b810190915201876148df565b615c0f565b9561332c61012b604051613160816148c3565b600281527f30250000000000000000000000000000000000000000000000000000000000006020820152604051998a917f3c74657874506174682073746172744f66667365743d2200000000000000000060208401526131ca815180926020603787019101614826565b82017f2220687265663d2223466c6f6174696e6754657874222066696c6c3d2223666660378201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e00000000000000000061010982015261330782518093602061012085019101614826565b016a1e17ba32bc3a2830ba341f60a91b6101208201520361010b81018a5201886148df565b6133368184615c77565b9261351561012b604051613349816148c3565b600481527f2d3530250000000000000000000000000000000000000000000000000000000060208201526040519687917f3c74657874506174682073746172744f66667365743d2200000000000000000060208401526133b3815180926020603787019101614826565b82017f2220687265663d2223466c6f6174696e6754657874222066696c6c3d2223666660378201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e0000000000000000006101098201526134f082518093602061012085019101614826565b016a1e17ba32bc3a2830ba341f60a91b6101208201520361010b8101875201856148df565b615c77565b906136f961012b60405161352d816148c3565b600381527f353025000000000000000000000000000000000000000000000000000000000060208201526040519485917f3c74657874506174682073746172744f66667365743d220000000000000000006020840152613597815180926020603787019101614826565b82017f2220687265663d2223466c6f6174696e6754657874222066696c6c3d2223666660378201527f662220666f6e742d66616d696c793d2227436f7572696572204e6577272c417260578201527f69616c2c6d6f6e6f7370616365222066696c6c2d6f7061636974793d222e3822607782015271103337b73a16b9b4bd329e91191b383c111f60711b60978201527f3c616e696d6174652061646469746976653d2273756d2220617474726962757460a98201527f654e616d653d2273746172744f66667365742220626567696e3d22307322206460c98201527f75723d22353073222066726f6d3d2230252220726570656174436f756e743d2260e98201527f696e646566696e6974652220746f3d2231303025222f3e0000000000000000006101098201526136d482518093602061012085019101614826565b016a1e17ba32bc3a2830ba341f60a91b6101208201520361010b8101855201836148df565b6040519586937f3c7465787420746578742d72656e646572696e673d226f7074696d697a65537060208601527f656564223e000000000000000000000000000000000000000000000000000000604086015261375f815180926020604589019101614826565b8401613775825180936020604585019101614826565b0161378a825180936020604585019101614826565b0161379f825180936020604585019101614826565b01661e17ba32bc3a1f60c91b604582015203602c8101845201826148df565b613a9e61019a6101408401516101a0850151906137ff6137f96137f36137ed60e060408b01519a0151946155e2565b946155e2565b976155e2565b916155e2565b956040519687937f3c75736520687265663d2223476c6f77222066696c6c2d6f7061636974793d2260208601527f2e39222f3e00000000000000000000000000000000000000000000000000000060408601527f3c75736520687265663d2223476c6f772220783d22313030302220793d22313060458601527f3030222066696c6c2d6f7061636974793d222e39222f3e00000000000000000060658601527f3c75736520687265663d22234c6f676f2220783d223137302220793d22313730607c8601527f22207472616e73666f726d3d227363616c65282e3629222f3e3c757365206872609c8601527f65663d2223486f7572676c6173732220783d223135302220793d22393022207460bc8601527f72616e73666f726d3d22726f746174652831302922207472616e73666f726d2d60dc8601527f6f726967696e3d2235303020353030222f3e000000000000000000000000000060fc8601527f3c75736520687265663d222350726f67726573732220783d220000000000000061010e8601526101279061399a815180926020858a019101614826565b8501937f2220793d22373930222f3e00000000000000000000000000000000000000000080948180948801527f3c75736520687265663d22235374617475732220783d2200000000000000000061013288015261014996613a048251809360208b85019101614826565b01958601527f3c75736520687265663d2223416d6f756e742220783d2200000000000000000061015486015261016b94613a478251809360208985019101614826565b01938401527f3c75736520687265663d22234475726174696f6e2220783d220000000000000061017684015261018f92613a8a8251809360208785019101614826565b01918201520361017a8101855201836148df565b6040519586937f3c73766720786d6c6e733d22687474703a2f2f7777772e77332e6f72672f323060208601527f30302f737667222077696474683d223130303022206865696768743d2231303060408601527f30222076696577426f783d2230203020313030302031303030223e00000000006060860152613b2a815180926020607b89019101614826565b8401613b40825180936020607b85019101614826565b01613b55825180936020607b85019101614826565b01613b6a825180936020607b85019101614826565b017f3c2f7376673e0000000000000000000000000000000000000000000000000000607b8201520360618101845201826148df565b6101605260a051610100516040517fb971302a0000000000000000000000000000000000000000000000000000000081526024803560048301529091602091839182906001600160a01b03165afa908115614305576000916142ba575b6142b661424f614154614245609487613d3b6089613c198a614946565b9260c0608001516040519485927f5b7b2274726169745f74797065223a224173736574222c2276616c7565223a226020850152613c60815180926020604088019101614826565b8301907f227d2c7b2274726169745f74797065223a2253656e646572222c2276616c756560408301527f223a22000000000000000000000000000000000000000000000000000000000091826060820152613cc5825180936020606385019101614826565b01907f227d2c7b2274726169745f74797065223a22537461747573222c2276616c756560638301526083820152613d06825180936020608685019101614826565b017f227d5d000000000000000000000000000000000000000000000000000000000060868201520360698101845201826148df565b6101a05160a05161403e61017e613d536024356155e2565b9360a060800151613d6e6001600160a01b0360805116614946565b90604051968793613f2b60208601987f54686973204e465420726570726573656e74732061207061796d656e742073748a527f7265616d20696e2061205361626c696572205632200000000000000000000000604088015282516020840190613ddb8160558b0184614826565b8801947f20636f6e74726163742e20546865206f776e6572206f662074686973204e465460558701527f2063616e207769746864726177207468652073747265616d656420617373657460758701527f732c207768696368206172652064656e6f6d696e6174656420696e2000000000609587015282516020840196613e658260b183018a614826565b017f2e5c6e5c6e2d2053747265616d2049443a20000000000000000000000000000060b1820152613ea082518093602060c385019101614826565b01613ed97f5c6e2d2000000000000000000000000000000000000000000000000000000000958660c384015251809360c7840190614826565b01947f20416464726573733a2000000000000000000000000000000000000000000000958660c7820152613f1782518093602060d185019101614826565b019260d184015251809360d5840190614826565b019060d5820152613f4682518093602060df85019101614826565b017f5c6e5c6e0000000000000000000000000000000000000000000000000000000060df8201527fe29aa0efb88f205741524e494e473a205472616e7366657272696e672074686560e38201527f204e4654206d616b657320746865206e6577206f776e657220746865207265636101038201527f697069656e74206f66207468652073747265616d2e205468652066756e6473206101238201527f617265206e6f74206175746f6d61746963616c6c792077697468647261776e206101438201527f666f72207468652070726576696f757320726563697069656e742e00000000006101638201520361015e8101855201836148df565b6101a051906141af6140516024356155e2565b916140d0602d604051809560208201976a029b0b13634b2b9102b19160ad1b8952614086815180926020602b87019101614826565b82017f2023000000000000000000000000000000000000000000000000000000000000602b8201526140c18251809360208785019101614826565b0103600d8101865201846148df565b610160516140dd90615733565b94604051998a977f7b2261747472696275746573223a00000000000000000000000000000000000060208a015261411e815180926020602e8d019101614826565b8801917f2c226465736372697074696f6e223a2200000000000000000000000000000000602e840152518093603e840190614826565b01917f222c2265787465726e616c5f75726c223a2268747470733a2f2f7361626c6965603e8401527f722e636f6d222c226e616d65223a220000000000000000000000000000000000605e840152518093606d840190614826565b017f222c22696d616765223a22646174613a696d6167652f7376672b786d6c3b6261606d8201527f736536342c000000000000000000000000000000000000000000000000000000608d820152614210825180936020609285019101614826565b017f227d00000000000000000000000000000000000000000000000000000000000060928201520360748101845201826148df565b60e0819052615733565b6142a2603d60405180937f646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c00000060208301526142928151809260208686019101614826565b810103601d8101845201826148df565b604051918291602083526020830190614849565b0390f35b90506020813d6020116142fd575b816142d5602093836148df565b810103126142f85751906001600160a01b03821682036142f85790614154613bfc565b600080fd5b3d91506142c8565b6040513d6000823e3d90fd5b634e487b7160e01b600052604160045260246000fd5b6040518061012081011067ffffffffffffffff6101208301111761431157610120810160405260f881527f3c7061746820643d226d3438312e34362c3530342e3130317635382e3434396360208201527f2d322e33352e37372d342e38322c312e35312d372e33392c322e32332d33302e60408201527f332c382e35342d37342e36352c31332e39322d3132342e30362c31332e39322d60608201527f35332e362c302d3130312e32342d362e33332d3133312e34372d31362e31367660808201527f2d35382e343339683236322e39325a222066696c6c3d2275726c282353616e6460a08201527f426f74746f6d29222f3e3c656c6c697073652063783d22333530222063793d2260c08201527f3530342e313031222072783d223133312e343632222072793d2232382e31303860e08201527f222066696c6c3d2275726c282353616e64546f7029222f3e0000000000000000610100820152996123df565b604051806101c081011067ffffffffffffffff6101c083011117614311576101c0810160405261019981527f3c706f6c79676f6e20706f696e74733d22333530203335302e3032362034313560208201527f2e3033203238342e39373820323835203238342e39373820333530203335302e60408201527f303236222066696c6c3d2275726c282353616e64426f74746f6d29222f3e3c7060608201527f61746820643d226d3431362e3334312c3238312e39373563302c2e3931342d2e60808201527f3335342c312e3830392d312e3033352c322e36382d352e3534322c372e30373660a08201527f2d33322e3636312c31322e34352d36352e32382c31322e34352d33322e36323460c08201527f2c302d35392e3733382d352e3337342d36352e32382d31322e34352d2e36383160e08201527f2d2e3837322d312e3033352d312e3736372d312e3033352d322e36382c302d2e6101008201527f3931342e3335342d312e3830382c312e3033352d322e3637362c352e3534322d6101208201527f372e3037362c33322e3635362d31322e34352c36352e32382d31322e34352c336101408201527f322e3631392c302c35392e3733382c352e3337342c36352e32382c31322e34356101608201527f2e3638312e3836372c312e3033352c312e3736322c312e3033352c322e3637366101808201527f5a222066696c6c3d2275726c282353616e64546f7029222f3e000000000000006101a082015299612192565b6146cf9198506146bb614c4d565b906020815191012090602081519101201490565b9638611d3a565b905093610d61565b905094610a32565b60d09461072c565b614710915060203d602011614716575b61470881836148df565b810190614929565b38610517565b503d6146fe565b614736915060203d6020116147165761470881836148df565b386104c1565b634e487b7160e01b600052601260045260246000fd5b614774915060203d60201161477a575b61476c81836148df565b810190614901565b3861023f565b503d614762565b506020813d6020116147b6575b8161479b602093836148df565b810103126142f8575160058110156142f8576101e5906101db565b3d915061478e565b6147d7915060203d60201161477a5761476c81836148df565b38610181565b90506020813d60201161481e575b816147f8602093836148df565b810103126142f857516001600160a01b03811681036142f8576001600160a01b03610100565b3d91506147eb565b60005b8381106148395750506000910152565b8181015183820152602001614829565b9060209161486281518092818552858086019101614826565b601f01601f1916010190565b610140810190811067ffffffffffffffff82111761431157604052565b6020810190811067ffffffffffffffff82111761431157604052565b6060810190811067ffffffffffffffff82111761431157604052565b6040810190811067ffffffffffffffff82111761431157604052565b90601f8019910116810190811067ffffffffffffffff82111761431157604052565b908160209103126142f857516fffffffffffffffffffffffffffffffff811681036142f85790565b908160209103126142f8575164ffffffffff811681036142f85790565b6001600160a01b03166040519061495c826148a7565b602a8252602082016040368237825115614a755760309053815160019060011015614a7557607860218401536029905b8082116149fa57505061499c5790565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602060248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152fd5b9091600f81166010811015614a60577f3031323334353637383961626364656600000000000000000000000000000000901a614a3684866158b2565b5360041c918015614a4b57600019019061498c565b60246000634e487b7160e01b81526011600452fd5b60246000634e487b7160e01b81526032600452fd5b634e487b7160e01b600052603260045260246000fd5b67ffffffffffffffff811161431157601f01601f191660200190565b3d15614ad2573d90614ab882614a8b565b91614ac660405193846148df565b82523d6000602084013e565b606090565b6020818303126142f85780519067ffffffffffffffff82116142f8570181601f820112156142f8578051614b0a81614a8b565b92614b1860405194856148df565b818452602082840101116142f857614b369160208085019101614826565b90565b6000809160405160208101906395d89b4160e01b825260048152614b5c816148c3565b51915afa614b68614aa7565b90158015614c08575b614bce5780602080614b8893518301019101614ad7565b601e815111600014614b365750604051614ba1816148c3565b600b81527f4c6f6e672053796d626f6c000000000000000000000000000000000000000000602082015290565b50604051614bdb816148c3565b600581527f4552433230000000000000000000000000000000000000000000000000000000602082015290565b50604081511115614b71565b60405190614c21826148c3565b600782527f536574746c6564000000000000000000000000000000000000000000000000006020830152565b60405190614c5a826148c3565b600882527f4465706c657465640000000000000000000000000000000000000000000000006020830152565b6005811015614d6c5760048103614ca05750614b36614c4d565b60038103614ce25750604051614cb5816148c3565b600881527f43616e63656c6564000000000000000000000000000000000000000000000000602082015290565b60018103614d245750604051614cf7816148c3565b600981527f53747265616d696e670000000000000000000000000000000000000000000000602082015290565b600203614d3357614b36614c14565b604051614d3f816148c3565b600781527f50656e64696e6700000000000000000000000000000000000000000000000000602082015290565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b031660409081516395d89b4160e01b8152600081600481855afa908115614f5e57600091614f3b575b50614e178351614dc1816148c3565b601181527f5341422d56322d4c4f434b55502d4c494e0000000000000000000000000000006020918201528251908301207fc66b376a19264d832c1bc254000c18944ca5aa57ed50f4ea637c4da424d4c3bb1490565b15614e5557505051614e28816148c3565b600d81527f4c6f636b7570204c696e65617200000000000000000000000000000000000000602082015290565b614eb98351614e63816148c3565b601181527f5341422d56322d4c4f434b55502d44594e0000000000000000000000000000006020918201528251908301207f6ab655856fa5352de8c05542b1937ac63c59342da992602767c02734cc5391651490565b15614ef757505051614eca816148c3565b600e81527f4c6f636b75702044796e616d6963000000000000000000000000000000000000602082015290565b614f379083519384937f814a8a2e000000000000000000000000000000000000000000000000000000008552600485015260248401526044830190614849565b0390fd5b614f5891503d806000833e614f5081836148df565b810190614ad7565b38614db2565b83513d6000823e3d90fd5b60405160208101907f313ce56700000000000000000000000000000000000000000000000000000000825260048152614fa1816148c3565b6000928392839251915afa614fb4614aa7565b9080614feb575b15614fe757602081805181010312614fe357602001519060ff82168203614fe0575090565b80fd5b5080fd5b5090565b506020815114614fbb565b60405190615003826148c3565b600482527f2667743b000000000000000000000000000000000000000000000000000000006020830152565b6040519061503c826148c3565b600482527f266c743b000000000000000000000000000000000000000000000000000000006020830152565b906150d092949360405195869260209461508a81518092888089019101614826565b840161509e82518093888085019101614826565b016150b182518093878085019101614826565b016150c482518093868085019101614826565b010380855201836148df565b565b80156153e257600091806153bd575090505b600190808281101561514e575050506150fb61502f565b614b36602260405183615118829551809260208086019101614826565b81017f203100000000000000000000000000000000000000000000000000000000000060208201520360028101845201826148df565b66038d7ea4c6800011156153605760409081519060a0820182811067ffffffffffffffff821117614311578084526151858161488b565b600081528252825190615197826148c3565b8482526020917f4b00000000000000000000000000000000000000000000000000000000000000838201528284015283516151d1816148c3565b8581527f4d0000000000000000000000000000000000000000000000000000000000000083820152848401528351615208816148c3565b8581527f42000000000000000000000000000000000000000000000000000000000000008382015260608401528351615240816148c3565b8581527f5400000000000000000000000000000000000000000000000000000000000000838201526080840152600091856000965b615334575b50845194615287866148c3565b600790600787527f2623383830353b0000000000000000000000000000000000000000000000000083880152519560005b8281106153215750505050615302615308917f20000000000000000000000000000000000000000000000000000000000000006027870152600886526152fd866148c3565b6155e2565b916158c3565b916005851015614a7557614b369460051b015192615068565b81810184015188820185015283016152b8565b9591926103e89081851061535757508680916064600a8704069504930196615275565b9392965061527a565b505061536a614ff6565b614b36602860405183615387829551809260208086019101614826565b81017f203939392e39395400000000000000000000000000000000000000000000000060208201520360088101845201826148df565b600a0a9182156153ce5750046150e4565b80634e487b7160e01b602492526012600452fd5b50506040516153f0816148c3565b600181527f3000000000000000000000000000000000000000000000000000000000000000602082015290565b6201518091030480615485575061543261502f565b614b3660266040518361544f829551809260208086019101614826565b81017f203120446179000000000000000000000000000000000000000000000000000060208201520360068101845201826148df565b61270f8111615554576001810361551157614b3660206154d96040516154aa816148c3565b600481527f204461790000000000000000000000000000000000000000000000000000000083820152936155e2565b60405193816154f18693518092868087019101614826565b820161550582518093868085019101614826565b010380845201826148df565b614b3660206154d9604051615525816148c3565b600581527f204461797300000000000000000000000000000000000000000000000000000083820152936155e2565b5061555d614ff6565b614b36602a6040518361557a829551809260208086019101614826565b81017f2039393939204461797300000000000000000000000000000000000000000000602082015203600a8101845201826148df565b906155ba82614a8b565b6155c760405191826148df565b82815280926155d8601f1991614a8b565b0190602036910137565b806000917a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000080821015615725575b506d04ee2d6d415b85acef810000000080831015615716575b50662386f26fc1000080831015615707575b506305f5e100808310156156f8575b50612710808310156156e9575b5060648210156156d9575b600a809210156156cf575b60019081602161567a600187016155b0565b95860101905b61568c575b5050505090565b600019019083907f30313233343536373839616263646566000000000000000000000000000000008282061a8353049182156156ca57919082615680565b615685565b9160010191615668565b919060646002910491019161565d565b60049193920491019138615652565b60089193920491019138615645565b60109193920491019138615636565b60209193920491019138615624565b60409350810491503861560b565b80511561589e57604051615746816148a7565b604081527f4142434445464748494a4b4c4d4e4f505152535455565758595a61626364656660208201527f6768696a6b6c6d6e6f707172737475767778797a303132333435363738392b2f6040820152815191600292600281018091116158885760038091047f3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81168103615888576157e5906002959492951b6155b0565b936020850193839284518501935b84811061583557505050505060039051068060011461582257600214615817575090565b603d90600019015390565b50603d9081600019820153600119015390565b8360049197929394959701918251600190603f9082828260121c16880101518453828282600c1c16880101518385015382828260061c1688010151888501531685010151868201530195939291906157f3565b634e487b7160e01b600052601160045260246000fd5b506040516158ab8161488b565b6000815290565b908151811015614a75570160200190565b806158d557506040516158ab8161488b565b600a81101561593a576158e7906155e2565b614b36602260405180937f2e30000000000000000000000000000000000000000000000000000000000000602083015261592a8151809260208686019101614826565b81010360028101845201826148df565b615943906155e2565b614b36602160405180937f2e0000000000000000000000000000000000000000000000000000000000000060208301526159868151809260208686019101614826565b81010360018101845201826148df565b604051906159a3826148c3565b601082527f68736c283233302c3231252c31312529000000000000000000000000000000006020830152565b8015615c01576159dd615996565b9061271090810390811161588857614b36916159fb610136926155e2565b6040519485927f3c672066696c6c3d226e6f6e65223e000000000000000000000000000000000060208501527f3c636972636c652063783d22313636222063793d2235302220723d2232322220602f8501527f7374726f6b653d22000000000000000000000000000000000000000000000000604f850152615a87815180926020605788019101614826565b83017f22207374726f6b652d77696474683d223130222f3e000000000000000000000060578201527f3c636972636c652063783d22313636222063793d2235302220706174684c656e606c8201527f6774683d2231303030302220723d22323222207374726f6b653d220000000000608c820152615b0f82518093602060a785019101614826565b017f22207374726f6b652d6461736861727261793d22313030303022207374726f6b60a78201527f652d646173686f66667365743d2200000000000000000000000000000000000060c7820152615b7082518093602060d585019101614826565b017f22207374726f6b652d6c696e656361703d22726f756e6422207374726f6b652d60d58201527f77696474683d223522207472616e73666f726d3d22726f74617465282d39302960f58201527f22207472616e73666f726d2d6f726967696e3d22313636203530222f3e000000610115820152631e17b39f60e11b610132820152036101168101845201826148df565b50506040516158ab8161488b565b60306150d0919392936040519481615c31879351809260208087019101614826565b820164010714051160dd1b60208201526a029b0b13634b2b9102b19160ad1b6025820152615c688251809360208785019101614826565b010360108101855201836148df565b60256150d0919392936040519481615c99879351809260208087019101614826565b820164010714051160dd1b6020820152615cbc8251809360208785019101614826565b010360058101855201836148df565b60009080518015615d4157906000916000915b818310615cf057505050600d02900390565b909193603b60f81b7fff00000000000000000000000000000000000000000000000000000000000000615d2387856158b2565b511614615d39575b600d01936001019190615cde565b849350615d2b565b505050600090565b60009080518015615d4157906000916000915b818310615d6e5750505060041b900390565b909193603b60f81b7fff00000000000000000000000000000000000000000000000000000000000000615da187856158b2565b511614615db7575b601001936001019190615d5c565b849350615da956fea164736f6c6343000817000a"; - - /*////////////////////////////////////////////////////////////////////////// - DEPLOYERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Deploys {SablierV2Comptroller} from precompiled bytecode. - function deployComptroller(address initialAdmin) public returns (ISablierV2Comptroller comptroller) { - bytes memory creationBytecode = bytes.concat(BYTECODE_COMPTROLLER, abi.encode(initialAdmin)); - assembly { - comptroller := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) - } - require( - address(comptroller) != address(0), "Sablier V2 Precompiles: deployment failed for Comptroller contract" - ); - } - - /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode, passing a default value for the - /// `maxSegmentCount` parameter. - /// @dev Notes: - /// - A default value is passed for `maxSegmentCount`. - /// - A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. - function deployLockupDynamic( - address initialAdmin, - ISablierV2Comptroller initialComptroller - ) - public - returns (ISablierV2LockupDynamic lockupDynamic) - { - uint256 maxSegmentCount = MAX_SEGMENT_COUNT; - lockupDynamic = deployLockupDynamic(initialAdmin, initialComptroller, maxSegmentCount); - } - - /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. - /// @dev A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. - function deployLockupDynamic( - address initialAdmin, - ISablierV2Comptroller initialComptroller, - uint256 maxSegmentCount - ) - public - returns (ISablierV2LockupDynamic lockupDynamic) - { - ISablierV2NFTDescriptor nftDescriptor = new SablierV2NFTDescriptor(); - lockupDynamic = deployLockupDynamic(initialAdmin, initialComptroller, nftDescriptor, maxSegmentCount); - } - - /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. - /// @dev A default value is passed for `maxSegmentCount`. - function deployLockupDynamic( - address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor nftDescriptor - ) - public - returns (ISablierV2LockupDynamic lockupDynamic) - { - lockupDynamic = deployLockupDynamic(initialAdmin, initialComptroller, nftDescriptor, MAX_SEGMENT_COUNT); - } - - /// @notice Deploys {SablierV2LockupDynamic} from precompiled bytecode. - function deployLockupDynamic( - address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor nftDescriptor, - uint256 maxSegmentCount - ) - public - returns (ISablierV2LockupDynamic lockupDynamic) - { - bytes memory creationBytecode = bytes.concat( - BYTECODE_LOCKUP_DYNAMIC, abi.encode(initialAdmin, initialComptroller, nftDescriptor, maxSegmentCount) - ); - assembly { - lockupDynamic := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) - } - require( - address(lockupDynamic) != address(0), "Sablier V2 Precompiles: deployment failed for LockupDynamic contract" - ); - } - - /// @notice Deploys {SablierV2LockupLinear} from precompiled bytecode. - /// @dev A dummy {SablierV2NFTDescriptor} is deployed so that the user does not have to provide one. - function deployLockupLinear( - address initialAdmin, - ISablierV2Comptroller initialComptroller - ) - public - returns (ISablierV2LockupLinear lockupLinear) - { - ISablierV2NFTDescriptor nftDescriptor = new SablierV2NFTDescriptor(); - lockupLinear = deployLockupLinear(initialAdmin, initialComptroller, nftDescriptor); - } - - /// @notice Deploys {SablierV2LockupLinear} from precompiled bytecode. - function deployLockupLinear( - address initialAdmin, - ISablierV2Comptroller initialComptroller, - ISablierV2NFTDescriptor nftDescriptor - ) - public - returns (ISablierV2LockupLinear lockupLinear) - { - bytes memory creationBytecode = - bytes.concat(BYTECODE_LOCKUP_LINEAR, abi.encode(initialAdmin, initialComptroller, nftDescriptor)); - assembly { - lockupLinear := create(0, add(creationBytecode, 0x20), mload(creationBytecode)) - } - require( - address(lockupLinear) != address(0), "Sablier V2 Precompiles: deployment failed for LockupLinear contract" - ); - } - - /// @notice Deploys {SablierV2NFTDescriptor} from precompiled bytecode. - function deployNFTDescriptor() public returns (ISablierV2NFTDescriptor nftDescriptor) { - bytes memory bytecode = BYTECODE_NFT_DESCRIPTOR; - assembly { - nftDescriptor := create(0, add(bytecode, 0x20), mload(bytecode)) - } - require( - address(nftDescriptor) != address(0), "Sablier V2 Precompiles: deployment failed for NFTDescriptor contract" - ); - } - - /// @notice Deploys all V2 Core contracts in the following order: - /// - /// 1. {SablierV2Comptroller} - /// 2. {SablierV2NFTDescriptor} - /// 3. {SablierV2LockupDynamic} - /// 4. {SablierV2LockupLinear} - function deployCore(address initialAdmin) - public - returns ( - ISablierV2Comptroller comptroller, - ISablierV2LockupDynamic lockupDynamic, - ISablierV2LockupLinear lockupLinear, - ISablierV2NFTDescriptor nftDescriptor - ) - { - comptroller = deployComptroller(initialAdmin); - nftDescriptor = deployNFTDescriptor(); - lockupDynamic = deployLockupDynamic(initialAdmin, comptroller); - lockupLinear = deployLockupLinear(initialAdmin, comptroller); - } -} diff --git a/test/utils/Precompiles.t.sol b/test/utils/Precompiles.t.sol index 998f48a5c..5dcd11e88 100644 --- a/test/utils/Precompiles.t.sol +++ b/test/utils/Precompiles.t.sol @@ -1,19 +1,18 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.19 <0.9.0; +pragma solidity >=0.8.22 <0.9.0; import { LibString } from "solady/src/utils/LibString.sol"; -import { ISablierV2Comptroller } from "../../src/interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2LockupDynamic } from "../../src/interfaces/ISablierV2LockupDynamic.sol"; -import { ISablierV2LockupLinear } from "../../src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2NFTDescriptor } from "../../src/interfaces/ISablierV2NFTDescriptor.sol"; +import { Precompiles } from "precompiles/Precompiles.sol"; +import { ISablierV2LockupDynamic } from "src/interfaces/ISablierV2LockupDynamic.sol"; +import { ISablierV2LockupLinear } from "src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "src/interfaces/ISablierV2LockupTranched.sol"; +import { ISablierV2NFTDescriptor } from "src/interfaces/ISablierV2NFTDescriptor.sol"; import { Base_Test } from "../Base.t.sol"; -import { Precompiles } from "./Precompiles.sol"; contract Precompiles_Test is Base_Test { using LibString for address; - using LibString for string; Precompiles internal precompiles = new Precompiles(); @@ -23,31 +22,32 @@ contract Precompiles_Test is Base_Test { } } - function test_DeployComptroller() external onlyTestOptimizedProfile { - address actualComptroller = address(precompiles.deployComptroller(users.admin)); - address expectedComptroller = address(deployOptimizedComptroller(users.admin)); - assertEq(actualComptroller.code, expectedComptroller.code, "bytecodes mismatch"); - } - function test_DeployLockupDynamic() external onlyTestOptimizedProfile { - ISablierV2Comptroller comptroller = precompiles.deployComptroller(users.admin); - address actualLockupDynamic = address(precompiles.deployLockupDynamic(users.admin, comptroller, nftDescriptor)); + address actualLockupDynamic = address(precompiles.deployLockupDynamic(users.admin, nftDescriptor)); address expectedLockupDynamic = - address(deployOptimizedLockupDynamic(users.admin, comptroller, nftDescriptor, defaults.MAX_SEGMENT_COUNT())); + address(deployOptimizedLockupDynamic(users.admin, nftDescriptor, precompiles.MAX_SEGMENT_COUNT())); bytes memory expectedLockupDynamicCode = adjustBytecode(expectedLockupDynamic.code, expectedLockupDynamic, actualLockupDynamic); assertEq(actualLockupDynamic.code, expectedLockupDynamicCode, "bytecodes mismatch"); } function test_DeployLockupLinear() external onlyTestOptimizedProfile { - ISablierV2Comptroller comptroller = precompiles.deployComptroller(users.admin); - address actualLockupLinear = address(precompiles.deployLockupLinear(users.admin, comptroller, nftDescriptor)); - address expectedLockupLinear = address(deployOptimizedLockupLinear(users.admin, comptroller, nftDescriptor)); + address actualLockupLinear = address(precompiles.deployLockupLinear(users.admin, nftDescriptor)); + address expectedLockupLinear = address(deployOptimizedLockupLinear(users.admin, nftDescriptor)); bytes memory expectedLockupLinearCode = adjustBytecode(expectedLockupLinear.code, expectedLockupLinear, actualLockupLinear); assertEq(actualLockupLinear.code, expectedLockupLinearCode, "bytecodes mismatch"); } + function test_DeployLockupTranched() external onlyTestOptimizedProfile { + address actualLockupTranched = address(precompiles.deployLockupTranched(users.admin, nftDescriptor)); + address expectedLockupTranched = + address(deployOptimizedLockupTranched(users.admin, nftDescriptor, precompiles.MAX_TRANCHE_COUNT())); + bytes memory expectedLockupTranchedCode = + adjustBytecode(expectedLockupTranched.code, expectedLockupTranched, actualLockupTranched); + assertEq(actualLockupTranched.code, expectedLockupTranchedCode, "bytecodes mismatch"); + } + function test_DeployNFTDescriptor() external onlyTestOptimizedProfile { address actualNFTDescriptor = address(precompiles.deployNFTDescriptor()); address expectedNFTDescriptor = address(deployOptimizedNFTDescriptor()); @@ -56,18 +56,18 @@ contract Precompiles_Test is Base_Test { function test_DeployCore() external onlyTestOptimizedProfile { ( - ISablierV2Comptroller actualComptroller, ISablierV2LockupDynamic actualLockupDynamic, ISablierV2LockupLinear actualLockupLinear, + ISablierV2LockupTranched actualLockupTranched, ISablierV2NFTDescriptor actualNFTDescriptor ) = precompiles.deployCore(users.admin); ( - ISablierV2Comptroller expectedComptroller, ISablierV2LockupDynamic expectedLockupDynamic, ISablierV2LockupLinear expectedLockupLinear, + ISablierV2LockupTranched expectedLockupTranched, ISablierV2NFTDescriptor expectedNFTDescriptor - ) = deployOptimizedCore(users.admin, defaults.MAX_SEGMENT_COUNT()); + ) = deployOptimizedCore(users.admin, precompiles.MAX_SEGMENT_COUNT(), precompiles.MAX_TRANCHE_COUNT()); bytes memory expectedLockupDynamicCode = adjustBytecode( address(expectedLockupDynamic).code, address(expectedLockupDynamic), address(actualLockupDynamic) @@ -77,9 +77,13 @@ contract Precompiles_Test is Base_Test { address(expectedLockupLinear).code, address(expectedLockupLinear), address(actualLockupLinear) ); - assertEq(address(actualComptroller).code, address(expectedComptroller).code, "bytecodes mismatch"); + bytes memory expectedLockupTranchedCode = adjustBytecode( + address(expectedLockupTranched).code, address(expectedLockupTranched), address(actualLockupTranched) + ); + assertEq(address(actualLockupDynamic).code, expectedLockupDynamicCode, "bytecodes mismatch"); assertEq(address(actualLockupLinear).code, expectedLockupLinearCode, "bytecodes mismatch"); + assertEq(address(actualLockupTranched).code, expectedLockupTranchedCode, "bytecodes mismatch"); assertEq(address(actualNFTDescriptor).code, address(expectedNFTDescriptor).code, "bytecodes mismatch"); } @@ -95,9 +99,10 @@ contract Precompiles_Test is Base_Test { returns (bytes memory) { return vm.parseBytes( - vm.toString(bytecode).replace({ - search: expectedAddress.toHexStringNoPrefix(), - replacement: actualAddress.toHexStringNoPrefix() + vm.replace({ + input: vm.toString(bytecode), + from: expectedAddress.toHexStringNoPrefix(), + to: actualAddress.toHexStringNoPrefix() }) ); } diff --git a/test/utils/Types.sol b/test/utils/Types.sol index 25997cd07..2ba8fdb1c 100644 --- a/test/utils/Types.sol +++ b/test/utils/Types.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; struct Users { // Default admin for all Sablier V2 contracts. diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol index f2e483e6e..0d7192fb1 100644 --- a/test/utils/Utils.sol +++ b/test/utils/Utils.sol @@ -1,21 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.19; +pragma solidity >=0.8.22; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { PRBMathUtils } from "@prb/math/test/utils/Utils.sol"; +import { CommonBase } from "forge-std/src/Base.sol"; -import { Vm } from "@prb/test/src/PRBTest.sol"; -import { StdUtils } from "forge-std/src/StdUtils.sol"; - -import { LockupDynamic } from "../../src/types/DataTypes.sol"; - -abstract contract Utils is StdUtils, PRBMathUtils { - /// @dev The virtual address of the Foundry VM. - address private constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - - /// @dev An instance of the Foundry VM, which contains cheatcodes for testing. - Vm private constant vm = Vm(VM_ADDRESS); +import { LockupDynamic, LockupTranched } from "../../src/types/DataTypes.sol"; +abstract contract Utils is CommonBase, PRBMathUtils { /// @dev Bounds a `uint128` number. function boundUint128(uint128 x, uint128 min, uint128 max) internal pure returns (uint128) { return uint128(_bound(uint256(x), uint256(min), uint256(max))); @@ -31,32 +23,77 @@ abstract contract Utils is StdUtils, PRBMathUtils { return uint40(block.timestamp); } - /// @dev Turns the segments with deltas into canonical segments, which have milestones. - function getSegmentsWithMilestones(LockupDynamic.SegmentWithDelta[] memory segments) + /// @dev Turns the segments with durations into canonical segments, which have timestamps. + function getSegmentsWithTimestamps(LockupDynamic.SegmentWithDuration[] memory segments) internal view - returns (LockupDynamic.Segment[] memory segmentsWithMilestones) + returns (LockupDynamic.Segment[] memory segmentsWithTimestamps) { unchecked { - segmentsWithMilestones = new LockupDynamic.Segment[](segments.length); - segmentsWithMilestones[0] = LockupDynamic.Segment({ + segmentsWithTimestamps = new LockupDynamic.Segment[](segments.length); + segmentsWithTimestamps[0] = LockupDynamic.Segment({ amount: segments[0].amount, exponent: segments[0].exponent, - milestone: getBlockTimestamp() + segments[0].delta + timestamp: getBlockTimestamp() + segments[0].duration }); for (uint256 i = 1; i < segments.length; ++i) { - segmentsWithMilestones[i] = LockupDynamic.Segment({ + segmentsWithTimestamps[i] = LockupDynamic.Segment({ amount: segments[i].amount, exponent: segments[i].exponent, - milestone: segmentsWithMilestones[i - 1].milestone + segments[i].delta + timestamp: segmentsWithTimestamps[i - 1].timestamp + segments[i].duration + }); + } + } + } + + /// @dev Turns the tranches with durations into canonical tranches, which have timestamps. + function getTranchesWithTimestamps(LockupTranched.TrancheWithDuration[] memory tranches) + internal + view + returns (LockupTranched.Tranche[] memory tranchesWithTimestamps) + { + unchecked { + tranchesWithTimestamps = new LockupTranched.Tranche[](tranches.length); + tranchesWithTimestamps[0] = LockupTranched.Tranche({ + amount: tranches[0].amount, + timestamp: getBlockTimestamp() + tranches[0].duration + }); + for (uint256 i = 1; i < tranches.length; ++i) { + tranchesWithTimestamps[i] = LockupTranched.Tranche({ + amount: tranches[i].amount, + timestamp: tranchesWithTimestamps[i - 1].timestamp + tranches[i].duration }); } } } + /// @dev Checks if the Foundry profile is "benchmark". + function isBenchmarkProfile() internal view returns (bool) { + string memory profile = vm.envOr({ name: "FOUNDRY_PROFILE", defaultValue: string("default") }); + return Strings.equal(profile, "benchmark"); + } + /// @dev Checks if the Foundry profile is "test-optimized". - function isTestOptimizedProfile() internal returns (bool) { + function isTestOptimizedProfile() internal view returns (bool) { string memory profile = vm.envOr({ name: "FOUNDRY_PROFILE", defaultValue: string("default") }); return Strings.equal(profile, "test-optimized"); } + + /// @dev Returns the largest of the provided `uint40` numbers. + function maxOfThree(uint40 a, uint40 b, uint40 c) internal pure returns (uint40) { + uint40 max = a; + if (b > max) { + max = b; + } + if (c > max) { + max = c; + } + return max; + } + + /// @dev Stops the active prank and sets a new one. + function resetPrank(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + } }