Skip to content

Commit

Permalink
feat: support Blast network (#292)
Browse files Browse the repository at this point in the history
* test: refactor changePrank with resetPrank

test: update blockNumber in createSelectFork

* feat: new interfaces to support Blast assets

* test: add mock assets for integration tests

* test: update precompiles

* refactor: BlastGovernor function names

* build: update bun lockfile

* refactor: reorder struct to match v1.1.1

* fix: configure ERC20 yield to Claimable

* refactor: SablierV2Blast contract

* test: integration and fork tests for Blast configuration

* ci: add fork test for Blast

* refactor: avoid configuring yield in MerkleStreamer constructor
test: add test_ConstructorWhen_RebasingAsset
test: update mock contracts

* build: update lockfile

* test: update precompile
test: polish tests

* test: use mint for funding addresses with rebasing asset

* test: removed BWETH forked asset

* test: remove redundant tests

* test: update mainnet details

* test: add core mainnet addresses

* test: remove BlastMock
  • Loading branch information
smol-ninja committed Jul 4, 2024
1 parent 730566c commit 62cb598
Show file tree
Hide file tree
Showing 18 changed files with 117 additions and 55 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export API_KEY_INFURA="YOUR_API_KEY_INFURA"
export EOA="YOUR_EOA_ADDRESS"
export FOUNDRY_PROFILE="lite"
export MNEMONIC="YOUR_MNEMONIC"
export RPC_URL_BLAST="YOUR_RPC_URL_BLAST"
export RPC_URL_MAINNET="YOUR_RPC_URL_MAINNET"
2 changes: 1 addition & 1 deletion .github/workflows/ci-deep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
test-fork:
needs: ["lint", "build"]
secrets:
RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }}
RPC_URL_MAINNET: ${{ secrets.RPC_URL_BLAST }}
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main"
with:
foundry-fuzz-runs: ${{ inputs.forkFuzzRuns || 1000 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-fork.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
test-fork:
needs: ["lint", "build"]
secrets:
RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }}
RPC_URL_MAINNET: ${{ secrets.RPC_URL_BLAST }}
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main"
with:
foundry-profile: "test-optimized"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
test-fork:
needs: ["lint", "build"]
secrets:
RPC_URL_MAINNET: ${{ secrets.RPC_URL_MAINNET }}
RPC_URL_MAINNET: ${{ secrets.RPC_URL_BLAST }}
uses: "sablier-labs/reusable-workflows/.github/workflows/forge-test.yml@main"
with:
foundry-fuzz-runs: 20
Expand Down
Binary file removed bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}"
avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}"
base = "https://mainnet.base.org"
blast = "https://rpc.blast.io"
bnb = "https://bsc-dataseed.binance.org"
ethereum = "${RPC_URL_MAINNET}"
gnosis = "https://rpc.gnosischain.com"
Expand Down
4 changes: 2 additions & 2 deletions src/abstracts/SablierV2MerkleLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import { Adminable } from "@sablier/v2-core/src/abstracts/Adminable.sol";
import { SablierV2Blast } from "@sablier/v2-core/src/abstracts/SablierV2Blast.sol";

import { ISablierV2MerkleLockup } from "../interfaces/ISablierV2MerkleLockup.sol";
import { MerkleLockup } from "../types/DataTypes.sol";
Expand All @@ -15,7 +15,7 @@ import { Errors } from "../libraries/Errors.sol";
/// @notice See the documentation in {ISablierV2MerkleLockup}.
abstract contract SablierV2MerkleLockup is
ISablierV2MerkleLockup, // 2 inherited component
Adminable // 1 inherited component
SablierV2Blast // 3 inherited component
{
using BitMaps for BitMaps.BitMap;
using SafeERC20 for IERC20;
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/ISablierV2MerkleLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IAdminable } from "@sablier/v2-core/src/interfaces/IAdminable.sol";
import { ISablierV2Blast } from "@sablier/v2-core/src/interfaces/blast/ISablierV2Blast.sol";

