Skip to content

Commit

Permalink
Add the scripting for deploying the lockup contract
Browse files Browse the repository at this point in the history
  • Loading branch information
motechFR committed Nov 13, 2024
1 parent 0f7cf26 commit b48282f
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 2 deletions.
30 changes: 30 additions & 0 deletions __test__/deployWeeklyVesting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { viem } from 'hardhat';
import type { Address } from 'viem';

import { walletFromKey } from './generateWallets';

export async function deployWeeklyVesting({ ScoutERC20Address }: { ScoutERC20Address: Address }) {
const sablierAdminAccount = await walletFromKey();

const SablierNFTDescriptor = await viem.deployContract('SablierV2NFTDescriptor', []);

const SablierLockupTranched = await viem.deployContract('SablierV2LockupTranched', [
sablierAdminAccount.account.address,
SablierNFTDescriptor.address,
BigInt(52)
]);

const WeeklyERC20Vesting = await viem.deployContract('LockupWeeklyStreamCreator', [
ScoutERC20Address,
SablierLockupTranched.address
]);

// Return the proxy with the implementation ABI attached
return {
WeeklyERC20Vesting,
SablierLockupTranched,
SablierNFTDescriptor
};
}

export type WeeklyVestingTestFixture = Awaited<ReturnType<typeof deployWeeklyVesting>>;
108 changes: 108 additions & 0 deletions __test__/utils/deployWeeklyVesting.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mine, time } from '@nomicfoundation/hardhat-network-helpers';
import { parseEventLogs } from 'viem';

import { prettyPrint } from '../../lib/prettyPrint';
import type { ProtocolERC20TestFixture } from '../deployScoutTokenERC20';
import { deployScoutTokenERC20 } from '../deployScoutTokenERC20';
import { deployWeeklyVesting, type WeeklyVestingTestFixture } from '../deployWeeklyVesting';
import type { GeneratedWallet } from '../generateWallets';
import { walletFromKey } from '../generateWallets';

