From a322ba684812fefb6fbf41cf6fd12e88904df662 Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu <99738872+andreivladbrg@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:29:31 +0300 Subject: [PATCH] feat: add protocol scripts (#1049) feat: create a markdown deployment file with docs structure build: remove problematic etherscan fields --- .gitignore | 1 + foundry.toml | 7 +- script/Base.s.sol | 10 +- script/DeployDeterministicProtocol.s.sol | 42 --- script/DeployProtocol.s.sol | 38 --- .../DeployDeterministicProtocol.s.sol | 86 +++++++ script/protocol/DeployProtocol.s.sol | 79 ++++++ script/protocol/DeploymentLogger.s.sol | 239 ++++++++++++++++++ 8 files changed, 413 insertions(+), 89 deletions(-) delete mode 100644 script/DeployDeterministicProtocol.s.sol delete mode 100644 script/DeployProtocol.s.sol create mode 100644 script/protocol/DeployDeterministicProtocol.s.sol create mode 100644 script/protocol/DeployProtocol.s.sol create mode 100644 script/protocol/DeploymentLogger.s.sol diff --git a/.gitignore b/.gitignore index 7a74302fe..f8d9ec505 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ out-svg lcov.info package-lock.json pnpm-lock.yaml +script/protocol/*.md yarn.lock diff --git a/foundry.toml b/foundry.toml index 9c2162f12..0b65c10c5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ { access = "read", path = "./out-optimized" }, { access = "read", path = "package.json" }, { access = "read-write", path = "./benchmark/results" }, + { access = "read-write", path = "./script/protocol"} ] gas_limit = 9223372036854775807 optimizer = true @@ -88,22 +89,16 @@ avalanche = { key = "${SNOWTRACE_API_KEY}" } base = { key = "${BASESCAN_API_KEY}" } base_sepolia = { key = "${BASESCAN_API_KEY}" } - berachain_artio = { key = "verifyContract", url = "https://api.routescan.io/v2/network/testnet/evm/80085/etherscan" } bnb = { key = "${BSCSCAN_API_KEY}" } gnosis = { key = "${GNOSISSCAN_API_KEY}" } linea = { key = "${LINEASCAN_API_KEY}" } linea_sepolia = { key = "${LINEASCAN_API_KEY}" } mainnet = { key = "${ETHERSCAN_API_KEY}" } - mode = { key = "verifyContract", url = "https://explorer.mode.network/api?" } - mode_sepolia = { key = "verifyContract", url = "https://sepolia.explorer.mode.network/api?" } optimism = { key = "${OPTIMISTIC_API_KEY}" } optimism_sepolia = { key = "${OPTIMISTIC_API_KEY}" } polygon = { key = "${POLYGONSCAN_API_KEY}" } scroll = { key = "${SCROLLSCAN_API_KEY}" } - sei = { key = "verifyContract", url = "https://seitrace/pacific-1/api?" } - sei_testnet = { key = "verifyContract", url = "https://blockscout.com/poa/core/api?" } sepolia = { key = "${SEPOLIASCAN_KEY}" } - taiko_hekla = { key = "verifyContract", url = "https://blockscoutapi.hekla.taiko.xyz/api?" } taiko_mainnet = { key = "${TAIKO_MAINNET_API_KEY}" } [fmt] diff --git a/script/Base.s.sol b/script/Base.s.sol index 7fbf27703..b60612601 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -72,16 +72,20 @@ contract BaseScript is Script { /// /// Notes: /// - The salt format is "ChainID , Version ". - /// - The version is obtained from `package.json`. function constructCreate2Salt() public view returns (bytes32) { string memory chainId = block.chainid.toString(); - string memory json = vm.readFile("package.json"); - string memory version = json.readString(".version"); + string memory version = getVersion(); string memory create2Salt = string.concat("ChainID ", chainId, ", Version ", version); console2.log("The CREATE2 salt is \"%s\"", create2Salt); return bytes32(abi.encodePacked(create2Salt)); } + /// @dev The version is obtained from `package.json`. + function getVersion() internal view returns (string memory) { + string memory json = vm.readFile("package.json"); + return json.readString(".version"); + } + /// @dev Populates the segment & tranche count map. Values can be updated using the `update-counts.sh` script. function populateSegmentAndTrancheCountMap() internal { // forgefmt: disable-start diff --git a/script/DeployDeterministicProtocol.s.sol b/script/DeployDeterministicProtocol.s.sol deleted file mode 100644 index 6f1b5eb6a..000000000 --- a/script/DeployDeterministicProtocol.s.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { LockupNFTDescriptor } from "./../src/core/LockupNFTDescriptor.sol"; -import { SablierLockupDynamic } from "./../src/core/SablierLockupDynamic.sol"; -import { SablierLockupLinear } from "./../src/core/SablierLockupLinear.sol"; -import { SablierLockupTranched } from "./../src/core/SablierLockupTranched.sol"; -import { SablierBatchLockup } from "./../src/periphery/SablierBatchLockup.sol"; -import { SablierMerkleFactory } from "./../src/periphery/SablierMerkleFactory.sol"; -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys the Lockup Protocol at deterministic addresses across chains. -contract DeployDeterministicProtocol is BaseScript { - /// @dev Deploy via Forge. - function run(address initialAdmin) - public - virtual - broadcast - returns ( - LockupNFTDescriptor nftDescriptor, - SablierLockupDynamic lockupDynamic, - SablierLockupLinear lockupLinear, - SablierLockupTranched lockupTranched, - SablierBatchLockup batchLockup, - SablierMerkleFactory merkleFactory - ) - { - bytes32 salt = constructCreate2Salt(); - - // Deploy Core. - nftDescriptor = new LockupNFTDescriptor{ salt: salt }(); - lockupDynamic = - new SablierLockupDynamic{ salt: salt }(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); - lockupLinear = new SablierLockupLinear{ salt: salt }(initialAdmin, nftDescriptor); - lockupTranched = - new SablierLockupTranched{ salt: salt }(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); - - // Deploy Periphery. - batchLockup = new SablierBatchLockup{ salt: salt }(); - merkleFactory = new SablierMerkleFactory{ salt: salt }(); - } -} diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol deleted file mode 100644 index 1631ef306..000000000 --- a/script/DeployProtocol.s.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22 <0.9.0; - -import { LockupNFTDescriptor } from "./../src/core/LockupNFTDescriptor.sol"; -import { SablierLockupDynamic } from "./../src/core/SablierLockupDynamic.sol"; -import { SablierLockupLinear } from "./../src/core/SablierLockupLinear.sol"; -import { SablierLockupTranched } from "./../src/core/SablierLockupTranched.sol"; -import { SablierBatchLockup } from "./../src/periphery/SablierBatchLockup.sol"; -import { SablierMerkleFactory } from "./../src/periphery/SablierMerkleFactory.sol"; -import { BaseScript } from "./Base.s.sol"; - -/// @notice Deploys the Lockup Protocol. -contract DeployProtocol is BaseScript { - /// @dev Deploy via Forge. - function run(address initialAdmin) - public - virtual - broadcast - returns ( - LockupNFTDescriptor nftDescriptor, - SablierLockupDynamic lockupDynamic, - SablierLockupLinear lockupLinear, - SablierLockupTranched lockupTranched, - SablierBatchLockup batchLockup, - SablierMerkleFactory merkleFactory - ) - { - // Deploy Core. - nftDescriptor = new LockupNFTDescriptor(); - lockupDynamic = new SablierLockupDynamic(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); - lockupLinear = new SablierLockupLinear(initialAdmin, nftDescriptor); - lockupTranched = new SablierLockupTranched(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); - - // Deploy Periphery. - batchLockup = new SablierBatchLockup(); - merkleFactory = new SablierMerkleFactory(); - } -} diff --git a/script/protocol/DeployDeterministicProtocol.s.sol b/script/protocol/DeployDeterministicProtocol.s.sol new file mode 100644 index 000000000..e110b8ecc --- /dev/null +++ b/script/protocol/DeployDeterministicProtocol.s.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { LockupNFTDescriptor } from "../../src/core/LockupNFTDescriptor.sol"; +import { SablierLockupDynamic } from "../../src/core/SablierLockupDynamic.sol"; +import { SablierLockupLinear } from "../../src/core/SablierLockupLinear.sol"; +import { SablierLockupTranched } from "../../src/core/SablierLockupTranched.sol"; +import { SablierMerkleFactory } from "../../src/periphery/SablierMerkleFactory.sol"; +import { SablierBatchLockup } from "../../src/periphery/SablierBatchLockup.sol"; + +import { DeploymentLogger } from "./DeploymentLogger.s.sol"; + +/// @notice Deploys the Lockup Protocol at deterministic addresses across chains. +contract DeployDeterministicProtocol is DeploymentLogger("deterministic") { + /// @dev Deploys the protocol with the admin set in `adminMap`. + function run() + public + virtual + broadcast + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + address initialAdmin = adminMap[block.chainid]; + + (nftDescriptor, lockupDynamic, lockupLinear, lockupTranched, batchLockup, merkleLockupFactory) = + _run(initialAdmin); + } + + /// @dev Deploys the protocol with the given `initialAdmin`. + function run(address initialAdmin) + internal + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + (nftDescriptor, lockupDynamic, lockupLinear, lockupTranched, batchLockup, merkleLockupFactory) = + _run(initialAdmin); + } + + /// @dev Common logic for the run functions. + function _run(address initialAdmin) + internal + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + bytes32 salt = constructCreate2Salt(); + + // Deploy Core. + nftDescriptor = new LockupNFTDescriptor{ salt: salt }(); + lockupDynamic = + new SablierLockupDynamic{ salt: salt }(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierLockupLinear{ salt: salt }(initialAdmin, nftDescriptor); + lockupTranched = + new SablierLockupTranched{ salt: salt }(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); + + // Deploy Periphery. + batchLockup = new SablierBatchLockup{ salt: salt }(); + merkleLockupFactory = new SablierMerkleFactory{ salt: salt }(); + + appendToFileDeployedAddresses( + address(lockupDynamic), + address(lockupLinear), + address(lockupTranched), + address(nftDescriptor), + address(batchLockup), + address(merkleLockupFactory) + ); + } +} diff --git a/script/protocol/DeployProtocol.s.sol b/script/protocol/DeployProtocol.s.sol new file mode 100644 index 000000000..7a8a6384d --- /dev/null +++ b/script/protocol/DeployProtocol.s.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { LockupNFTDescriptor } from "../../src/core/LockupNFTDescriptor.sol"; +import { SablierLockupDynamic } from "../../src/core/SablierLockupDynamic.sol"; +import { SablierLockupLinear } from "../../src/core/SablierLockupLinear.sol"; +import { SablierLockupTranched } from "../../src/core/SablierLockupTranched.sol"; +import { SablierMerkleFactory } from "../../src/periphery/SablierMerkleFactory.sol"; +import { SablierBatchLockup } from "../../src/periphery/SablierBatchLockup.sol"; + +import { DeploymentLogger } from "./DeploymentLogger.s.sol"; + +/// @notice Deploys the Lockup Protocol. +contract DeployProtocol is DeploymentLogger("non_deterministic") { + /// @dev Deploys the protocol with the admin set in `adminMap`. + function run() + public + virtual + broadcast + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + address initialAdmin = adminMap[block.chainid]; + + (nftDescriptor, lockupDynamic, lockupLinear, lockupTranched, batchLockup, merkleLockupFactory) = + _run(initialAdmin); + } + + /// @dev Deploys the protocol with the given `initialAdmin`. + function run(address initialAdmin) + internal + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + (nftDescriptor, lockupDynamic, lockupLinear, lockupTranched, batchLockup, merkleLockupFactory) = + _run(initialAdmin); + } + + /// @dev Common logic for the run functions. + function _run(address initialAdmin) + internal + returns ( + LockupNFTDescriptor nftDescriptor, + SablierLockupDynamic lockupDynamic, + SablierLockupLinear lockupLinear, + SablierLockupTranched lockupTranched, + SablierBatchLockup batchLockup, + SablierMerkleFactory merkleLockupFactory + ) + { + nftDescriptor = new LockupNFTDescriptor(); + lockupDynamic = new SablierLockupDynamic(initialAdmin, nftDescriptor, segmentCountMap[block.chainid]); + lockupLinear = new SablierLockupLinear(initialAdmin, nftDescriptor); + lockupTranched = new SablierLockupTranched(initialAdmin, nftDescriptor, trancheCountMap[block.chainid]); + batchLockup = new SablierBatchLockup(); + merkleLockupFactory = new SablierMerkleFactory(); + + appendToFileDeployedAddresses( + address(lockupDynamic), + address(lockupLinear), + address(lockupTranched), + address(nftDescriptor), + address(batchLockup), + address(merkleLockupFactory) + ); + } +} diff --git a/script/protocol/DeploymentLogger.s.sol b/script/protocol/DeploymentLogger.s.sol new file mode 100644 index 000000000..3b2496b98 --- /dev/null +++ b/script/protocol/DeploymentLogger.s.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22 <0.9.0; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { stdJson } from "forge-std/src/StdJson.sol"; + +import { BaseScript } from "../Base.s.sol"; + +/// @dev This contract appends to the `script` directory a markdown file with the deployed addresses in the format used +/// in the docs: https://docs.sablier.com/contracts/v2/deployments. +abstract contract DeploymentLogger is BaseScript { + using Strings for address; + using Strings for string; + using Strings for uint256; + + /// @dev The address of the default Sablier admin. + address internal constant DEFAULT_SABLIER_ADMIN = 0xb1bEF51ebCA01EB12001a639bDBbFF6eEcA12B9F; + + /// @dev Admin address mapped by the chain Id. + mapping(uint256 chainId => address admin) internal adminMap; + + /// @dev Chain names mapped by the chain Id. + mapping(uint256 chainId => string name) internal chainNameMap; + + /// @dev The path to the file where the deployment addresses are stored. + string internal deploymentFile; + + /// @dev Explorer URL mapped by the chain Id. + mapping(uint256 chainId => string explorerUrl) internal explorerMap; + + constructor(string memory deterministicOrNot) { + // Populate the admin map. + populateAdminMap(); + + // Populate the chain name map. + populateChainNameMap(); + + // Populate the explorer URLs. + populateExplorerMap(); + + // If there is no admin set for a specific chain, use the default Sablier admin. + if (adminMap[block.chainid] == address(0)) { + adminMap[block.chainid] = DEFAULT_SABLIER_ADMIN; + } + + // If there is no explorer URL set for a specific chain, use a placeholder. + if (explorerMap[block.chainid].equal("")) { + explorerMap[block.chainid] = "N/A"; + } + + // If there is no chain name set for a specific chain, use the chain ID. + if (chainNameMap[block.chainid].equal("")) { + chainNameMap[block.chainid] = string.concat("Chain ID: ", block.chainid.toString()); + } + + // Set the deployment file path. + deploymentFile = string.concat("script/protocol/", deterministicOrNot, ".md"); + + // Append the chain name to the deployment file. + _appendToFile(string.concat("## ", chainNameMap[block.chainid], "\n")); + } + + /// @dev Function to append the deployed addresses to the deployment file. + function appendToFileDeployedAddresses( + address lockupDynamic, + address lockupLinear, + address lockupTranched, + address nftDescriptor, + address batchLockup, + address merkleFactory + ) + internal + { + string memory coreTitle = " ### Core\n"; + _appendToFile(coreTitle); + + string memory firstTwoLines = "| Contract | Address | Deployment |\n | :------- | :------ | :----------|"; + _appendToFile(firstTwoLines); + + string memory lockupDynamicLine = _getContractLine({ + contractName: "SablierLockupDynamic", + contractAddress: lockupDynamic.toHexString(), + coreOrPeriphery: "core" + }); + _appendToFile(lockupDynamicLine); + + string memory lockupLinearLine = _getContractLine({ + contractName: "SablierLockupLinear", + contractAddress: lockupLinear.toHexString(), + coreOrPeriphery: "core" + }); + _appendToFile(lockupLinearLine); + + string memory lockupTranchedLine = _getContractLine({ + contractName: "SablierLockupTranched", + contractAddress: lockupTranched.toHexString(), + coreOrPeriphery: "core" + }); + _appendToFile(lockupTranchedLine); + + string memory nftDescriptorLine = _getContractLine({ + contractName: "SablierNFTDescriptor", + contractAddress: nftDescriptor.toHexString(), + coreOrPeriphery: "core" + }); + _appendToFile(nftDescriptorLine); + + string memory peripheryTitle = "\n ### Periphery\n\n"; + _appendToFile(peripheryTitle); + _appendToFile(firstTwoLines); + + string memory batchLockupLine = _getContractLine({ + contractName: "SablierBatchLockup", + contractAddress: batchLockup.toHexString(), + coreOrPeriphery: "periphery" + }); + _appendToFile(batchLockupLine); + + string memory merkleFactoryLine = _getContractLine({ + contractName: "MerkleFactory", + contractAddress: merkleFactory.toHexString(), + coreOrPeriphery: "periphery" + }); + _appendToFile(merkleFactoryLine); + + _appendToFile("\n"); + } + + /// @dev Populates the admin map. The reason the chain IDs configured for the admin map do not match the other + /// maps is that we only have multisigs for the chains listed below, otherwise, the default admin is used.​ + function populateAdminMap() internal { + adminMap[42_161] = 0xF34E41a6f6Ce5A45559B1D3Ee92E141a3De96376; // Arbitrum + adminMap[43_114] = 0x4735517616373c5137dE8bcCDc887637B8ac85Ce; // Avalanche + adminMap[8453] = 0x83A6fA8c04420B3F9C7A4CF1c040b63Fbbc89B66; // Base + adminMap[56] = 0x6666cA940D2f4B65883b454b7Bc7EEB039f64fa3; // BNB + adminMap[100] = 0x72ACB57fa6a8fa768bE44Db453B1CDBa8B12A399; // Gnosis + adminMap[1] = 0x79Fb3e81aAc012c08501f41296CCC145a1E15844; // Mainnet + adminMap[59_144] = 0x72dCfa0483d5Ef91562817C6f20E8Ce07A81319D; // Linea + adminMap[10] = 0x43c76FE8Aec91F63EbEfb4f5d2a4ba88ef880350; // Optimism + adminMap[137] = 0x40A518C5B9c1d3D6d62Ba789501CE4D526C9d9C6; // Polygon + adminMap[534_352] = 0x0F7Ad835235Ede685180A5c611111610813457a9; // Scroll + } + + /// @dev Populates the chain name map. + function populateChainNameMap() internal { + chainNameMap[42_161] = "Arbitrum"; + chainNameMap[43_114] = "Avalanche"; + chainNameMap[8453] = "Base"; + chainNameMap[84_532] = "Base Sepolia"; + chainNameMap[80_084] = "Berachain Bartio"; + chainNameMap[81_457] = "Blast"; + chainNameMap[168_587_773] = "Blast Sepolia"; + chainNameMap[56] = "BNB Smart Chain"; + chainNameMap[100] = "Gnosis"; + chainNameMap[1890] = "Lightlink"; + chainNameMap[59_144] = "Linea"; + chainNameMap[59_141] = "Linea Sepolia"; + chainNameMap[1] = "Mainnet"; + chainNameMap[333_000_333] = "Meld"; + chainNameMap[34_443] = "Mode"; + chainNameMap[919] = "Mode Sepolia"; + chainNameMap[2810] = "Morph Holesky"; + chainNameMap[10] = "Optimism"; + chainNameMap[11_155_420] = "Optimism Sepolia"; + chainNameMap[137] = "Polygon"; + chainNameMap[534_352] = "Scroll"; + chainNameMap[11_155_111] = "Sepolia"; + chainNameMap[53_302] = "Superseed Sepolia"; + chainNameMap[167_009] = "Taiko Hekla"; + chainNameMap[167_000] = "Taiko Mainnet"; + } + + /// @dev Populates the explorer map. + function populateExplorerMap() internal { + explorerMap[42_161] = "https://arbiscan.io/address/"; + explorerMap[43_114] = "https://snowtrace.io/address/"; + explorerMap[8453] = "https://basescan.org/address/"; + explorerMap[84_532] = "https://sepolia.basescan.org/address/"; + explorerMap[80_084] = "https://bartio.beratrail.io/address/"; + explorerMap[81_457] = "https://blastscan.io/address/"; + explorerMap[168_587_773] = "https://sepolia.blastscan.io/address/"; + explorerMap[56] = "https://bscscan.com/address/"; + explorerMap[1] = "https://etherscan.io/address/"; + explorerMap[100] = "https://gnosisscan.io/address/"; + explorerMap[59_144] = "https://lineascan.build/address/"; + explorerMap[59_141] = "https://sepolia.lineascan.build/address/"; + explorerMap[1890] = "https://phoenix.lightlink.io/address/"; + explorerMap[34_443] = "https://explorer.mode.network/address/"; + explorerMap[919] = "https://sepolia.explorer.mode.network/address/"; + explorerMap[2810] = "https://explorer-holesky.morphl2.io/address/"; + explorerMap[333_000_333] = "https://meldscan.io/address/"; + explorerMap[10] = "https://optimistic.etherscan.io/address/"; + explorerMap[11_155_420] = "https://sepolia-optimistic.etherscan.io/address/"; + explorerMap[137] = "https://polygonscan.com/address/"; + explorerMap[534_352] = "https://scrollscan.com/address/"; + explorerMap[11_155_111] = "https://sepolia.etherscan.io/address/"; + explorerMap[53_302] = "https://sepolia-explorer.superseed.xyz/address/"; + explorerMap[167_009] = "https://explorer.hekla.taiko.xyz/address/"; + explorerMap[167_000] = "https://taikoscan.io/address/"; + } + + /// @dev Append a line to the deployment file path. + function _appendToFile(string memory line) private { + vm.writeLine({ path: deploymentFile, data: line }); + } + + /// @dev Returns a string for a single contract line formatted according to the docs. + function _getContractLine( + string memory contractName, + string memory contractAddress, + string memory coreOrPeriphery + ) + private + view + returns (string memory) + { + string memory version = getVersion(); + version = string.concat("v", version); + + return string.concat( + "| ", + contractName, + " | [", + contractAddress, + "](", + explorerMap[block.chainid], + contractAddress, + ") | [", + coreOrPeriphery, + "-", + version, + "](https://github.com/sablier-labs/v2-deployments/tree/main/", + coreOrPeriphery, + "/", + version, + ") |" + ); + } +}