Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cache/
out/
.env
.env*
test-command.txt
broadcast/
.DS_Store
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,28 @@ To set up the project locally, follow these steps:
3. **Compile Contracts**

```base
forge build --optimize --optimizer-runs 200 --use solc:0.8.20
forge build --optimize --optimizer-runs 200 --use solc:0.8.28 --sizes
```

## Deployment

```bash
# Setup env var for deployment
cp env.production.abstract-beta.template .env
# Modify .env and update all relevant values

# To dry-run the deploy scripts (to Ethereum mainnet)
forge script scripts/deploy.abstract-beta.s.sol --use solc:0.8.28 --optimize --optimizer-runs 200 --rpc-url https://your/rpc/endpt

# To deploy to Ethereum mainnet (and verify on Etherscan)
forge script scripts/deploy.abstract-beta.s.sol --use solc:0.8.28 --optimize --optimizer-runs 200 --rpc-url https://your/rpc/endpt --broadcast --verify --verifier etherscan --etherscan-api-key $ETHERSCAN_API_KEY

# The scripts will save the Safe txs to out/safeTxs.json, which you can import to the Transaction Builder in Safe web UI for execution
# It should include txs for:
# 1. Approve MetavestController (without recipient overrides) for escrowing the vesting token
# 2. Approve MetavestController (with recipient overrides) for escrowing the vesting token
# 3~. Create Metavest for each grantee (one transaction each)
#
# Always double check the content before signing:
cat out/safeTxs.json
```
34 changes: 34 additions & 0 deletions env.production.abstract-beta.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TODO (1) fill in your deployer private key
DEPLOYER_PRIVATE_KEY=

# TODO (2) Update number of grantees
NUM_GRANTEES=2

# GRANTEE_CONTROLLER_TYPE:
# - 0: without recipient override (grantee can specify their desired recipient addresses)
# - 1: with recipient override (authority overrides all grantees' recipient address)

# Instead of the commonly-used "hire date", MetaVesT's vesting start time is defined as "when the cliff happens".
# Ex. 10000 token (18-decimals) grants vesting over 4 years, hired on 2025/01/01 with a 6-month vesting cliff, unlock over 1 year (unlock start time TBD)
# Parameters may look like the example below:

GRANTEE_ADDR_0=0x48d206948C366396a86A449DdD085FDbfC280B4b # grantee EOA (grantee must use this account to send txs to the grant smart contract)
GRANTEE_AMOUNT_0=10000000000000000000000 # 10000 token
GRANTEE_VESTING_START_TIME_0=1751328000 # 2025/07/01 = 6 months after hired date
GRANTEE_VESTING_CLIFF_CREDIT_0=1250000000000000000000 # (10000 * 6 / 48) * 1e18
GRANTEE_VESTING_RATE_0=79274479959411 # 10000e18 // (4 * 365 * 24 * 3600)
GRANTEE_UNLOCKING_CLIFF_CREDIT_0=0
GRANTEE_UNLOCK_RATE_0=317097919837645 # 10000e18 // (1 * 365 * 24 * 3600)
GRANTEE_CONTROLLER_TYPE_0=0

# Ex2. 20000 token (18-decimals) grants vesting over 4 years, hired on 2024/10/01 with a 6-month vesting cliff, unlock over 1 year (unlock start time TBD)
GRANTEE_ADDR_1=0x5ff4e90Efa2B88cf3cA92D63d244a78a88219Abf # grantee EOA (grantee must use this account to send txs to the grant smart contract)
GRANTEE_AMOUNT_1=20000000000000000000000 # 20000 token (18-decimals)
GRANTEE_VESTING_START_TIME_1=1743465600 # 2025/04/01 = 6 months after hired date
GRANTEE_VESTING_CLIFF_CREDIT_1=2500000000000000000000 # (20000 * 6 / 48) * 1e18
GRANTEE_VESTING_RATE_1=158548959918822 # 20000e18 // (4 * 365 * 24 * 3600)
GRANTEE_UNLOCKING_CLIFF_CREDIT_1=0
GRANTEE_UNLOCK_RATE_1=634195839675291 # 20000e18 // (1 * 365 * 24 * 3600)
GRANTEE_CONTROLLER_TYPE_1=1

