Skip to content
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

feat: include shape in merkle contracts #18

Merged
merged 7 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Binary file modified bun.lockb
Binary file not shown.
33 changes: 3 additions & 30 deletions src/SablierMerkleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,7 @@ contract SablierMerkleFactory is
returns (ISablierMerkleInstant merkleInstant)
{
// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.token,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name))
)
);
bytes32 salt = keccak256(abi.encodePacked(msg.sender, abi.encode(baseParams)));
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

// Compute the fee for the user.
uint256 fee = _computeFeeForUser(msg.sender);
Expand Down Expand Up @@ -124,19 +114,7 @@ contract SablierMerkleFactory is
{
// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.token,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name)),
lockup,
cancelable,
transferable,
abi.encode(schedule)
)
abi.encodePacked(msg.sender, abi.encode(baseParams), lockup, cancelable, transferable, abi.encode(schedule))
);

// Compute the fee for the user.
Expand Down Expand Up @@ -258,12 +236,7 @@ contract SablierMerkleFactory is
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.token,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name)),
abi.encode(baseParams),
lockup,
cancelable,
transferable,
Expand Down
3 changes: 2 additions & 1 deletion src/SablierMerkleLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ contract SablierMerkleLL is
sender: admin,
recipient: recipient,
totalAmount: amount,
asset: TOKEN,
token: TOKEN,
cancelable: CANCELABLE,
transferable: TRANSFERABLE,
timestamps: timestamps,
shape: shape,
broker: Broker({ account: address(0), fee: ZERO })
}),
LockupLinear.UnlockAmounts({ start: schedule.startAmount, cliff: schedule.cliffAmount }),
Expand Down
3 changes: 2 additions & 1 deletion src/SablierMerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ contract SablierMerkleLT is
sender: admin,
recipient: recipient,
totalAmount: amount,
asset: TOKEN,
token: TOKEN,
cancelable: CANCELABLE,
transferable: TRANSFERABLE,
timestamps: Lockup.Timestamps({ start: startTime, end: endTime }),
shape: shape,
broker: Broker({ account: address(0), fee: ZERO })
}),
tranches
Expand Down
10 changes: 10 additions & 0 deletions src/abstracts/SablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ abstract contract SablierMerkleBase is
/// @inheritdoc ISablierMerkleBase
string public override ipfsCID;

/// @inheritdoc ISablierMerkleBase
string public override shape;
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Packed booleans that record the history of claims.
BitMaps.BitMap internal _claimedBitMap;

Expand All @@ -69,6 +72,13 @@ abstract contract SablierMerkleBase is
ipfsCID = params.ipfsCID;
MERKLE_ROOT = params.merkleRoot;
NAME = bytes32(abi.encodePacked(params.name));

