Skip to content

feat: supports EIP-712 and EIP-1271 signatures to claim airdrop #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@chainlink/contracts": "1.3.0",
"@openzeppelin/contracts": "5.3.0",
"@prb/math": "4.1.0",
"@sablier/evm-utils": "github:sablier-labs/evm-utils#e81a04b",
"@sablier/evm-utils": "github:sablier-labs/evm-utils#dc59988",
"@sablier/lockup": "github:sablier-labs/lockup#0c8f8fa",
},
"devDependencies": {
Expand Down Expand Up @@ -169,7 +169,7 @@

"@prb/math": ["@prb/[email protected]", "", {}, "sha512-ef5Xrlh3BeX4xT5/Wi810dpEPq2bYPndRxgFIaKSU1F/Op/s8af03kyom+mfU7gEpvfIZ46xu8W0duiHplbBMg=="],

"@sablier/evm-utils": ["@sablier/evm-utils@github:sablier-labs/evm-utils#e81a04b", {}, "sablier-labs-evm-utils-e81a04b"],
"@sablier/evm-utils": ["@sablier/evm-utils@github:sablier-labs/evm-utils#dc59988", {}, "sablier-labs-evm-utils-dc59988"],

"@sablier/lockup": ["@sablier/lockup@github:sablier-labs/lockup#0c8f8fa", { "dependencies": { "@openzeppelin/contracts": "5.3.0", "@prb/math": "4.1.0", "@sablier/evm-utils": "github:sablier-labs/evm-utils#64835cd" }, "peerDependencies": { "@prb/math": "4.x.x" } }, "sablier-labs-lockup-0c8f8fa"],

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@openzeppelin/contracts": "5.3.0",
"@prb/math": "4.1.0",
"@sablier/lockup": "github:sablier-labs/lockup#0c8f8fa",
"@sablier/evm-utils": "github:sablier-labs/evm-utils#e81a04b"
"@sablier/evm-utils": "github:sablier-labs/evm-utils#dc59988"
},
"devDependencies": {
"forge-std": "github:foundry-rs/forge-std#v1.9.7",
Expand Down
41 changes: 32 additions & 9 deletions src/SablierMerkleInstant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s

import { SablierMerkleBase } from "./abstracts/SablierMerkleBase.sol";
import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol";
import { Errors } from "./libraries/Errors.sol";
import { MerkleInstant } from "./types/DataTypes.sol";

/*
Expand Down Expand Up @@ -71,10 +70,10 @@ contract SablierMerkleInstant is
payable
override
{
// Check, Effect and Interaction: Pre-process the claim parameters.
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

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

Expand All @@ -89,18 +88,42 @@ contract SablierMerkleInstant is
payable
override
{
// Check: `to` must not be the zero address.
if (to == address(0)) {
revert Errors.SablierMerkleInstant_ToZeroAddress();
}
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

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

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

/// @inheritdoc ISablierMerkleInstant
function claimViaSig(
uint256 index,
address recipient,
address to,
uint128 amount,
bytes32[] calldata merkleProof,
bytes calldata signature
)
external
payable
override
{
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

// Check: the signature is valid and the recovered signer matches the recipient.
_checkSignature(index, recipient, to, amount, signature);

// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

// Interaction: Post-process the claim parameters on behalf of the recipient.
_postProcessClaim(index, recipient, to, amount);
}

/*//////////////////////////////////////////////////////////////////////////
PRIVATE STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
41 changes: 32 additions & 9 deletions src/SablierMerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Lockup, LockupLinear } from "@sablier/lockup/src/types/DataTypes.sol";

import { SablierMerkleLockup } from "./abstracts/SablierMerkleLockup.sol";
import { ISablierMerkleLL } from "./interfaces/ISablierMerkleLL.sol";
import { Errors } from "./libraries/Errors.sol";
import { MerkleLL } from "./types/DataTypes.sol";

