Skip to content

Commit

Permalink
feat: use CREATE2 to deploy merkle lockup campaigns (#355)
Browse files Browse the repository at this point in the history
* feat: use create2 to deploy merkle lockup campaigns

* test: computeMerkleLL and computeMerkleLT

* build: update bun lockfile

* chore: update precompiles

* refactor: remove compute functions

* refactor: calculate "totalPercentage" in MerkleLT

refactor: remove total percentage from salt

* feat: include msg.sender in create2 salt

* chore: update precompiles

* refactor: add tranchesWithPercentages to create2 salt

* chore: update bun lockfile

* build: update precompiles

* docs: use contract state

---------

Co-authored-by: Paul Razvan Berg <[email protected]>
  • Loading branch information
smol-ninja and PaulRBerg authored Jun 18, 2024
1 parent 9ea29b2 commit 46ca3bc
Show file tree
Hide file tree
Showing 19 changed files with 405 additions and 124 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion precompiles/Precompiles.sol

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions src/SablierV2MerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ pragma solidity >=0.8.22;
import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { uUNIT } from "@prb/math/src/UD2x18.sol";
import { UD60x18, ud60x18, ZERO } from "@prb/math/src/UD60x18.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { Broker, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol";

import { SablierV2MerkleLockup } from "./abstracts/SablierV2MerkleLockup.sol";
import { ISablierV2MerkleLT } from "./interfaces/ISablierV2MerkleLT.sol";
import { Errors } from "./libraries/Errors.sol";
import { MerkleLockup, MerkleLT } from "./types/DataTypes.sol";

/// @title SablierV2MerkleLT
Expand All @@ -28,6 +30,9 @@ contract SablierV2MerkleLT is
/// @inheritdoc ISablierV2MerkleLT
ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED;

/// @inheritdoc ISablierV2MerkleLT
uint64 public immutable override TOTAL_PERCENTAGE;

/// @dev The tranches with their respective unlock percentages and durations.
MerkleLT.TrancheWithPercentage[] internal _tranchesWithPercentages;

Expand All @@ -46,12 +51,14 @@ contract SablierV2MerkleLT is
{
LOCKUP_TRANCHED = lockupTranched;

// 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.
uint256 count = tranchesWithPercentages.length;
for (uint256 i = 0; i < count; ++i) {
// Calculate the total percentage of the tranches and save them in the contract state.
uint64 totalPercentage;
for (uint256 i = 0; i < tranchesWithPercentages.length; ++i) {
uint64 percentage = tranchesWithPercentages[i].unlockPercentage.unwrap();
totalPercentage += percentage;
_tranchesWithPercentages.push(tranchesWithPercentages[i]);
}
TOTAL_PERCENTAGE = totalPercentage;

// Max approve the Sablier contract to spend funds from the MerkleLockup contract.
ASSET.forceApprove(address(LOCKUP_TRANCHED), type(uint256).max);
Expand Down Expand Up @@ -81,6 +88,11 @@ contract SablierV2MerkleLT is
override
returns (uint256 streamId)
{
// Check: the sum of percentages equals 100%.
if (TOTAL_PERCENTAGE != uUNIT) {
revert Errors.SablierV2MerkleLT_TotalPercentageNotOneHundred(TOTAL_PERCENTAGE);
}

// Generate the Merkle tree leaf by hashing the corresponding parameters. Hashing twice prevents second
// preimage attacks.
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, recipient, amount))));
Expand Down
50 changes: 37 additions & 13 deletions src/SablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { uUNIT } from "@prb/math/src/UD2x18.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";

import { ISablierV2MerkleLL } from "./interfaces/ISablierV2MerkleLL.sol";
import { ISablierV2MerkleLockupFactory } from "./interfaces/ISablierV2MerkleLockupFactory.sol";
import { ISablierV2MerkleLT } from "./interfaces/ISablierV2MerkleLT.sol";
import { Errors } from "./libraries/Errors.sol";
import { SablierV2MerkleLL } from "./SablierV2MerkleLL.sol";
import { SablierV2MerkleLT } from "./SablierV2MerkleLT.sol";
import { MerkleLockup, MerkleLT } from "./types/DataTypes.sol";
Expand All @@ -32,8 +30,25 @@ contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory {
external
returns (ISablierV2MerkleLL merkleLL)
{
// Deploy the MerkleLockup contract with CREATE.
merkleLL = new SablierV2MerkleLL(baseParams, lockupLinear, streamDurations);
// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.asset,
baseParams.cancelable,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name)),
baseParams.transferable,
lockupLinear,
abi.encode(streamDurations)
)
);

// Deploy the MerkleLockup contract with CREATE2.
merkleLL = new SablierV2MerkleLL{ salt: salt }(baseParams, lockupLinear, streamDurations);