# TODO (3) Update the above grantee parameters as needed
5 changes: 5 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[profile.default]
fs_permissions = [
{ access = "read-write", path = "out" },
{ access = "read-write", path = "res" },
]
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
openzeppelin-contracts=lib/openzeppelin-contracts/contracts
openzeppelin-contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts
35 changes: 35 additions & 0 deletions res/safeTxs-development.abstract-beta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"meta": {
"name": "Transactions Batch",
"description": "",
"createdFromSafeAddress": "",
"txBuilderVersion": "",
"createdFromOwnerAddress": "",
"checksum": ""
},
"createdAt": 1766526996000,
"transactions": [
{
"value": "0",
"data": "0x095ea7b3000000000000000000000000cef4761cc320fdc28034f00b754e8b608028f42000000000000000000000000000000000000000000000021e19e0c9bab2400000",
"to": "0xA581b1b0D31B0528C20801E56EeEaF0834a8C907"
},
{
"to": "0xA581b1b0D31B0528C20801E56EeEaF0834a8C907",
"data": "0x095ea7b3000000000000000000000000387116083c8788426fc91d6689972e2ad6d5451200000000000000000000000000000000000000000000043c33c1937564800000",
"value": "0"
},
{
"value": "0",
"data": "0x99ec93bb000000000000000000000000000000000000000000000000000000000000000200000000000000000000000048d206948c366396a86a449ddd085fdbfc280b4b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021e19e0c9bab2400000000000000000000000000000000000000000000000000043c33c1937564800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048198737bd730000000000000000000000000000000000000000000000000000000068632500000000000000000000000000000000000000000000000000000120661cdef5cd000000000000000000000000000000000000000000000000000000006d182000000000000000000000000000a581b1b0d31b0528c20801e56eeeaf0834a8c907000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000b9e5ae881f36083cb914205f19eaa265d76eef53000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"to": "0xCEf4761CC320Fdc28034f00B754E8b608028f420"
},
{
"data": "0x99ec93bb00000000000000000000000000000000000000000000000000000000000000020000000000000000000000005ff4e90efa2b88cf3ca92d63d244a78a88219abf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043c33c19375648000000000000000000000000000000000000000000000000000878678326eac9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090330e6f7ae60000000000000000000000000000000000000000000000000000000067eb2c80000000000000000000000000000000000000000000000000000240cc39bdeb9b000000000000000000000000000000000000000000000000000000006d182000000000000000000000000000a581b1b0d31b0528c20801e56eeeaf0834a8c907000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000b9e5ae881f36083cb914205f19eaa265d76eef53000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"to": "0x387116083c8788426Fc91d6689972e2ad6d54512",
"value": "0"
}
],
"version": "1.0",
"chainId": "1"
}
217 changes: 217 additions & 0 deletions scripts/deploy.abstract-beta.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import {AbstractBetaSepolia} from "./lib/AbstractBetaSepolia.sol";
import {AbstractBeta} from "./lib/AbstractBeta.sol";
import {SafeUtils} from "./lib/SafeUtils.sol";
import {MockERC20} from "../test/mocks/MockERC20.sol";
import {GnosisTransaction} from "./lib/safe.sol";
import {RestrictedTokenFactory} from "../src/RestrictedTokenFactory.sol";
import {RestrictedTokenAward} from "../src/RestrictedTokenAllocation.sol";
import {Script, console2} from "forge-std/Script.sol";
import {TokenOptionFactory} from "../src/TokenOptionFactory.sol";
import {VestingAllocationFactory} from "../src/VestingAllocationFactory.sol";
import {metavestController} from "../src/MetaVesTController.sol";
import {BaseAllocation} from "../src/BaseAllocation.sol";

