Skip to content

Commit 9cbce83

Browse files
committed
feat: adds claim-via-signature function
refactor: adds a shared function that reverts if `to` address is zero
1 parent 0b12a17 commit 9cbce83

File tree

11 files changed

+458
-87
lines changed

11 files changed

+458
-87
lines changed

src/SablierMerkleInstant.sol

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
66

77
import { SablierMerkleBase } from "./abstracts/SablierMerkleBase.sol";
88
import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol";
9-
import { Errors } from "./libraries/Errors.sol";
109
import { MerkleInstant } from "./types/DataTypes.sol";
1110

1211
/*
@@ -71,10 +70,10 @@ contract SablierMerkleInstant is
7170
payable
7271
override
7372
{
74-
// Check, Effect and Interaction: Pre-process the claim parameters.
73+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
7574
_preProcessClaim(index, recipient, amount, merkleProof);
7675

77-
// Interaction: Post-process the claim parameters.
76+
// Interaction: Post-process the claim parameters on behalf of the recipient.
7877
_postProcessClaim({ index: index, recipient: recipient, to: recipient, amount: amount });
7978
}
8079

@@ -89,18 +88,42 @@ contract SablierMerkleInstant is
8988
payable
9089
override
9190
{
92-
// Check: `to` must not be the zero address.
93-
if (to == address(0)) {
94-
revert Errors.SablierMerkleInstant_ToZeroAddress();
95-
}
91+
// Check: `to` is not the zero address.
92+
_revertIfToZeroAddress(to);
9693

97-
// Check, Effect and Interaction: Pre-process the claim parameters.
94+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of `msg.sender`.
9895
_preProcessClaim({ index: index, recipient: msg.sender, amount: amount, merkleProof: merkleProof });
9996

100-
// Interaction: Post-process the claim parameters.
97+
// Interaction: Post-process the claim parameters on behalf of `msg.sender`.
10198
_postProcessClaim({ index: index, recipient: msg.sender, to: to, amount: amount });
10299
}
103100

101+
/// @inheritdoc ISablierMerkleInstant
102+
function claimViaSig(
103+
uint256 index,
104+
address recipient,
105+
address to,
106+
uint128 amount,
107+
bytes32[] calldata merkleProof,
108+
bytes calldata signature
109+
)
110+
external
111+
payable
112+
override
113+
{
114+
// Check: `to` is not the zero address.
115+
_revertIfToZeroAddress(to);
116+
117+
// Check: the signature is valid and the recovered signer matches the recipient.
118+
_checkSignature(index, recipient, to, amount, signature);
119+
120+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
121+
_preProcessClaim(index, recipient, amount, merkleProof);
122+
123+
// Interaction: Post-process the claim parameters on behalf of the recipient.
124+
_postProcessClaim(index, recipient, to, amount);
125+
}
126+
104127
/*//////////////////////////////////////////////////////////////////////////
105128
PRIVATE STATE-CHANGING FUNCTIONS
106129
//////////////////////////////////////////////////////////////////////////*/

src/SablierMerkleLL.sol

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol";
88

99
import { SablierMerkleLockup } from "./abstracts/SablierMerkleLockup.sol";
1010
import { ISablierMerkleLL } from "./interfaces/ISablierMerkleLL.sol";
11-
import { Errors } from "./libraries/Errors.sol";
1211
import { MerkleLL } from "./types/DataTypes.sol";
1312

1413
/*
@@ -104,10 +103,10 @@ contract SablierMerkleLL is
104103
payable
105104
override
106105
{
107-
// Check, Effect and Interaction: Pre-process the claim parameters.
106+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
108107
_preProcessClaim(index, recipient, amount, merkleProof);
109108

110-
// Effect and Interaction: Post-process the claim parameters.
109+
// Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
111110
_postProcessClaim({ index: index, recipient: recipient, to: recipient, amount: amount });
112111
}
113112

@@ -122,18 +121,42 @@ contract SablierMerkleLL is
122121
payable
123122
override
124123
{
125-
// Check: `to` must not be the zero address.
126-
if (to == address(0)) {
127-
revert Errors.SablierMerkleLL_ToZeroAddress();
128-
}
124+
// Check: `to` is not the zero address.
125+
_revertIfToZeroAddress(to);
129126

130-
// Check, Effect and Interaction: Pre-process the claim parameters.
127+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of `msg.sender`.
131128
_preProcessClaim({ index: index, recipient: msg.sender, amount: amount, merkleProof: merkleProof });
132129

133-
// Effect and Interaction: Post-process the claim parameters.
130+
// Effect and Interaction: Post-process the claim parameters on behalf of `msg.sender`.
134131
_postProcessClaim({ index: index, recipient: msg.sender, to: to, amount: amount });
135132
}
136133

134+
/// @inheritdoc ISablierMerkleLL
135+
function claimViaSig(
136+
uint256 index,
137+
address recipient,
138+
address to,
139+
uint128 amount,
140+
bytes32[] calldata merkleProof,
141+
bytes calldata signature
142+
)
143+
external
144+
payable
145+
override
146+
{
147+
// Check: `to` is not the zero address.
148+
_revertIfToZeroAddress(to);
149+
150+
// Check: the signature is valid and the recovered signer matches the recipient.
151+
_checkSignature(index, recipient, to, amount, signature);
152+
153+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
154+
_preProcessClaim(index, recipient, amount, merkleProof);
155+
156+
// Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
157+
_postProcessClaim(index, recipient, to, amount);
158+
}
159+
137160
/*//////////////////////////////////////////////////////////////////////////
138161
PRIVATE STATE-CHANGING FUNCTIONS
139162
//////////////////////////////////////////////////////////////////////////*/

