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

MerkleLockup for LockupTranched #297

Merged
merged 21 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc0932b
feat: implement SablierV2MerkleLockupLT contract
andreivladbrg Feb 28, 2024
6b73af7
build: include Tranched contracts in prepare-artifacts
andreivladbrg Mar 1, 2024
d7acbb2
chore: use plural for tranches percantages
andreivladbrg Mar 3, 2024
70a8a06
fix: handle the case when protocol fee is greater than zero
andreivladbrg Mar 5, 2024
15e4b06
refactor: rename variables
andreivladbrg Mar 5, 2024
141956a
test: remove unused import
andreivladbrg Mar 5, 2024
6edef5d
chore: add named arg in custom error
andreivladbrg Mar 6, 2024
0131f40
refactor: use plural in getter function
andreivladbrg Mar 6, 2024
291124c
feat: add CreateMerkleLockupLT in script
smol-ninja Mar 6, 2024
af2e233
test: add LT fork tests for USDC and USDT
smol-ninja Mar 7, 2024
e4ce631
ci: add fork tests to ci with 20 runs
smol-ninja Mar 7, 2024
2e0da7f
refactor: order imports alphabetically (#301)
smol-ninja Mar 7, 2024
7279f5f
refactor: remove protocol fee handle in _calculateTranches
andreivladbrg Mar 7, 2024
38159b4
refactor: remove getTranchesWithPercentages test
andreivladbrg Mar 7, 2024
0d12300
refactor: rename struct variable to unlockPercentage
andreivladbrg Mar 8, 2024
b815e25
build: update lockfile
smol-ninja Mar 8, 2024
6ad73ed
docs: add unlock with percentages in Lockup Tranche
smol-ninja Mar 8, 2024
1b49ebf
refactor: rename variable to totalPercentage
andreivladbrg Mar 8, 2024
0587327
test: increase coverage by testing when there is a rounding error in
andreivladbrg Mar 10, 2024
b1b0bde
test: improve rounding error test in claim
andreivladbrg Mar 11, 2024
812670b
chore: be more precise in explanatory comment
andreivladbrg Mar 11, 2024
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ jobs:
match-path: "test/utils/**/*.sol"
name: "Utils tests"

test-fork:
needs: ["lint", "build"]
secrets:
RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }}
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main"
with:
foundry-fuzz-runs: 20
foundry-profile: "test-optimized"
match-path: "test/fork/**/*.sol"
name: "Fork tests"

coverage:
needs: ["lint", "build"]
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-coverage.yml@main"
Expand Down
Binary file modified bun.lockb
Binary file not shown.
37 changes: 37 additions & 0 deletions script/CreateMerkleLockupLT.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22 <0.9.0;

import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";

import { BaseScript } from "./Base.s.sol";

import { ISablierV2MerkleLockupFactory } from "../src/interfaces/ISablierV2MerkleLockupFactory.sol";
import { ISablierV2MerkleLockupLT } from "../src/interfaces/ISablierV2MerkleLockupLT.sol";
import { MerkleLockup, MerkleLockupLT } from "../src/types/DataTypes.sol";

contract CreateMerkleLockupLT is BaseScript {
struct Params {
MerkleLockup.ConstructorParams baseParams;
ISablierV2LockupTranched lockupTranched;
MerkleLockupLT.TrancheWithPercentage[] tranchesWithPercentages;
uint256 campaignTotalAmount;
uint256 recipientsCount;
}

function run(
ISablierV2MerkleLockupFactory merkleLockupFactory,
Params calldata params
)
public
broadcast
returns (ISablierV2MerkleLockupLT merkleLockupLT)
{
merkleLockupLT = merkleLockupFactory.createMerkleLockupLT(
params.baseParams,
params.lockupTranched,
params.tranchesWithPercentages,
params.campaignTotalAmount,
params.recipientsCount
);
}
}
7 changes: 5 additions & 2 deletions script/DeployProtocol.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22 <0.9.0;
import { SablierV2Comptroller } from "@sablier/v2-core/src/SablierV2Comptroller.sol";
import { SablierV2LockupDynamic } from "@sablier/v2-core/src/SablierV2LockupDynamic.sol";
import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol";
import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol";
import { SablierV2NFTDescriptor } from "@sablier/v2-core/src/SablierV2NFTDescriptor.sol";
import { BaseScript } from "./Base.s.sol";