// Log the creation of the MerkleLockup contract, including some metadata that is not stored on-chain.
emit CreateMerkleLL(merkleLL, baseParams, lockupLinear, streamDurations, aggregateAmount, recipientCount);
Expand All @@ -51,24 +66,33 @@ contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory {
returns (ISablierV2MerkleLT merkleLT)
{
// Calculate the sum of percentages and durations across all tranches.
uint64 totalPercentage;
uint256 totalDuration;
for (uint256 i = 0; i < tranchesWithPercentages.length; ++i) {
uint64 percentage = tranchesWithPercentages[i].unlockPercentage.unwrap();
totalPercentage = totalPercentage + percentage;
unchecked {
// Safe to use `unchecked` because its only used in the event.
totalDuration += tranchesWithPercentages[i].duration;
}
}

// Check: the sum of percentages equals 100%.
if (totalPercentage != uUNIT) {
revert Errors.SablierV2MerkleLockupFactory_TotalPercentageNotOneHundred(totalPercentage);
}
// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
msg.sender,
baseParams.asset,
baseParams.cancelable,
baseParams.expiration,
baseParams.initialAdmin,
abi.encode(baseParams.ipfsCID),
baseParams.merkleRoot,
bytes32(abi.encodePacked(baseParams.name)),
baseParams.transferable,
lockupTranched,
abi.encode(tranchesWithPercentages)
)
);

// Deploy the MerkleLockup contract with CREATE.
merkleLT = new SablierV2MerkleLT(baseParams, lockupTranched, tranchesWithPercentages);
// Deploy the MerkleLockup contract with CREATE2.
merkleLT = new SablierV2MerkleLT{ salt: salt }(baseParams, lockupTranched, tranchesWithPercentages);