src/SablierMerkleLT.sol

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ contract SablierMerkleLT is
114114
payable
115115
override
116116
{
117-
// Check, Effect and Interaction: Pre-process the claim parameters.
117+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
118118
_preProcessClaim(index, recipient, amount, merkleProof);
119119

120-
// Check, Effect and Interaction: Post-process the claim parameters.
120+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
121121
_postProcessClaim({ index: index, recipient: recipient, to: recipient, amount: amount });
122122
}
123123

@@ -132,18 +132,42 @@ contract SablierMerkleLT is
132132
payable
133133
override
134134
{
135-
// Check: `to` must not be the zero address.
136-
if (to == address(0)) {
137-
revert Errors.SablierMerkleLT_ToZeroAddress();
138-
}
135+
// Check: `to` is not the zero address.
136+
_revertIfToZeroAddress(to);
139137

140-
// Check, Effect and Interaction: Pre-process the claim parameters.
138+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of `msg.sender`.
141139
_preProcessClaim({ index: index, recipient: msg.sender, amount: amount, merkleProof: merkleProof });
142140

143-
// Check, Effect and Interaction: Post-process the claim parameters.
141+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of `msg.sender`.
144142
_postProcessClaim({ index: index, recipient: msg.sender, to: to, amount: amount });
145143
}
146144

145+
/// @inheritdoc ISablierMerkleLT
146+
function claimViaSig(
147+
uint256 index,
148+
address recipient,
149+
address to,
150+
uint128 amount,
151+
bytes32[] calldata merkleProof,
152+
bytes calldata signature
153+
)
154+
external
155+
payable
156+
override
157+
{
158+
// Check: `to` is not the zero address.
159+
_revertIfToZeroAddress(to);
160+
161+
// Check: the signature is valid and the recovered signer matches the recipient.
162+
_checkSignature(index, recipient, to, amount, signature);
163+
164+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
165+
_preProcessClaim(index, recipient, amount, merkleProof);
166+
167+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
168+
_postProcessClaim(index, recipient, to, amount);
169+
}
170+
147171
/*//////////////////////////////////////////////////////////////////////////
148172
PRIVATE READ-ONLY FUNCTIONS
149173
//////////////////////////////////////////////////////////////////////////*/

src/SablierMerkleVCA.sol

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ contract SablierMerkleVCA is
155155
payable
156156
override
157157
{
158-
// Check, Effect and Interaction: Pre-process the claim parameters.
158+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
159159
_preProcessClaim({ index: index, recipient: recipient, amount: fullAmount, merkleProof: merkleProof });
160160

161-
// Check, Effect and Interaction: Post-process the claim parameters.
161+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
162162
_postProcessClaim({ index: index, recipient: recipient, to: recipient, fullAmount: fullAmount });
163163
}
164164

@@ -173,18 +173,42 @@ contract SablierMerkleVCA is
173173
payable
174174
override
175175
{
176-
// Check: `to` must not be the zero address.
177-
if (to == address(0)) {
178-
revert Errors.SablierMerkleVCA_ToZeroAddress();
179-
}
176+
// Check: `to` is not the zero address.
177+
_revertIfToZeroAddress(to);
180178

181-
// Check, Effect and Interaction: Pre-process the claim parameters.
179+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of `msg.sender`.
182180
_preProcessClaim({ index: index, recipient: msg.sender, amount: fullAmount, merkleProof: merkleProof });
183181

184-
// Check, Effect and Interaction: Post-process the claim parameters.
182+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of `msg.sender`.
185183
_postProcessClaim({ index: index, recipient: msg.sender, to: to, fullAmount: fullAmount });
186184
}
187185

186+
/// @inheritdoc ISablierMerkleVCA
187+
function claimViaSig(
188+
uint256 index,
189+
address recipient,
190+
address to,
191+
uint128 fullAmount,
192+
bytes32[] calldata merkleProof,
193+
bytes calldata signature
194+
)
195+
external
196+
payable
197+
override
198+
{
199+
// Check: `to` is not the zero address.
200+
_revertIfToZeroAddress(to);
201+
202+
// Check: the signature is valid and the recovered signer matches the recipient.
203+
_checkSignature(index, recipient, to, fullAmount, signature);
204+
205+
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
206+
_preProcessClaim(index, recipient, fullAmount, merkleProof);
207+
208+
// Check, Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
209+
_postProcessClaim(index, recipient, to, fullAmount);
210+
}
211+
188212
/*//////////////////////////////////////////////////////////////////////////
189213
PRIVATE READ-ONLY FUNCTIONS
190214
//////////////////////////////////////////////////////////////////////////*/

