From bb1fa21c8d4a97c34258ecfa6a8c5f64a97d66c3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Jul 2023 00:07:07 +1000 Subject: [PATCH 1/2] Refactor of Hardhat tasks --- contracts/package.json | 2 +- contracts/tasks/account.js | 225 ------------- contracts/tasks/block.js | 30 ++ contracts/tasks/curve.js | 558 ++++++++++++++++++++++++-------- contracts/tasks/ousd.js | 24 -- contracts/tasks/smokeTest.js | 8 +- contracts/tasks/storageSlots.js | 4 +- contracts/tasks/tasks.js | 555 ++++++++++++++++++++++++++++--- contracts/tasks/tokens.js | 107 ++++++ contracts/tasks/valueChecker.js | 109 +++++++ contracts/tasks/vault.js | 341 +++++++++++++------ contracts/utils/assets.js | 5 +- contracts/utils/regex.js | 18 ++ contracts/utils/signers.js | 50 ++- contracts/utils/txLogger.js | 32 ++ 15 files changed, 1524 insertions(+), 544 deletions(-) create mode 100644 contracts/tasks/block.js delete mode 100644 contracts/tasks/ousd.js create mode 100644 contracts/tasks/tokens.js create mode 100644 contracts/tasks/valueChecker.js create mode 100644 contracts/utils/regex.js create mode 100644 contracts/utils/txLogger.js diff --git a/contracts/package.json b/contracts/package.json index 121912cf5b..f36c5fbc1f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -11,7 +11,7 @@ "node": "yarn run node:fork", "node:fork": "./node.sh fork", "lint": "yarn run lint:js && yarn run lint:sol", - "lint:js": "eslint \"test/**/*.js\"", + "lint:js": "eslint \"test/**/*.js\" \"tasks/**/*.js\"", "lint:sol": "solhint \"contracts/**/*.sol\"", "prettier": "yarn run prettier:js && yarn run prettier:sol", "prettier:check": "prettier -c \"*.js\" \"deploy/**/*.js\" \"scripts/**/*.js\" \"smoke/**/*.js\" \"scripts/**/*.js\" \"tasks/**/*.js\" \"test/**/*.js\" \"utils/**/*.js\"", diff --git a/contracts/tasks/account.js b/contracts/tasks/account.js index b8aad185d7..86b31f77cf 100644 --- a/contracts/tasks/account.js +++ b/contracts/tasks/account.js @@ -1,5 +1,3 @@ -const _ = require("lodash"); - // USDT has its own ABI because of non standard returns const usdtAbi = require("../test/abi/usdt.json").abi; const daiAbi = require("../test/abi/erc20.json"); @@ -15,12 +13,6 @@ const defaultAccountIndex = 4; // By default, fund each test account with 10k worth of each stable coin. const defaultFundAmount = 10000; -// By default, mint 1k worth of OUSD for each test account. -const defaultMintAmount = 1000; - -// By default, redeem 1k worth of OUSD for each test account. -const defaultRedeemAmount = 1000; - /** * Prints test accounts. */ @@ -55,7 +47,6 @@ async function fund(taskArguments, hre) { usdtUnits, daiUnits, usdcUnits, - tusdUnits, isFork, isLocalhost, } = require("../test/helpers"); @@ -173,219 +164,6 @@ async function fund(taskArguments, hre) { } } -/** - * Mints OUSD using USDT on local or fork. - */ -async function mint(taskArguments, hre) { - const addresses = require("../utils/addresses"); - const { usdtUnits, isFork, isLocalhost } = require("../test/helpers"); - - if (!isFork) { - throw new Error("Task can only be used on fork"); - } - - const ousd = await ethers.getContractAt("OUSD", addresses.mainnet.OUSDProxy); - const vault = await ethers.getContractAt( - "IVault", - addresses.mainnet.VaultProxy - ); - - const usdt = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); - - const numAccounts = Number(taskArguments.num) || defaultNumAccounts; - const accountIndex = Number(taskArguments.index) || defaultAccountIndex; - const mintAmount = taskArguments.amount || defaultMintAmount; - - const signers = await hre.ethers.getSigners(); - for (let i = accountIndex; i < accountIndex + numAccounts; i++) { - const signer = signers[i]; - const address = signer.address; - console.log( - `Minting ${mintAmount} OUSD for account ${i} at address ${address}` - ); - - // Ensure the account has sufficient USDT balance to cover the mint. - const usdtBalance = await usdt.balanceOf(address); - if (usdtBalance.lt(usdtUnits(mintAmount))) { - throw new Error( - `Account USDT balance insufficient to mint the requested amount` - ); - } - - // for some reason we need to call impersonateAccount even on default list of signers - await hre.network.provider.request({ - method: "hardhat_impersonateAccount", - params: [signer.address], - }); - - // Reset approval before requesting a fresh one, or non first approve calls will fail - await usdt - .connect(signer) - .approve(vault.address, "0x0", { gasLimit: 1000000 }); - await usdt - .connect(signer) - .approve(vault.address, usdtUnits(mintAmount), { gasLimit: 1000000 }); - - // Mint. - await vault - .connect(signer) - .mint(usdt.address, usdtUnits(mintAmount), 0, { gasLimit: 2000000 }); - - // Show new account's balance. - const ousdBalance = await ousd.balanceOf(address); - console.log( - "New OUSD balance=", - hre.ethers.utils.formatUnits(ousdBalance, 18) - ); - } -} - -/** - * Redeems OUSD on fork for specific account - */ -async function redeemFor(taskArguments, hre) { - const addresses = require("../utils/addresses"); - const { - ousdUnits, - ousdUnitsFormat, - daiUnitsFormat, - usdcUnitsFormat, - usdtUnitsFormat, - isFork, - isLocalhost, - } = require("../test/helpers"); - - if (!isFork) { - throw new Error("Task can only be used on fork"); - } - - const ousd = await ethers.getContractAt("OUSD", addresses.mainnet.OUSDProxy); - const vault = await ethers.getContractAt( - "IVault", - addresses.mainnet.VaultProxy - ); - const dai = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.DAI); - const usdc = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.USDC); - const usdt = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); - - const address = taskArguments.account; - - const signer = await hre.ethers.getSigner(address); - await hre.network.provider.request({ - method: "hardhat_impersonateAccount", - params: [address], - }); - - const redeemAmount = taskArguments.amount; - - console.log(`Redeeming ${redeemAmount} OUSD for address ${address}`); - - // Show the current balances. - let ousdBalance = await ousd.balanceOf(address); - let daiBalance = await dai.balanceOf(address); - let usdcBalance = await usdc.balanceOf(address); - let usdtBalance = await usdt.balanceOf(address); - console.log("OUSD balance=", ousdUnitsFormat(ousdBalance, 18)); - console.log("DAI balance=", daiUnitsFormat(daiBalance, 18)); - console.log("USDC balance=", usdcUnitsFormat(usdcBalance, 6)); - console.log("USDT balance=", usdtUnitsFormat(usdtBalance, 6)); - - const redeemAmountInt = parseInt(redeemAmount); - // Redeem. - await vault - .connect(signer) - .redeem( - ousdUnits(redeemAmount), - ousdUnits((redeemAmountInt - redeemAmountInt * 0.05).toString()), - { gasLimit: 2500000 } - ); - - // Show the new balances. - ousdBalance = await ousd.balanceOf(address); - daiBalance = await dai.balanceOf(address); - usdcBalance = await usdc.balanceOf(address); - usdtBalance = await usdt.balanceOf(address); - console.log("New OUSD balance=", ousdUnitsFormat(ousdBalance, 18)); - console.log("New DAI balance=", daiUnitsFormat(daiBalance, 18)); - console.log("New USDC balance=", usdcUnitsFormat(usdcBalance, 18)); - console.log("New USDT balance=", usdtUnitsFormat(usdtBalance, 18)); -} - -/** - * Redeems OUSD on local or fork. - */ -async function redeem(taskArguments, hre) { - const addresses = require("../utils/addresses"); - const { - ousdUnits, - ousdUnitsFormat, - daiUnitsFormat, - usdcUnitsFormat, - usdtUnitsFormat, - isFork, - isLocalhost, - } = require("../test/helpers"); - - if (!isFork && !isLocalhost) { - throw new Error("Task can only be used on local or fork"); - } - - const ousdProxy = await ethers.getContract("OUSDProxy"); - const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); - - const vaultProxy = await ethers.getContract("VaultProxy"); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); - - let dai, usdc, usdt; - if (isFork) { - dai = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.DAI); - usdc = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.USDC); - usdt = await hre.ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); - } else { - dai = await hre.ethers.getContract("MockDAI"); - usdc = await hre.ethers.getContract("MockUSDC"); - usdt = await hre.ethers.getContract("MockUSDT"); - } - - const numAccounts = Number(taskArguments.num) || defaultNumAccounts; - const accountIndex = Number(taskArguments.index) || defaultAccountIndex; - const redeemAmount = taskArguments.amount || defaultRedeemAmount; - - const signers = await hre.ethers.getSigners(); - for (let i = accountIndex; i < accountIndex + numAccounts; i++) { - const signer = signers[i]; - const address = signer.address; - console.log( - `Redeeming ${redeemAmount} OUSD for account ${i} at address ${address}` - ); - - // Show the current balances. - let ousdBalance = await ousd.balanceOf(address); - let daiBalance = await dai.balanceOf(address); - let usdcBalance = await usdc.balanceOf(address); - let usdtBalance = await usdt.balanceOf(address); - console.log("OUSD balance=", ousdUnitsFormat(ousdBalance, 18)); - console.log("DAI balance=", daiUnitsFormat(daiBalance, 18)); - console.log("USDC balance=", usdcUnitsFormat(usdcBalance, 6)); - console.log("USDT balance=", usdtUnitsFormat(usdtBalance, 6)); - - // Redeem. - await vault - .connect(signer) - .redeem(ousdUnits(redeemAmount), 0, { gasLimit: 2000000 }); - - // Show the new balances. - ousdBalance = await ousd.balanceOf(address); - daiBalance = await dai.balanceOf(address); - usdcBalance = await usdc.balanceOf(address); - usdtBalance = await usdt.balanceOf(address); - console.log("New OUSD balance=", ousdUnitsFormat(ousdBalance, 18)); - console.log("New DAI balance=", daiUnitsFormat(daiBalance, 18)); - console.log("New USDC balance=", usdcUnitsFormat(usdcBalance, 18)); - console.log("New USDT balance=", usdtUnitsFormat(usdtBalance, 18)); - } -} - // Sends OUSD to a destination address. async function transfer(taskArguments) { const { @@ -437,8 +215,5 @@ async function transfer(taskArguments) { module.exports = { accounts, fund, - mint, - redeem, - redeemFor, transfer, }; diff --git a/contracts/tasks/block.js b/contracts/tasks/block.js new file mode 100644 index 0000000000..6dc2fbbcd2 --- /dev/null +++ b/contracts/tasks/block.js @@ -0,0 +1,30 @@ +const log = require("../utils/logger")("task:block"); + +async function getBlock(block) { + // Get the block to get all the data from + const blockTag = !block ? await hre.ethers.provider.getBlockNumber() : block; + log(`block: ${blockTag}`); + + return blockTag; +} + +async function getDiffBlocks(taskArguments, hre) { + // Get the block to get all the data from + const blockTag = !taskArguments.block + ? await hre.ethers.provider.getBlockNumber() + : taskArguments.block; + console.log(`block: ${blockTag}`); + const fromBlockTag = taskArguments.fromBlock || 0; + const diffBlocks = fromBlockTag > 0; + + return { + diffBlocks, + blockTag, + fromBlockTag, + }; +} + +module.exports = { + getBlock, + getDiffBlocks, +}; diff --git a/contracts/tasks/curve.js b/contracts/tasks/curve.js index 3b646f0888..2df4adc8fd 100644 --- a/contracts/tasks/curve.js +++ b/contracts/tasks/curve.js @@ -1,85 +1,85 @@ const { formatUnits, parseUnits } = require("ethers/lib/utils"); const { BigNumber } = require("ethers"); -const poolAbi = require("../test/abi/ousdMetapool.json"); +const ousdPoolAbi = require("../test/abi/ousdMetapool.json"); +const oethPoolAbi = require("../test/abi/oethMetapool.json"); const addresses = require("../utils/addresses"); const { resolveAsset } = require("../utils/assets"); +const { getDiffBlocks } = require("./block"); +const { getSigner } = require("../utils/signers"); + +const log = require("../utils/logger")("task:curve"); /** - * Dumps the current state of a Curve Metapool pool used for AMO + * Hardhat task to dump the current state of a Curve Metapool pool used for AMO */ -async function curvePool(taskArguments, hre) { - // Get the block to get all the data from - const blockTag = !taskArguments.block - ? await hre.ethers.provider.getBlockNumber() - : taskArguments.block; - console.log(`block: ${blockTag}`); - const fromBlockTag = taskArguments.fromBlock || 0; - const diffBlocks = fromBlockTag > 0; +async function curvePoolTask(taskArguments, hre) { + const poolOTokenSymbol = taskArguments.pool; - // Get symbols of tokens in the pool - const oTokenSymbol = taskArguments.pool; - const assetSymbol = oTokenSymbol === "OETH" ? "ETH " : "3CRV"; + const { blockTag, fromBlockTag, diffBlocks } = await getDiffBlocks( + taskArguments, + hre + ); - // Get the contract addresses - const poolAddr = - oTokenSymbol === "OETH" - ? addresses.mainnet.CurveOETHMetaPool - : addresses.mainnet.CurveOUSDMetaPool; - const strategyAddr = - oTokenSymbol === "OETH" - ? addresses.mainnet.ConvexOETHAMOStrategy - : addresses.mainnet.ConvexOUSDAMOStrategy; - const convexRewardsPoolAddr = - oTokenSymbol === "OETH" - ? addresses.mainnet.CVXETHRewardsPool - : addresses.mainnet.CVXRewardsPool; - const poolLPSymbol = oTokenSymbol === "OETH" ? "OETHCRV-f" : "OUSD3CRV-f"; - const vaultAddr = - oTokenSymbol === "OETH" - ? addresses.mainnet.OETHVaultProxy - : addresses.mainnet.VaultProxy; - const oTokenAddr = - oTokenSymbol === "OETH" - ? addresses.mainnet.OETHProxy - : addresses.mainnet.OUSDProxy; - // TODO condition to set to WETH or 3CRV - const asset = await resolveAsset("WETH"); + await curvePool({ + poolOTokenSymbol, + diffBlocks, + blockTag, + fromBlockTag, + }); +} - // Load all the contracts - const pool = await hre.ethers.getContractAt(poolAbi, poolAddr); - const cvxRewardPool = await ethers.getContractAt( - "IRewardStaking", - convexRewardsPoolAddr - ); - const amoStrategy = await ethers.getContractAt("IStrategy", strategyAddr); - const oToken = await ethers.getContractAt("IERC20", oTokenAddr); - const vault = await ethers.getContractAt("IVault", vaultAddr); +/** + * Dumps the current state of a Curve Metapool pool used for AMO + */ +async function curvePool({ + poolOTokenSymbol, + diffBlocks = false, + blockTag, + fromBlockTag, +}) { + // Get symbols and contracts + const { oTokenSymbol, assetSymbol, poolLPSymbol, pool } = + await curveContracts(poolOTokenSymbol); // Get Metapool data - const poolBalancesBefore = - diffBlocks && - (await pool.get_balances({ - blockTag: fromBlockTag, - })); - const poolBalances = await pool.get_balances({ blockTag }); + const totalLPsBefore = + diffBlocks && (await pool.totalSupply({ blockTag: fromBlockTag })); + const totalLPs = await pool.totalSupply({ blockTag }); const virtualPriceBefore = diffBlocks && (await pool.get_virtual_price({ blockTag: fromBlockTag, })); const virtualPrice = await pool.get_virtual_price({ blockTag }); - console.log( - `LP virtual price: ${formatUnits( - virtualPrice - )} ${oTokenSymbol} ${displayDiff( - diffBlocks, - virtualPrice, - virtualPriceBefore, - 6 - )}` + const invariant = virtualPrice.mul(totalLPs).div(parseUnits("1")); + const invariantBefore = + diffBlocks && virtualPriceBefore.mul(totalLPsBefore).div(parseUnits("1")); + + displayProperty( + "Pool LP total supply", + poolLPSymbol, + totalLPs, + totalLPsBefore, + 6 + ); + displayProperty("Invariant (D) ", "", invariant, invariantBefore, 6); + displayProperty( + "LP virtual price", + assetSymbol, + virtualPrice, + virtualPriceBefore, + 6 ); + // Pool balances + const poolBalancesBefore = + diffBlocks && + (await pool.get_balances({ + blockTag: fromBlockTag, + })); + const poolBalances = await pool.get_balances({ blockTag }); + // swap 1 OETH for ETH (OETH/ETH) const price1Before = diffBlocks && @@ -92,10 +92,12 @@ async function curvePool(taskArguments, hre) { parseUnits("1"), { blockTag } ); - console.log( - `${oTokenSymbol}/${assetSymbol} price : ${formatUnits( - price1 - )} ${displayDiff(diffBlocks, price1, price1Before, 6)}` + displayProperty( + `${oTokenSymbol} price`, + `${oTokenSymbol}/${assetSymbol}`, + price1, + price1Before, + 6 ); // swap 1 ETH for OETH (ETH/OETH) @@ -110,33 +112,18 @@ async function curvePool(taskArguments, hre) { parseUnits("1"), { blockTag } ); - console.log( - `${assetSymbol}/${oTokenSymbol} price : ${formatUnits( - price2 - )} ${displayDiff(diffBlocks, price2, price2Before, 6)}` - ); - - // Get the Strategy's Metapool LPs in the Convex pool - const vaultLPsBefore = - diffBlocks && - (await cvxRewardPool.balanceOf(strategyAddr, { blockTag: fromBlockTag })); - const vaultLPs = await cvxRewardPool.balanceOf(strategyAddr, { blockTag }); - const totalLPsBefore = - diffBlocks && (await pool.totalSupply({ blockTag: fromBlockTag })); - const totalLPs = await pool.totalSupply({ blockTag }); - console.log( - `vault Metapool LPs : ${displayPortion( - vaultLPs, - totalLPs, - poolLPSymbol, - "total supply" - )} ${displayDiff(diffBlocks, vaultLPs, vaultLPsBefore)}` + displayProperty( + `${assetSymbol} price`, + `${assetSymbol}/${oTokenSymbol}`, + price2, + price2Before, + 6 ); // Total Metapool assets const totalBalances = poolBalances[0].add(poolBalances[1]); console.log( - `\ntotal assets in pool : ${displayPortion( + `total assets in pool : ${displayPortion( poolBalances[0], totalBalances, assetSymbol, @@ -154,6 +141,55 @@ async function curvePool(taskArguments, hre) { )} ${displayDiff(diffBlocks, poolBalances[1], poolBalancesBefore[1])}` ); + return { + totalLPsBefore, + totalLPs, + poolBalancesBefore, + poolBalances, + totalBalances, + }; +} + +/** + * hardhat task that dumps the current state of a AMO Strategy + */ +async function amoStrategyTask(taskArguments, hre) { + const poolOTokenSymbol = taskArguments.pool; + + const { blockTag, fromBlockTag, diffBlocks } = await getDiffBlocks( + taskArguments, + hre + ); + + const { totalLPsBefore, totalLPs, poolBalancesBefore, poolBalances } = + await curvePool({ + poolOTokenSymbol, + diffBlocks, + blockTag, + fromBlockTag, + }); + + // Get symbols and contracts + const { + oTokenSymbol, + assetSymbol, + poolLPSymbol, + asset, + oToken, + cvxRewardPool, + amoStrategy, + vault, + } = await curveContracts(poolOTokenSymbol); + + // Strategy's Metapool LPs in the Convex pool + const vaultLPsBefore = + diffBlocks && + (await cvxRewardPool.balanceOf(amoStrategy.address, { + blockTag: fromBlockTag, + })); + const vaultLPs = await cvxRewardPool.balanceOf(amoStrategy.address, { + blockTag, + }); // total vault value const vaultTotalValueBefore = diffBlocks && (await vault.totalValue({ blockTag: fromBlockTag })); @@ -179,9 +215,18 @@ async function curvePool(taskArguments, hre) { diffBlocks && oTokenSupplyBefore.sub(strategyOTokensInPoolBefore); const vaultAdjustedTotalSupply = oTokenSupply.sub(strategyOTokensInPool); + // Strategy's Metapool LPs in the Convex pool + console.log( + `\nvault Metapool LPs : ${displayPortion( + vaultLPs, + totalLPs, + poolLPSymbol, + "total supply" + )} ${displayDiff(diffBlocks, vaultLPs, vaultLPsBefore)}` + ); // Strategy's share of the assets in the pool console.log( - `\nassets owned by strategy : ${displayPortion( + `assets owned by strategy : ${displayPortion( strategyAssetsInPool, vaultAdjustedTotalValue, assetSymbol, @@ -260,38 +305,29 @@ async function curvePool(taskArguments, hre) { const assetsInVaultBefore = diffBlocks && - (await asset.balanceOf(vaultAddr, { + (await asset.balanceOf(vault.address, { blockTag: fromBlockTag, })); - const assetsInVault = await asset.balanceOf(vaultAddr, { blockTag }); - console.log( - `\nAssets in vault : ${formatUnits( - assetsInVault - )} ${assetSymbol} ${displayDiff( - diffBlocks, - assetsInVault, - assetsInVaultBefore - )}` + const assetsInVault = await asset.balanceOf(vault.address, { blockTag }); + displayProperty( + "Assets in vault", + assetSymbol, + assetsInVault, + assetsInVaultBefore ); - // Vault's total value v total supply - console.log( - `\nOToken total supply : ${formatUnits( - oTokenSupply - )} ${oTokenSymbol} ${displayDiff( - diffBlocks, - oTokenSupply, - oTokenSupplyBefore - )}` + console.log(""); + displayProperty( + "OToken total supply", + oTokenSymbol, + oTokenSupply, + oTokenSupplyBefore ); - console.log( - `vault assets value : ${formatUnits( - vaultTotalValue - )} ${assetSymbol} ${displayDiff( - diffBlocks, - vaultTotalValue, - vaultTotalValueBefore - )}` + displayProperty( + "vault assets value", + assetSymbol, + vaultTotalValue, + vaultTotalValueBefore ); console.log( `total value - supply : ${displayRatio( @@ -302,23 +338,17 @@ async function curvePool(taskArguments, hre) { )}` ); // Adjusted total value v total supply - console.log( - `OToken adjust supply : ${formatUnits( - vaultAdjustedTotalSupply - )} ${oTokenSymbol} ${displayDiff( - diffBlocks, - vaultAdjustedTotalSupply, - vaultAdjustedTotalSupplyBefore - )}` + displayProperty( + "OToken adjust supply", + oTokenSymbol, + vaultAdjustedTotalSupply, + vaultAdjustedTotalSupplyBefore ); - console.log( - `vault adjusted value : ${formatUnits( - vaultAdjustedTotalValue - )} ${assetSymbol} ${displayDiff( - diffBlocks, - vaultAdjustedTotalValue, - vaultAdjustedTotalValueBefore - )}` + displayProperty( + "vault adjusted value", + assetSymbol, + vaultAdjustedTotalValue, + vaultAdjustedTotalValueBefore ); console.log( `adjusted value - supply : ${displayRatio( @@ -329,6 +359,39 @@ async function curvePool(taskArguments, hre) { )}` ); + // User balances + if (taskArguments.user) { + const user = taskArguments.user; + + // Report asset (ETH or 3CRV) balance + const userAssetBalanceBefore = + oTokenSymbol === "OETH" + ? await hre.ethers.provider.getBalance(user, fromBlockTag) + : await asset.balanceOf(user, { blockTag: fromBlockTag }); + const userAssetBalance = + oTokenSymbol === "OETH" + ? await hre.ethers.provider.getBalance(user, blockTag) + : await asset.balanceOf(user, { blockTag }); + console.log(""); + displayProperty( + "User asset balance", + assetSymbol, + userAssetBalance, + userAssetBalanceBefore + ); + + const userOTokenBalanceBefore = + diffBlocks && (await oToken.balanceOf(user, { blockTag: fromBlockTag })); + const userOTokenBalance = await oToken.balanceOf(user, { blockTag }); + displayProperty( + "User OToken balance", + oTokenSymbol, + userOTokenBalance, + userOTokenBalanceBefore + ); + } + + // Strategy's net minted and threshold const netMintedForStrategy = await vault.netOusdMintedForStrategy({ blockTag, }); @@ -336,14 +399,17 @@ async function curvePool(taskArguments, hre) { await vault.netOusdMintForStrategyThreshold({ blockTag }); const netMintedForStrategyDiff = netMintedForStrategyThreshold.sub(netMintedForStrategy); - console.log( - `\nNet minted for strategy : ${formatUnits(netMintedForStrategy)}` - ); - console.log( - `Net minted threshold : ${formatUnits(netMintedForStrategyThreshold)}` + console.log(""); + displayProperty("Net minted for strategy", assetSymbol, netMintedForStrategy); + displayProperty( + "Net minted threshold", + assetSymbol, + netMintedForStrategyThreshold ); - console.log( - `Net minted for strat diff: ${formatUnits(netMintedForStrategyDiff)}` + displayProperty( + "Net minted for strat diff", + assetSymbol, + netMintedForStrategyDiff ); } @@ -365,6 +431,23 @@ function displayDiff(diffBlocks, newValue, oldValue, precision = 2) { )}${displayPercentage}`; } +function displayProperty( + desc, + unitDesc, + currentValue, + oldValue = false, + decimals +) { + console.log( + `${desc.padEnd(25)}: ${formatUnits(currentValue)} ${unitDesc} ${displayDiff( + oldValue != false, + currentValue, + oldValue, + decimals + )}` + ); +} + function displayPortion(amount, total, units, comparison, precision = 2) { const basisPoints = amount .mul(BigNumber.from(10).pow(2 + precision)) @@ -392,6 +475,203 @@ function displayRatio(a, b, aBefore, bBefore, precision = 6) { )}% ${diffBeforeDisplay}`; } +/************************************ + Curve functions that write + ************************************/ + +async function curveAddTask(taskArguments) { + const { assets, min, otokens, slippage, symbol } = taskArguments; + + // Get symbols and contracts + const { assetSymbol, oTokenSymbol, oToken, pool, poolLPSymbol } = + await curveContracts(symbol); + + const signer = await getSigner(); + const signerAddress = await signer.getAddress(); + + const oTokenAmount = parseUnits(otokens.toString()); + const assetAmount = parseUnits(assets.toString()); + log( + `Adding ${formatUnits(oTokenAmount)} ${oTokenSymbol} and ${formatUnits( + assetAmount + )} ${assetSymbol} to ${poolLPSymbol}` + ); + + const assetBalance = await hre.ethers.provider.getBalance(signerAddress); + const oTokenBalance = await oToken.balanceOf(signerAddress); + log( + `Signer balances ${signerAddress}\n${formatUnits( + assetBalance + )} ${assetSymbol}` + ); + log(`${formatUnits(oTokenBalance)} ${oTokenSymbol}`); + + let minLpTokens; + if (min != undefined) { + minLpTokens = parseUnits(min.toString()); + } else { + const virtualPrice = await pool.get_virtual_price(); + // 3Crv = USD / virtual price + const estimatedLpTokens = oTokenAmount.add(assetAmount).div(virtualPrice); + const slippageScaled = slippage * 100; + minLpTokens = estimatedLpTokens.mul(10000 - slippageScaled).div(10000); + } + log(`min LP tokens: ${formatUnits(minLpTokens)}`); + + if (oTokenAmount.gt(0)) { + await oToken.connect(signer).approve(pool.address, oTokenAmount); + } + + const override = oTokenSymbol === "OETH" ? { value: assetAmount } : {}; + // prettier-ignore + const tx = await pool + .connect(signer)["add_liquidity(uint256[2],uint256)"]( + [assetAmount, oTokenAmount], + minLpTokens, override + ); + + // get event data + const receipt = await tx.wait(); + log(`${receipt.events.length} events emitted`); + const event = receipt.events?.find((e) => e.event === "AddLiquidity"); + log(`invariant ${formatUnits(event.args.invariant)}`); + log( + `fees ${formatUnits(event.args.fees[0])} ${assetSymbol} ${formatUnits( + event.args.fees[1] + )} ${oTokenSymbol}` + ); + log(`token_supply ${formatUnits(event.args.token_supply)}`); +} + +async function curveRemoveTask(taskArguments) { + const { assets, otokens, slippage, symbol } = taskArguments; + + // Get symbols and contracts + const { assetSymbol, oTokenSymbol, oToken, pool, poolLPSymbol } = + await curveContracts(symbol); + + const signer = await getSigner(); + + const oTokenAmount = parseUnits(otokens.toString()); + const assetAmount = parseUnits(assets.toString()); + log( + `Adding ${formatUnits(oTokenAmount)} ${oTokenSymbol} and ${formatUnits( + assetAmount + )} ${assetSymbol} to ${poolLPSymbol}` + ); + + const virtualPrice = await pool.get_virtual_price(); + // 3Crv = USD / virtual price + const estimatedLpTokens = oTokenAmount.add(assetAmount).div(virtualPrice); + const slippageScaled = slippage * 100; + const maxLpTokens = estimatedLpTokens.mul(10000 + slippageScaled).div(10000); + console.log(`min LP tokens: ${formatUnits(maxLpTokens)}`); + + if (oTokenAmount.gt(0)) { + await oToken.connect(signer).approve(pool.address, oTokenAmount); + } + + const override = oTokenSymbol === "OETH" ? { value: assetAmount } : {}; + // prettier-ignore + await pool + .connect(signer)["remove_liquidity_imbalance(uint256[2],uint256)"]( + [assetAmount, oTokenAmount], + maxLpTokens, override + ); + + // TODO get LPs burned +} + +async function curveSwapTask(taskArguments) { + const { amount, from, min, symbol } = taskArguments; + + // Get symbols and contracts + const { pool } = await curveContracts(symbol); + + const signer = await getSigner(); + + const fromAmount = parseUnits(from.toString()); + const minAmount = parseUnits(min.toString()); + log(`Swapping ${formatUnits(fromAmount)} ${from}`); + + const fromIndex = from === "ETH" || from === "3CRV" ? 0 : 1; + const toIndex = from === "ETH" || from === "3CRV" ? 1 : 0; + + const override = from === "ETH" ? { value: amount } : {}; + // prettier-ignore + await pool + .connect(signer).exchange( + fromIndex, + toIndex, + fromAmount, + minAmount, + override + ); + + // TODO get LPs burned +} + +async function curveContracts(oTokenSymbol) { + // Get symbols of tokens in the pool + const assetSymbol = oTokenSymbol === "OETH" ? "ETH " : "3CRV"; + + // Get the contract addresses + const poolAddr = + oTokenSymbol === "OETH" + ? addresses.mainnet.CurveOETHMetaPool + : addresses.mainnet.CurveOUSDMetaPool; + const strategyAddr = + oTokenSymbol === "OETH" + ? addresses.mainnet.ConvexOETHAMOStrategy + : addresses.mainnet.ConvexOUSDAMOStrategy; + const convexRewardsPoolAddr = + oTokenSymbol === "OETH" + ? addresses.mainnet.CVXETHRewardsPool + : addresses.mainnet.CVXRewardsPool; + const poolLPSymbol = oTokenSymbol === "OETH" ? "OETHCRV-f" : "OUSD3CRV-f"; + const vaultAddr = + oTokenSymbol === "OETH" + ? addresses.mainnet.OETHVaultProxy + : addresses.mainnet.VaultProxy; + const oTokenAddr = + oTokenSymbol === "OETH" + ? addresses.mainnet.OETHProxy + : addresses.mainnet.OUSDProxy; + + // Load all the contracts + const asset = + oTokenSymbol === "OETH" + ? await resolveAsset("WETH") + : await resolveAsset("3CRV"); + const pool = + oTokenSymbol === "OETH" + ? await hre.ethers.getContractAt(oethPoolAbi, poolAddr) + : await hre.ethers.getContractAt(ousdPoolAbi, poolAddr); + const cvxRewardPool = await ethers.getContractAt( + "IRewardStaking", + convexRewardsPoolAddr + ); + const amoStrategy = await ethers.getContractAt("IStrategy", strategyAddr); + const oToken = await ethers.getContractAt("IERC20", oTokenAddr); + const vault = await ethers.getContractAt("IVault", vaultAddr); + + return { + oTokenSymbol, + assetSymbol, + poolLPSymbol, + pool, + cvxRewardPool, + amoStrategy, + oToken, + asset, + vault, + }; +} + module.exports = { - curvePool, + amoStrategyTask, + curvePoolTask, + curveAddTask, + curveRemoveTask, + curveSwapTask, }; diff --git a/contracts/tasks/ousd.js b/contracts/tasks/ousd.js deleted file mode 100644 index 4d134fb9b8..0000000000 --- a/contracts/tasks/ousd.js +++ /dev/null @@ -1,24 +0,0 @@ -// Displays an account OUSD balance and credits. -async function balance(taskArguments) { - const ousdProxy = await ethers.getContract("OUSDProxy"); - const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); - - const balance = await ousd.balanceOf(taskArguments.account); - const credits = await ousd.creditsBalanceOf(taskArguments.account); - console.log( - "OUSD balance=", - ethers.utils.formatUnits(balance.toString(), 18) - ); - console.log( - "OUSD credits=", - ethers.utils.formatUnits(credits[0].toString(), 18) - ); - console.log( - "OUSD creditsPerToken=", - ethers.utils.formatUnits(credits[1].toString(), 18) - ); -} - -module.exports = { - balance, -}; diff --git a/contracts/tasks/smokeTest.js b/contracts/tasks/smokeTest.js index 35f4bc02e5..0b57d87e55 100644 --- a/contracts/tasks/smokeTest.js +++ b/contracts/tasks/smokeTest.js @@ -8,11 +8,7 @@ * param receives return value of `beforeDeploy` function. `beforeDeployData` can be used to validate * that the state change is ok */ -const { - getDeployScripts, - getLastDeployScript, - getFilesInFolder, -} = require("../utils/fileSystem"); +const { getDeployScripts, getFilesInFolder } = require("../utils/fileSystem"); const readline = require("readline"); readline.emitKeypressEvents(process.stdin); @@ -51,7 +47,7 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function smokeTestCheck(taskArguments, hre) { +async function smokeTestCheck(taskArguments) { const deployId = taskArguments.deployid; if (!deployId) { diff --git a/contracts/tasks/storageSlots.js b/contracts/tasks/storageSlots.js index d82a76472e..7462d3fa69 100644 --- a/contracts/tasks/storageSlots.js +++ b/contracts/tasks/storageSlots.js @@ -256,7 +256,7 @@ const enrichLayoutData = (layout) => { } else { const fixedArraySize = parseInt(arrayType); sItem.bits = [...Array(fixedArraySize).keys()].map( - (_) => itemToBytesMap[itemType] + () => itemToBytesMap[itemType] ); } } else if (mappingRegex.test(sItem.type)) { @@ -284,7 +284,7 @@ const enrichLayoutData = (layout) => { let currentSlot = 0; let currentSlotBits = 0; // assign slots to mappings - layout.storage = layout.storage.map((sItem, i) => { + layout.storage = layout.storage.map((sItem) => { // current slot is not empty and new slot is required if (sItem.newSlot && currentSlotBits !== 0) { currentSlot += 1; diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index 2610653e68..6b513cb9fb 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -1,10 +1,9 @@ -const { task, types } = require("hardhat/config"); +const { subtask, task, types } = require("hardhat/config"); -const { fund, mint, redeem, redeemFor, transfer } = require("./account"); +const { fund } = require("./account"); const { debug } = require("./debug"); const { env } = require("./env"); const { execute, executeOnFork, proposal, governors } = require("./governance"); -const { balance } = require("./ousd"); const { smokeTest, smokeTestCheck } = require("./smokeTest"); const { storeStorageLayoutForAllContracts, @@ -20,8 +19,34 @@ const { checkOUSDBalances, supplyStakingContractWithOGN, } = require("./compensation"); -const { allocate, capital, harvest, rebase, yield } = require("./vault"); -const { curvePool } = require("./curve"); +const { + tokenAllowance, + tokenBalance, + tokenApprove, + tokenTransfer, + tokenTransferFrom, +} = require("./tokens"); +const { + allocate, + capital, + depositToStrategy, + mint, + rebase, + redeem, + redeemAll, + withdrawFromStrategy, + withdrawAllFromStrategy, + withdrawAllFromStrategies, + yieldTask, +} = require("./vault"); +const { checkDelta, getDelta, takeSnapshot } = require("./valueChecker"); +const { + amoStrategyTask, + curveAddTask, + curveRemoveTask, + curveSwapTask, + curvePoolTask, +} = require("./curve"); // Environment tasks. task("env", "Check env vars are properly set for a Mainnet deployment", env); @@ -36,56 +61,350 @@ task("fund", "Fund accounts on local or fork") "Fund accounts from the .env file instead of mnemonic" ) .setAction(fund); -task("mint", "Mint OUSD on local or fork") - .addOptionalParam("num", "Number of accounts to mint for") - .addOptionalParam("index", "Account start index") - .addOptionalParam("amount", "Amount of OUSD to mint") - .setAction(mint); -task("redeem", "Redeem OUSD on local or fork") - .addOptionalParam("num", "Number of accounts to redeem for") - .addOptionalParam("index", "Account start index") - .addOptionalParam("amount", "Amount of OUSD to redeem") - .setAction(redeem); -task("redeemFor", "Redeem OUSD on local or fork") - .addOptionalParam("account", "Account that calls the redeem") - .addOptionalParam("amount", "Amount of OUSD to redeem") - .setAction(redeemFor); -task("transfer", "Transfer OUSD") - .addParam("index", "Account index") - .addParam("amount", "Amount of OUSD to transfer") - .addParam("to", "Destination address") - .setAction(transfer); // Debug tasks. task("debug", "Print info about contracts and their configs", debug); -// OUSD tasks. -task("balance", "Get OUSD balance of an account") - .addParam("account", "The account's address") - .setAction(balance); +// Token tasks. +subtask("allowance", "Get the token allowance an owner has given to a spender") + .addParam( + "symbol", + "Symbol of the token. eg OETH, WETH, USDT or OGV", + undefined, + types.string + ) + .addParam( + "spender", + "The address of the account or contract that can spend the tokens" + ) + .addOptionalParam( + "owner", + "The address of the account or contract allowing the spending. Default to the signer" + ) + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .setAction(tokenAllowance); +task("allowance").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("balance", "Get the token balance of an account or contract") + .addParam( + "symbol", + "Symbol of the token. eg OETH, WETH, USDT or OGV", + undefined, + types.string + ) + .addOptionalParam( + "account", + "The address of the account or contract. Default to the signer" + ) + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .setAction(tokenBalance); +task("balance").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("approve", "Approve an account or contract to spend tokens") + .addParam( + "symbol", + "Symbol of the token. eg OETH, WETH, USDT or OGV", + undefined, + types.string + ) + .addParam( + "amount", + "Amount of tokens that can be spent", + undefined, + types.float + ) + .addParam( + "spender", + "Address of the account or contract that can spend the tokens", + undefined, + types.string + ) + .setAction(tokenApprove); +task("approve").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("transfer", "Transfer tokens to an account or contract") + .addParam( + "symbol", + "Symbol of the token. eg OETH, WETH, USDT or OGV", + undefined, + types.string + ) + .addParam("amount", "Amount of tokens to transfer", undefined, types.float) + .addParam("to", "Destination address", undefined, types.string) + .setAction(tokenTransfer); +task("transfer").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("transferFrom", "Transfer tokens from an account or contract") + .addParam( + "symbol", + "Symbol of the token. eg OETH, WETH, USDT or OGV", + undefined, + types.string + ) + .addParam("amount", "Amount of tokens to transfer", undefined, types.float) + .addParam("from", "Source address", undefined, types.string) + .addOptionalParam( + "to", + "Destination address. Default to signer", + undefined, + types.string + ) + .setAction(tokenTransferFrom); +task("transferFrom").setAction(async (_, __, runSuper) => { + return runSuper(); +}); // Vault tasks. -task("allocate", "Call allocate() on the Vault", allocate); -task("capital", "Set the Vault's pauseCapital flag", capital); -task("harvest", "Call harvest() on Vault", harvest); -task("rebase", "Call rebase() on the Vault", rebase); -task("yield", "Artificially generate yield on the Vault", yield); +task("allocate", "Call allocate() on the Vault") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .setAction(allocate); +task("allocate").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +task("capital", "Set the Vault's pauseCapital flag") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam( + "pause", + "Whether to pause or unpause the capital allocation", + "true", + types.boolean + ) + .setAction(capital); +task("capital").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +task("rebase", "Call rebase() on the Vault") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .setAction(rebase); +task("rebase").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +task("yield", "Artificially generate yield on the OUSD Vault", yieldTask); + +subtask("mint", "Mint OTokens from the Vault using collateral assets") + .addParam( + "asset", + "Symbol of the collateral asset to deposit. eg WETH, frxETH, USDT, DAI", + undefined, + types.string + ) + .addParam( + "amount", + "Amount of collateral assets to deposit", + undefined, + types.float + ) + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addOptionalParam("min", "Minimum amount of OTokens to mint", 0, types.float) + .setAction(mint); +task("mint").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("redeem", "Redeem OTokens for collateral assets from the Vault") + .addParam("amount", "Amount of OTokens to burn", undefined, types.float) + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addOptionalParam( + "min", + "Minimum amount of collateral to receive", + 0, + types.float + ) + .setAction(redeem); +task("redeem").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("redeemAll", "Redeem all OTokens for collateral assets from the Vault") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addOptionalParam( + "min", + "Minimum amount of collateral to receive", + 0, + types.float + ) + .setAction(redeemAll); +task("redeemAll").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask( + "depositToStrategy", + "Deposits vault collateral assets to a vault strategy" +) + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam( + "strategy", + "Address or contract name of the strategy", + undefined, + types.string + ) + .addParam( + "assets", + "Comma separated list of token symbols with no spaces. eg DAI,USDT,USDC or WETH", + undefined, + types.string + ) + .addParam( + "amounts", + "Comma separated list of token amounts with no spaces. eg 1000.123456789,2000.89,5000.123456 or 23.987", + undefined, + types.string + ) + .setAction(depositToStrategy); +task("depositToStrategy").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("withdrawFromStrategy", "Withdraw assets from a vault strategy") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam( + "strategy", + "Address or contract name of the strategy", + undefined, + types.string + ) + .addParam( + "assets", + "Comma separated list of token symbols with no spaces. eg DAI,USDT,USDC or WETH", + undefined, + types.string + ) + .addParam( + "amounts", + "Comma separated list of token amounts with no spaces. eg 1000.123456789,2000.89,5000.123456 or 23.987", + undefined, + types.string + ) + .setAction(withdrawFromStrategy); +task("withdrawFromStrategy").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("withdrawAllFromStrategy", "Withdraw all assets from a vault strategy") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam( + "strategy", + "Address or contract name of the strategy", + undefined, + types.string + ) + .setAction(withdrawAllFromStrategy); +task("withdrawAllFromStrategy").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask( + "withdrawAllFromStrategies", + "Withdraw all assets from all of a vault's strategies" +) + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .setAction(withdrawAllFromStrategies); +task("withdrawAllFromStrategies").setAction(async (_, __, runSuper) => { + return runSuper(); +}); // Governance tasks -task("execute", "Execute a governance proposal") +subtask("execute", "Execute a governance proposal") .addParam("id", "Proposal ID") .addOptionalParam("governor", "Override Governor address") .setAction(execute); -task("executeOnFork", "Enqueue and execute a proposal on the Fork") +task("execute").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("executeOnFork", "Enqueue and execute a proposal on the Fork") .addParam("id", "Id of the proposal") .addOptionalParam("gaslimit", "Execute proposal gas limit") .setAction(executeOnFork); -task("proposal", "Dumps the state of a proposal") +task("executeOnFork").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("proposal", "Dumps the state of a proposal") .addParam("id", "Id of the proposal") .setAction(proposal); -task("governors", "Get list of governors for all contracts").setAction( +task("proposal").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("governors", "Get list of governors for all contracts").setAction( governors ); +task("governors").setAction(async (_, __, runSuper) => { + return runSuper(); +}); // Compensation tasks task("isAdjusterLocked", "Is adjuster on Compensation claims locked").setAction( @@ -155,7 +474,33 @@ task("showStorageLayout", "Visually show the storage layout of the contract") .setAction(showStorageLayout); // Curve Pools -task("curvePool", "Dumps the current state of a Curve pool") +subtask("curvePool", "Dumps the current state of a Curve pool") + .addParam("pool", "Symbol of the curve Metapool. OUSD or OETH") + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .addOptionalParam( + "fromBlock", + "Block number to compare back to. (default: no diff)", + undefined, + types.int + ) + .addOptionalParam( + "user", + "Address of user adding, removing or swapping tokens. (default: no user)", + undefined, + types.string + ) + .setAction(curvePoolTask); +task("curvePool").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +// Curve Pools +subtask("amoStrat", "Dumps the current state of a AMO strategy") .addParam("pool", "Symbol of the curve Metapool. OUSD or OETH") .addOptionalParam( "block", @@ -169,4 +514,136 @@ task("curvePool", "Dumps the current state of a Curve pool") undefined, types.int ) - .setAction(curvePool); + .addOptionalParam( + "user", + "Address of user adding, removing or swapping tokens. (default: no user)", + undefined, + types.string + ) + .setAction(amoStrategyTask); +task("amoStrat").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("curveAdd", "Add liquidity to Curve Metapool") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam("otokens", "Amount of OTokens. eg OETH or OUSD", 0, types.float) + .addParam("assets", "Amount of assets. eg ETH or 3CRV", 0, types.float) + .addOptionalParam( + "slippage", + "Max allowed slippage as a percentage to 2 decimal places.", + 1.0, + types.float + ) + .addOptionalParam( + "min", + "Min Metapool LP tokens to be minted.", + undefined, + types.float + ) + .setAction(curveAddTask); +task("curveAdd").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("curveRemove", "Remove liquidity from Curve Metapool") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam("otokens", "Amount of OTokens. eg OETH or OUSD", 0, types.float) + .addParam("assets", "Amount of assets. eg ETH or 3CRV", 0, types.float) + .addOptionalParam( + "slippage", + "Max allowed slippage as a percentage to 2 decimal places.", + 1.0, + types.float + ) + .setAction(curveRemoveTask); +task("curveRemove").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("curveSwap", "Swap Metapool tokens") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam( + "from", + "Symbol of the from token. eg OETH, ETH, 3CRV, OUSD", + undefined, + types.string + ) + .addParam("amount", "Amount of from tokens.", 0, types.float) + .addOptionalParam("min", "Min tokens out.", 0, types.float) + .setAction(curveSwapTask); +task("curveSwap").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +// Vault Value Checker +subtask("vaultDelta", "Get a vaults's delta values") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addOptionalParam( + "block", + "Block number. (default: latest)", + undefined, + types.int + ) + .setAction(getDelta); +task("vaultDelta").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("takeSnapshot", "Takes a snapshot of a vault's values") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .setAction(takeSnapshot); +task("takeSnapshot").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask("checkDelta", "Checks a vault's delta values") + .addOptionalParam( + "symbol", + "Symbol of the OToken. eg OETH or OUSD", + "OETH", + types.string + ) + .addParam("profit", "Expected profit", undefined, types.float) + .addParam("profitVariance", "Allowed profit variance", undefined, types.float) + .addParam( + "vaultChange", + "Expected change in total supply ", + undefined, + types.float + ) + .addParam( + "vaultChangeVariance", + "Allowed total supply variance", + undefined, + types.float + ) + .setAction(checkDelta); +task("checkDelta").setAction(async (_, __, runSuper) => { + return runSuper(); +}); diff --git a/contracts/tasks/tokens.js b/contracts/tasks/tokens.js new file mode 100644 index 0000000000..a2a7742949 --- /dev/null +++ b/contracts/tasks/tokens.js @@ -0,0 +1,107 @@ +const { parseUnits, formatUnits } = require("ethers/lib/utils"); + +const { resolveAsset } = require("../utils/assets"); +const { getSigner } = require("../utils/signers"); +const { logTxDetails } = require("../utils/txLogger"); +const { ethereumAddress } = require("../utils/regex"); +const { getBlock } = require("./block"); + +const log = require("../utils/logger")("task:tokens"); + +async function tokenBalance(taskArguments) { + const { account, block, symbol } = taskArguments; + const signer = await getSigner(); + + const asset = await resolveAsset(symbol); + const accountAddr = account || (await signer.getAddress()); + + const blockTag = await getBlock(block); + + const balance = await asset + .connect(signer) + .balanceOf(accountAddr, { blockTag }); + + const decimals = await asset.decimals(); + console.log(`${accountAddr} has ${formatUnits(balance, decimals)} ${symbol}`); +} +async function tokenAllowance(taskArguments) { + const { block, owner, spender, symbol } = taskArguments; + const signer = await getSigner(); + + const asset = await resolveAsset(symbol); + const ownerAddr = owner || (await signer.getAddress()); + + const blockTag = await getBlock(block); + + const balance = await asset + .connect(signer) + .allowance(ownerAddr, spender, { blockTag }); + + const decimals = await asset.decimals(); + console.log( + `${ownerAddr} has allowed ${spender} to spend ${formatUnits( + balance, + decimals + )} ${symbol}` + ); +} + +async function tokenApprove(taskArguments) { + const { amount, symbol, spender } = taskArguments; + const signer = await getSigner(); + + if (!spender.match(ethereumAddress)) { + throw new Error(`Invalid Ethereum address: ${spender}`); + } + + const asset = await resolveAsset(symbol); + const assetUnits = parseUnits(amount.toString(), await asset.decimals()); + + log(`About to approve ${spender} to spend ${amount} ${symbol}`); + const tx = await asset.connect(signer).approve(spender, assetUnits); + await logTxDetails(tx, "approve"); +} + +async function tokenTransfer(taskArguments) { + const { amount, symbol, to } = taskArguments; + const signer = await getSigner(); + + if (!to.match(ethereumAddress)) { + throw new Error(`Invalid Ethereum address: ${to}`); + } + + const asset = await resolveAsset(symbol); + const assetUnits = parseUnits(amount.toString(), await asset.decimals()); + + log(`About to transfer ${amount} ${symbol} to ${to}`); + const tx = await asset.connect(signer).transfer(to, assetUnits); + await logTxDetails(tx, "transfer"); +} + +async function tokenTransferFrom(taskArguments) { + const { amount, symbol, from, to } = taskArguments; + const signer = await getSigner(); + + if (!from.match(ethereumAddress)) { + throw new Error(`Invalid from Ethereum address: ${to}`); + } + if (to && !to.match(ethereumAddress)) { + throw new Error(`Invalid to Ethereum address: ${to}`); + } + const toAddr = to || (await signer.getAddress()); + + const asset = await resolveAsset(symbol); + const assetUnits = parseUnits(amount.toString(), await asset.decimals()); + + log(`About to transfer ${amount} ${symbol} from ${from} to ${toAddr}`); + const tx = await asset.connect(signer).transferFrom(from, toAddr, assetUnits); + await logTxDetails(tx, "transferFrom"); +} + +module.exports = { + tokenAllowance, + tokenBalance, + tokenApprove, + tokenTransfer, + tokenTransferFrom, +}; diff --git a/contracts/tasks/valueChecker.js b/contracts/tasks/valueChecker.js new file mode 100644 index 0000000000..5356654ce7 --- /dev/null +++ b/contracts/tasks/valueChecker.js @@ -0,0 +1,109 @@ +const { parseUnits, formatUnits } = require("ethers/lib/utils"); + +const { getSigner } = require("../utils/signers"); +const { logTxDetails } = require("../utils/txLogger"); +const { getBlock } = require("./block"); + +const log = require("../utils/logger")("task:valueChecker"); + +async function resolveValueChecker(symbol) { + const contractPrefix = symbol === "OUSD" ? "" : symbol; + const vaultValueChecker = await hre.ethers.getContract( + `${contractPrefix}VaultValueChecker` + ); + + log( + `Resolved ${symbol} VaultValueChecker to address ${vaultValueChecker.address}` + ); + + const vaultProxy = await hre.ethers.getContract( + `${contractPrefix}VaultProxy` + ); + const vault = await hre.ethers.getContractAt("IVault", vaultProxy.address); + log(`Resolved ${symbol} Vault to address ${vault.address}`); + + const oToken = await hre.ethers.getContract(symbol); + log(`Resolved ${symbol} to address ${oToken.address}`); + + return { oToken, vaultValueChecker, vault }; +} + +async function takeSnapshot(taskArguments) { + const { symbol } = taskArguments; + const signer = await getSigner(); + + const { vaultValueChecker } = await resolveValueChecker(symbol); + + log(`About to take vault value snapshot`); + const tx = await vaultValueChecker.connect(signer).takeSnapshot(); + await logTxDetails(tx, "takeSnapshot"); +} + +async function getDelta(taskArguments) { + const { block, symbol } = taskArguments; + + const signer = await getSigner(); + const signerAddr = await signer.getAddress(); + + const { oToken, vaultValueChecker, vault } = await resolveValueChecker( + symbol + ); + + const blockTag = getBlock(block); + + const snapshot = await vaultValueChecker.snapshots(signerAddr, { blockTag }); + + const vaultValueAfter = await vault.totalValue({ blockTag }); + const vaultValueDelta = vaultValueAfter.sub(snapshot.vaultValue); + + log( + `Value change ${formatUnits(vaultValueAfter)} - ${formatUnits( + snapshot.vaultValue + )} = ${formatUnits(vaultValueDelta)}` + ); + + const totalSupplyAfter = await oToken.totalSupply({ blockTag }); + const totalSupplyDelta = totalSupplyAfter.sub(snapshot.totalSupply); + log( + `Supply change ${formatUnits(totalSupplyAfter)} - ${formatUnits( + snapshot.totalSupply + )} = ${formatUnits(totalSupplyDelta)} ${symbol}` + ); + + const profit = vaultValueDelta.sub(totalSupplyDelta); + log( + `Profit: ${formatUnits(vaultValueDelta)} - ${formatUnits( + totalSupplyDelta + )} = ${formatUnits(profit)} ` + ); +} + +async function checkDelta(taskArguments) { + const { profit, profitVariance, symbol, vaultChange, vaultChangeVariance } = + taskArguments; + const signer = await getSigner(); + + const contract = await resolveValueChecker(symbol); + + const profitUnits = parseUnits(profit.toString()); + const profitVarianceUnits = parseUnits(profitVariance.toString()); + const vaultChangeUnits = parseUnits(vaultChange.toString()); + const vaultChangeVarianceUnits = parseUnits(vaultChangeVariance.toString()); + + log(`About to take check value value delta`); + const tx = await contract + .connect(signer) + .checkDelta( + profitUnits, + profitVarianceUnits, + vaultChangeUnits, + vaultChangeVarianceUnits + ); + await logTxDetails(tx, "checkDelta"); +} + +module.exports = { + takeSnapshot, + checkDelta, + getDelta, +}; diff --git a/contracts/tasks/vault.js b/contracts/tasks/vault.js index a59aad2d7f..0c8788727c 100644 --- a/contracts/tasks/vault.js +++ b/contracts/tasks/vault.js @@ -1,79 +1,61 @@ -const { utils } = require("ethers"); +const { parseUnits } = require("ethers/lib/utils"); const addresses = require("../utils/addresses"); +const { resolveAsset } = require("../utils/assets"); +const { getSigner } = require("../utils/signers"); +const { logTxDetails } = require("../utils/txLogger"); +const { ethereumAddress } = require("../utils/regex"); -async function allocate(taskArguments, hre) { - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = hre.ethers.provider.getSigner(deployerAddr); +const log = require("../utils/logger")("task:vault"); - const vaultProxy = await hre.ethers.getContract("VaultProxy"); +async function getContract(hre, symbol) { + const contractPrefix = symbol === "OUSD" ? "" : symbol; + const vaultProxy = await hre.ethers.getContract( + `${contractPrefix}VaultProxy` + ); const vault = await hre.ethers.getContractAt("IVault", vaultProxy.address); + log(`Resolved ${symbol} Vault to address ${vault.address}`); - console.log( - "Sending a transaction to call allocate() on", - vaultProxy.address - ); - let transaction; - transaction = await vault.connect(sDeployer).allocate(); - console.log("Sent. Transaction hash:", transaction.hash); - console.log("Waiting for confirmation..."); - await hre.ethers.provider.waitForTransaction(transaction.hash); - console.log("Allocate transaction confirmed"); -} + const oTokenProxy = await ethers.getContract(`${symbol}Proxy`); + const oToken = await ethers.getContractAt(symbol, oTokenProxy.address); + log(`Resolved ${symbol} OToken to address ${oToken.address}`); -async function harvest(taskArguments, hre) { - const { isMainnet, isFork } = require("../test/helpers"); - const { executeProposal } = require("../utils/deploy"); - const { proposeArgs } = require("../utils/governor"); + return { + vault, + oToken, + }; +} - if (isMainnet) { - throw new Error("The harvest task can not be used on mainnet"); - } - const { governorAddr } = await getNamedAccounts(); - const sGovernor = hre.ethers.provider.getSigner(governorAddr); +async function allocate(taskArguments, hre) { + const symbol = taskArguments.symbol; + const signer = await getSigner(); - const vaultProxy = await hre.ethers.getContract("VaultProxy"); - const vault = await hre.ethers.getContractAt("IVault", vaultProxy.address); + const { vault } = await getContract(hre, symbol); - if (isFork) { - // On the fork, impersonate the guardian and execute a proposal to call harvest. - const propDescription = "Call harvest on vault"; - const propArgs = await proposeArgs([ - { - contract: vault, - signature: "harvest()", - }, - ]); - await executeProposal(propArgs, propDescription); - } else { - // Localhost network. Call harvest directly from the governor account. - console.log( - "Sending a transaction to call harvest() on", - vaultProxy.address - ); - await vault.connect(sGovernor)["harvest()"](); - } - console.log("Harvest done"); + log( + `About to send a transaction to call allocate() on the ${symbol} vault with address ${vault.address}` + ); + const tx = await vault.connect(signer).allocate(); + await logTxDetails(tx, "allocate"); } async function rebase(taskArguments, hre) { - const { withConfirmation } = require("../utils/deploy"); - - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = hre.ethers.provider.getSigner(deployerAddr); + const symbol = taskArguments.symbol; + const signer = await getSigner(); - const vaultProxy = await hre.ethers.getContract("VaultProxy"); - const vault = await hre.ethers.getContractAt("IVault", vaultProxy.address); + const { vault } = await getContract(hre, symbol); - console.log("Sending a transaction to call rebase() on", vaultProxy.address); - await withConfirmation(vault.connect(sDeployer).rebase()); - console.log("Rebase transaction confirmed"); + log( + `About to send a transaction to call rebase() on the ${symbol} vault with address ${vault.address}` + ); + const tx = await vault.connect(signer).rebase(); + await logTxDetails(tx, "harvest"); } /** * Artificially generate yield on the vault by sending it USDT. */ -async function yield(taskArguments, hre) { +async function yieldTask(taskArguments, hre) { const usdtAbi = require("../test/abi/usdt.json").abi; const { ousdUnitsFormat, @@ -100,84 +82,231 @@ async function yield(taskArguments, hre) { usdt = await hre.ethers.getContract("MockUSDT"); } - const vaultProxy = await ethers.getContract("VaultProxy"); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); - - const ousdProxy = await ethers.getContract("OUSDProxy"); - const ousd = await ethers.getContractAt("OUSD", ousdProxy.address); + const { vault, oToken } = await getContract(hre, "OUSD"); - console.log("Sending yield to vault"); - let usdtBalance = await usdt.balanceOf(vaultProxy.address); - console.log("USDT vault balance", usdtUnitsFormat(usdtBalance)); + log("Sending yield to vault"); + let usdtBalance = await usdt.balanceOf(vault.address); + log("USDT vault balance", usdtUnitsFormat(usdtBalance)); let vaultValue = await vault.totalValue(); - console.log("Vault value", ousdUnitsFormat(vaultValue)); - let supply = await ousd.totalSupply(); - console.log("OUSD supply", ousdUnitsFormat(supply)); + log("Vault value", ousdUnitsFormat(vaultValue)); + let supply = await oToken.totalSupply(); + log("OUSD supply", ousdUnitsFormat(supply)); // Transfer 100k USDT to the vault. - await usdt - .connect(richSigner) - .transfer(vaultProxy.address, usdtUnits("100000")); + await usdt.connect(richSigner).transfer(vault.address, usdtUnits("100000")); - usdtBalance = await usdt.balanceOf(vaultProxy.address); - console.log("USDT vault balance", usdtUnitsFormat(usdtBalance)); + usdtBalance = await usdt.balanceOf(vault.address); + log("USDT vault balance", usdtUnitsFormat(usdtBalance)); vaultValue = await vault.totalValue(); - console.log("Vault value", ousdUnitsFormat(vaultValue)); - supply = await ousd.totalSupply(); - console.log("OUSD supply", ousdUnitsFormat(supply)); + log("Vault value", ousdUnitsFormat(vaultValue)); + supply = await oToken.totalSupply(); + log("OUSD supply", ousdUnitsFormat(supply)); } /** * Call the Vault's admin pauseCapital method. */ async function capital(taskArguments, hre) { - const { isMainnet, isFork } = require("../test/helpers"); - const { executeProposal } = require("../utils/deploy"); + const symbol = taskArguments.symbol; + const { isMainnet } = require("../test/helpers"); const { proposeArgs } = require("../utils/governor"); - const param = taskArguments.pause.toLowerCase(); - if (param !== "true" && param !== "false") - throw new Error("Set unpause param to true or false"); - const pause = param === "true"; - console.log("Setting Vault capitalPause to", pause); + const pause = taskArguments.pause; + log("Setting Vault capitalPause to", pause); - const { governorAddr } = await getNamedAccounts(); - const sGovernor = await hre.ethers.provider.getSigner(governorAddr); - - const cVaultProxy = await hre.ethers.getContract("VaultProxy"); - const cVault = await hre.ethers.getContractAt( - "VaultAdmin", - cVaultProxy.address - ); + const sGovernor = await getSigner(); - const propDescription = pause ? "Call pauseCapital" : "Call unpauseCapital"; - const signature = pause ? "pauseCapital()" : "unpauseCapital()"; - const propArgs = await proposeArgs([{ contract: cVault, signature }]); + const { vault } = await getContract(hre, symbol); if (isMainnet) { + const signature = pause ? "pauseCapital()" : "unpauseCapital()"; + const propArgs = await proposeArgs([{ contract: vault, signature }]); + // On Mainnet this has to be handled manually via a multi-sig tx. - console.log("propose, enqueue and execute a governance proposal."); - console.log(`Governor address: ${governorAddr}`); - console.log(`Proposal [targets, values, sigs, datas]:`); - console.log(JSON.stringify(propArgs, null, 2)); - } else if (isFork) { - // On Fork, simulate the governance proposal and execution flow that takes place on Mainnet. - await executeProposal(propArgs, propDescription); + log("propose, enqueue and execute a governance proposal."); + log(`Proposal [targets, values, sigs, datas]:`); + log(JSON.stringify(propArgs, null, 2)); } else { if (pause) { - cVault.connect(sGovernor).pauseCapital(); - console.log("Capital paused on vault."); + const tx = await vault.connect(sGovernor).pauseCapital(); + await logTxDetails(tx, "pauseCapital"); } else { - cVault.connect(sGovernor).unpauseCapital(); - console.log("Capital unpaused on vault."); + const tx = await vault.connect(sGovernor).unpauseCapital(); + await logTxDetails(tx, "unpauseCapital"); } } } +async function mint(taskArguments, hre) { + const { amount, asset, symbol, min } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const cAsset = await resolveAsset(asset); + const assetUnits = parseUnits(amount.toString(), await cAsset.decimals()); + const minUnits = parseUnits(min.toString()); + + await cAsset.connect(signer).approve(vault.address, assetUnits); + + log(`About to mint ${symbol} using ${amount} ${asset}`); + const tx = await vault + .connect(signer) + .mint(cAsset.address, assetUnits, minUnits); + await logTxDetails(tx, "mint"); +} + +async function redeem(taskArguments, hre) { + const { amount, min, symbol } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const oTokenUnits = parseUnits(amount.toString()); + const minUnits = parseUnits(min.toString()); + + log(`About to redeem ${amount} ${symbol}`); + const tx = await vault.connect(signer).redeem(oTokenUnits, minUnits); + await logTxDetails(tx, "redeem"); +} + +async function redeemAll(taskArguments, hre) { + const { min, symbol } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const minUnits = parseUnits(min.toString()); + + log(`About to redeem all ${symbol} tokens`); + const tx = await vault.connect(signer).redeemAll(minUnits); + await logTxDetails(tx, "redeemAll"); +} + +async function resolveStrategyAddress(strategy, hre) { + let strategyAddr = strategy; + if (!strategy.match(ethereumAddress)) { + const strategyContract = await hre.ethers.getContract(strategy); + if (!strategyContract?.address) { + throw Error(`Invalid strategy address or contract name: ${strategy}`); + } + strategyAddr = strategyContract.address; + } + log(`Resolve ${strategy} strategy to address: ${strategyAddr}`); + + return strategyAddr; +} + +async function resolveAssets(assets) { + if (!assets) { + throw Error( + `Invalid assets list: ${assets}. Must be a comma separated list of token symbols. eg DAI,USDT,USDC or WETH` + ); + } + const assetsSymbols = assets.split(","); + const assetContracts = await Promise.all( + assetsSymbols.map(async (symbol) => resolveAsset(symbol)) + ); + const assetAddresses = assetContracts.map((contract) => contract.address); + + log(`${assetAddresses.length} addresses: ${assetAddresses}`); + + return { assetAddresses, assetContracts }; +} + +async function resolveAmounts(amounts, assetContracts) { + if (!amounts) { + throw Error( + `Invalid amounts list: ${amounts}. Must be a comma separated list of floating points numbers` + ); + } + const amountsArr = amounts.split(","); + const amountUnits = await Promise.all( + amountsArr.map(async (amount, i) => + parseUnits(amount, await assetContracts[i].decimals()) + ) + ); + log(`${amountUnits.length} amounts: ${amountUnits}`); + + return amountUnits; +} + +async function depositToStrategy(taskArguments, hre) { + const { amounts, assets, symbol, strategy } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const strategyAddr = await resolveStrategyAddress(strategy, hre); + + const { assetAddresses, assetContracts } = await resolveAssets(assets); + + const amountUnits = await resolveAmounts(amounts, assetContracts); + + log( + `About to deposit to the ${strategy} strategy, amounts ${amounts} for assets ${assets}` + ); + const tx = await vault + .connect(signer) + .depositToStrategy(strategyAddr, assetAddresses, amountUnits); + await logTxDetails(tx, "depositToStrategy"); +} + +async function withdrawFromStrategy(taskArguments, hre) { + const { amounts, assets, symbol, strategy } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const strategyAddr = await resolveStrategyAddress(strategy, hre); + + const { assetAddresses, assetContracts } = await resolveAssets(assets); + + const amountUnits = await resolveAmounts(amounts, assetContracts); + + log( + `About to withdraw from the ${strategy} strategy, amounts ${amounts} for assets ${assets}` + ); + const tx = await vault + .connect(signer) + .withdrawFromStrategy(strategyAddr, assetAddresses, amountUnits); + await logTxDetails(tx, "withdrawFromStrategy"); +} + +async function withdrawAllFromStrategy(taskArguments, hre) { + const { symbol, strategy } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + const strategyAddr = await resolveStrategyAddress(strategy, hre); + + log(`About to withdraw all from the ${strategy} strategy`); + const tx = await vault.connect(signer).withdrawAllFromStrategy(strategyAddr); + await logTxDetails(tx, "withdrawAllFromStrategy"); +} + +async function withdrawAllFromStrategies(taskArguments, hre) { + const { symbol } = taskArguments; + const signer = await getSigner(); + + const { vault } = await getContract(hre, symbol); + + log(`About to withdraw all from all strategies`); + const tx = await vault.connect(signer).withdrawAllFromStrategies(); + await logTxDetails(tx, "withdrawAllFromStrategies"); +} + module.exports = { allocate, capital, - harvest, + depositToStrategy, + mint, rebase, - yield, + redeem, + redeemAll, + withdrawFromStrategy, + withdrawAllFromStrategy, + withdrawAllFromStrategies, + yieldTask, }; diff --git a/contracts/utils/assets.js b/contracts/utils/assets.js index cbfc167e57..fae2b42c42 100644 --- a/contracts/utils/assets.js +++ b/contracts/utils/assets.js @@ -1,5 +1,7 @@ const addresses = require("../utils/addresses"); +const log = require("../utils/logger")("task:assets"); + /** * Resolves a token symbol to a ERC20 token contract. * @param {string} symbol token symbol of the asset. eg OUSD, USDT, stETH, CRV... @@ -19,7 +21,8 @@ const resolveAsset = async (symbol) => { if (!assetAddr) { throw Error(`Failed to resolve symbol "${symbol}" to an address`); } - const asset = await ethers.getContractAt("IERC20", assetAddr); + log(`Resolved ${symbol} to ${assetAddr}`); + const asset = await ethers.getContractAt("IERC20Metadata", assetAddr); return asset; } const asset = await ethers.getContract("Mock" + symbol); diff --git a/contracts/utils/regex.js b/contracts/utils/regex.js new file mode 100644 index 0000000000..d76a5cdac3 --- /dev/null +++ b/contracts/utils/regex.js @@ -0,0 +1,18 @@ +const bytes = /^0x([A-Fa-f0-9]{1,})$/; + +const bytesFixed = (x) => new RegExp(`^0x([A-Fa-f0-9]{${x * 2}})$`); + +const bytes32 = bytesFixed(32); +const ethereumAddress = bytesFixed(20); +const transactionHash = bytes32; + +const privateKey = /^[A-Fa-f0-9]{1,64}$/; + +module.exports = { + bytes, + bytesFixed, + bytes32, + ethereumAddress, + transactionHash, + privateKey, +}; diff --git a/contracts/utils/signers.js b/contracts/utils/signers.js index cae38c6c0e..0c6f627154 100644 --- a/contracts/utils/signers.js +++ b/contracts/utils/signers.js @@ -1,7 +1,54 @@ -const { parseEther } = require("ethers").utils; +const { parseEther, Wallet } = require("ethers").utils; +const { ethereumAddress, privateKey } = require("./regex"); const log = require("./logger")("utils:signers"); +/** + * Signer factory that gets a signer for a hardhat test or task + * If address is passed, use that address as signer. + * If DEPLOYER_PK or GOVERNOR_PK is set, use that private key as signer. + * If a fork and IMPERSONATE is set, impersonate that account. + * else get the first signer from the hardhat node. + * @param {*} address optional address of the signer + * @returns + */ +async function getSigner(address) { + if (address) { + if (!address.match(ethereumAddress)) { + throw Error(`Invalid format of address`); + } + return await hre.ethers.provider.getSigner(address); + } + const pk = process.env.DEPLOYER_PK || process.env.GOVERNOR_PK; + if (pk) { + if (!pk.match(privateKey)) { + throw Error(`Invalid format of private key`); + } + const wallet = new Wallet(pk, hre.ethers.provider); + log(`Using signer ${await wallet.getAddress()} from private key`); + return wallet; + } + + if (process.env.FORK === "true" && process.env.IMPERSONATE) { + let address = process.env.IMPERSONATE; + if (!address.match(ethereumAddress)) { + throw Error( + `Environment variable IMPERSONATE is an invalid Ethereum address or contract name` + ); + } + log( + `Impersonating account ${address} from IMPERSONATE environment variable` + ); + return await impersonateAndFund(address); + } + + const signers = await hre.ethers.getSigners(); + const signer = signers[0]; + log(`Using signer ${await signer.getAddress()}`); + + return signer; +} + /** * Impersonate an account when connecting to a forked node. * @param {*} account the address of the contract or externally owned account to impersonate @@ -47,6 +94,7 @@ async function impersonateAndFund(account, amount = "100") { } module.exports = { + getSigner, impersonateAccount, impersonateAndFund, }; diff --git a/contracts/utils/txLogger.js b/contracts/utils/txLogger.js new file mode 100644 index 0000000000..22afe6561c --- /dev/null +++ b/contracts/utils/txLogger.js @@ -0,0 +1,32 @@ +const { formatUnits } = require("ethers/lib/utils"); + +const log = require("./logger")("utils:txLogger"); + +/** + * Log transaction details after the tx has been sent and mined. + * @param {ContractTransaction} tx transaction sent to the network + * @param {string} method description of the tx. eg method name + * @returns {ContractReceipt} transaction receipt + */ +async function logTxDetails(tx, method) { + log( + `Sent ${method} transaction with hash ${tx.hash} from ${ + tx.from + } with gas price ${tx.gasPrice?.toNumber() / 1e9} Gwei` + ); + const receipt = await tx.wait(); + + // Calculate tx cost in Wei + const txCost = receipt.gasUsed.mul(tx.gasPrice ?? 0); + log( + `Processed ${method} tx in block ${receipt.blockNumber}, using ${ + receipt.gasUsed + } gas costing ${formatUnits(txCost)} ETH` + ); + + return receipt; +} + +module.exports = { + logTxDetails, +}; From c8fd4ebb7e49de90eedb6da3db5963063cfff452 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Jul 2023 00:16:03 +1000 Subject: [PATCH 2/2] Added missing OETH Metapool ABI --- contracts/test/abi/oethMetapool.json | 578 +++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 contracts/test/abi/oethMetapool.json diff --git a/contracts/test/abi/oethMetapool.json b/contracts/test/abi/oethMetapool.json new file mode 100644 index 0000000000..343e425d91 --- /dev/null +++ b/contracts/test/abi/oethMetapool.json @@ -0,0 +1,578 @@ +[ + { + "name": "Transfer", + "inputs": [ + { "name": "sender", "type": "address", "indexed": true }, + { "name": "receiver", "type": "address", "indexed": true }, + { "name": "value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + { "name": "owner", "type": "address", "indexed": true }, + { "name": "spender", "type": "address", "indexed": true }, + { "name": "value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "TokenExchange", + "inputs": [ + { "name": "buyer", "type": "address", "indexed": true }, + { "name": "sold_id", "type": "int128", "indexed": false }, + { "name": "tokens_sold", "type": "uint256", "indexed": false }, + { "name": "bought_id", "type": "int128", "indexed": false }, + { "name": "tokens_bought", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "AddLiquidity", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "token_amounts", "type": "uint256[2]", "indexed": false }, + { "name": "fees", "type": "uint256[2]", "indexed": false }, + { "name": "invariant", "type": "uint256", "indexed": false }, + { "name": "token_supply", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidity", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "token_amounts", "type": "uint256[2]", "indexed": false }, + { "name": "fees", "type": "uint256[2]", "indexed": false }, + { "name": "token_supply", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidityOne", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "token_amount", "type": "uint256", "indexed": false }, + { "name": "coin_amount", "type": "uint256", "indexed": false }, + { "name": "token_supply", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidityImbalance", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "token_amounts", "type": "uint256[2]", "indexed": false }, + { "name": "fees", "type": "uint256[2]", "indexed": false }, + { "name": "invariant", "type": "uint256", "indexed": false }, + { "name": "token_supply", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RampA", + "inputs": [ + { "name": "old_A", "type": "uint256", "indexed": false }, + { "name": "new_A", "type": "uint256", "indexed": false }, + { "name": "initial_time", "type": "uint256", "indexed": false }, + { "name": "future_time", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "StopRampA", + "inputs": [ + { "name": "A", "type": "uint256", "indexed": false }, + { "name": "t", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "CommitNewFee", + "inputs": [{ "name": "new_fee", "type": "uint256", "indexed": false }], + "anonymous": false, + "type": "event" + }, + { + "name": "ApplyNewFee", + "inputs": [{ "name": "fee", "type": "uint256", "indexed": false }], + "anonymous": false, + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "initialize", + "inputs": [ + { "name": "_name", "type": "string" }, + { "name": "_symbol", "type": "string" }, + { "name": "_coins", "type": "address[4]" }, + { "name": "_rate_multipliers", "type": "uint256[4]" }, + { "name": "_A", "type": "uint256" }, + { "name": "_fee", "type": "uint256" } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "transfer", + "inputs": [ + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "transferFrom", + "inputs": [ + { "name": "_from", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "approve", + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "permit", + "inputs": [ + { "name": "_owner", "type": "address" }, + { "name": "_spender", "type": "address" }, + { "name": "_value", "type": "uint256" }, + { "name": "_deadline", "type": "uint256" }, + { "name": "_v", "type": "uint8" }, + { "name": "_r", "type": "bytes32" }, + { "name": "_s", "type": "bytes32" } + ], + "outputs": [{ "name": "", "type": "bool" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "last_price", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ema_price", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_balances", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256[2]" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "admin_fee", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "A", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "A_precise", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_p", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "price_oracle", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_virtual_price", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "calc_token_amount", + "inputs": [ + { "name": "_amounts", "type": "uint256[2]" }, + { "name": "_is_deposit", "type": "bool" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "payable", + "type": "function", + "name": "add_liquidity", + "inputs": [ + { "name": "_amounts", "type": "uint256[2]" }, + { "name": "_min_mint_amount", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "payable", + "type": "function", + "name": "add_liquidity", + "inputs": [ + { "name": "_amounts", "type": "uint256[2]" }, + { "name": "_min_mint_amount", "type": "uint256" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_dy", + "inputs": [ + { "name": "i", "type": "int128" }, + { "name": "j", "type": "int128" }, + { "name": "dx", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "payable", + "type": "function", + "name": "exchange", + "inputs": [ + { "name": "i", "type": "int128" }, + { "name": "j", "type": "int128" }, + { "name": "_dx", "type": "uint256" }, + { "name": "_min_dy", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "payable", + "type": "function", + "name": "exchange", + "inputs": [ + { "name": "i", "type": "int128" }, + { "name": "j", "type": "int128" }, + { "name": "_dx", "type": "uint256" }, + { "name": "_min_dy", "type": "uint256" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity", + "inputs": [ + { "name": "_burn_amount", "type": "uint256" }, + { "name": "_min_amounts", "type": "uint256[2]" } + ], + "outputs": [{ "name": "", "type": "uint256[2]" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity", + "inputs": [ + { "name": "_burn_amount", "type": "uint256" }, + { "name": "_min_amounts", "type": "uint256[2]" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256[2]" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity_imbalance", + "inputs": [ + { "name": "_amounts", "type": "uint256[2]" }, + { "name": "_max_burn_amount", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity_imbalance", + "inputs": [ + { "name": "_amounts", "type": "uint256[2]" }, + { "name": "_max_burn_amount", "type": "uint256" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "calc_withdraw_one_coin", + "inputs": [ + { "name": "_burn_amount", "type": "uint256" }, + { "name": "i", "type": "int128" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity_one_coin", + "inputs": [ + { "name": "_burn_amount", "type": "uint256" }, + { "name": "i", "type": "int128" }, + { "name": "_min_received", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity_one_coin", + "inputs": [ + { "name": "_burn_amount", "type": "uint256" }, + { "name": "i", "type": "int128" }, + { "name": "_min_received", "type": "uint256" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "ramp_A", + "inputs": [ + { "name": "_future_A", "type": "uint256" }, + { "name": "_future_time", "type": "uint256" } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "stop_ramp_A", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "admin_balances", + "inputs": [{ "name": "i", "type": "uint256" }], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "withdraw_admin_fees", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "commit_new_fee", + "inputs": [{ "name": "_new_fee", "type": "uint256" }], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "apply_new_fee", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_ma_exp_time", + "inputs": [{ "name": "_ma_exp_time", "type": "uint256" }], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "version", + "inputs": [], + "outputs": [{ "name": "", "type": "string" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "coins", + "inputs": [{ "name": "arg0", "type": "uint256" }], + "outputs": [{ "name": "", "type": "address" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "balances", + "inputs": [{ "name": "arg0", "type": "uint256" }], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "fee", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_fee", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "admin_action_deadline", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "initial_A", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_A", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "initial_A_time", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_A_time", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "name", + "inputs": [], + "outputs": [{ "name": "", "type": "string" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [{ "name": "", "type": "string" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "balanceOf", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "allowance", + "inputs": [ + { "name": "arg0", "type": "address" }, + { "name": "arg1", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "DOMAIN_SEPARATOR", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "nonces", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ma_exp_time", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ma_last_time", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }] + } +]