-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
{Safe} calldata generator script (#7)
* validator registration calldata generator for safe multisig * Create {Safe} tx json file for batch registration * add Holesky support in safe json
- Loading branch information
Showing
11 changed files
with
358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Compiler files | ||
cache/ | ||
out/ | ||
|
||
# Ignores development broadcast logs | ||
!/broadcast | ||
/broadcast/*/31337/ | ||
/broadcast/**/dry-run/ | ||
|
||
# Docs | ||
docs/ | ||
|
||
# Dotenv file | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Utility scripts | ||
|
||
### Validator registration using Safe multisig | ||
|
||
1. Make sure that Safe has enough VT & pufETH for the registrations | ||
2. Run the calldata generation script | ||
```bash | ||
forge script script/GenerateBLSKeysAndRegisterValidatorsCalldata.s.sol:GenerateBLSKeysAndRegisterValidatorsCalldata --rpc-url=$ETH_RPC_URL -vvv --ffi | ||
``` | ||
3. Copy & paste the addresses & calldata in the Safe Transaction Builder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[profile.default] | ||
src = "src" | ||
out = "out" | ||
libs = ["lib"] | ||
|
||
allow_paths = ["../node_modules", "./node_modules"] | ||
block_number = 0 # Our RAVE evidence is generated for blockhash(0) which is bytes32(0) | ||
fs_permissions = [{ access = "read-write", path = "./"}] | ||
optimizer = true | ||
optimizer_runs = 200 | ||
evm_version = "cancun" # is live on mainnet | ||
seed = "0x1337" | ||
solc = "0.8.26" | ||
|
||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options | ||
|
||
[fmt] | ||
line_length = 120 | ||
int_types = "long" | ||
tab_width = 4 | ||
quote_style = "double" | ||
bracket_spacing = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "utility-scripts", | ||
"description": "", | ||
"version": "1.0.0", | ||
"author": { | ||
"name": "Puffer Finance", | ||
"url": "https://puffer.fi" | ||
}, | ||
"dependencies": { | ||
"@openzeppelin/contracts": "5.0.1", | ||
"mainnet-contracts": "*", | ||
"@openzeppelin/contracts-upgradeable": "5.0.1", | ||
"eigenlayer-contracts": "https://github.com/Layr-Labs/eigenlayer-contracts.git#1bf4c12", | ||
"eigenlayer-middleware": "https://github.com/bxmmm1/eigenlayer-middleware.git#dbf6c1a" | ||
}, | ||
"devDependencies": { | ||
"forge-std": "github:foundry-rs/forge-std#v1.8.2" | ||
}, | ||
"homepage": "https://puffer.fi", | ||
"keywords": [ | ||
"blockchain", | ||
"foundry", | ||
"smart-contracts", | ||
"solidity", | ||
"web3", | ||
"ethereum", | ||
"puffer", | ||
"puffer-finance", | ||
"solidity", | ||
"LRT", | ||
"eigenlayer", | ||
"restaking", | ||
"liquid-staking" | ||
], | ||
"scripts": { | ||
"fmt": "forge fmt", | ||
"build": "forge build" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
const fs = require('fs'); | ||
|
||
// Read input JSON data from file (assuming it's saved as input.json) | ||
const inputJson = fs.readFileSync('safe-registration-file.json', 'utf8'); | ||
|
||
// Parse the JSON data | ||
const data = JSON.parse(inputJson); | ||
|
||
// Remove the surrounding quotes from the string | ||
const trimmedString = data.transactions.substring(1, data.transactions.length - 1); | ||
|
||
// // Replace escaped backslashes with a single backslash | ||
let cleanedString = trimmedString.replace(/\\\\/g, '\\'); | ||
cleanedString = cleanedString.replace(/\"{/g, '{'); | ||
cleanedString = "[" + cleanedString.replace(/\}"/g, '}') + "]"; | ||
|
||
// Parse the cleaned string into a JavaScript array | ||
const jsonArray = JSON.parse(cleanedString); | ||
|
||
// Update the data object with the parsed transactions array | ||
data.transactions = jsonArray; | ||
|
||
// Convert data back to JSON format | ||
const outputJson = JSON.stringify(data, null, 2); | ||
|
||
// Print the output JSON | ||
fs.writeFileSync('safe-registration-file.json', outputJson, 'utf8') |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
ds-test/=node_modules/forge-std/lib/ds-test/src/ | ||
forge-std/=node_modules/forge-std/src/ | ||
@openzeppelin-contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ | ||
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ | ||
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ | ||
eigenlayer/=node_modules/eigenlayer-contracts/src/contracts | ||
eigenlayer-middleware/=node_modules/eigenlayer-middleware/src/ | ||
eigenlayer-test/=node_modules/eigenlayer-contracts/src/test | ||
eigenlayer-contracts/=node_modules/eigenlayer-contracts/ | ||
mainnet-contracts/=node_modules/mainnet-contracts/ | ||
rave/=node_modules/rave/src/ | ||
rave-test/=node_modules/rave/test/ |
231 changes: 231 additions & 0 deletions
231
utility-scripts/script/GenerateBLSKeysAndRegisterValidatorsCalldata.s.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity >=0.8.0 <0.9.0; | ||
|
||
import "forge-std/Script.sol"; | ||
import { stdJson } from "forge-std/StdJson.sol"; | ||
import { Permit } from "../../mainnet-contracts/src/structs/Permit.sol"; | ||
import { ValidatorKeyData } from "mainnet-contracts/src/struct/ValidatorKeyData.sol"; | ||
import { IPufferProtocol } from "mainnet-contracts/src/interface/IPufferProtocol.sol"; | ||
import { PufferProtocol } from "mainnet-contracts/src/PufferProtocol.sol"; | ||
import { PufferVaultV2 } from "mainnet-contracts/src/PufferVaultV2.sol"; | ||
import { ValidatorTicket } from "mainnet-contracts/src/ValidatorTicket.sol"; | ||
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; | ||
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
|
||
/** | ||
* See the docs for more detailed information: https://docs.puffer.fi/nodes/registration#batch-registering-validators | ||
* | ||
* To run the simulation: | ||
* | ||
* forge script script/GenerateBLSKeysAndRegisterValidatorsCalldata.s.sol:GenerateBLSKeysAndRegisterValidatorsCalldata --rpc-url=$RPC_URL -vvv --ffi | ||
* | ||
*/ | ||
contract GenerateBLSKeysAndRegisterValidatorsCalldata is Script { | ||
address validatorTicketAddress; | ||
PufferVaultV2 internal pufETH; | ||
ValidatorTicket internal validatorTicket; | ||
address internal protocolAddress; | ||
PufferProtocol internal pufferProtocol; | ||
string internal registrationJson; | ||
|
||
string forkVersion; | ||
|
||
bytes32 moduleToRegisterTo; | ||
|
||
mapping(bytes32 keyHash => bool registered) internal pubKeys; | ||
bytes[] internal registeredPubKeys; | ||
|
||
struct Tx { | ||
address to; | ||
bytes data; | ||
} | ||
|
||
function setUp() public { | ||
if (block.chainid == 17000) { | ||
// Holesky | ||
validatorTicketAddress = 0xB028194785178a94Fe608994A4d5AD84c285A640; | ||
protocolAddress = 0xE00c79408B9De5BaD2FDEbB1688997a68eC988CD; | ||
pufferProtocol = PufferProtocol(protocolAddress); | ||
forkVersion = "0x01017000"; | ||
} else if (block.chainid == 1) { | ||
// Mainnet | ||
validatorTicketAddress = 0x7D26AD6F6BA9D6bA1de0218Ae5e20CD3a273a55A; | ||
protocolAddress = 0xf7b6B32492c2e13799D921E84202450131bd238B; | ||
pufferProtocol = PufferProtocol(protocolAddress); | ||
forkVersion = "0x00000000"; | ||
} | ||
|
||
pufETH = pufferProtocol.PUFFER_VAULT(); | ||
validatorTicket = pufferProtocol.VALIDATOR_TICKET(); | ||
} | ||
|
||
function run() public { | ||
uint256 guardiansLength = pufferProtocol.GUARDIAN_MODULE().getGuardians().length; | ||
|
||
uint256 specificModule = vm.promptUint("Do you want to register to a specific module? (0: No, 1: Yes)"); | ||
if (specificModule == 1) { | ||
uint256 pufferModuleIdx = vm.promptUint( | ||
"Please enter the module number to which you wish to register. Enter '0' to register to PUFFER_MODULE_0, Enter '1' to register to PUFFER_MODULE_1, ..." | ||
); | ||
moduleToRegisterTo = | ||
bytes32(abi.encodePacked(string.concat("PUFFER_MODULE_", vm.toString(pufferModuleIdx)))); | ||
} | ||
|
||
uint256 numberOfValidators = vm.promptUint("How many validators would you like to register?"); | ||
require(numberOfValidators > 0, "Number of validators must be greater than 0"); | ||
|
||
uint256 vtAmount = vm.promptUint("Enter the VT amount per validator (28 is minimum)"); | ||
require(vtAmount >= 28, "VT amount must be at least 28"); | ||
|
||
address safe = vm.promptAddress("Enter the safe address"); | ||
require(safe != address(0), "Invalid safe address"); | ||
|
||
// Validate pufETH & VT balances | ||
_validateBalances(safe, numberOfValidators, vtAmount); | ||
|
||
bytes32[] memory moduleWeights = pufferProtocol.getModuleWeights(); | ||
uint256 moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); | ||
|
||
bytes memory approveVTCalldata = | ||
abi.encodeCall(ERC20.approve, (protocolAddress, vtAmount * numberOfValidators * 1 ether)); | ||
bytes memory approvePufETHCalldata = | ||
abi.encodeCall(ERC20.approve, (protocolAddress, 2 ether * numberOfValidators)); | ||
|
||
// 2 token approvals + validator registrations | ||
Tx[] memory transactions = new Tx[](numberOfValidators + 2); | ||
transactions[0] = Tx({ to: validatorTicketAddress, data: approveVTCalldata }); | ||
transactions[1] = Tx({ to: address(pufETH), data: approvePufETHCalldata }); | ||
|
||
for (uint256 i = 0; i < numberOfValidators; ++i) { | ||
// Select the module to register to | ||
bytes32 moduleName = moduleWeights[(moduleSelectionIndex + i) % moduleWeights.length]; | ||
|
||
// If the user specified a module to register to, use that instead | ||
if (moduleToRegisterTo != bytes32(0)) { | ||
require(pufferProtocol.getModuleAddress(moduleToRegisterTo) != address(0), "Invalid Puffer Module"); | ||
moduleName = moduleToRegisterTo; | ||
} | ||
|
||
_generateValidatorKey(i, moduleName); | ||
|
||
// Read the registration JSON file | ||
registrationJson = vm.readFile(string.concat("./registration-data/", vm.toString(i), ".json")); | ||
|
||
bytes[] memory blsEncryptedPrivKeyShares = new bytes[](guardiansLength); | ||
blsEncryptedPrivKeyShares[0] = stdJson.readBytes(registrationJson, ".bls_enc_priv_key_shares[0]"); | ||
|
||
ValidatorKeyData memory validatorData = ValidatorKeyData({ | ||
blsPubKey: stdJson.readBytes(registrationJson, ".bls_pub_key"), | ||
signature: stdJson.readBytes(registrationJson, ".signature"), | ||
depositDataRoot: stdJson.readBytes32(registrationJson, ".deposit_data_root"), | ||
blsEncryptedPrivKeyShares: blsEncryptedPrivKeyShares, | ||
blsPubKeySet: stdJson.readBytes(registrationJson, ".bls_pub_key_set"), | ||
raveEvidence: "" | ||
}); | ||
|
||
Permit memory pufETHPermit; | ||
pufETHPermit.amount = 2 ether; | ||
|
||
Permit memory vtPermit; | ||
vtPermit.amount = vtAmount * 1 ether; | ||
|
||
bytes memory registerValidatorKeyCalldata = | ||
abi.encodeCall(PufferProtocol.registerValidatorKey, (validatorData, moduleName, pufETHPermit, vtPermit)); | ||
|
||
transactions[i + 2] = Tx({ to: protocolAddress, data: registerValidatorKeyCalldata }); | ||
|
||
registeredPubKeys.push(validatorData.blsPubKey); | ||
} | ||
|
||
// Create Safe TX JSON | ||
_createSafeJson(safe, transactions); | ||
|
||
console.log("Validator PubKeys:"); | ||
for (uint256 i = 0; i < registeredPubKeys.length; ++i) { | ||
console.logBytes(registeredPubKeys[i]); | ||
} | ||
} | ||
|
||
function _createSafeJson(address safe, Tx[] memory transactions) internal { | ||
// First we need to craft the JSON file for the transactions batch | ||
string memory root = "root"; | ||
|
||
vm.serializeString(root, "version", "\"1.0\""); | ||
vm.serializeUint(root, "createdAt", block.timestamp * 1000); | ||
// Needs to be a string | ||
vm.serializeString(root, "chainId", string.concat("\"", Strings.toString(block.chainid), "\"")); | ||
|
||
string memory meta = "meta"; | ||
vm.serializeString(meta, "name", "Transactions Batch"); | ||
vm.serializeString(meta, "txBuilderVersion", "\"1.16.5\""); | ||
vm.serializeAddress(meta, "createdFromSafeAddress", safe); | ||
vm.serializeString(meta, "createdFromOwnerAddress", ""); | ||
vm.serializeString(meta, "checksum", ""); | ||
string memory metaOutput = vm.serializeString(meta, "description", ""); | ||
|
||
string[] memory txs = new string[](transactions.length); | ||
|
||
for (uint256 i = 0; i < transactions.length; ++i) { | ||
string memory singleTx = "tx"; | ||
|
||
vm.serializeAddress(singleTx, "to", transactions[i].to); | ||
vm.serializeString(singleTx, "value", "\"0\""); | ||
txs[i] = vm.serializeBytes(singleTx, "data", transactions[i].data); | ||
} | ||
|
||
vm.serializeString(root, "transactions", txs); | ||
string memory finalJson = vm.serializeString(root, "meta", metaOutput); | ||
vm.writeJson(finalJson, "./safe-registration-file.json"); | ||
|
||
// Because foundry doesn't support creating JSON array of objects, we need to run NodeJS script to convert this to a valid JSON | ||
|
||
string[] memory inputs = new string[](2); | ||
inputs[0] = "node"; | ||
inputs[1] = "parse-foundry-json"; | ||
vm.ffi(inputs); | ||
} | ||
|
||
// Validates the pufETH and VT balances for the `safe` (node operator) | ||
function _validateBalances(address safe, uint256 numberOfValidators, uint256 vtBalancePerValidator) internal view { | ||
uint256 pufETHRequired = pufETH.convertToSharesUp(numberOfValidators * 2 ether); | ||
|
||
if (pufETH.balanceOf(safe) < pufETHRequired) { | ||
revert("Insufficient pufETH balance"); | ||
} | ||
|
||
uint256 vtRequired = numberOfValidators * vtBalancePerValidator * 1 ether; | ||
|
||
if (validatorTicket.balanceOf(safe) < vtRequired) { | ||
revert("Insufficient VT balance"); | ||
} | ||
} | ||
|
||
// Generates a new validator key using coral https://github.com/PufferFinance/coral/tree/main | ||
function _generateValidatorKey(uint256 idx, bytes32 moduleName) internal { | ||
uint256 numberOfGuardians = pufferProtocol.GUARDIAN_MODULE().getGuardians().length; | ||
bytes[] memory guardianPubKeys = pufferProtocol.GUARDIAN_MODULE().getGuardiansEnclavePubkeys(); | ||
address moduleAddress = IPufferProtocol(protocolAddress).getModuleAddress(moduleName); | ||
bytes memory withdrawalCredentials = IPufferProtocol(protocolAddress).getWithdrawalCredentials(moduleAddress); | ||
|
||
string[] memory inputs = new string[](17); | ||
inputs[0] = "coral-cli"; | ||
inputs[1] = "validator"; | ||
inputs[2] = "keygen"; | ||
inputs[3] = "--guardian-threshold"; | ||
inputs[4] = vm.toString(numberOfGuardians); | ||
inputs[5] = "--module-name"; | ||
inputs[6] = vm.toString(moduleName); | ||
inputs[7] = "--withdrawal-credentials"; | ||
inputs[8] = vm.toString(withdrawalCredentials); | ||
inputs[9] = "--guardian-pubkeys"; | ||
inputs[10] = vm.toString(guardianPubKeys[0]); //@todo: Add support for multiple guardians | ||
inputs[11] = "--fork-version"; | ||
inputs[12] = forkVersion; | ||
inputs[13] = "--password-file"; | ||
inputs[14] = "validator-keystore-password.txt"; | ||
inputs[15] = "--output-file"; | ||
inputs[16] = string.concat("./registration-data/", vm.toString(idx), ".json"); | ||
|
||
vm.ffi(inputs); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
securePassword |