// Log the creation of the MerkleLockup contract, including some metadata that is not stored on-chain.
emit CreateMerkleLT(
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/ISablierV2MerkleLT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ interface ISablierV2MerkleLT is ISablierV2MerkleLockup {
/// @notice The address of the {SablierV2LockupTranched} contract.
function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched);

/// @notice The total percentage of the tranches.
function TOTAL_PERCENTAGE() external view returns (uint64);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -32,6 +35,7 @@ interface ISablierV2MerkleLT is ISablierV2MerkleLockup {
/// - The campaign must not have expired.
/// - The stream must not have been claimed already.
/// - The Merkle proof must be valid.
/// - TOTAL_PERCENTAGE must be equal to 100%.
///
/// @param index The index of the recipient in the Merkle tree.
/// @param recipient The address of the stream holder.
Expand Down
5 changes: 1 addition & 4 deletions src/interfaces/ISablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ISablierV2MerkleLT } from "./ISablierV2MerkleLT.sol";
import { MerkleLockup, MerkleLT } from "../types/DataTypes.sol";

/// @title ISablierV2MerkleLockupFactory
/// @notice Deploys MerkleLockup campaigns with CREATE.
/// @notice Deploys MerkleLockup campaigns with CREATE2.
interface ISablierV2MerkleLockupFactory {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
Expand Down Expand Up @@ -63,9 +63,6 @@ interface ISablierV2MerkleLockupFactory {
/// @notice Creates a new MerkleLockup campaign with a LockupTranched distribution.
/// @dev Emits a {CreateMerkleLT} event.
///
/// Requirements:
/// - The sum of the tranches' unlock percentages must equal 100% = 1e18.
///
/// @param baseParams Struct encapsulating the {SablierV2MerkleLockup} parameters, which are documented in
/// {DataTypes}.
/// @param lockupTranched The address of the {SablierV2LockupTranched} contract.
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ library Errors {
error SablierV2MerkleLockup_StreamClaimed(uint256 index);

/*//////////////////////////////////////////////////////////////////////////
SABLIER-V2-MERKLE-LOCKUP-FACTORY
SABLIER-V2-MERKLE-LT
//////////////////////////////////////////////////////////////////////////*/

/// @notice Thrown when the sum of the tranches' unlock percentages does not equal 100%.
error SablierV2MerkleLockupFactory_TotalPercentageNotOneHundred(uint64 totalPercentage);
/// @notice Thrown when trying to claim from an LT campaign with tranches' unlock percentages not adding up to 100%.
error SablierV2MerkleLT_TotalPercentageNotOneHundred(uint64 totalPercentage);
}
140 changes: 139 additions & 1 deletion test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol";
import { ISablierV2MerkleLockupFactory } from "src/interfaces/ISablierV2MerkleLockupFactory.sol";
import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol";
import { SablierV2BatchLockup } from "src/SablierV2BatchLockup.sol";
import { SablierV2MerkleLL } from "src/SablierV2MerkleLL.sol";
import { SablierV2MerkleLockupFactory } from "src/SablierV2MerkleLockupFactory.sol";
import { SablierV2MerkleLT } from "src/SablierV2MerkleLT.sol";

import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol";
import { Assertions } from "./utils/Assertions.sol";
Expand Down Expand Up @@ -55,7 +57,6 @@ abstract contract Base_Test is
ISablierV2LockupLinear internal lockupLinear;
ISablierV2LockupTranched internal lockupTranched;
ISablierV2MerkleLockupFactory internal merkleLockupFactory;
uint256 internal merkleLockupFactoryNonce;
ISablierV2MerkleLL internal merkleLL;
ISablierV2MerkleLT internal merkleLT;

Expand Down Expand Up @@ -256,4 +257,141 @@ abstract contract Base_Test is
{
vm.expectCall({ callee: asset_, count: count, data: abi.encodeCall(IERC20.transferFrom, (from, to, amount)) });
}

/*//////////////////////////////////////////////////////////////////////////
MERKLE-LOCKUP
//////////////////////////////////////////////////////////////////////////*/

function computeMerkleLLAddress(
address admin,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (address)
{
return computeMerkleLLAddress(admin, dai, merkleRoot, expiration);
}

function computeMerkleLLAddress(
address admin,
IERC20 asset_,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (address)
{
bytes32 salt = keccak256(
abi.encodePacked(
users.alice,
address(asset_),
defaults.CANCELABLE(),
expiration,
admin,
abi.encode(defaults.IPFS_CID()),
merkleRoot,
defaults.NAME_BYTES32(),
defaults.TRANSFERABLE(),
lockupLinear,
abi.encode(defaults.durations())
)
);
bytes32 creationBytecodeHash = keccak256(getMerkleLLBytecode(admin, asset_, merkleRoot, expiration));
return vm.computeCreate2Address({
salt: salt,
initCodeHash: creationBytecodeHash,
deployer: address(merkleLockupFactory)
});
}

function computeMerkleLTAddress(
address admin,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (address)
{
return computeMerkleLTAddress(admin, dai, merkleRoot, expiration);
}

function computeMerkleLTAddress(
address admin,
IERC20 asset_,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (address)
{
bytes32 salt = keccak256(
abi.encodePacked(
users.alice,
address(asset_),
defaults.CANCELABLE(),
expiration,
admin,
abi.encode(defaults.IPFS_CID()),
merkleRoot,
defaults.NAME_BYTES32(),
defaults.TRANSFERABLE(),
lockupTranched,
abi.encode(defaults.tranchesWithPercentages())
)
);
bytes32 creationBytecodeHash = keccak256(getMerkleLTBytecode(admin, asset_, merkleRoot, expiration));
return vm.computeCreate2Address({
salt: salt,
initCodeHash: creationBytecodeHash,
deployer: address(merkleLockupFactory)
});
}

function getMerkleLLBytecode(
address admin,
IERC20 asset_,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (bytes memory)
{
bytes memory constructorArgs =
abi.encode(defaults.baseParams(admin, asset_, expiration, merkleRoot), lockupLinear, defaults.durations());
if (!isTestOptimizedProfile()) {
return bytes.concat(type(SablierV2MerkleLL).creationCode, constructorArgs);
} else {
return
bytes.concat(vm.getCode("out-optimized/SablierV2MerkleLL.sol/SablierV2MerkleLL.json"), constructorArgs);
}
}

function getMerkleLTBytecode(
address admin,
IERC20 asset_,
bytes32 merkleRoot,
uint40 expiration
)
internal
view
returns (bytes memory)
{
bytes memory constructorArgs = abi.encode(
defaults.baseParams(admin, asset_, expiration, merkleRoot),
lockupTranched,
defaults.tranchesWithPercentages()
);
if (!isTestOptimizedProfile()) {
return bytes.concat(type(SablierV2MerkleLT).creationCode, constructorArgs);
} else {
return
bytes.concat(vm.getCode("out-optimized/SablierV2MerkleLT.sol/SablierV2MerkleLT.json"), constructorArgs);
}
}
}
2 changes: 1 addition & 1 deletion test/fork/merkle-lockup/MerkleLL.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ abstract contract MerkleLL_Fork_Test is Fork_Test {
MerkleBuilder.sortLeaves(leaves);
vars.merkleRoot = getRoot(leaves.toBytes32());

vars.expectedLL = vm.computeCreateAddress(address(merkleLockupFactory), ++merkleLockupFactoryNonce);
vars.expectedLL = computeMerkleLLAddress(params.admin, FORK_ASSET, vars.merkleRoot, params.expiration);

vars.baseParams = defaults.baseParams({
admin: params.admin,
Expand Down
2 changes: 1 addition & 1 deletion test/fork/merkle-lockup/MerkleLT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ abstract contract MerkleLT_Fork_Test is Fork_Test {
MerkleBuilder.sortLeaves(leaves);
vars.merkleRoot = getRoot(leaves.toBytes32());

vars.expectedLT = vm.computeCreateAddress(address(merkleLockupFactory), ++merkleLockupFactoryNonce);
vars.expectedLT = computeMerkleLTAddress(params.admin, FORK_ASSET, vars.merkleRoot, params.expiration);

vars.baseParams = defaults.baseParams({
admin: params.admin,
Expand Down
Loading

0 comments on commit 46ca3bc

Please sign in to comment.