src/abstracts/SablierMerkleBase.sol

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/inte
55
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
66
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
77
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
8+
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
9+
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
810
import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
911
import { Adminable } from "@sablier/evm-utils/src/Adminable.sol";
1012
import { ISablierFactoryMerkleBase } from "./../interfaces/ISablierFactoryMerkleBase.sol";
@@ -54,6 +56,18 @@ abstract contract SablierMerkleBase is
5456
/// @inheritdoc ISablierMerkleBase
5557
uint256 public override minFeeUSD;
5658

59+
/// @dev The struct type hash used for computing the domain separator for EIP-712 and EIP-1271 signatures.
60+
bytes32 private constant _CLAIM_TYPEHASH =
61+
keccak256("Claim(uint256 index,address recipient,address to,uint128 amount)");
62+
63+
/// @dev The domain separator, as required by EIP-712 and EIP-1271, used for signing claim to prevent replay attacks
64+
/// across different campaigns.
65+
bytes32 private immutable _DOMAIN_SEPARATOR;
66+
67+
/// @dev The domain type hash used for computing the domain separator for EIP-712 and EIP-1271 signatures.
68+
bytes32 private constant _DOMAIN_TYPE_HASH =
69+
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");
70+
5771
/// @dev Packed booleans that record the history of claims.
5872
BitMaps.BitMap internal _claimedBitMap;
5973

@@ -74,12 +88,18 @@ abstract contract SablierMerkleBase is
7488
)
7589
Adminable(initialAdmin)
7690
{
91+
// Compute the domain separator to be used for claiming using an EIP-712 or EIP-1271 signature.
92+
_DOMAIN_SEPARATOR = keccak256(
93+
abi.encode(_DOMAIN_TYPE_HASH, keccak256("Sablier Merkle Airdrops Protocol"), block.chainid, address(this))
94+
);
95+
7796
CAMPAIGN_START_TIME = campaignStartTime;
7897
EXPIRATION = expiration;
7998
FACTORY = ISablierFactoryMerkleBase(msg.sender);
8099
MERKLE_ROOT = merkleRoot;
81100
ORACLE = FACTORY.oracle();
82101
TOKEN = token;
102+
83103
campaignName = campaignName_;
84104
ipfsCID = ipfsCID_;
85105
minFeeUSD = FACTORY.minFeeUSDFor(campaignCreator);
@@ -154,11 +174,53 @@ abstract contract SablierMerkleBase is
154174
});
155175
}
156176

177+
/*//////////////////////////////////////////////////////////////////////////
178+
INTERNAL READ-ONLY FUNCTIONS
179+
//////////////////////////////////////////////////////////////////////////*/
180+
181+
/// @dev Verifies the signature against the provided parameters. It supports both EIP-712 and EIP-1271 signatures.
182+
function _checkSignature(
183+
uint256 index,
184+
address recipient,
185+
address to,
186+
uint128 amount,
187+
bytes calldata signature
188+
)
189+
internal
190+
view
191+
{
192+
// Encode the claim parameters using claim type hash and hash it.
193+
bytes32 claimHash = keccak256(abi.encode(_CLAIM_TYPEHASH, index, recipient, to, amount));
194+
195+
// Returns the keccak256 digest of the claim parameters using claim hash and the domain separator.
196+
bytes32 digest = MessageHashUtils.toTypedDataHash(_DOMAIN_SEPARATOR, claimHash);
197+
198+
// If recipient is an EOA, `isValidSignatureNow` recovers the signer using ECDSA from the signature and the
199+
// digest. It returns true if the recovered signer matches the recipient. If the recipient is a contract,
200+
// `isValidSignatureNow` checks if the recipient implements the `IERC1271` interface and returns the magic value
201+
// as per EIP-1271 for the given digest and signature.
202+
bool isSignatureValid =
203+
SignatureChecker.isValidSignatureNow({ signer: recipient, hash: digest, signature: signature });
204+
205+
// Check: `isSignatureValid` is true.
206+
if (!isSignatureValid) {
207+
revert Errors.SablierMerkleBase_InvalidSignature();
208+
}
209+
}
210+
211+
/// @dev Reverts if the `to` address is the zero address.
212+
function _revertIfToZeroAddress(address to) internal pure {
213+
// Check: `to` is not the zero address.
214+
if (to == address(0)) {
215+
revert Errors.SablierMerkleBase_ToZeroAddress();
216+
}
217+
}
218+
157219
/*//////////////////////////////////////////////////////////////////////////
158220
PRIVATE READ-ONLY FUNCTIONS
159221
//////////////////////////////////////////////////////////////////////////*/
160222

161-
/// @dev See the documentation for the user-facing functions that call this internal function.
223+
/// @dev See the documentation for the user-facing functions that call this private function.
162224
function _calculateMinFeeWei() private view returns (uint256) {
163225
// If the oracle is not set, return 0.
164226
if (ORACLE == address(0)) {

0 commit comments

Comments
 (0)