// If the shape string exceeds 32 bytes, truncate it to prevent `claim` from reverting.
if (bytes(params.shape).length > 32) {
shape = string(abi.encodePacked(bytes32(abi.encodePacked(params.shape))));
} else {
shape = params.shape;
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
}
PaulRBerg marked this conversation as resolved.
Show resolved Hide resolved
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/ISablierMerkleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ interface ISablierMerkleBase is IAdminable {
/// @notice Retrieves the name of the campaign.
function name() external returns (string memory);
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Retrieves the shape of the lockup stream that the campaign produces upon claiming.
function shape() external view returns (string memory);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
3 changes: 3 additions & 0 deletions src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ library MerkleBase {
/// @param ipfsCID The content identifier for indexing the contract on IPFS.
/// @param merkleRoot The Merkle root of the claim data.
/// @param name The name of the campaign.
/// @param shape The shape of Lockup stream useful to differentiate between streams in the UI. It is truncated if
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
/// exceeding 32 bytes.
struct ConstructorParams {
IERC20 token;
uint40 expiration;
address initialAdmin;
string ipfsCID;
bytes32 merkleRoot;
string name;
string shape;
}
}

Expand Down
43 changes: 20 additions & 23 deletions tests/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SablierMerkleFactory } from "src/SablierMerkleFactory.sol";
import { SablierMerkleInstant } from "src/SablierMerkleInstant.sol";
import { SablierMerkleLL } from "src/SablierMerkleLL.sol";
import { SablierMerkleLT } from "src/SablierMerkleLT.sol";
import { MerkleBase } from "src/types/DataTypes.sol";
import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol";
import { Assertions } from "./utils/Assertions.sol";
import { Constants } from "./utils/Constants.sol";
Expand Down Expand Up @@ -191,17 +192,13 @@ abstract contract Base_Test is Assertions, Constants, DeployOptimized, Modifiers
view
returns (address)
{
bytes32 salt = keccak256(
abi.encodePacked(
caller,
address(token_),
expiration,
campaignOwner,
abi.encode(defaults.IPFS_CID()),
merkleRoot,
defaults.NAME_BYTES32()
)
);
MerkleBase.ConstructorParams memory baseParams = defaults.baseParams();
baseParams.token = token_;
baseParams.expiration = expiration;
baseParams.initialAdmin = campaignOwner;
baseParams.merkleRoot = merkleRoot;

bytes32 salt = keccak256(abi.encodePacked(caller, abi.encode(baseParams)));
bytes32 creationBytecodeHash =
keccak256(getMerkleInstantBytecode(campaignOwner, token_, merkleRoot, expiration, fee));
return vm.computeCreate2Address({
Expand All @@ -223,15 +220,15 @@ abstract contract Base_Test is Assertions, Constants, DeployOptimized, Modifiers
view
returns (address)
{
MerkleBase.ConstructorParams memory baseParams = defaults.baseParams();
baseParams.token = token_;
baseParams.expiration = expiration;
baseParams.initialAdmin = campaignOwner;
baseParams.merkleRoot = merkleRoot;
bytes32 salt = keccak256(
abi.encodePacked(
caller,
address(token_),
expiration,
campaignOwner,
abi.encode(defaults.IPFS_CID()),
merkleRoot,
defaults.NAME_BYTES32(),
abi.encode(baseParams),
lockup,
defaults.CANCELABLE(),
defaults.TRANSFERABLE(),
Expand Down Expand Up @@ -259,15 +256,15 @@ abstract contract Base_Test is Assertions, Constants, DeployOptimized, Modifiers
view
returns (address)
{
MerkleBase.ConstructorParams memory baseParams = defaults.baseParams();
baseParams.token = token_;
baseParams.expiration = expiration;
baseParams.initialAdmin = campaignOwner;
baseParams.merkleRoot = merkleRoot;
bytes32 salt = keccak256(
abi.encodePacked(
caller,
address(token_),
expiration,
campaignOwner,
abi.encode(defaults.IPFS_CID()),
merkleRoot,
defaults.NAME_BYTES32(),
abi.encode(baseParams),
lockup,
defaults.CANCELABLE(),
defaults.TRANSFERABLE(),
Expand Down
22 changes: 11 additions & 11 deletions tests/fork/merkle-campaign/MerkleLL.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,26 +199,26 @@ abstract contract MerkleLL_Fork_Test is Fork_Test {

// Assert that the stream has been created successfully.
assertEq(
lockup.getDepositedAmount(vars.expectedStreamId), vars.amounts[params.posBeforeSort], "deposited amount"
lockup.getCliffTime(vars.expectedStreamId), getBlockTimestamp() + defaults.CLIFF_DURATION(), "cliff time"
);
assertEq(lockup.getRefundedAmount(vars.expectedStreamId), 0, "refunded amount");
assertEq(lockup.getWithdrawnAmount(vars.expectedStreamId), 0, "withdrawn amount");
assertEq(lockup.getAsset(vars.expectedStreamId), FORK_TOKEN, "token");
assertEq(
lockup.getCliffTime(vars.expectedStreamId), getBlockTimestamp() + defaults.CLIFF_DURATION(), "cliff time"
lockup.getDepositedAmount(vars.expectedStreamId), vars.amounts[params.posBeforeSort], "deposited amount"
);
assertEq(lockup.getEndTime(vars.expectedStreamId), getBlockTimestamp() + defaults.TOTAL_DURATION(), "end time");
assertEq(lockup.getLockupModel(vars.expectedStreamId), Lockup.Model.LOCKUP_LINEAR);
assertEq(lockup.getRecipient(vars.expectedStreamId), vars.recipients[params.posBeforeSort], "recipient");
assertEq(lockup.getRefundedAmount(vars.expectedStreamId), 0, "refunded amount");
assertEq(lockup.getSender(vars.expectedStreamId), params.campaignOwner, "sender");
assertEq(lockup.getStartTime(vars.expectedStreamId), getBlockTimestamp(), "start time");
assertEq(lockup.getUnderlyingToken(vars.expectedStreamId), FORK_TOKEN, "token");
assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).cliff, defaults.CLIFF_AMOUNT(), "unlock amounts cliff");
assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).start, defaults.START_AMOUNT(), "unlock amounts start");
assertEq(lockup.getWithdrawnAmount(vars.expectedStreamId), 0, "withdrawn amount");
assertEq(lockup.isCancelable(vars.expectedStreamId), defaults.CANCELABLE(), "is cancelable");
assertEq(lockup.isDepleted(vars.expectedStreamId), false, "is depleted");
assertEq(lockup.isStream(vars.expectedStreamId), true, "is stream");
assertEq(lockup.isTransferable(vars.expectedStreamId), defaults.TRANSFERABLE(), "is transferable");
assertEq(lockup.getRecipient(vars.expectedStreamId), vars.recipients[params.posBeforeSort], "recipient");
assertEq(lockup.getSender(vars.expectedStreamId), params.campaignOwner, "sender");
assertEq(lockup.getStartTime(vars.expectedStreamId), getBlockTimestamp(), "start time");
assertEq(lockup.wasCanceled(vars.expectedStreamId), false, "was canceled");
assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).start, defaults.START_AMOUNT(), "unlock amounts start");
assertEq(lockup.getUnlockAmounts(vars.expectedStreamId).cliff, defaults.CLIFF_AMOUNT(), "unlock amounts cliff");
assertEq(lockup.getLockupModel(vars.expectedStreamId), Lockup.Model.LOCKUP_LINEAR);

assertTrue(vars.merkleLL.hasClaimed(vars.indexes[params.posBeforeSort]));

Expand Down
18 changes: 9 additions & 9 deletions tests/fork/merkle-campaign/MerkleLT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,12 @@ abstract contract MerkleLT_Fork_Test is Fork_Test {
assertEq(
lockup.getDepositedAmount(vars.expectedStreamId), vars.amounts[params.posBeforeSort], "deposited amount"
);
assertEq(lockup.getRefundedAmount(vars.expectedStreamId), 0, "refunded amount");
assertEq(lockup.getWithdrawnAmount(vars.expectedStreamId), 0, "withdrawn amount");
assertEq(lockup.getAsset(vars.expectedStreamId), FORK_TOKEN, "token");
assertEq(lockup.getEndTime(vars.expectedStreamId), getBlockTimestamp() + defaults.TOTAL_DURATION(), "end time");
assertEq(lockup.isCancelable(vars.expectedStreamId), defaults.CANCELABLE(), "is cancelable");
assertEq(lockup.isDepleted(vars.expectedStreamId), false, "is depleted");
assertEq(lockup.isStream(vars.expectedStreamId), true, "is stream");
assertEq(lockup.isTransferable(vars.expectedStreamId), defaults.TRANSFERABLE(), "is transferable");
assertEq(lockup.getLockupModel(vars.expectedStreamId), Lockup.Model.LOCKUP_TRANCHED);
assertEq(lockup.getRecipient(vars.expectedStreamId), vars.recipients[params.posBeforeSort], "recipient");
assertEq(lockup.getRefundedAmount(vars.expectedStreamId), 0, "refunded amount");
assertEq(lockup.getSender(vars.expectedStreamId), params.campaignOwner, "sender");
assertEq(lockup.getStartTime(vars.expectedStreamId), getBlockTimestamp(), "start time");
assertEq(lockup.wasCanceled(vars.expectedStreamId), false, "was canceled");
assertEq(
lockup.getTranches(vars.expectedStreamId),
defaults.tranchesMerkleLT({
Expand All @@ -219,7 +213,13 @@ abstract contract MerkleLT_Fork_Test is Fork_Test {
}),
"tranches"
);
assertEq(lockup.getLockupModel(vars.expectedStreamId), Lockup.Model.LOCKUP_TRANCHED);
assertEq(lockup.getUnderlyingToken(vars.expectedStreamId), FORK_TOKEN, "token");
assertEq(lockup.getWithdrawnAmount(vars.expectedStreamId), 0, "withdrawn amount");
assertEq(lockup.isCancelable(vars.expectedStreamId), defaults.CANCELABLE(), "is cancelable");
assertEq(lockup.isDepleted(vars.expectedStreamId), false, "is depleted");
assertEq(lockup.isStream(vars.expectedStreamId), true, "is stream");
assertEq(lockup.isTransferable(vars.expectedStreamId), defaults.TRANSFERABLE(), "is transferable");
assertEq(lockup.wasCanceled(vars.expectedStreamId), false, "was canceled");

assertTrue(vars.merkleLT.hasClaimed(vars.indexes[params.posBeforeSort]));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ contract CreateMerkleLL_Integration_Test is Integration_Test {
});
}

function test_WhenShapeExceeds32Bytes() external whenNameNotTooLong givenCampaignNotExists {
MerkleBase.ConstructorParams memory baseParams = defaults.baseParams();
baseParams.shape = "this string is longer than 32 bytes";

ISablierMerkleLL actualLL = merkleFactory.createMerkleLL({
baseParams: baseParams,
lockup: lockup,
cancelable: defaults.CANCELABLE(),
transferable: defaults.TRANSFERABLE(),
schedule: defaults.schedule(),
aggregateAmount: defaults.AGGREGATE_AMOUNT(),
recipientCount: defaults.RECIPIENT_COUNT()
});

// It should create the campaign with shape truncated to 32 bytes.
string memory actualShape = actualLL.shape();
string memory expectedShape = "this string is longer than 32 by";
assertEq(actualShape, expectedShape, "shape");
}

function test_GivenCustomFeeSet(
address campaignOwner,
uint40 expiration,
Expand All @@ -66,6 +86,7 @@ contract CreateMerkleLL_Integration_Test is Integration_Test {
external
whenNameNotTooLong
givenCampaignNotExists
whenShapeNotExceed32Bytes
{
// Set the custom fee to 0 for this test.
resetPrank(users.admin);
Expand Down Expand Up @@ -113,6 +134,7 @@ contract CreateMerkleLL_Integration_Test is Integration_Test {
external
whenNameNotTooLong
givenCampaignNotExists
whenShapeNotExceed32Bytes
{
address expectedLL = computeMerkleLLAddress(campaignOwner, expiration);

Expand Down Expand Up @@ -141,6 +163,9 @@ contract CreateMerkleLL_Integration_Test is Integration_Test {
assertGt(address(actualLL).code.length, 0, "MerkleLL contract not created");
assertEq(address(actualLL), expectedLL, "MerkleLL contract does not match computed address");

// It should set the correct shape.
assertEq(actualLL.shape(), defaults.SHAPE(), "shape");

// It should create the campaign with custom fee.
assertEq(actualLL.FEE(), defaults.FEE(), "default fee");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ CreateMerkleLL_Integration_Test
├── given campaign already exists
│ └── it should revert
└── given campaign not exists
├── given custom fee set
│ ├── it should create the campaign with custom fee
│ ├── it should set the current factory address
│ └── it should emit a {CreateMerkleLL} event
└── given custom fee not set
├── it should create the campaign with default fee
├── it should set the current factory address
└── it should emit a {CreateMerkleLL} event
├── when shape exceeds 32 bytes
│ └── it should create the campaign with shape truncated to 32 bytes
└── when shape not exceed 32 bytes
├── given custom fee set
│ ├── it should create the campaign with custom fee
│ ├── it should set the current factory address
│ └── it should emit a {CreateMerkleLL} event
└── given custom fee not set
├── it should create the campaign with default fee
├── it should set the current factory address
└── it should emit a {CreateMerkleLL} event
Loading
Loading