Expand All @@ -14,7 +15,7 @@ import { SablierV2Batch } from "../src/SablierV2Batch.sol";
contract DeployProtocol is BaseScript {
function run(
address initialAdmin,
uint256 maxSegmentCount
uint256 maxCount
)
public
virtual
Expand All @@ -23,6 +24,7 @@ contract DeployProtocol is BaseScript {
SablierV2Comptroller comptroller,
SablierV2LockupDynamic lockupDynamic,
SablierV2LockupLinear lockupLinear,
SablierV2LockupTranched lockupTranched,
SablierV2NFTDescriptor nftDescriptor,
SablierV2Batch batch,
SablierV2MerkleLockupFactory merkleLockupFactory
Expand All @@ -31,8 +33,9 @@ contract DeployProtocol is BaseScript {
// Deploy V2 Core.
comptroller = new SablierV2Comptroller(initialAdmin);
nftDescriptor = new SablierV2NFTDescriptor();
lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxSegmentCount);
lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxCount);
lockupLinear = new SablierV2LockupLinear(initialAdmin, comptroller, nftDescriptor);
lockupTranched = new SablierV2LockupTranched(initialAdmin, comptroller, nftDescriptor, maxCount);

batch = new SablierV2Batch();
merkleLockupFactory = new SablierV2MerkleLockupFactory();
Expand Down
2 changes: 2 additions & 0 deletions shell/prepare-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ FOUNDRY_PROFILE=optimized forge build
cp out-optimized/SablierV2Batch.sol/SablierV2Batch.json $artifacts
cp out-optimized/SablierV2MerkleLockupFactory.sol/SablierV2MerkleLockupFactory.json $artifacts
cp out-optimized/SablierV2MerkleLockupLL.sol/SablierV2MerkleLockupLL.json $artifacts
cp out-optimized/SablierV2MerkleLockupLT.sol/SablierV2MerkleLockupLT.json $artifacts

interfaces=./artifacts/interfaces
cp out-optimized/ISablierV2Batch.sol/ISablierV2Batch.json $interfaces
cp out-optimized/ISablierV2MerkleLockupFactory.sol/ISablierV2MerkleLockupFactory.json $interfaces
cp out-optimized/ISablierV2MerkleLockupLL.sol/ISablierV2MerkleLockupLL.json $interfaces
cp out-optimized/ISablierV2MerkleLockupLT.sol/ISablierV2MerkleLockupLT.json $interfaces

erc20=./artifacts/interfaces/erc20
cp out-optimized/IERC20.sol/IERC20.json $erc20
Expand Down
60 changes: 57 additions & 3 deletions src/SablierV2MerkleLockupFactory.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { UD60x18, ud } from "@prb/math/src/UD60x18.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 { SablierV2MerkleLockupLL } from "./SablierV2MerkleLockupLL.sol";
import { ISablierV2MerkleLockupFactory } from "./interfaces/ISablierV2MerkleLockupFactory.sol";
import { ISablierV2MerkleLockupLL } from "./interfaces/ISablierV2MerkleLockupLL.sol";
import { MerkleLockup } from "./types/DataTypes.sol";
import { ISablierV2MerkleLockupLT } from "./interfaces/ISablierV2MerkleLockupLT.sol";
import { Errors } from "./libraries/Errors.sol";
import { SablierV2MerkleLockupLL } from "./SablierV2MerkleLockupLL.sol";
import { SablierV2MerkleLockupLT } from "./SablierV2MerkleLockupLT.sol";
import { MerkleLockup, MerkleLockupLT } from "./types/DataTypes.sol";

/// @title SablierV2MerkleLockupFactory
/// @notice See the documentation in {ISablierV2MerkleLockupFactory}.
Expand Down Expand Up @@ -51,4 +56,53 @@ contract SablierV2MerkleLockupFactory is ISablierV2MerkleLockupFactory {
merkleLockupLL, baseParams, lockupLinear, streamDurations, aggregateAmount, recipientsCount
);
}

/// @notice inheritdoc ISablierV2MerkleLockupFactory
function createMerkleLockupLT(
MerkleLockup.ConstructorParams memory baseParams,
ISablierV2LockupTranched lockupTranched,
MerkleLockupLT.TrancheWithPercentage[] memory tranchesWithPercentages,
uint256 aggregateAmount,
uint256 recipientsCount
)
external
returns (ISablierV2MerkleLockupLT merkleLockupLT)
{
// Calculate the sum of percentages across all tranches.
UD60x18 percentagesSum;
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
uint256 trancheCount = tranchesWithPercentages.length;
for (uint256 i = 0; i < trancheCount; ++i) {
UD60x18 percentage = (tranchesWithPercentages[i].unlockPercentage).intoUD60x18();
percentagesSum = percentagesSum.add(percentage);
}

// Checks: the sum of percentages equals 100%.
if (!percentagesSum.eq(ud(1e18))) {
revert Errors.SablierV2MerkleLockupFactory_PercentageSumNotEqualOneHundred(percentagesSum.intoUint256());
}

// Hash the parameters to generate a salt.
bytes32 salt = keccak256(
abi.encodePacked(
baseParams.initialAdmin,
baseParams.asset,
abi.encode(baseParams.ipfsCID),
bytes32(abi.encodePacked(baseParams.name)),
baseParams.merkleRoot,
baseParams.expiration,
baseParams.cancelable,
baseParams.transferable,
lockupTranched,
abi.encode(tranchesWithPercentages)
)
);

// Deploy the Merkle Lockup contract with CREATE2.
merkleLockupLT = new SablierV2MerkleLockupLT{ salt: salt }(baseParams, lockupTranched, tranchesWithPercentages);

// Log the creation of the Merkle Lockup, including some metadata that is not stored on-chain.
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
emit CreateMerkleLockupLT(
merkleLockupLT, baseParams, lockupTranched, tranchesWithPercentages, aggregateAmount, recipientsCount
);
}
}
2 changes: 1 addition & 1 deletion src/SablierV2MerkleLockupLL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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 { ud } from "@prb/math/src/UD60x18.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { ud } from "@prb/math/src/UD60x18.sol";

