diff --git a/.gitignore b/.gitignore index 5a167569..182de8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ typechain #Hardhat files cache artifacts + +#Generated files +RolloverBatch.json diff --git a/spot-contracts/tasks/ganache.sh b/spot-contracts/tasks/ganache.sh index f600460c..8a75e716 100644 --- a/spot-contracts/tasks/ganache.sh +++ b/spot-contracts/tasks/ganache.sh @@ -53,10 +53,6 @@ yarn hardhat --network ganache ops:trancheAndRollover \ --perp-address 0x89967625335C35c5FE1F3C1c03D37fdEb6f415Ed \ --collateral-amount 200 -yarn hardhat --network ganache ops:trancheAndRolloverMax \ - --router-address 0x4a57d51af3a8a90905a5F756E0B28cC2888A1bD5 \ - --perp-address 0x89967625335C35c5FE1F3C1c03D37fdEb6f415Ed - yarn hardhat --network ganache ops:increaseTimeBy 300 yarn hardhat --network ganache ops:updateState 0x89967625335C35c5FE1F3C1c03D37fdEb6f415Ed diff --git a/spot-contracts/tasks/goeril.sh b/spot-contracts/tasks/goeril.sh index 7b186c30..62b69aad 100644 --- a/spot-contracts/tasks/goeril.sh +++ b/spot-contracts/tasks/goeril.sh @@ -31,6 +31,8 @@ yarn hardhat --network goerli deploy:Router ## OPS yarn hardhat --network goerli ops:info 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 +yarn hardhat --network goerli ops:updateState 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 + yarn hardhat --network goerli ops:trancheAndDeposit \ --router-address 0x5e902bdCC408550b4BD612678bE2d57677664Dc9 \ --perp-address 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 \ @@ -47,6 +49,11 @@ yarn hardhat --network goerli ops:redeemTranches \ yarn hardhat --network goerli ops:redeemTranches \ --bond-issuer-address 0xAb7d17864463dEdA6c19060Ad6556e1B218c5Ba0 +yarn hardhat --network goerli ops:preview_tx:trancheAndRollover \ + --wallet-address [INSERT_WALLET_ADDRESS] \ + --router-address 0x5e902bdCC408550b4BD612678bE2d57677664Dc9 \ + --perp-address 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 + yarn hardhat --network goerli ops:trancheAndRollover \ --router-address 0x5e902bdCC408550b4BD612678bE2d57677664Dc9 \ --perp-address 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 \ @@ -60,4 +67,4 @@ yarn hardhat --network goerli ops:rebase:MockAMPL \ ######################################################################## ## upgrade -yarn hardhat --network goerli upgrade:perp:testnet 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 \ No newline at end of file +yarn hardhat --network goerli upgrade:perp:testnet 0x95014Bc18F82a98CFAA3253fbD3184125A01f848 diff --git a/spot-contracts/tasks/helpers.ts b/spot-contracts/tasks/helpers.ts index c8eab010..793cb9b0 100644 --- a/spot-contracts/tasks/helpers.ts +++ b/spot-contracts/tasks/helpers.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as path from "path"; -import { ContractFactory } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ContractFactory, Contract, utils } from "ethers"; const EXTERNAL_ARTIFACTS_PATH = path.join(__dirname, "/../external-artifacts"); export async function getContractFactoryFromExternalArtifacts(ethers: any, name: string): Promise { @@ -11,3 +12,78 @@ export async function getContractFactoryFromExternalArtifacts(ethers: any, name: export async function sleep(sleepSec: number) { await new Promise(resolve => setTimeout(resolve, sleepSec)); } + +interface ContractInput { + internalType: string; + name: string; + type: string; + components?: ContractInput[]; +} + +interface ContractMethod { + inputs: ContractInput[]; + name: string; + payable: boolean; +} + +interface BatchFileMeta { + txBuilderVersion?: string; + name: string; + description?: string; +} + +interface BatchTransaction { + to: string; + value: string; + data?: string; + contractMethod?: ContractMethod; + contractInputsValues?: { [key: string]: string }; +} + +interface BatchFile { + version: string; + chainId: string; + createdAt: number; + meta: BatchFileMeta; + transactions: BatchTransaction[]; +} + +export interface ProposedTransaction { + contract: Contract; + method: string; + args: any[]; +} + +function encodeContractTx(p: ProposedTransaction): BatchTransaction { + const methodFragment = JSON.parse(p.contract.interface.getFunction(p.method).format(utils.FormatTypes.json)); + return { + to: p.contract.address, + value: "0", + data: "", + contractMethod: methodFragment, + contractInputsValues: methodFragment.inputs + .map((m: ContractInput) => m.name) + .reduce((m: { [key: string]: string }, e: string, i: number) => { + m[e] = p.args[i]; + return m; + }, {}), + }; +} + +export async function generateGnosisSafeBatchFile( + hre: HardhatRuntimeEnvironment, + transactions: ProposedTransaction[], +): Promise { + const chainId = (await hre.ethers.provider.getNetwork()).chainId; + return { + version: "1.0", + chainId: `${chainId}`, + createdAt: Date.now(), + meta: { + name: "Transaction Batch", + description: "Script generated transaction batch. Verify manually before execution!", + txBuilderVersion: "1.11.1", + }, + transactions: transactions.map(encodeContractTx), + }; +} diff --git a/spot-contracts/tasks/mainnet.sh b/spot-contracts/tasks/mainnet.sh index c8aef873..3f513df2 100644 --- a/spot-contracts/tasks/mainnet.sh +++ b/spot-contracts/tasks/mainnet.sh @@ -69,6 +69,11 @@ yarn hardhat --network mainnet ops:redeemTranches \ yarn hardhat --network mainnet ops:redeemTranches \ --bond-issuer-address 0x2E2E49eDCd5ce08677Bab6d791C863f1361B52F2 +yarn hardhat --network mainnet ops:preview_tx:trancheAndRollover \ + --wallet-address [INSERT_WALLET_ADDRESS] \ + --router-address 0x38f600e08540178719BF656e6B43FC15A529c393 \ + --perp-address 0xC1f33e0cf7e40a67375007104B929E49a581bafE + yarn hardhat --network mainnet ops:trancheAndRollover \ --router-address 0x38f600e08540178719BF656e6B43FC15A529c393 \ --perp-address 0xC1f33e0cf7e40a67375007104B929E49a581bafE \ diff --git a/spot-contracts/tasks/ops/index.ts b/spot-contracts/tasks/ops/index.ts index 4810f5fc..0511c8b5 100644 --- a/spot-contracts/tasks/ops/index.ts +++ b/spot-contracts/tasks/ops/index.ts @@ -1,3 +1,4 @@ import "./ampl"; import "./perp"; +import "./perp_rollover"; import "./testnet"; diff --git a/spot-contracts/tasks/ops/perp.ts b/spot-contracts/tasks/ops/perp.ts index a29b9ac8..440c5bfc 100644 --- a/spot-contracts/tasks/ops/perp.ts +++ b/spot-contracts/tasks/ops/perp.ts @@ -1,7 +1,7 @@ import { getAdminAddress, getImplementationAddress } from "@openzeppelin/upgrades-core"; import { task, types } from "hardhat/config"; import { TaskArguments } from "hardhat/types"; -import { utils, constants, Contract, BigNumber } from "ethers"; +import { utils, constants, BigNumber } from "ethers"; task("ops:info") .addPositionalParam("perpAddress", "the address of the perp contract", undefined, types.string, false) @@ -121,6 +121,41 @@ task("ops:info") console.log("---------------------------------------------------------------"); }); +task("ops:perp:updateKeeper", "Updates the keeper address of perpetual tranche") + .addParam("address", "the perpetual tranche contract address", undefined, types.string, false) + .addParam("newKeeperAddress", "the address of the new keeper", undefined, types.string, false) + .addParam("fromIdx", "the index of sender", 0, types.int) + .setAction(async function (args: TaskArguments, hre) { + const { address, newKeeperAddress } = args; + const perp = await hre.ethers.getContractAt("PerpetualTranche", address); + + const signer = (await hre.ethers.getSigners())[args.fromIdx]; + const signerAddress = await signer.getAddress(); + console.log("Signer", signerAddress); + + console.log(`Updating keeper to ${newKeeperAddress}`); + const tx = await perp.updateKeeper(newKeeperAddress); + console.log(tx.hash); + await tx.wait(); + }); + +task("ops:perp:pause", "Pauses operations on the perpetual tranche contract") + .addParam("address", "the perpetual tranche contract address", undefined, types.string, false) + .addParam("fromIdx", "the index of sender", 0, types.int) + .setAction(async function (args: TaskArguments, hre) { + const { address } = args; + const perp = await hre.ethers.getContractAt("PerpetualTranche", address); + + const signer = (await hre.ethers.getSigners())[args.fromIdx]; + const signerAddress = await signer.getAddress(); + console.log("Signer", signerAddress); + + console.log(`Pausing`); + const tx = await perp.pause(); + console.log(tx.hash); + await tx.wait(); + }); + task("ops:updateState") .addPositionalParam("perpAddress", "the address of the perp contract", undefined, types.string, false) .addParam("fromIdx", "the index of sender", 0, types.int) @@ -282,311 +317,3 @@ task("ops:redeem") console.log("Signer balance", utils.formatUnits(await perp.balanceOf(signerAddress), await perp.decimals())); }); - -task("ops:redeemTranches") - .addParam("bondIssuerAddress", "the address of the bond issuer contract", undefined, types.string, false) - .addParam("fromIdx", "the index of sender", 0, types.int) - .setAction(async function (args: TaskArguments, hre) { - const { bondIssuerAddress } = args; - const bondIssuer = await hre.ethers.getContractAt("BondIssuer", bondIssuerAddress); - - console.log("---------------------------------------------------------------"); - console.log("Execution:"); - const signer = (await hre.ethers.getSigners())[args.fromIdx]; - const signerAddress = await signer.getAddress(); - console.log("Signer", signerAddress); - - const attemptMature = async (bond: Contract) => { - try { - console.log("Invoking Mature"); - const tx = await bond.connect(signer).mature(); - await tx.wait(); - console.log("Tx:", tx.hash); - } catch (e) { - console.log("Not up for maturity"); - } - }; - - // iterate through the bonds - const issuedCount = await bondIssuer.callStatic.issuedCount(); - for (let i = 0; i < issuedCount; i++) { - const bondAddress = await bondIssuer.callStatic.issuedBondAt(i); - const bond = await hre.ethers.getContractAt("IBondController", bondAddress); - - console.log("---------------------------------------------------------------"); - console.log("Processing bond", bondAddress); - - const trancheCount = await bond.trancheCount(); - const tranches = []; - for (let j = 0; j < trancheCount; j++) { - const [address, ratio] = await bond.tranches(j); - const tranche = await hre.ethers.getContractAt("ITranche", address); - const balance = await tranche.balanceOf(signerAddress); - const scalar = balance.div(ratio).mul("1000"); - tranches.push({ - idx: j, - address, - tranche, - ratio, - balance, - scalar, - }); - } - - await attemptMature(bond); - - if (!(await bond.isMature())) { - console.log("Redeeming based on balance"); - const minScalarTranche = tranches.sort((a, b) => - a.scalar.gte(b.scalar) ? (a.scalar.eq(b.scalar) ? 0 : 1) : -1, - )[0]; - if (minScalarTranche.scalar.gt("0")) { - const redemptionAmounts = tranches.map(t => t.ratio.mul(minScalarTranche.scalar).div("1000")); - console.log( - "Tranche balances", - tranches.map(t => t.balance), - ); - console.log("Redeeming tranches", redemptionAmounts); - const tx = await bond.connect(signer).redeem(redemptionAmounts); - await tx.wait(); - console.log("Tx:", tx.hash); - } - } else { - console.log("Redeeming mature"); - for (let j = 0; j < trancheCount; j++) { - if (tranches[j].balance.gt(0)) { - console.log("Redeeming", tranches[j].address); - const tx = await bond.connect(signer).redeemMature(tranches[j].address, tranches[j].balance); - await tx.wait(); - console.log("Tx:", tx.hash); - } - } - } - } - }); - -task("ops:trancheAndRollover") - .addParam("perpAddress", "the address of the perp contract", undefined, types.string, false) - .addParam("routerAddress", "the address of the router contract", undefined, types.string, false) - .addParam( - "collateralAmount", - "the total amount of collateral (in float) to tranche and use for rolling over", - undefined, - types.string, - false, - ) - .addParam("fromIdx", "the index of sender", 0, types.int) - .addFlag("dryRun", "skip execution") - .setAction(async function (args: TaskArguments, hre) { - const { perpAddress, routerAddress, collateralAmount, dryRun } = args; - - const router = await hre.ethers.getContractAt("RouterV1", routerAddress); - const perp = await hre.ethers.getContractAt("PerpetualTranche", perpAddress); - const bondIssuer = await hre.ethers.getContractAt("BondIssuer", await perp.bondIssuer()); - const collateralToken = await hre.ethers.getContractAt("MockERC20", await bondIssuer.collateral()); - - const fixedPtCollateralAmount = utils.parseUnits(collateralAmount, await collateralToken.decimals()); - const [depositBondAddress, trancheAddresses, depositTrancheAmts] = await router.callStatic.previewTranche( - perp.address, - fixedPtCollateralAmount, - ); - const depositBond = await hre.ethers.getContractAt("IBondController", depositBondAddress); - - console.log("---------------------------------------------------------------"); - console.log("Preview tranche:", collateralAmount); - const depositTranches = []; - for (let i = 0; i < trancheAddresses.length; i++) { - const tranche = await hre.ethers.getContractAt("ITranche", trancheAddresses[i]); - depositTranches.push(tranche); - console.log( - `tranches(${i}):`, - trancheAddresses[i], - utils.formatUnits(depositTrancheAmts[i].toString(), await collateralToken.decimals()), - ); - } - - console.log("---------------------------------------------------------------"); - console.log("Rollover list:"); - const reserveCount = (await perp.callStatic.getReserveCount()).toNumber(); - const upForRotation = await perp.callStatic.getReserveTokensUpForRollover(); - const reserveTokens = []; - const reserveTokenBalances = []; - const rotationTokens = []; - const rotationTokenBalances = []; - for (let i = 0; i < reserveCount; i++) { - const tranche = await hre.ethers.getContractAt("ITranche", await perp.callStatic.getReserveAt(i)); - const balance = await tranche.balanceOf(await perp.reserve()); - reserveTokens.push(tranche); - reserveTokenBalances.push(balance); - if (upForRotation[i] !== constants.AddressZero && balance.gt(0)) { - rotationTokens.push(tranche); - rotationTokenBalances.push(balance); - } - } - if (rotationTokens.length === 0) { - throw Error("No tokens up for rollover"); - } - - console.log("---------------------------------------------------------------"); - console.log("Rollover preview:"); - const feeToken = await hre.ethers.getContractAt("PerpetualTranche", await perp.feeToken()); - let totalRolloverAmt = BigNumber.from("0"); - let totalRolloverFee = BigNumber.from("0"); - - // continues to the next token when only DUST remains - const DUST_AMOUNT = utils.parseUnits("1", await perp.decimals()); - - const remainingTrancheInAmts: BigNumber[] = depositTrancheAmts.map((t: BigNumber) => t); - const remainingTokenOutAmts: BigNumber[] = rotationTokenBalances.map(b => b); - - const rolloverData: any[] = []; - for (let i = 0, j = 0; i < depositTranches.length && j < rotationTokens.length; ) { - const trancheIn = depositTranches[i]; - const tokenOut = rotationTokens[j]; - const [rd, , rolloverFee] = await router.callStatic.previewRollover( - perp.address, - trancheIn.address, - tokenOut.address, - remainingTrancheInAmts[i], - remainingTokenOutAmts[j], - ); - - if (rd.perpRolloverAmt.gt("0")) { - rolloverData.push({ - trancheIn, - tokenOut, - trancheInAmt: rd.trancheInAmt, - tokenOutAmt: rd.tokenOutAmt, - }); - - totalRolloverAmt = totalRolloverAmt.add(rd.perpRolloverAmt); - totalRolloverFee = totalRolloverFee.add(rolloverFee); - - remainingTrancheInAmts[i] = rd.remainingTrancheInAmt; - remainingTokenOutAmts[j] = remainingTokenOutAmts[j].sub(rd.tokenOutAmt); - - if (remainingTokenOutAmts[i].lte(DUST_AMOUNT)) { - j++; - } - if (remainingTrancheInAmts[i].lte(DUST_AMOUNT)) { - i++; - } - } else { - i++; - } - } - - console.log("depositBond", depositBond.address); - console.log("collateralAmount", fixedPtCollateralAmount); - console.log("rolloverAmt", utils.formatUnits(totalRolloverAmt, await perp.decimals())); - console.log("rolloverFee", utils.formatUnits(totalRolloverFee, await feeToken.decimals())); - console.log(rolloverData.map(r => [r.trancheIn.address, r.tokenOut.address, r.trancheInAmt])); - - if (dryRun) { - console.log("Skipping execution"); - return; - } - - console.log("---------------------------------------------------------------"); - console.log("Execution:"); - const signer = (await hre.ethers.getSigners())[args.fromIdx]; - const signerAddress = await signer.getAddress(); - console.log("Signer", signerAddress); - - console.log("Approving collateralToken to be spent"); - const allowance = await collateralToken.allowance(signerAddress, router.address); - if (allowance.lt(fixedPtCollateralAmount)) { - const tx1 = await collateralToken.connect(signer).approve(router.address, fixedPtCollateralAmount); - await tx1.wait(); - console.log("Tx", tx1.hash); - } - - let fee = BigNumber.from("0"); - if (totalRolloverFee.gt("0")) { - fee = totalRolloverFee; - console.log("Approving fees to be spent:"); - const tx2 = await feeToken.connect(signer).increaseAllowance(router.address, fee); - await tx2.wait(); - console.log("Tx", tx2.hash); - } - - // TODO: fee calculation has some rounding issues. Overpaying fixes it for now - fee = fee.mul("2"); - - console.log("Executing rollover:"); - const tx3 = await router.connect(signer).trancheAndRollover( - perp.address, - depositBond.address, - fixedPtCollateralAmount, - rolloverData.map(r => [r.trancheIn.address, r.tokenOut.address, r.trancheInAmt]), - fee, - ); - await tx3.wait(); - console.log("Tx", tx3.hash); - }); - -task("ops:trancheAndRolloverMax") - .addParam("perpAddress", "the address of the perp contract", undefined, types.string, false) - .addParam("routerAddress", "the address of the router contract", undefined, types.string, false) - .addParam("fromIdx", "the index of sender", 0, types.int) - .setAction(async function (args: TaskArguments, hre) { - const signer = (await hre.ethers.getSigners())[args.fromIdx]; - const signerAddress = await signer.getAddress(); - console.log("Signer", signerAddress); - - const { routerAddress, perpAddress } = args; - const perp = await hre.ethers.getContractAt("PerpetualTranche", perpAddress); - const bondIssuer = await hre.ethers.getContractAt("BondIssuer", await perp.bondIssuer()); - const collateralToken = await hre.ethers.getContractAt("MockERC20", await bondIssuer.collateral()); - const floatingPtCollateralAmount = utils.formatUnits( - await collateralToken.balanceOf(signerAddress), - await collateralToken.decimals(), - ); - - await hre.run("ops:trancheAndRollover", { - perpAddress, - routerAddress, - collateralAmount: floatingPtCollateralAmount, - fromIdx: args.fromIdx, - }); - - await hre.run("ops:redeemTranches", { - bondIssuerAddress: bondIssuer.address, - fromIdx: args.fromIdx, - }); - }); - -task("ops:perp:updateKeeper", "Updates the keeper address of perpetual tranche") - .addParam("address", "the perpetual tranche contract address", undefined, types.string, false) - .addParam("newKeeperAddress", "the address of the new keeper", undefined, types.string, false) - .addParam("fromIdx", "the index of sender", 0, types.int) - .setAction(async function (args: TaskArguments, hre) { - const { address, newKeeperAddress } = args; - const perp = await hre.ethers.getContractAt("PerpetualTranche", address); - - const signer = (await hre.ethers.getSigners())[args.fromIdx]; - const signerAddress = await signer.getAddress(); - console.log("Signer", signerAddress); - - console.log(`Updating keeper to ${newKeeperAddress}`); - const tx = await perp.updateKeeper(newKeeperAddress); - console.log(tx.hash); - await tx.wait(); - }); - -task("ops:perp:pause", "Pauses opeartions on the perpetual tranche contract") - .addParam("address", "the perpetual tranche contract address", undefined, types.string, false) - .addParam("fromIdx", "the index of sender", 0, types.int) - .setAction(async function (args: TaskArguments, hre) { - const { address } = args; - const perp = await hre.ethers.getContractAt("PerpetualTranche", address); - - const signer = (await hre.ethers.getSigners())[args.fromIdx]; - const signerAddress = await signer.getAddress(); - console.log("Signer", signerAddress); - - console.log(`Pausing`); - const tx = await perp.pause(); - console.log(tx.hash); - await tx.wait(); - }); diff --git a/spot-contracts/tasks/ops/perp_rollover.ts b/spot-contracts/tasks/ops/perp_rollover.ts new file mode 100644 index 00000000..ba68f85c --- /dev/null +++ b/spot-contracts/tasks/ops/perp_rollover.ts @@ -0,0 +1,398 @@ +import fs from "fs"; +import { task, types } from "hardhat/config"; +import { TaskArguments, HardhatRuntimeEnvironment } from "hardhat/types"; +import { utils, constants, Contract, BigNumber, Signer } from "ethers"; +import { generateGnosisSafeBatchFile, ProposedTransaction } from "../helpers"; + +async function matureBond(bond: Contract, signer: Signer) { + if (await bond.isMature()) { + return true; + } + try { + console.log("Invoking Mature"); + await bond.connect(signer).callStatic.mature(); + const tx = await bond.connect(signer).mature(); + await tx.wait(); + console.log("Tx:", tx.hash); + } catch (e) { + console.log("Not up for maturity"); + return false; + } + return true; +} + +async function getTrancheData(hre: HardhatRuntimeEnvironment, bond: Contract): Promise<[Contract, BigNumber][]> { + const trancheCount = await bond.trancheCount(); + const tranches: [Contract, BigNumber][] = []; + for (let i = 0; i < trancheCount; i++) { + const [address, ratio] = await bond.tranches(i); + const tranche = await hre.ethers.getContractAt("ITranche", address); + tranches.push([tranche, ratio]); + } + return tranches; +} + +function computeProportionalBalances(balances: BigNumber[], ratios: BigNumber[]): BigNumber[] { + if (balances.length !== ratios.length) { + throw Error("balances and ratios length mismatch"); + } + + const redeemableAmts: BigNumber[] = []; + let min = BigNumber.from(constants.MaxUint256); + for (let i = 0; i < balances.length && min.gt("0"); i++) { + const d = balances[i].mul("1000").div(ratios[i]); + if (d.lt(min)) { + min = d; + } + } + for (let i = 0; i < balances.length; i++) { + redeemableAmts[i] = ratios[i].mul(min).div("1000"); + } + return redeemableAmts; +} + +async function computeRedeemableTrancheAmounts(td: [Contract, BigNumber][], address: string): Promise { + const balances: BigNumber[] = []; + const ratios: BigNumber[] = []; + for (let i = 0; i < td.length; i++) { + balances.push(await td[i][0].balanceOf(address)); + ratios.push(td[i][1]); + } + return computeProportionalBalances(balances, ratios); +} + +interface RolloverData { + trancheIn: Contract; + tokenOut: Contract; + trancheInAmt: BigNumber; + tokenOutAmt: BigNumber; +} + +interface RolloverBatch { + depositBond: Contract; + depositTranches: Contract[]; + totalRolloverAmt: BigNumber; + totalRolloverFee: BigNumber; + remainingTrancheInAmts: BigNumber[]; + remainingTokenOutAmts: BigNumber[]; + rolloverData: RolloverData[]; + collateralUsed: BigNumber; + excessCollateral: BigNumber; +} + +async function computeRolloverBatchExact( + hre: HardhatRuntimeEnvironment, + router: Contract, + perp: Contract, + collateralUsed: BigNumber, +): Promise { + const bondIssuer = await hre.ethers.getContractAt("BondIssuer", await perp.bondIssuer()); + const collateralToken = await hre.ethers.getContractAt("MockERC20", await bondIssuer.collateral()); + const [depositBondAddress, trancheAddresses, depositTrancheAmts] = await router.callStatic.previewTranche( + perp.address, + collateralUsed, + ); + const depositBond = await hre.ethers.getContractAt("IBondController", depositBondAddress); + + // Fresh Tranches + const depositTranches = []; + const trancheRatios = []; + for (let i = 0; i < trancheAddresses.length; i++) { + depositTranches.push(await hre.ethers.getContractAt("ITranche", trancheAddresses[i])); + trancheRatios.push(await bondIssuer.trancheRatios(i)); + } + + // Tranches up for rollover + const reserveCount = (await perp.callStatic.getReserveCount()).toNumber(); + const upForRotation = await perp.callStatic.getReserveTokensUpForRollover(); + const reserveTokens = []; + const reserveTokenBalances = []; + const rotationTokens = []; + const rotationTokenBalances = []; + for (let i = 0; i < reserveCount; i++) { + const tranche = await hre.ethers.getContractAt("ITranche", await perp.callStatic.getReserveAt(i)); + const balance = await perp.callStatic.getReserveTrancheBalance(tranche.address); + reserveTokens.push(tranche); + reserveTokenBalances.push(balance); + if (upForRotation[i] !== constants.AddressZero && balance.gt(0)) { + rotationTokens.push(tranche); + rotationTokenBalances.push(balance); + } + } + + // continues to the next token when only DUST remains + const DUST_AMOUNT = utils.parseUnits("1", await perp.decimals()); + + // Amounts at the start + const remainingTrancheInAmts: BigNumber[] = depositTrancheAmts.map((t: BigNumber) => t); + const remainingTokenOutAmts: BigNumber[] = rotationTokenBalances.map(b => b); + + // For each tranche token, and each token up for rollover + // We try to rollover and once depleted (upto dust) and move on to the next pair + const rolloverData: RolloverData[] = []; + let totalRolloverAmt = BigNumber.from("0"); + let totalRolloverFee = BigNumber.from("0"); + + for (let i = 0, j = 0; i < depositTranches.length && j < rotationTokens.length; ) { + const trancheIn = depositTranches[i]; + const tokenOut = rotationTokens[j]; + const [rd, , rolloverFee] = await router.callStatic.previewRollover( + perp.address, + trancheIn.address, + tokenOut.address, + remainingTrancheInAmts[i], + remainingTokenOutAmts[j], + ); + + // trancheIn isn't accepted by perp, likely because yield=0 + if (rd.perpRolloverAmt.eq("0")) { + i++; + continue; + } + + rolloverData.push({ + trancheIn, + tokenOut, + trancheInAmt: rd.trancheInAmt, + tokenOutAmt: rd.tokenOutAmt, + }); + + totalRolloverAmt = totalRolloverAmt.add(rd.perpRolloverAmt); + totalRolloverFee = totalRolloverFee.add(rolloverFee); + + remainingTrancheInAmts[i] = rd.remainingTrancheInAmt; + remainingTokenOutAmts[j] = remainingTokenOutAmts[j].sub(rd.tokenOutAmt); + + // trancheIn tokens are exhausted + if (remainingTrancheInAmts[i].lte(DUST_AMOUNT)) { + i++; + } + + // tokenOut is exhausted + if (remainingTokenOutAmts[j].lte(DUST_AMOUNT)) { + j++; + } + } + + // calculate if any excess collateral was tranched + let excessCollateral = BigNumber.from("0"); + if (remainingTrancheInAmts[0].gt("0")) { + const excessTrancheTokens = computeProportionalBalances(remainingTrancheInAmts, trancheRatios); + excessCollateral = excessTrancheTokens.reduce((m, t) => m.add(t), BigNumber.from("0")); + try { + // fails if bond isn't issued + const depositBondTotalDebt = await depositBond.totalDebt(); + if (depositBondTotalDebt.gt(0)) { + const bondCollateralBalance = await collateralToken.balanceOf(depositBond.address); + excessCollateral = excessCollateral.mul(bondCollateralBalance).div(depositBondTotalDebt); + } + } catch (e) {} + } + + return { + depositBond, + depositTranches, + totalRolloverAmt, + totalRolloverFee, + remainingTrancheInAmts, + remainingTokenOutAmts, + rolloverData, + collateralUsed, + excessCollateral, + }; +} + +async function computeRolloverBatch( + hre: HardhatRuntimeEnvironment, + router: Contract, + perp: Contract, + collateralUsed: BigNumber, +): Promise { + const r = await computeRolloverBatchExact(hre, router, perp, collateralUsed); + return r.excessCollateral.eq("0") + ? r + : computeRolloverBatchExact(hre, router, perp, r.collateralUsed.sub(r.excessCollateral)); +} + +task("ops:redeemTranches") + .addParam("bondIssuerAddress", "the address of the bond issuer contract", undefined, types.string, false) + .addParam("fromIdx", "the index of sender", 0, types.int) + .setAction(async function (args: TaskArguments, hre: HardhatRuntimeEnvironment) { + const { bondIssuerAddress } = args; + const bondIssuer = await hre.ethers.getContractAt("BondIssuer", bondIssuerAddress); + + console.log("---------------------------------------------------------------"); + console.log("Execution:"); + const signer = (await hre.ethers.getSigners())[args.fromIdx]; + const signerAddress = await signer.getAddress(); + console.log("Signer", signerAddress); + + // iterate through the bonds + const issuedCount = await bondIssuer.callStatic.issuedCount(); + for (let i = 0; i < issuedCount; i++) { + const bondAddress = await bondIssuer.callStatic.issuedBondAt(i); + const bond = await hre.ethers.getContractAt("IBondController", bondAddress); + + console.log("---------------------------------------------------------------"); + console.log("Processing bond", bondAddress); + + const td = await getTrancheData(hre, bond); + const isMature = await matureBond(bond, signer); + + if (isMature) { + for (let j = 0; j < td.length; j++) { + const b = await td[j][0].balanceOf(signerAddress); + if (b.gt(0)) { + console.log("Redeeming mature tranche", td[j][0].address); + const tx = await bond.connect(signer).redeemMature(td[j][0].address, b); + await tx.wait(); + console.log("Tx:", tx.hash); + } + } + } else { + const redemptionAmounts = await computeRedeemableTrancheAmounts(td, signerAddress); + if (redemptionAmounts[0].gt("0")) { + console.log( + "Redeeming immature bond", + redemptionAmounts.map(a => a.toString()), + ); + const tx = await bond.connect(signer).redeem(redemptionAmounts); + await tx.wait(); + console.log("Tx:", tx.hash); + } + } + } + }); + +task("ops:trancheAndRollover") + .addParam("perpAddress", "the address of the perp contract", undefined, types.string, false) + .addParam("routerAddress", "the address of the router contract", undefined, types.string, false) + .addParam( + "collateralAmount", + "the total amount of collateral (in float) to tranche and use for rolling over", + undefined, + types.string, + false, + ) + .addParam("fromIdx", "the index of sender", 0, types.int) + .setAction(async function (args: TaskArguments, hre) { + const { perpAddress, routerAddress, collateralAmount } = args; + + const router = await hre.ethers.getContractAt("RouterV1", routerAddress); + const perp = await hre.ethers.getContractAt("PerpetualTranche", perpAddress); + const bondIssuer = await hre.ethers.getContractAt("BondIssuer", await perp.bondIssuer()); + const collateralToken = await hre.ethers.getContractAt("MockERC20", await bondIssuer.collateral()); + const feeToken = await hre.ethers.getContractAt("PerpetualTranche", await perp.feeToken()); + + const fixedPtCollateralAmount = utils.parseUnits(collateralAmount, await collateralToken.decimals()); + const { depositBond, totalRolloverFee, rolloverData } = await computeRolloverBatch( + hre, + router, + perp, + fixedPtCollateralAmount, + ); + + if (rolloverData.length === 0) { + throw Error("No tokens up for rollover"); + } + + console.log("---------------------------------------------------------------"); + console.log("Execution:"); + const signer = (await hre.ethers.getSigners())[args.fromIdx]; + const signerAddress = await signer.getAddress(); + console.log("Signer", signerAddress); + + console.log("Approving collateralToken to be spent"); + const allowance = await collateralToken.allowance(signerAddress, router.address); + if (allowance.lt(fixedPtCollateralAmount)) { + const tx1 = await collateralToken.connect(signer).approve(router.address, fixedPtCollateralAmount); + await tx1.wait(); + console.log("Tx", tx1.hash); + } + + let fee = BigNumber.from("0"); + if (totalRolloverFee.gt("0")) { + fee = totalRolloverFee; + console.log("Approving fees to be spent:"); + const tx2 = await feeToken.connect(signer).increaseAllowance(router.address, fee); + await tx2.wait(); + console.log("Tx", tx2.hash); + } + + // TODO: fee calculation has some rounding issues. Overpaying fixes it for now + fee = fee.mul("2"); + + console.log("Executing rollover:"); + const tx3 = await router.connect(signer).trancheAndRollover( + perp.address, + depositBond.address, + fixedPtCollateralAmount, + rolloverData.map(r => [r.trancheIn.address, r.tokenOut.address, r.trancheInAmt]), + fee, + ); + await tx3.wait(); + console.log("Tx", tx3.hash); + }); + +task("ops:preview_tx:trancheAndRollover") + .addParam("walletAddress", "the address of the wallet with the collateral token", undefined, types.string, false) + .addParam("perpAddress", "the address of the perp contract", undefined, types.string, false) + .addParam("routerAddress", "the address of the router contract", undefined, types.string, false) + .setAction(async function (args: TaskArguments, hre) { + const { walletAddress, perpAddress, routerAddress } = args; + + const router = await hre.ethers.getContractAt("RouterV1", routerAddress); + const perp = await hre.ethers.getContractAt("PerpetualTranche", perpAddress); + const bondIssuer = await hre.ethers.getContractAt("BondIssuer", await perp.bondIssuer()); + const collateralToken = await hre.ethers.getContractAt("MockERC20", await bondIssuer.collateral()); + + const maxCollateralAvaiable = await collateralToken.balanceOf(walletAddress); + const { depositBond, totalRolloverAmt, totalRolloverFee, rolloverData, collateralUsed } = + await computeRolloverBatch(hre, router, perp, maxCollateralAvaiable); + const rolloverDataInput = rolloverData.map(r => [ + r.trancheIn.address, + r.tokenOut.address, + r.trancheInAmt.toString(), + ]); + + console.log("---------------------------------------------------------------"); + console.log("Rollover preview"); + console.log("balanceAvailable", utils.formatUnits(maxCollateralAvaiable, await collateralToken.decimals())); + console.log("collateralUsed", utils.formatUnits(collateralUsed, await collateralToken.decimals())); + console.log("rolloverAmt", utils.formatUnits(totalRolloverAmt, await perp.decimals())); + + console.log("---------------------------------------------------------------"); + console.log("collateralToken", collateralToken.address); + console.log("router", router.address); + console.log("perp", perp.address); + console.log("depositBond", depositBond.address); + console.log("collateralAmountFixedPt", collateralUsed.toString()); + console.log("rolloverData", JSON.stringify(rolloverDataInput, null, 2)); + console.log("rolloverFeeFixedPt", totalRolloverFee.toString()); + + console.log("---------------------------------------------------------------"); + console.log("Execute the following transactions"); + + const tx1: ProposedTransaction = { + contract: collateralToken, + method: "approve", + args: [router.address, collateralUsed.toString()], + }; + const tx2: ProposedTransaction = { + contract: router, + method: "trancheAndRollover", + args: [ + perp.address, + depositBond.address, + collateralUsed.toString(), + JSON.stringify(rolloverDataInput), + totalRolloverFee.toString(), + ], + }; + + console.log({ to: tx1.contract.address, method: tx1.method, args: tx1.args }); + console.log({ to: tx2.contract.address, method: tx2.method, args: tx2.args }); + + console.log("Wrote tx batch to file:", "RolloverBatch.json"); + fs.writeFileSync("RolloverBatch.json", JSON.stringify(await generateGnosisSafeBatchFile(hre, [tx1, tx2]), null, 2)); + });