/// @title ISablierV2MerkleLockup
/// @notice A contract that lets user claim Sablier streams using Merkle proofs. A popular use case for MerkleLockup
/// is airstreams: a portmanteau of "airdrop" and "stream". This is an airdrop model where the tokens are distributed
/// over time, as opposed to all at once.
/// @dev This is the base interface for MerkleLockup. See the Sablier docs for more guidance: https://docs.sablier.com
interface ISablierV2MerkleLockup is IAdminable {
interface ISablierV2MerkleLockup is ISablierV2Blast {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
6 changes: 6 additions & 0 deletions test/Base.t.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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC20Rebasing } from "@sablier/v2-core/src/interfaces/blast/IERC20Rebasing.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
Expand All @@ -24,6 +25,7 @@ import { SablierV2MerkleLT } from "src/SablierV2MerkleLT.sol";

import { ERC20Mock } from "./mocks/erc20/ERC20Mock.sol";
import { Assertions } from "./utils/Assertions.sol";
import { ERC20RebasingMock } from "./mocks/blast/ERC20RebasingMock.sol";
import { Defaults } from "./utils/Defaults.sol";
import { DeployOptimized } from "./utils/DeployOptimized.sol";
import { Events } from "./utils/Events.sol";
Expand Down Expand Up @@ -53,6 +55,7 @@ abstract contract Base_Test is
ISablierV2BatchLockup internal batchLockup;
IERC20 internal dai;
Defaults internal defaults;
IERC20Rebasing internal erc20RebasingMock;
ISablierV2LockupDynamic internal lockupDynamic;
ISablierV2LockupLinear internal lockupLinear;
ISablierV2LockupTranched internal lockupTranched;
Expand All @@ -68,6 +71,9 @@ abstract contract Base_Test is
// Deploy the default test asset.
dai = new ERC20Mock("DAI Stablecoin", "DAI");

// Deploy the blast contracts.
erc20RebasingMock = new ERC20RebasingMock();

// Create users for testing.
users.alice = createUser("Alice");
users.admin = createUser("Admin");
Expand Down
4 changes: 2 additions & 2 deletions test/fork/Fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ abstract contract Fork_Test is Base_Test, V2CoreFuzzers {
//////////////////////////////////////////////////////////////////////////*/

function setUp() public virtual override {
// Fork Ethereum Mainnet at a specific block number.
vm.createSelectFork({ blockNumber: 18_821_300, urlOrAlias: "mainnet" });
// Fork Blast mainnet at a specific block number.
vm.createSelectFork({ blockNumber: 1_000_000, urlOrAlias: "blast" });

// Set up the base test contract.
Base_Test.setUp();
Expand Down
20 changes: 10 additions & 10 deletions test/fork/assets/USDC.t.sol → test/fork/assets/USDB.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ import { CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test } from "../ba
import { MerkleLL_Fork_Test } from "../merkle-lockup/MerkleLL.t.sol";
import { MerkleLT_Fork_Test } from "../merkle-lockup/MerkleLT.t.sol";

/// @dev An ERC-20 asset with 6 decimals.
IERC20 constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
/// @dev A USD token with rebasing yield deployed on Blast L2.
IERC20 constant usdb = IERC20(0x4300000000000000000000000000000000000003);

contract USDC_CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test(usdc)
contract USDB_CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test(usdb)
{ }

contract USDC_CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test(usdc)
contract USDB_CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test(usdb)
{ }

contract USDC_CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test(usdc)
contract USDB_CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is
CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test(usdb)
{ }

contract USDC_MerkleLL_Fork_Test is MerkleLL_Fork_Test(usdc) { }
contract USDB_MerkleLL_Fork_Test is MerkleLL_Fork_Test(usdb) { }

contract USDC_MerkleLT_Fork_Test is MerkleLT_Fork_Test(usdc) { }
contract USDB_MerkleLT_Fork_Test is MerkleLT_Fork_Test(usdb) { }
29 changes: 0 additions & 29 deletions test/fork/assets/USDT.t.sol

This file was deleted.

7 changes: 6 additions & 1 deletion test/fork/batch-lockup/createWithTimestampsLD.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LockupDynamic } from "@sablier/v2-core/src/types/DataTypes.sol";

import { BatchLockup } from "src/types/DataTypes.sol";

import { ERC20RebasingMock } from "../../mocks/blast/ERC20RebasingMock.sol";
import { ArrayBuilder } from "../../utils/ArrayBuilder.sol";
import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol";
import { Fork_Test } from "../Fork.t.sol";
Expand Down Expand Up @@ -43,7 +44,11 @@ abstract contract CreateWithTimestamps_LockupDynamic_BatchLockup_Fork_Test is Fo
uint256 firstStreamId = lockupDynamic.nextStreamId();
uint128 totalTransferAmount = params.perStreamAmount * params.batchSize;

deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) });
// Fund the sender address with rebasing asset.
resetPrank({ msgSender: ERC20RebasingMock(address(FORK_ASSET)).bridge() });
ERC20RebasingMock(address(FORK_ASSET)).mint(params.sender, uint256(totalTransferAmount));

