diff --git a/hardhat.config.ts b/hardhat.config.ts index 9d4a0909..ac6c655b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -24,6 +24,7 @@ import "./tasks/deploy_mock_twap_oracle"; import "./tasks/deploy_vesting"; import "./tasks/deploy_voting_escrow_impl"; import "./tasks/deploy_stable_swap"; +import "./tasks/deploy_stable_swap_maturity"; import "./tasks/deploy_stable_swap_wsteth"; import "./tasks/deploy_sub_governance"; import "./tasks/deploy_swap_router"; diff --git a/tasks/deploy_maturity_fund.ts b/tasks/deploy_maturity_fund.ts index a60d930b..e94ab2ff 100644 --- a/tasks/deploy_maturity_fund.ts +++ b/tasks/deploy_maturity_fund.ts @@ -45,17 +45,17 @@ task("deploy_maturity_fund", "Deploy MaturityFund contracts") const underlyingToken = await ethers.getContractAt("ERC20", underlyingAddress); const underlyingDecimals = await underlyingToken.decimals(); const underlyingSymbol: string = await underlyingToken.symbol(); - assert.match(underlyingSymbol, /^[a-zA-Z]+$/, "Invalid symbol"); + assert.match(underlyingSymbol, /^[a-zA-Z0-9.]+$/, "Invalid symbol"); console.log(`Underlying: ${underlyingToken.address}`); const shareSymbols: string[] = args.shareSymbols.split(",").filter(Boolean); for (const symbol of shareSymbols) { - assert.match(symbol, /^[a-zA-Z]+$/, "Invalid symbol"); + assert.match(symbol, /^[a-zA-Z0-9.]+$/, "Invalid symbol"); } assert.ok(shareSymbols.length == 3, "Share symbol count is not 3"); const shareNames: string[] = args.shareNames.split(",").filter(Boolean); for (const name of shareNames) { - assert.match(name, /^[a-zA-Z ]+$/, "Invalid name"); + assert.match(name, /^[a-zA-Z0-9. ]+$/, "Invalid name"); } assert.ok(shareNames.length == 3, "Share name count is not 3"); diff --git a/tasks/deploy_stable_swap_maturity.ts b/tasks/deploy_stable_swap_maturity.ts new file mode 100644 index 00000000..e4c8f467 --- /dev/null +++ b/tasks/deploy_stable_swap_maturity.ts @@ -0,0 +1,213 @@ +import { strict as assert } from "assert"; +import { task } from "hardhat/config"; +import { GOVERNANCE_CONFIG } from "../config"; +import { Addresses, saveAddressFile, loadAddressFile, newAddresses } from "./address_file"; +import type { GovernanceAddresses } from "./deploy_governance"; +import { updateHreSigner } from "./signers"; +import { BigNumber, Contract } from "ethers"; +import { FundAddresses } from "./deploy_fund"; +import { FeeDistrubtorAddresses } from "./deploy_fee_distributor"; +import { waitForContract } from "./utils"; + +export interface StableSwapAddresses extends Addresses { + kind: string; + underlyingSymbol: string; + base: string; + baseSymbol: string; + quote: string; + quoteSymbol: string; + bonus: string; + bonusSymbol: string; + feeDistributor: string; + swapBonus: string; + liquidityGauge: string; + stableSwap: string; +} + +task("deploy_stable_swap_maturity", "Deploy stable swap contracts for MaturityFund") + .addParam("kind", "Queen or Bishop stable swap") + .addParam("underlyingSymbol", "Underlying token symbol of the swap") + .addParam("quote", "Quote token address") + .addParam("bonus", "Bonus token address") + .addParam("ampl", "The ampl of the swap") + .addParam("feeRate", "The fee rate of the swap") + .addParam("adminFeeRate", "The admin fee rate of the swap") + .setAction(async function (args, hre) { + await updateHreSigner(hre); + const { ethers } = hre; + const { parseEther } = ethers.utils; + await hre.run("compile"); + + assert.match(args.kind, /^Queen|Bishop$/, "Invalid kind"); + const kind: "Queen" | "Bishop" = args.kind; + + const underlyingSymbol: string = args.underlyingSymbol; + assert.match(underlyingSymbol, /^[a-zA-Z0-9.]+$/, "Invalid symbol"); + + const quote = await ethers.getContractAt("ERC20", args.quote); + const quoteSymbol = await quote.symbol(); + const quoteDecimals = await quote.decimals(); + + const bonus = await ethers.getContractAt("ERC20", args.bonus); + const bonusSymbol = await bonus.symbol(); + + const fundAddresses = loadAddressFile( + hre, + `fund_${underlyingSymbol.toLowerCase()}` + ); + const feeDistributorAddresses = loadAddressFile( + hre, + `fee_distributor_${quoteSymbol.toLowerCase()}` + ); + const governanceAddresses = loadAddressFile(hre, "governance"); + + let base: Contract; + switch (kind) { + case "Queen": { + base = await ethers.getContractAt("ERC20", fundAddresses.shareQ); + break; + } + case "Bishop": { + base = await ethers.getContractAt("ERC20", fundAddresses.shareB); + break; + } + } + const baseSymbol = await base.symbol(); + + const ampl = BigNumber.from(args.ampl); + const feeRate = parseEther(args.feeRate); + const adminFeeRate = parseEther(args.adminFeeRate); + + const [deployer] = await ethers.getSigners(); + + // +0 SwapBonus + // +1 StableSwap + // +2 LiquidityGauge + const liquidityGaugeAddress = ethers.utils.getContractAddress({ + from: deployer.address, + nonce: (await deployer.getTransactionCount("pending")) + 2, + }); + + console.log( + `Deploying ${kind}StableSwap between ${baseSymbol}-${quoteSymbol} (Bonus: ${bonusSymbol}).` + ); + + const SwapBonus = await ethers.getContractFactory("SwapBonus"); + const swapBonus = await SwapBonus.deploy(liquidityGaugeAddress, bonus.address); + console.log(`SwapBonus: ${swapBonus.address}`); + await waitForContract(hre, swapBonus.address); + + let stableSwap: Contract; + switch (kind) { + case "Queen": { + const QueenStableSwap = await ethers.getContractFactory("QueenStableSwap"); + stableSwap = await QueenStableSwap.deploy( + liquidityGaugeAddress, + fundAddresses.fund, + quoteDecimals, + ampl, + feeDistributorAddresses.feeDistributor, + feeRate, + adminFeeRate + ); + break; + } + case "Bishop": { + const BishopStableSwap = await ethers.getContractFactory("BishopStableSwapV2"); + stableSwap = await BishopStableSwap.deploy( + liquidityGaugeAddress, + fundAddresses.fund, + quote.address, + quoteDecimals, + ampl, + feeDistributorAddresses.feeDistributor, + feeRate, + adminFeeRate, + 0 + ); + break; + } + } + console.log(`StableSwap: ${stableSwap.address}`); + await waitForContract(hre, stableSwap.address); + + const chessSchedule = await ethers.getContractAt( + "ChessSchedule", + governanceAddresses.chessSchedule + ); + + const LiquidityGauge = await ethers.getContractFactory("LiquidityGaugeV3"); + const liquidityGauge = await LiquidityGauge.deploy( + `Tranchess ${baseSymbol}-${quoteSymbol}`, + `${baseSymbol}LP`, + stableSwap.address, + chessSchedule.address, + governanceAddresses.chessController, + fundAddresses.fund, + governanceAddresses.votingEscrow, + swapBonus.address + ); + console.log(`LiquidityGauge: ${liquidityGauge.address}`); + await waitForContract(hre, liquidityGauge.address); + + if (kind == "Bishop") { + const controllerBallot = await ethers.getContractAt( + "ControllerBallotV2", + governanceAddresses.controllerBallot + ); + if ((await controllerBallot.owner()) === deployer.address) { + console.log("Adding LiquidityGauge to ControllerBallot"); + await (await controllerBallot.addPool(liquidityGauge.address)).wait(); + console.log( + "NOTE: Please transfer ownership of ControllerBallot to Timelock later" + ); + } else { + console.log("NOTE: Please add LiquidityGauge to ControllerBallot"); + } + + const chessSchedule = await ethers.getContractAt( + "ChessSchedule", + governanceAddresses.chessSchedule + ); + if ((await chessSchedule.owner()) === deployer.address) { + console.log("Adding LiquidityGauge to ChessSchedule's minter list"); + await (await chessSchedule.addMinter(liquidityGauge.address)).wait(); + console.log("NOTE: Please transfer ownership of ChessSchedule to Timelock later"); + } else { + console.log("NOTE: Please add LiquidityGauge to ChessSchedule's minter list"); + } + } + + console.log("Transfering StableSwap's ownership to TimelockController"); + await (await stableSwap.transferOwnership(governanceAddresses.timelockController)).wait(); + if (GOVERNANCE_CONFIG.TREASURY) { + console.log("Transfering StableSwap's pauser and SwapBonus's ownership to treasury"); + await (await stableSwap.transferPauserRole(GOVERNANCE_CONFIG.TREASURY)).wait(); + await (await swapBonus.transferOwnership(GOVERNANCE_CONFIG.TREASURY)).wait(); + } else { + console.log( + "NOTE: Please transfer StableSwap's pauser and SwapBonus's ownership to treasury" + ); + } + + const addresses: StableSwapAddresses = { + ...newAddresses(hre), + kind: kind, + underlyingSymbol, + base: base.address, + baseSymbol: baseSymbol, + quote: quote.address, + quoteSymbol: quoteSymbol, + bonus: bonus.address, + bonusSymbol: bonusSymbol, + feeDistributor: feeDistributorAddresses.feeDistributor, + swapBonus: swapBonus.address, + liquidityGauge: liquidityGauge.address, + stableSwap: stableSwap.address, + }; + saveAddressFile( + hre, + `${kind.toLowerCase()}_stable_swap_${underlyingSymbol.toLowerCase()}`, + addresses + ); + });