diff --git a/src/SablierV2MerkleStreamerLL.sol b/src/SablierV2MerkleStreamerLL.sol index 94207941..1fa4861e 100644 --- a/src/SablierV2MerkleStreamerLL.sol +++ b/src/SablierV2MerkleStreamerLL.sol @@ -50,7 +50,7 @@ contract SablierV2MerkleStreamerLL is bool cancelable, bool transferable ) - SablierV2MerkleStreamer(initialAdmin, asset, lockupLinear, merkleRoot, expiration, cancelable, transferable) + SablierV2MerkleStreamer(initialAdmin, asset, merkleRoot, expiration, cancelable, transferable) { LOCKUP_LINEAR = lockupLinear; streamDurations = streamDurations_; diff --git a/src/abstracts/SablierV2MerkleStreamer.sol b/src/abstracts/SablierV2MerkleStreamer.sol index 6b692355..75b50101 100644 --- a/src/abstracts/SablierV2MerkleStreamer.sol +++ b/src/abstracts/SablierV2MerkleStreamer.sol @@ -6,8 +6,6 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; -import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; import { ISablierV2MerkleStreamer } from "../interfaces/ISablierV2MerkleStreamer.sol"; import { Errors } from "../libraries/Errors.sol"; @@ -34,9 +32,6 @@ abstract contract SablierV2MerkleStreamer is /// @inheritdoc ISablierV2MerkleStreamer uint40 public immutable override EXPIRATION; - /// @inheritdoc ISablierV2MerkleStreamer - ISablierV2Lockup public immutable override LOCKUP; - /// @inheritdoc ISablierV2MerkleStreamer bytes32 public immutable override MERKLE_ROOT; @@ -58,7 +53,6 @@ abstract contract SablierV2MerkleStreamer is constructor( address initialAdmin, IERC20 asset, - ISablierV2Lockup lockup, bytes32 merkleRoot, uint40 expiration, bool cancelable, @@ -66,7 +60,6 @@ abstract contract SablierV2MerkleStreamer is ) { admin = initialAdmin; ASSET = asset; - LOCKUP = lockup; MERKLE_ROOT = merkleRoot; EXPIRATION = expiration; CANCELABLE = cancelable; @@ -93,12 +86,8 @@ abstract contract SablierV2MerkleStreamer is /// @inheritdoc ISablierV2MerkleStreamer function clawback(address to, uint128 amount) external override onlyAdmin { - // 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 = LOCKUP.comptroller().protocolFees(ASSET); - - // Checks: the campaign is not expired and the protocol fee is zero. - if (!hasExpired() && !protocolFee.gt(ud(0))) { + // Checks: the campaign is not expired. + if (!hasExpired()) { revert Errors.SablierV2MerkleStreamer_CampaignNotExpired({ currentTime: block.timestamp, expiration: EXPIRATION @@ -135,14 +124,5 @@ abstract contract SablierV2MerkleStreamer is if (!MerkleProof.verify(merkleProof, MERKLE_ROOT, leaf)) { revert Errors.SablierV2MerkleStreamer_InvalidProof(); } - - // 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 = LOCKUP.comptroller().protocolFees(ASSET); - - // Checks: the protocol fee is zero. - if (protocolFee.gt(ud(0))) { - revert Errors.SablierV2MerkleStreamer_ProtocolFeeNotZero(); - } } } diff --git a/src/interfaces/ISablierV2MerkleStreamer.sol b/src/interfaces/ISablierV2MerkleStreamer.sol index 6ab4f733..94c5d7de 100644 --- a/src/interfaces/ISablierV2MerkleStreamer.sol +++ b/src/interfaces/ISablierV2MerkleStreamer.sol @@ -3,7 +3,6 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IAdminable } from "@sablier/v2-core/src/interfaces/IAdminable.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; /// @title ISablierV2MerkleStreamer /// @notice A contract that lets user claim Sablier streams using Merkle proofs. An interesting use case for @@ -47,9 +46,6 @@ interface ISablierV2MerkleStreamer is IAdminable { /// @notice Returns a flag indicating whether the Merkle streamer has expired. function hasExpired() external view returns (bool); - /// @notice The address of the {SablierV2Lockup} contract. - function LOCKUP() external returns (ISablierV2Lockup); - /// @notice The root of the Merkle tree used to validate the claims. /// @dev This is an immutable state variable. function MERKLE_ROOT() external returns (bytes32); @@ -66,9 +62,6 @@ interface ISablierV2MerkleStreamer is IAdminable { /// /// @dev Emits a {Clawback} event. /// - /// Notes: - /// - If the protocol is not zero, the expiration check is not made. - /// /// Requirements: /// - The caller must be the admin. /// - The campaign must either be expired or not have an expiration. diff --git a/src/interfaces/ISablierV2MerkleStreamerLL.sol b/src/interfaces/ISablierV2MerkleStreamerLL.sol index eb53b90a..d69cbd01 100644 --- a/src/interfaces/ISablierV2MerkleStreamerLL.sol +++ b/src/interfaces/ISablierV2MerkleStreamerLL.sol @@ -29,7 +29,6 @@ interface ISablierV2MerkleStreamerLL is ISablierV2MerkleStreamer { /// Requirements: /// - The campaign must not have expired. /// - The stream must not have been claimed already. - /// - The protocol fee must be zero. /// - The Merkle proof must be valid. /// /// @param index The index of the recipient in the Merkle tree. diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index ede4400c..382e716a 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -23,9 +23,6 @@ library Errors { /// @notice Thrown when trying to claim with an invalid Merkle proof. error SablierV2MerkleStreamer_InvalidProof(); - /// @notice Thrown when trying to claim when the protocol fee is not zero. - error SablierV2MerkleStreamer_ProtocolFeeNotZero(); - /// @notice Thrown when trying to claim the same stream more than once. error SablierV2MerkleStreamer_StreamClaimed(uint256 index); } diff --git a/test/integration/merkle-streamer/ll/claim/claim.t.sol b/test/integration/merkle-streamer/ll/claim/claim.t.sol index 7da4d4a7..d2404229 100644 --- a/test/integration/merkle-streamer/ll/claim/claim.t.sol +++ b/test/integration/merkle-streamer/ll/claim/claim.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.22 <0.9.0; import { Lockup, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; +import { ud, UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -102,18 +102,16 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { _; } - function test_RevertGiven_ProtocolFeeNotZero() + function test_Claim_ProtocolFeeNotZero() external givenCampaignNotExpired givenNotClaimed givenIncludedInMerkleTree { - uint128 claimAmount = defaults.CLAIM_AMOUNT(); - bytes32[] memory merkleProof = defaults.index1Proof(); changePrank({ msgSender: users.admin }); comptroller.setProtocolFee({ asset: asset, newProtocolFee: ud(0.1e18) }); - vm.expectRevert(Errors.SablierV2MerkleStreamer_ProtocolFeeNotZero.selector); - merkleStreamerLL.claim({ index: 1, recipient: users.recipient1, amount: claimAmount, merkleProof: merkleProof }); + + test_Claim({ protocolFee: ud(0.1e18) }); } modifier givenProtocolFeeZero() { @@ -127,7 +125,12 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { givenIncludedInMerkleTree givenProtocolFeeZero { + test_Claim({ protocolFee: ud(0) }); + } + + function test_Claim(UD60x18 protocolFee) internal { uint256 expectedStreamId = lockupLinear.nextStreamId(); + uint128 feeAmount = uint128(ud(defaults.CLAIM_AMOUNT()).mul(protocolFee).intoUint256()); vm.expectEmit({ emitter: address(merkleStreamerLL) }); emit Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT(), expectedStreamId); @@ -135,7 +138,7 @@ contract Claim_Integration_Test is MerkleStreamer_Integration_Test { LockupLinear.Stream memory actualStream = lockupLinear.getStream(actualStreamId); LockupLinear.Stream memory expectedStream = LockupLinear.Stream({ - amounts: Lockup.Amounts({ deposited: defaults.CLAIM_AMOUNT(), refunded: 0, withdrawn: 0 }), + amounts: Lockup.Amounts({ deposited: defaults.CLAIM_AMOUNT() - feeAmount, refunded: 0, withdrawn: 0 }), asset: asset, cliffTime: uint40(block.timestamp) + defaults.CLIFF_DURATION(), endTime: uint40(block.timestamp) + defaults.TOTAL_DURATION(), diff --git a/test/integration/merkle-streamer/ll/claim/claim.tree b/test/integration/merkle-streamer/ll/claim/claim.tree index d59287b0..00fd2f61 100644 --- a/test/integration/merkle-streamer/ll/claim/claim.tree +++ b/test/integration/merkle-streamer/ll/claim/claim.tree @@ -16,7 +16,9 @@ claim.t.sol │ └── it should revert └── given the claim is included in the Merkle tree ├── given the protocol fee is greater than zero - │ └── it should revert + │ ├── it should mark the index as claimed + │ ├── it should create a LockupLinear stream + │ └── it should emit a {Claim} event └── given the protocol fee is not greater than zero ├── it should mark the index as claimed ├── it should create a LockupLinear stream diff --git a/test/integration/merkle-streamer/ll/clawback/clawback.t.sol b/test/integration/merkle-streamer/ll/clawback/clawback.t.sol index d5433173..fdaea6e9 100644 --- a/test/integration/merkle-streamer/ll/clawback/clawback.t.sol +++ b/test/integration/merkle-streamer/ll/clawback/clawback.t.sol @@ -2,7 +2,6 @@ pragma solidity >=0.8.22 <0.9.0; import { Errors as V2CoreErrors } from "@sablier/v2-core/src/libraries/Errors.sol"; -import { ud } from "@prb/math/src/UD60x18.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -24,11 +23,7 @@ contract Clawback_Integration_Test is MerkleStreamer_Integration_Test { _; } - modifier givenProtocolFeeZero() { - _; - } - - function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin givenProtocolFeeZero { + function test_RevertGiven_CampaignNotExpired() external whenCallerAdmin { vm.expectRevert( abi.encodeWithSelector( Errors.SablierV2MerkleStreamer_CampaignNotExpired.selector, block.timestamp, defaults.EXPIRATION() @@ -44,21 +39,11 @@ contract Clawback_Integration_Test is MerkleStreamer_Integration_Test { _; } - function test_Clawback() external whenCallerAdmin givenProtocolFeeZero givenCampaignExpired { + function test_Clawback() external whenCallerAdmin givenCampaignExpired { test_Clawback(users.admin); } - modifier givenProtocolFeeNotZero() { - comptroller.setProtocolFee({ asset: asset, newProtocolFee: ud(0.03e18) }); - _; - } - - function testFuzz_Clawback_CampaignNotExpired(address to) external whenCallerAdmin givenProtocolFeeNotZero { - vm.assume(to != address(0)); - test_Clawback(to); - } - - function testFuzz_Clawback(address to) external whenCallerAdmin givenCampaignExpired givenProtocolFeeNotZero { + function testFuzz_Clawback(address to) external whenCallerAdmin givenCampaignExpired { vm.assume(to != address(0)); test_Clawback(to); } diff --git a/test/integration/merkle-streamer/ll/clawback/clawback.tree b/test/integration/merkle-streamer/ll/clawback/clawback.tree index 55f5c04e..feef74bc 100644 --- a/test/integration/merkle-streamer/ll/clawback/clawback.tree +++ b/test/integration/merkle-streamer/ll/clawback/clawback.tree @@ -2,16 +2,8 @@ clawback.t.sol ├── when the caller is not the admin │ └── it should revert └── when the caller is the admin - ├── given the protocol fee is not greater than zero - │ ├── given the campaign has not expired - │ │ └── it should revert - │ └── given the campaign has expired - │ ├── it should perform the ERC-20 transfer - │ └── it should emit a {Clawback} event - └── given the protocol fee is greater than zero - ├── given the campaign has not expired - │ ├── it should perform the ERC-20 transfer - │ └── it should emit a {Clawback} event - └── given the campaign has expired - ├── it should perform the ERC-20 transfer - └── it should emit a {Clawback} event + ├── given the campaign has not expired + │ └── it should revert + └── given the campaign has expired + ├── it should perform the ERC-20 transfer + └── it should emit a {Clawback} event