/*
Expand Down Expand Up @@ -104,10 +103,10 @@ contract SablierMerkleLL is
payable
override
{
// Check, Effect and Interaction: Pre-process the claim parameters.
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

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

Expand All @@ -122,18 +121,42 @@ contract SablierMerkleLL is
payable
override
{
// Check: `to` must not be the zero address.
if (to == address(0)) {
revert Errors.SablierMerkleLL_ToZeroAddress();
}
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

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

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

/// @inheritdoc ISablierMerkleLL
function claimViaSig(
uint256 index,
address recipient,
address to,
uint128 amount,
bytes32[] calldata merkleProof,
bytes calldata signature
)
external
payable
override
{
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

// Check: the signature is valid and the recovered signer matches the recipient.
_checkSignature(index, recipient, to, amount, signature);

// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

// Effect and Interaction: Post-process the claim parameters on behalf of the recipient.
_postProcessClaim(index, recipient, to, amount);
}

/*//////////////////////////////////////////////////////////////////////////
PRIVATE STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
40 changes: 32 additions & 8 deletions src/SablierMerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ contract SablierMerkleLT is
payable
override
{
// Check, Effect and Interaction: Pre-process the claim parameters.
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

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

Expand All @@ -132,18 +132,42 @@ contract SablierMerkleLT is
payable
override
{
// Check: `to` must not be the zero address.
if (to == address(0)) {
revert Errors.SablierMerkleLT_ToZeroAddress();
}
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

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

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

/// @inheritdoc ISablierMerkleLT
function claimViaSig(
uint256 index,
address recipient,
address to,
uint128 amount,
bytes32[] calldata merkleProof,
bytes calldata signature
)
external
payable
override
{
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

// Check: the signature is valid and the recovered signer matches the recipient.
_checkSignature(index, recipient, to, amount, signature);

// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, amount, merkleProof);

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

/*//////////////////////////////////////////////////////////////////////////
PRIVATE READ-ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
40 changes: 32 additions & 8 deletions src/SablierMerkleVCA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ contract SablierMerkleVCA is
payable
override
{
// Check, Effect and Interaction: Pre-process the claim parameters.
// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim({ index: index, recipient: recipient, amount: fullAmount, merkleProof: merkleProof });

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

Expand All @@ -173,18 +173,42 @@ contract SablierMerkleVCA is
payable
override
{
// Check: `to` must not be the zero address.
if (to == address(0)) {
revert Errors.SablierMerkleVCA_ToZeroAddress();
}
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

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

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

/// @inheritdoc ISablierMerkleVCA
function claimViaSig(
uint256 index,
address recipient,
address to,
uint128 fullAmount,
bytes32[] calldata merkleProof,
bytes calldata signature
)
external
payable
override
{
// Check: `to` is not the zero address.
_revertIfToZeroAddress(to);

// Check: the signature is valid and the recovered signer matches the recipient.
_checkSignature(index, recipient, to, fullAmount, signature);

// Check, Effect and Interaction: Pre-process the claim parameters on behalf of the recipient.
_preProcessClaim(index, recipient, fullAmount, merkleProof);

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

/*//////////////////////////////////////////////////////////////////////////
PRIVATE READ-ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
56 changes: 55 additions & 1 deletion src/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/inte
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import { Adminable } from "@sablier/evm-utils/src/Adminable.sol";
import { ISablierFactoryMerkleBase } from "./../interfaces/ISablierFactoryMerkleBase.sol";
import { ISablierMerkleBase } from "./../interfaces/ISablierMerkleBase.sol";
import { Errors } from "./../libraries/Errors.sol";
import { SignatureHash } from "./../libraries/SignatureHash.sol";

/// @title SablierMerkleBase
/// @notice See the documentation in {ISablierMerkleBase}.
Expand All @@ -27,6 +30,9 @@ abstract contract SablierMerkleBase is
/// @inheritdoc ISablierMerkleBase
uint40 public immutable override CAMPAIGN_START_TIME;

/// @inheritdoc ISablierMerkleBase
bytes32 public immutable DOMAIN_SEPARATOR;

/// @inheritdoc ISablierMerkleBase
uint40 public immutable override EXPIRATION;

Expand Down Expand Up @@ -74,12 +80,18 @@ abstract contract SablierMerkleBase is
)
Adminable(initialAdmin)
{
// Compute the domain separator to be used for claiming using an EIP-712 or EIP-1271 signature.
DOMAIN_SEPARATOR = keccak256(
abi.encode(SignatureHash.DOMAIN_TYPEHASH, SignatureHash.PROTOCOL_NAME, block.chainid, address(this))
);

CAMPAIGN_START_TIME = campaignStartTime;
EXPIRATION = expiration;
FACTORY = ISablierFactoryMerkleBase(msg.sender);
MERKLE_ROOT = merkleRoot;
ORACLE = FACTORY.oracle();
TOKEN = token;

campaignName = campaignName_;
ipfsCID = ipfsCID_;
minFeeUSD = FACTORY.minFeeUSDFor(campaignCreator);
Expand Down Expand Up @@ -154,11 +166,53 @@ abstract contract SablierMerkleBase is
});
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL READ-ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @dev Verifies the signature against the provided parameters. It supports both EIP-712 and EIP-1271 signatures.
function _checkSignature(
uint256 index,
address recipient,
address to,
uint128 amount,
bytes calldata signature
)
internal
view
{
// Encode the claim parameters using claim type hash and hash it.
bytes32 claimHash = keccak256(abi.encode(SignatureHash.CLAIM_TYPEHASH, index, recipient, to, amount));

// Returns the keccak256 digest of the claim parameters using claim hash and the domain separator.
bytes32 digest = MessageHashUtils.toTypedDataHash({ domainSeparator: DOMAIN_SEPARATOR, structHash: claimHash });

// If recipient is an EOA, `isValidSignatureNow` recovers the signer using ECDSA from the signature and the
// digest. It returns true if the recovered signer matches the recipient. If the recipient is a contract,
// `isValidSignatureNow` checks if the recipient implements the `IERC1271` interface and returns the magic value
// as per EIP-1271 for the given digest and signature.
bool isSignatureValid =
SignatureChecker.isValidSignatureNow({ signer: recipient, hash: digest, signature: signature });

// Check: `isSignatureValid` is true.
if (!isSignatureValid) {
revert Errors.SablierMerkleBase_InvalidSignature();
}
}

/// @dev Reverts if the `to` address is the zero address.
function _revertIfToZeroAddress(address to) internal pure {
// Check: `to` is not the zero address.
if (to == address(0)) {
revert Errors.SablierMerkleBase_ToZeroAddress();
}
}

/*//////////////////////////////////////////////////////////////////////////
PRIVATE READ-ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

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