contract DeployAbstractBetaScript is Script {
function run() public {
runWithArgs(
// Ethereum mainnet
"MetaLexMetaVest.Abstract.v1.0.0",
vm.envUint("DEPLOYER_PRIVATE_KEY"),
AbstractBetaSepolia.getDefault()

// Sepolia
// "MetaLexMetaVest.Abstract.v0.1.0",
// vm.envUint("DEPLOYER_PRIVATE_KEY"),
// AbstractBetaSepolia.getDefault()
);
}

function runWithArgs(
string memory saltStr,
uint256 deployerPrivateKey,
AbstractBeta.Config memory config
) public returns (
metavestController controllerWithoutOverride,
metavestController controllerWithOverride,
AbstractBeta.GrantInfo[] memory,
GnosisTransaction[] memory provisionSafeTxs,
GnosisTransaction[] memory grantSafeTxs,
GnosisTransaction[] memory allSafeTxs
) {
bytes32 salt = keccak256(bytes(saltStr));
address deployer = vm.addr(deployerPrivateKey);

console2.log("");
console2.log("=== DeployAbstractBetaControllersScript ===");
console2.log("saltStr: %s", saltStr);
console2.log("deployer: %s", deployer);
console2.log("vesting token: %s", config.vestingToken);
console2.log("payment token: %s", config.paymentToken);
console2.log("");

AbstractBeta.GrantInfo[] memory grants = AbstractBeta.loadGrants();

vm.startBroadcast(deployerPrivateKey);

// (1) Deploy factories and controllers

{
VestingAllocationFactory vestingAllocationFactory = new VestingAllocationFactory{salt: salt}();
TokenOptionFactory tokenOptionFactory = new TokenOptionFactory{salt: salt}();
RestrictedTokenFactory restrictedTokenFactory = new RestrictedTokenFactory{salt: salt}();

config.controllerWithoutOverride = new metavestController{salt: bytes32(uint256(salt) + 0)}(
config.authority, // _authority
config.dao, // _dao
address(0), // _recipientOverride
address(vestingAllocationFactory),
address(tokenOptionFactory),
address(restrictedTokenFactory)
);

config.controllerWithOverride = new metavestController{salt: bytes32(uint256(salt) + 1)}(
config.authority, // _authority
config.dao, // _dao
config.escrowMultisig, // _recipientOverride
address(vestingAllocationFactory),
address(tokenOptionFactory),
address(restrictedTokenFactory)
);

console2.log("Deployed controllers:");
console2.log(" controllerWithoutOverride: ", address(config.controllerWithoutOverride));
console2.log(" controllerWithOverride: ", address(config.controllerWithOverride));
console2.log("");
}

vm.stopBroadcast();

// (2a) Prepare Safe txs (vesting token approval & grants creation)

// Calculate total vesting token needed for all grants
uint256 totalVestingTokenAmountWithoutOverride = 0;
uint256 totalVestingTokenAmountWithOverride = 0;
for (uint256 i = 0; i < grants.length; i++) {
if (grants[i].controllerType == AbstractBeta.ControllerType.WithoutOverride) {
totalVestingTokenAmountWithoutOverride += grants[i].amount;
} else {
totalVestingTokenAmountWithOverride += grants[i].amount;
}
}

console2.log("Preparing Safe tx for approving vesting tokens...");
provisionSafeTxs = new GnosisTransaction[](2);
provisionSafeTxs[0] = GnosisTransaction({
to: config.vestingToken,
value: 0,
data: abi.encodeWithSelector(
ERC20.approve.selector,
address(config.controllerWithoutOverride),
totalVestingTokenAmountWithoutOverride
)
});
provisionSafeTxs[1] = GnosisTransaction({
to: config.vestingToken,
value: 0,
data: abi.encodeWithSelector(
ERC20.approve.selector,
address(config.controllerWithOverride),
totalVestingTokenAmountWithOverride
)
});

console2.log("Preparing Safe txs for grants creation:");
grantSafeTxs = _generateGrantSafeTxs(config, grants);

allSafeTxs = new GnosisTransaction[](provisionSafeTxs.length + grantSafeTxs.length);

// (2b) Create Safe txs JSON file

{
uint256 safeTxIdx = 0;
for (uint256 i = 0; i < provisionSafeTxs.length; i++) {
allSafeTxs[safeTxIdx++] = provisionSafeTxs[i];
}
for (uint256 i = 0; i < grantSafeTxs.length; i++) {
allSafeTxs[safeTxIdx++] = grantSafeTxs[i];
}

string memory safeTxJson = SafeUtils.formatSafeTxJson(allSafeTxs);

console2.log("Safe tx JSON (can be imported to Safe Transaction Builder):");
console2.log("==== JSON data start ====");
console2.log(safeTxJson);
console2.log("==== JSON data end ====");

string memory safeTxJsonPath = "./out/safeTxs.json";
vm.writeJson(safeTxJson, safeTxJsonPath);
console2.log("JSON file written to: %s", safeTxJsonPath);
}

return (
config.controllerWithoutOverride,
config.controllerWithOverride,
grants,
provisionSafeTxs,
grantSafeTxs,
allSafeTxs
);
}

function _generateGrantSafeTxs(
AbstractBeta.Config memory config,
AbstractBeta.GrantInfo[] memory grants
) internal returns (GnosisTransaction[] memory safeTxs) {
safeTxs = new GnosisTransaction[](grants.length);

for (uint256 i = 0; i < grants.length; i++) {
AbstractBeta.GrantInfo memory grant = grants[i];

metavestController controller = _getController(grant, config);

safeTxs[i] = GnosisTransaction({
to: address(controller),
value: 0,
data: abi.encodeWithSignature(
"createMetavest(uint8,address,address,(uint256,uint128,uint128,uint160,uint48,uint160,uint48,address),(uint256,bool,bool,address[])[],uint256,address,uint256,uint256)",
metavestController.metavestType.RestrictedTokenAward,
grant.grantee,
address(0), // no preference
BaseAllocation.Allocation({
tokenContract: config.vestingToken,
tokenStreamTotal: grant.amount,
vestingCliffCredit: grant.vestingCliffCredit,
unlockingCliffCredit: grant.unlockingCliffCredit,
vestingRate: grant.vestingRate,
vestingStartTime: grant.vestingStartTime,
unlockRate: grant.unlockRate,
unlockStartTime: config.unlockStartTime
}),
new BaseAllocation.Milestone[](0),
config.exercisePrice,
address(config.paymentToken),
config.shortStopDuration,
0 // no-op: _longStopDate
)
});

console2.log(" #%d:", i + 1);
console2.log(" grantee: %s", grant.grantee);
console2.log(" amount: %d", grant.amount);
console2.log(" vestingStartTime: %d", grant.vestingStartTime);
console2.log(" vestingCliffCredit: %d", grant.vestingCliffCredit);
console2.log(" vestingRate: %d", grant.vestingRate);
console2.log(" unlockingCliffCredit: %d", grant.unlockingCliffCredit);
console2.log(" unlockRate: %d", grant.unlockRate);
console2.log(" controllerType: %d", uint8(grant.controllerType));
console2.log("");
}
}

function _getController(AbstractBeta.GrantInfo memory grant, AbstractBeta.Config memory config) internal returns (metavestController) {
return (grant.controllerType == AbstractBeta.ControllerType.WithoutOverride)
? config.controllerWithoutOverride
: config.controllerWithOverride;
}
}
35 changes: 35 additions & 0 deletions scripts/deploy.mock-erc20.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Script, console2} from "forge-std/Script.sol";
import {MockERC20} from "../test/mocks/MockERC20.sol";

contract DeployMockErc20Script is Script {
function run() public {

string memory saltStr = "MetaLexMetaVest.Abstract.mockPaymentToken.dev.0";
bytes32 salt = keccak256(bytes(saltStr));

string memory tokenName = "Payment Token";
string memory tokenSymbol = "PAY";
uint8 tokenDecimals = 6;

uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);

console2.log("");
console2.log("=== DeployMockErc20Script ===");
console2.log("saltStr: %s", saltStr);
console2.log("deployer: %s", deployer);
console2.log("");

vm.startBroadcast(deployerPrivateKey);

MockERC20 mockToken = new MockERC20{salt: salt}(tokenName, tokenSymbol, tokenDecimals);
console2.log("deployed mock token: %s", address(mockToken));

vm.stopBroadcast();

console2.log("Deployed addresses:");
console2.log(" mock token: %s", address(mockToken));
console2.log(" decimals: %d", mockToken.decimals());
console2.log("");
}
}
Loading