import { SablierV2MerkleLockup } from "./abstracts/SablierV2MerkleLockup.sol";
import { ISablierV2MerkleLockupLL } from "./interfaces/ISablierV2MerkleLockupLL.sol";
Expand Down
160 changes: 160 additions & 0 deletions src/SablierV2MerkleLockupLT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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 { ud, UD60x18 } 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 { ISablierV2MerkleLockupLT } from "./interfaces/ISablierV2MerkleLockupLT.sol";
import { MerkleLockup, MerkleLockupLT } from "./types/DataTypes.sol";

/// @title SablierV2MerkleLockupLT
/// @notice See the documentation in {ISablierV2MerkleLockupLT}.
contract SablierV2MerkleLockupLT is
ISablierV2MerkleLockupLT, // 2 inherited components
SablierV2MerkleLockup // 4 inherited components
{
using BitMaps for BitMaps.BitMap;
using SafeERC20 for IERC20;

/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2MerkleLockupLT
ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED;

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

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

/// @dev Constructs the contract by initializing the immutable state variables, and max approving the Sablier
/// contract.
constructor(
MerkleLockup.ConstructorParams memory baseParams,
ISablierV2LockupTranched lockupTranched,
MerkleLockupLT.TrancheWithPercentage[] memory tranchesWithPercentages
)
SablierV2MerkleLockup(baseParams)
{
LOCKUP_TRANCHED = lockupTranched;

// Since Solidity lacks a syntax for copying arrays 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) {
_tranchesWithPercentages.push(tranchesWithPercentages[i]);
}

// Max approve the Sablier contract to spend funds from the Merkle Lockup contract.
ASSET.forceApprove(address(LOCKUP_TRANCHED), type(uint256).max);
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2MerkleLockupLT
function getTranchesWithPercentages()
external
view
override
returns (MerkleLockupLT.TrancheWithPercentage[] memory)
{
return _tranchesWithPercentages;
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2MerkleLockupLT
function claim(
uint256 index,
address recipient,
uint128 amount,
bytes32[] calldata merkleProof
)
external
override
returns (uint256 streamId)
{
// 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))));

// Checks: validate the function.
_checkClaim(index, leaf, merkleProof);

// Calculate the tranches based on the `amount`.
LockupTranched.TrancheWithDuration[] memory tranches = _calculateTranches(amount);

// Effects: mark the index as claimed.
_claimedBitMap.set(index);

// Interactions: create the stream via {SablierV2LockupTranched}.
streamId = LOCKUP_TRANCHED.createWithDurations(
LockupTranched.CreateWithDurations({
sender: admin,
recipient: recipient,
totalAmount: amount,
asset: ASSET,
cancelable: CANCELABLE,
transferable: TRANSFERABLE,
tranches: tranches,
broker: Broker({ account: address(0), fee: ud(0) })
})
);

// Log the claim.
emit Claim(index, recipient, amount, streamId);
}

/// @dev Calculates the stream tranches based on Merkle tree amount and predefined percentage for each tranche.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
function _calculateTranches(uint128 amount)
internal
view
returns (LockupTranched.TrancheWithDuration[] memory tranches)
{
// Load the tranches in memory to save gas.
MerkleLockupLT.TrancheWithPercentage[] memory tranchesWithPercentages = _tranchesWithPercentages;

// Declare the variables need for calculation.
UD60x18 trancheAmountsSum;
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
uint256 trancheCount = tranchesWithPercentages.length;
tranches = new LockupTranched.TrancheWithDuration[](trancheCount);

UD60x18 udAmount = ud(amount);

// Iterate over each tranche to calculate its amount based on its percentage.
for (uint256 i = 0; i < trancheCount; ++i) {
// Convert the tranche's percentage to `UD60x18` for calculation.
UD60x18 percentage = (tranchesWithPercentages[i].unlockPercentage).intoUD60x18();

// Calculate the tranche's amount by applying its percentage to the `amount`.
UD60x18 trancheAmount = udAmount.mul(percentage);

// Sum all tranche amounts.
trancheAmountsSum = trancheAmountsSum.add(trancheAmount);

// Assign calculated amount and duration to the tranche.
tranches[i] = LockupTranched.TrancheWithDuration({
amount: trancheAmount.intoUint128(),
duration: tranchesWithPercentages[i].duration
});
}

// Adjust the last tranche amount to prevent claim failure due to rounding differences during calculations. We
// need to ensure the protocol invariant: the sum of all tranches' amounts equals the deposit amount.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
if (!udAmount.eq(trancheAmountsSum)) {
tranches[trancheCount - 1].amount += udAmount.intoUint128() - trancheAmountsSum.intoUint128();
}
}
}
Loading
Loading