resetPrank({ msgSender: params.sender });
approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) });

LockupDynamic.CreateWithTimestamps memory createWithTimestamps = LockupDynamic.CreateWithTimestamps({
Expand Down
7 changes: 6 additions & 1 deletion test/fork/batch-lockup/createWithTimestampsLL.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";

import { BatchLockup } from "src/types/DataTypes.sol";

import { ERC20RebasingMock } from "../../mocks/blast/ERC20RebasingMock.sol";
import { ArrayBuilder } from "../../utils/ArrayBuilder.sol";
import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol";
import { Fork_Test } from "../Fork.t.sol";
Expand Down Expand Up @@ -42,7 +43,11 @@ abstract contract CreateWithTimestamps_LockupLinear_BatchLockup_Fork_Test is For
uint256 firstStreamId = lockupLinear.nextStreamId();
uint128 totalTransferAmount = params.perStreamAmount * params.batchSize;

deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) });
// Fund the sender address with rebasing asset.
resetPrank({ msgSender: ERC20RebasingMock(address(FORK_ASSET)).bridge() });
ERC20RebasingMock(address(FORK_ASSET)).mint(params.sender, uint256(totalTransferAmount));

resetPrank({ msgSender: params.sender });
approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) });

LockupLinear.CreateWithTimestamps memory createParams = LockupLinear.CreateWithTimestamps({
Expand Down
7 changes: 6 additions & 1 deletion test/fork/batch-lockup/createWithTimestampsLT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol";

import { BatchLockup } from "src/types/DataTypes.sol";

import { ERC20RebasingMock } from "../../mocks/blast/ERC20RebasingMock.sol";
import { ArrayBuilder } from "../../utils/ArrayBuilder.sol";
import { BatchLockupBuilder } from "../../utils/BatchLockupBuilder.sol";
import { Fork_Test } from "../Fork.t.sol";
Expand Down Expand Up @@ -43,7 +44,11 @@ abstract contract CreateWithTimestamps_LockupTranched_BatchLockup_Fork_Test is F
uint256 firstStreamId = lockupTranched.nextStreamId();
uint128 totalTransferAmount = params.perStreamAmount * params.batchSize;

deal({ token: address(FORK_ASSET), to: params.sender, give: uint256(totalTransferAmount) });
// Fund the sender address with rebasing asset.
resetPrank({ msgSender: ERC20RebasingMock(address(FORK_ASSET)).bridge() });
ERC20RebasingMock(address(FORK_ASSET)).mint(params.sender, uint256(totalTransferAmount));

resetPrank({ msgSender: params.sender });
approveContract({ asset_: FORK_ASSET, from: params.sender, spender: address(batchLockup) });

LockupTranched.CreateWithTimestamps memory createWithTimestamps = LockupTranched.CreateWithTimestamps({
Expand Down
6 changes: 4 additions & 2 deletions test/fork/merkle-lockup/MerkleLL.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Lockup, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { ISablierV2MerkleLL } from "src/interfaces/ISablierV2MerkleLL.sol";
import { MerkleLockup } from "src/types/DataTypes.sol";

import { ERC20RebasingMock } from "../../mocks/blast/ERC20RebasingMock.sol";
import { MerkleBuilder } from "../../utils/MerkleBuilder.sol";
import { Fork_Test } from "../Fork.t.sol";

Expand Down Expand Up @@ -122,8 +123,9 @@ abstract contract MerkleLL_Fork_Test is Fork_Test {
recipientCount: vars.recipientCount
});

// Fund the MerkleLockup contract.
deal({ token: address(FORK_ASSET), to: address(vars.merkleLL), give: vars.aggregateAmount });
// Fund the MerkleLockup with rebasing asset.
resetPrank({ msgSender: ERC20RebasingMock(address(FORK_ASSET)).bridge() });
ERC20RebasingMock(address(FORK_ASSET)).mint(address(vars.merkleLL), vars.aggregateAmount);

assertGt(address(vars.merkleLL).code.length, 0, "MerkleLL contract not created");
assertEq(address(vars.merkleLL), vars.expectedLL, "MerkleLL contract does not match computed address");
Expand Down
6 changes: 4 additions & 2 deletions test/fork/merkle-lockup/MerkleLT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Lockup, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol
import { ISablierV2MerkleLT } from "src/interfaces/ISablierV2MerkleLT.sol";
import { MerkleLockup } from "src/types/DataTypes.sol";

import { ERC20RebasingMock } from "../../mocks/blast/ERC20RebasingMock.sol";
import { MerkleBuilder } from "../../utils/MerkleBuilder.sol";
import { Fork_Test } from "../Fork.t.sol";

Expand Down Expand Up @@ -124,8 +125,9 @@ abstract contract MerkleLT_Fork_Test is Fork_Test {
recipientCount: vars.recipientCount
});

// Fund the MerkleLockup contract.
deal({ token: address(FORK_ASSET), to: address(vars.merkleLT), give: vars.aggregateAmount });
// Fund the MerkleLockup with rebasing asset.
resetPrank({ msgSender: ERC20RebasingMock(address(FORK_ASSET)).bridge() });
ERC20RebasingMock(address(FORK_ASSET)).mint(address(vars.merkleLT), vars.aggregateAmount);

assertGt(address(vars.merkleLT).code.length, 0, "MerkleLT contract not created");
assertEq(address(vars.merkleLT), vars.expectedLT, "MerkleLT contract does not match computed address");
Expand Down
64 changes: 64 additions & 0 deletions test/mocks/blast/ERC20RebasingMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import { IERC20Rebasing, YieldMode } from "@sablier/v2-core/src/interfaces/blast/IERC20Rebasing.sol";

/// @dev The following mock contract is inspired from Blast repository
/// https://github.com/blast-io/blast/blob/master/blast-optimism/packages/contracts-bedrock/src/L2/ERC20Rebasing.sol
/// https://github.com/blast-io/blast/blob/master/blast-optimism/packages/contracts-bedrock/src/L2/Shares.sol
/// https://github.com/blast-io/blast/blob/master/blast-optimism/packages/contracts-bedrock/src/L2/USDB.sol
contract ERC20RebasingMock is ERC20("ERC20Rebasing Mock", "REB-MOCK"), IERC20Rebasing {
mapping(address account => uint256 amount) private _claimable;
mapping(address account => YieldMode mode) private _yieldMode;

// Bool to handle default yield modes
bool private isYieldSet;

function addValue(uint256 value) external { }

function bridge() external view returns (address) { }

function getClaimableAmount(address account) public view override returns (uint256 amount) {
require(getConfiguration(account) == YieldMode.CLAIMABLE, "ERC20RebasingMock: not claimable");

return _claimable[account];
}

function getConfiguration(address account) public view override returns (YieldMode) {
if (!isYieldSet) {
return YieldMode.AUTOMATIC;
}
return _yieldMode[account];
}

function claim(address recipient, uint256 amount) public override returns (uint256 claimed) {
require(recipient != address(0), "ERC20RebasingMock: zero address");
require(getConfiguration(msg.sender) == YieldMode.CLAIMABLE, "ERC20RebasingMock: not claimable");
require(_claimable[msg.sender] >= amount, "ERC20RebasingMock: insufficient");

_claimable[msg.sender] -= amount;
_mint(recipient, amount);

return amount;
}

function configure(YieldMode yieldMode) public override returns (uint256) {
_yieldMode[msg.sender] = yieldMode;
return balanceOf(msg.sender);
}

function mint(address _to, uint256 _amount) external { }

function price() external view returns (uint256) { }

function REPORTER() public view returns (address) { }

// Test helper function
function setClaimableAmount(address account, uint256 amount) public {
_claimable[account] = amount;
}

function symbol() public view override returns (string memory) { }
}

0 comments on commit 62cb598

Please sign in to comment.