describe('deployScoutTokenERC20', () => {
let weeklyVesting: WeeklyVestingTestFixture;
let ProtocolERC20: ProtocolERC20TestFixture;
let streamCreator: GeneratedWallet;
let recipient: GeneratedWallet;

beforeAll(async () => {
ProtocolERC20 = await deployScoutTokenERC20();
weeklyVesting = await deployWeeklyVesting({
ScoutERC20Address: ProtocolERC20.ProtocolERC20.address
});
recipient = await walletFromKey();
streamCreator = await walletFromKey();
});

it('should deploy the weekly vesting contract, with functional vesting', async () => {
const { SablierLockupTranched, WeeklyERC20Vesting } = weeklyVesting;

const { ProtocolERC20AdminAccount } = ProtocolERC20;

// Fund the stream creator with some tokens
await ProtocolERC20.transferProtocolERC20({
args: { to: streamCreator.account.address, amount: 20_000 },
wallet: ProtocolERC20AdminAccount
});

const amountToVest = 10_000;

const totalWeeks = 10;

// Approve the contract
await ProtocolERC20.approveProtocolERC20({
args: { spender: WeeklyERC20Vesting.address, amount: amountToVest },
wallet: streamCreator
});

const receipt = await WeeklyERC20Vesting.write.createStream(
[
recipient.account.address,
BigInt(amountToVest) * ProtocolERC20.ProtocolERC20_DECIMAL_MULTIPLIER,
BigInt(totalWeeks)
],
{
account: streamCreator.account
}
);

const output = await streamCreator.getTransactionReceipt({ hash: receipt });

const streamLog = parseEventLogs({
abi: SablierLockupTranched.abi,
logs: output.logs,
eventName: ['CreateLockupTranchedStream']
})[0].args;

const recipientErc20Balance = await ProtocolERC20.balanceOfProtocolERC20({
account: recipient.account.address
});

expect(recipientErc20Balance).toBe(0);

await time.setNextBlockTimestamp(streamLog.tranches[0].timestamp);

await mine();

const current = await time.latest();

console.log({ current });

const withdrawReceipt = await SablierLockupTranched.write.withdrawMax(
[streamLog.streamId, recipient.account.address],
{
account: recipient.account
}
);

const withdrawOutput = await recipient.getTransactionReceipt({ hash: withdrawReceipt });

const withdrawLog = parseEventLogs({
abi: SablierLockupTranched.abi,
logs: withdrawOutput.logs,
eventName: ['WithdrawFromLockupStream']
})[0].args;

const recipientErc20BalanceAfter = await ProtocolERC20.balanceOfProtocolERC20({
account: recipient.account.address
});

prettyPrint({
recipientErc20BalanceAfter,
amountToVest,
streamLog,
withdrawLog
});

expect(recipientErc20BalanceAfter).toBe(amountToVest / totalWeeks);
});
});
86 changes: 86 additions & 0 deletions contracts/protocol/vesting/LockupWeeklyStreamCreator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ud60x18} from "@prb/math/src/UD60x18.sol";
import {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 here so that it is compiled and included in the hardhat artifacts
import {SablierV2NFTDescriptor} from "@sablier/v2-core/src/SablierV2NFTDescriptor.sol";
import {SablierV2LockupTranched} from "@sablier/v2-core/src/SablierV2LockupTranched.sol";

/// @notice Example of how to create a Lockup Linear stream.
/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear
contract LockupWeeklyStreamCreator {
// sepolia addresses
IERC20 public SCOUT;

ISablierV2LockupTranched public LOCKUP_TRANCHED;

constructor(address _erc20, address _lockupTranched) {
SCOUT = IERC20(_erc20);
LOCKUP_TRANCHED = ISablierV2LockupTranched(_lockupTranched);
}

/// @dev For this function to work, the sender must have approved this dummy contract to spend SCOUT.
function createStream(
address recipient,
uint128 totalAmount,
uint128 _weeks
) public returns (uint256 streamId) {
// Transfer the provided amount of SCOUT tokens to this contract
SCOUT.transferFrom(msg.sender, address(this), totalAmount);

// Approve the Sablier contract to spend SCOUT
SCOUT.approve(address(LOCKUP_TRANCHED), totalAmount);

// Declare the params struct
LockupTranched.CreateWithDurations memory params;

// Declare the function parameters
params.sender = msg.sender; // The sender will be able to cancel the stream
params.recipient = address(recipient); // The recipient of the streamed assets
params.totalAmount = uint128(totalAmount); // Total amount is the amount inclusive of all fees
params.asset = SCOUT; // The streaming asset
params.cancelable = true; // Whether the stream will be cancelable or not
params.transferable = true; // Whether the stream will be transferable or not

// Declare some dummy tranches
params.tranches = new LockupTranched.TrancheWithDuration[](_weeks);

uint128 _streamed = 0;

uint128 _amountPerWeek = totalAmount / _weeks;

for (uint256 i = 0; i < _weeks; i++) {
if (i != _weeks - 1) {
params.tranches[i] = LockupTranched.TrancheWithDuration({
amount: _amountPerWeek,
duration: 1 weeks
});

_streamed += _amountPerWeek;
} else {
uint128 _remainder = totalAmount - _streamed;
params.tranches[i] = LockupTranched.TrancheWithDuration({
amount: _remainder,
duration: 1 weeks
});

_streamed += _remainder;
}
}

// Wondering what's up with that ud60x18 function? It's a casting function that wraps a basic integer to the UD60x18 value type. This type is part of the math library PRBMath, which is used in Sablier for fixed-point calculations.
params.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined

// Create the LockupTranched stream
streamId = LOCKUP_TRANCHED.createWithDurations(params);
}

function _weeksToSeconds(uint256 _weeks) internal pure returns (uint256) {
return _weeks * 60 * 60 * 24 * 7;
}
}
3 changes: 3 additions & 0 deletions contracts/protocol/vesting/Readme.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## List of Sablier deployments
https://docs.sablier.com/contracts/v2/deployments#base
https://docs.sablier.com/contracts/v2/deployments#base-sepolia
35 changes: 33 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"@nomicfoundation/hardhat-ignition-viem": "^0.15.0",
"@nomicfoundation/hardhat-toolbox-viem": "^3.0.0",
"@pinata/sdk": "^2.1.0",
"@sablier/v2-core": "^1.2.0",
"@sablier/v2-periphery": "^1.2.0",
"@types/jest": "^29.5.13",
"dotenv-cli": "^7.4.2",
"hardhat-jest": "^1.0.8",
Expand Down

0 comments on commit b48282f

Please sign in to comment.