From 3a2652ddc7860b42c9751eca973fde29d6ee3f7c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 May 2021 23:51:05 +1000 Subject: [PATCH 1/7] feat: wip hh task for over boosted accounts --- package.json | 3 + tasks.config.ts | 3 +- ...sVault.ts => deployBoostedSavingsVault.ts} | 0 tasks/poker.ts | 107 ++++++++++++++++++ tasks/utils/tokens.ts | 8 ++ yarn.lock | 35 +++++- 6 files changed, 154 insertions(+), 2 deletions(-) rename tasks/{BoostedSavingsVault.ts => deployBoostedSavingsVault.ts} (100%) create mode 100644 tasks/poker.ts diff --git a/package.json b/package.json index 85845dcb..4c93d712 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,9 @@ "eslint-plugin-react": "^7.18.3", "ethereum-waffle": "^3.0.0", "ethers": "^5.0.26", + "ethers-multicall": "^0.2.0", + "graphql": "^15.5.0", + "graphql-request": "^3.4.0", "hardhat": "^2.0.8", "hardhat-gas-reporter": "^1.0.1", "hardhat-typechain": "0.3.3", diff --git a/tasks.config.ts b/tasks.config.ts index 227a6a1f..fc9ef748 100644 --- a/tasks.config.ts +++ b/tasks.config.ts @@ -1,6 +1,7 @@ import config from "./hardhat.config" import "./tasks/deployAaveIntegration" +import "./tasks/deployBoostedSavingsVault" import "./tasks/deployMbtc" import "./tasks/deployFeeders" import "./tasks/deployMV3" @@ -10,7 +11,7 @@ import "./tasks/mBTC" import "./tasks/mUSD" import "./tasks/SaveWrapper" import "./tasks/FeederWrapper" -import "./tasks/BoostedSavingsVault" import "./tasks/ops" +import "./tasks/poker" export default config diff --git a/tasks/BoostedSavingsVault.ts b/tasks/deployBoostedSavingsVault.ts similarity index 100% rename from tasks/BoostedSavingsVault.ts rename to tasks/deployBoostedSavingsVault.ts diff --git a/tasks/poker.ts b/tasks/poker.ts new file mode 100644 index 00000000..8bc3876c --- /dev/null +++ b/tasks/poker.ts @@ -0,0 +1,107 @@ +import { formatUnits } from "@ethersproject/units" +import { fullScale } from "@utils/constants" +import { BN, simpleToExactAmount } from "@utils/math" +import { Signer } from "ethers" +import { Contract, Provider } from "ethers-multicall" +import { gql, GraphQLClient } from "graphql-request" +import { task, types } from "hardhat/config" +import { getDefenderSigner } from "./utils/defender-utils" +import { logTxDetails } from "./utils/deploy-utils" +import { MTA } from "./utils/tokens" + +const maxVMTA = simpleToExactAmount(300000, 18) +const maxBoost = simpleToExactAmount(4, 18) +const minBoost = simpleToExactAmount(1, 18) +const floor = simpleToExactAmount(95, 16) +const coeff = BN.from(45) +const priceCoeff = simpleToExactAmount(1, 17) + +const calcBoost = (raw: BN, vMTA: BN, priceCoefficient = priceCoeff, decimals = 18): BN => { + // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) + const scaledBalance = raw.mul(priceCoefficient).div(simpleToExactAmount(1, decimals)) + + if (scaledBalance.lt(simpleToExactAmount(1, decimals))) return minBoost + + let denom = parseFloat(formatUnits(scaledBalance)) + denom **= 0.875 + const flooredMTA = vMTA.gt(maxVMTA) ? maxVMTA : vMTA + let rhs = floor.add(flooredMTA.mul(coeff).div(10).mul(fullScale).div(simpleToExactAmount(denom))) + rhs = rhs.gt(minBoost) ? rhs : minBoost + return rhs.gt(maxBoost) ? maxBoost : rhs +} + +type AccountBalance = { [index: string]: BN } +const getAccountBalanceMap = async (accounts: string[], tokenAddress: string, signer: Signer): Promise => { + const abi = ["function balanceOf(address owner) view returns (uint256)", "function decimals() view returns (uint8)"] + const token = new Contract(tokenAddress, abi) + + const ethcallProvider = new Provider(signer.provider) + await ethcallProvider.init() + + const callPromises = accounts.map((account) => token.balanceOf(account)) + + console.log(`About to get vMTA balances for ${accounts.length} accounts`) + const balances = (await ethcallProvider.all(callPromises)) as BN[] + const accountBalances: AccountBalance = {} + balances.forEach((balance, i) => { + accountBalances[accounts[i]] = balance + }) + return accountBalances +} + +task("over-boost", "Pokes accounts that are over boosted") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs) => { + const signer = await getDefenderSigner(taskArgs.speed) + + const gqlClient = new GraphQLClient("https://api.thegraph.com/subgraphs/name/mstable/mstable-feeder-pools") + + const query = gql` + { + boostedSavingsVaults { + id + boostCoeff + priceCoeff + stakingToken { + symbol + } + accounts(where: { rawBalance_gt: "0" }) { + account { + id + } + rawBalance + boostedBalance + rewardPerTokenPaid + rewards + lastClaim + lastAction + } + } + } + ` + const gqlData = await gqlClient.request(query) + + // Maps GQL to a list if accounts (addresses) in each vault + const vaultAccounts = gqlData.boostedSavingsVaults.map((vault) => vault.accounts.map((account) => account.account.id)) + const accountsWithDuplicates = vaultAccounts.flat() + const accountsUnique = [...new Set(accountsWithDuplicates)] + + const vMtcBalancesMap = await getAccountBalanceMap(accountsUnique, MTA.saving, signer) + + // For each Boosted Vault + gqlData.boostedSavingsVaults.forEach((vault) => { + console.log(`\nVault with id ${vault.id} for token ${vault.stakingToken.symbol}, ${vault.accounts.length} accounts`) + console.log("Account, Raw Balance, Boosted Balance, rewardPerTokenPaid, boost, vMTA balance") + + vault.accounts.forEach((account, i) => { + const boost = calcBoost(BN.from(account.rawBalance), vMtcBalancesMap[account.account.id]) + console.log( + `${account.account.id}, ${formatUnits(account.rawBalance)}, ${formatUnits(account.boostedBalance)}, ${formatUnits( + account.rewardPerTokenPaid, + )}, ${formatUnits(boost)}, ${formatUnits(vMtcBalancesMap[account.account.id])}`, + ) + }) + }) + }) + +module.exports = {} diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index 32d60d29..9c2f2497 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -156,4 +156,12 @@ export const TBTC: Token = { saving: "0x760ea8CfDcC4e78d8b9cA3088ECD460246DC0731", } +export const MTA: Token = { + symbol: "MTA", + address: "0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", + decimals: 18, + quantityFormatter: "USD", + saving: "0xaE8bC96DA4F9A9613c323478BE181FDb2Aa0E1BF", +} + export const tokens = [mUSD, mBTC, sUSD, USDC, USDT, DAI, GUSD, BUSD, renBTC, sBTC, WBTC, HBTC, TBTC] diff --git a/yarn.lock b/yarn.lock index eb760101..e77b6813 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3107,6 +3107,13 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: node-fetch "2.1.2" whatwg-fetch "2.0.4" +cross-fetch@^3.0.6: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4448,6 +4455,13 @@ ethereumjs-wallet@0.6.5: utf8 "^3.0.0" uuid "^3.3.2" +ethers-multicall@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ethers-multicall/-/ethers-multicall-0.2.0.tgz#91c54e941dbff52079d2af65ff50569b83c6cd75" + integrity sha512-dZeuPw+ROFQZElF8gm1OMvE5pdApRtILQdcG1xDPdGIqixtDGNz8xXGQAdBG/nAdt97tXlMbgp4/mmY5PRAnhA== + dependencies: + ethers "^5.0.0" + ethers@^4.0.32, ethers@^4.0.40: version "4.0.48" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.48.tgz#330c65b8133e112b0613156e57e92d9009d8fbbe" @@ -4683,6 +4697,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-files@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" + integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5361,6 +5380,20 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graphql-request@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.4.0.tgz#3a400cd5511eb3c064b1873afb059196bbea9c2b" + integrity sha512-acrTzidSlwAj8wBNO7Q/UQHS8T+z5qRGquCQRv9J1InwR01BBWV9ObnoE+JS5nCCEj8wSGS0yrDXVDoRiKZuOg== + dependencies: + cross-fetch "^3.0.6" + extract-files "^9.0.0" + form-data "^3.0.0" + +graphql@^15.5.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" + integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -7331,7 +7364,7 @@ node-fetch@2.1.2: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= -node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== From 497c489e17d194cb4f8020c729e14b2e83f7cc6c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Sat, 15 May 2021 00:07:45 +1000 Subject: [PATCH 2/7] feat: get priceCoeff and boostCoeff --- tasks/poker.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tasks/poker.ts b/tasks/poker.ts index 8bc3876c..c06a07ad 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ import { formatUnits } from "@ethersproject/units" import { fullScale } from "@utils/constants" import { BN, simpleToExactAmount } from "@utils/math" @@ -5,6 +7,7 @@ import { Signer } from "ethers" import { Contract, Provider } from "ethers-multicall" import { gql, GraphQLClient } from "graphql-request" import { task, types } from "hardhat/config" +import { BoostedSavingsVault__factory } from "types/generated" import { getDefenderSigner } from "./utils/defender-utils" import { logTxDetails } from "./utils/deploy-utils" import { MTA } from "./utils/tokens" @@ -13,10 +16,10 @@ const maxVMTA = simpleToExactAmount(300000, 18) const maxBoost = simpleToExactAmount(4, 18) const minBoost = simpleToExactAmount(1, 18) const floor = simpleToExactAmount(95, 16) -const coeff = BN.from(45) -const priceCoeff = simpleToExactAmount(1, 17) +// const coeff = BN.from(48) +// const priceCoeff = simpleToExactAmount(58000) -const calcBoost = (raw: BN, vMTA: BN, priceCoefficient = priceCoeff, decimals = 18): BN => { +const calcBoost = (raw: BN, vMTA: BN, priceCoefficient: BN, boostCoeff: BN, decimals = 18): BN => { // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) const scaledBalance = raw.mul(priceCoefficient).div(simpleToExactAmount(1, decimals)) @@ -25,7 +28,7 @@ const calcBoost = (raw: BN, vMTA: BN, priceCoefficient = priceCoeff, decimals = let denom = parseFloat(formatUnits(scaledBalance)) denom **= 0.875 const flooredMTA = vMTA.gt(maxVMTA) ? maxVMTA : vMTA - let rhs = floor.add(flooredMTA.mul(coeff).div(10).mul(fullScale).div(simpleToExactAmount(denom))) + let rhs = floor.add(flooredMTA.mul(boostCoeff).div(10).mul(fullScale).div(simpleToExactAmount(denom))) rhs = rhs.gt(minBoost) ? rhs : minBoost return rhs.gt(maxBoost) ? maxBoost : rhs } @@ -89,19 +92,25 @@ task("over-boost", "Pokes accounts that are over boosted") const vMtcBalancesMap = await getAccountBalanceMap(accountsUnique, MTA.saving, signer) // For each Boosted Vault - gqlData.boostedSavingsVaults.forEach((vault) => { - console.log(`\nVault with id ${vault.id} for token ${vault.stakingToken.symbol}, ${vault.accounts.length} accounts`) - console.log("Account, Raw Balance, Boosted Balance, rewardPerTokenPaid, boost, vMTA balance") + for (const vault of gqlData.boostedSavingsVaults) { + const boostVault = BoostedSavingsVault__factory.connect(vault.id, signer) + const priceCoeff = await boostVault.priceCoeff() + const boostCoeff = await boostVault.boostCoeff() + + console.log( + `\nVault with id ${vault.id} for token ${vault.stakingToken.symbol}, ${vault.accounts.length} accounts, price coeff ${priceCoeff}, boost coeff ${boostCoeff}`, + ) + console.log("Account, Raw Balance, Boosted Balance, rewardPerTokenPaid, vMTA balance, boost") vault.accounts.forEach((account, i) => { - const boost = calcBoost(BN.from(account.rawBalance), vMtcBalancesMap[account.account.id]) + const boost = calcBoost(BN.from(account.rawBalance), priceCoeff, boostCoeff, vMtcBalancesMap[account.account.id]) console.log( `${account.account.id}, ${formatUnits(account.rawBalance)}, ${formatUnits(account.boostedBalance)}, ${formatUnits( account.rewardPerTokenPaid, - )}, ${formatUnits(boost)}, ${formatUnits(vMtcBalancesMap[account.account.id])}`, + )}, ${formatUnits(vMtcBalancesMap[account.account.id])}, ${formatUnits(boost)}`, ) }) - }) + } }) module.exports = {} From be175bde29d20deb487e7d34ce7ba4fef8723676 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 May 2021 17:12:21 +1000 Subject: [PATCH 3/7] feat: created Poker contract --- contracts/savings/Poker.sol | 37 +++++++++++++++++++ tasks/poker.ts | 72 ++++++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 contracts/savings/Poker.sol diff --git a/contracts/savings/Poker.sol b/contracts/savings/Poker.sol new file mode 100644 index 00000000..2b6c5029 --- /dev/null +++ b/contracts/savings/Poker.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +import { IBoostedVaultWithLockup } from "../interfaces/IBoostedVaultWithLockup.sol"; + +struct PokeVaultAccounts { + // Address of the Boosted Vault + address boostVault; + // List of accounts to be poked + address[] accounts; +} + +/** + * @title Poker + * @author mStable + * @notice Pokes accounts on boosted vaults so their vault balances can be recalculated. + * @dev VERSION: 1.0 + * DATE: 2021-04-17 + */ +contract Poker { + + /** + * @dev For each boosted vault, poke all the over boosted accounts. + * @param _vaultAccounts An array of PokeVaultAccounts structs + */ + function poker(PokeVaultAccounts[] calldata _vaultAccounts) external { + uint vaultCount = _vaultAccounts.length; + for(uint i = 0; i < vaultCount; i++) { + IBoostedVaultWithLockup boostVault = IBoostedVaultWithLockup(_vaultAccounts[i].boostVault); + + uint accountsLength = _vaultAccounts[i].accounts.length; + for(uint j = 0; i < accountsLength; j++) { + boostVault.pokeBoost(_vaultAccounts[i].accounts[j]); + } + } + } +} \ No newline at end of file diff --git a/tasks/poker.ts b/tasks/poker.ts index c06a07ad..938f0592 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -7,17 +7,15 @@ import { Signer } from "ethers" import { Contract, Provider } from "ethers-multicall" import { gql, GraphQLClient } from "graphql-request" import { task, types } from "hardhat/config" -import { BoostedSavingsVault__factory } from "types/generated" +import { BoostedSavingsVault__factory, Poker, Poker__factory } from "types/generated" import { getDefenderSigner } from "./utils/defender-utils" -import { logTxDetails } from "./utils/deploy-utils" +import { deployContract, logTxDetails } from "./utils/deploy-utils" import { MTA } from "./utils/tokens" const maxVMTA = simpleToExactAmount(300000, 18) const maxBoost = simpleToExactAmount(4, 18) const minBoost = simpleToExactAmount(1, 18) const floor = simpleToExactAmount(95, 16) -// const coeff = BN.from(48) -// const priceCoeff = simpleToExactAmount(58000) const calcBoost = (raw: BN, vMTA: BN, priceCoefficient: BN, boostCoeff: BN, decimals = 18): BN => { // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) @@ -80,10 +78,19 @@ task("over-boost", "Pokes accounts that are over boosted") lastAction } } + _meta { + block { + number + } + } } ` const gqlData = await gqlClient.request(query) + // eslint-disable-next-line no-underscore-dangle + const blockNumber = gqlData._meta.block.number + console.log(`Results for block number ${blockNumber}`) + // Maps GQL to a list if accounts (addresses) in each vault const vaultAccounts = gqlData.boostedSavingsVaults.map((vault) => vault.accounts.map((account) => account.account.id)) const accountsWithDuplicates = vaultAccounts.flat() @@ -97,20 +104,69 @@ task("over-boost", "Pokes accounts that are over boosted") const priceCoeff = await boostVault.priceCoeff() const boostCoeff = await boostVault.boostCoeff() + const overBoosted: any[] = [] + console.log( `\nVault with id ${vault.id} for token ${vault.stakingToken.symbol}, ${vault.accounts.length} accounts, price coeff ${priceCoeff}, boost coeff ${boostCoeff}`, ) - console.log("Account, Raw Balance, Boosted Balance, rewardPerTokenPaid, vMTA balance, boost") + console.log("Account, Raw Balance, Boosted Balance, vMTA balance, vMTA diff, Boost Actual, Boost Expected, Boost Diff") vault.accounts.forEach((account, i) => { - const boost = calcBoost(BN.from(account.rawBalance), priceCoeff, boostCoeff, vMtcBalancesMap[account.account.id]) + const boostActual = BN.from(account.boostedBalance).mul(1000).div(account.rawBalance).toNumber() + const boostExpected = calcBoost(BN.from(account.rawBalance), vMtcBalancesMap[account.account.id], priceCoeff, boostCoeff) + .div(BN.from(10).pow(15)) + .toNumber() + const boostDiff = boostActual - boostExpected + const vMtaExtra = vMtcBalancesMap[account.account.id].mul(boostDiff).div(1000) + if (vMtaExtra.gt(simpleToExactAmount(100))) { + overBoosted.push({ + ...account, + boostActual, + boostExpected, + boostDiff, + vMtaExtra, + }) + } console.log( `${account.account.id}, ${formatUnits(account.rawBalance)}, ${formatUnits(account.boostedBalance)}, ${formatUnits( - account.rewardPerTokenPaid, - )}, ${formatUnits(vMtcBalancesMap[account.account.id])}, ${formatUnits(boost)}`, + vMtcBalancesMap[account.account.id], + )}, ${formatUnits(vMtaExtra)}, ${formatUnits(boostActual, 3)}, ${formatUnits(boostExpected, 3)}, ${formatUnits( + boostDiff, + 3, + )}`, ) }) + + console.log(`${overBoosted.length} of ${vault.accounts.length} over boosted`) + overBoosted.forEach((account) => { + console.log(`${account.account.id} ${formatUnits(account.boostDiff, 3)}, ${formatUnits(account.vMtaExtra)}`) + }) + const pokeAccounts = overBoosted.map((account) => account.account.id) + console.log(pokeAccounts) } }) +task("deployPoker", "Deploys the Poker contract").setAction(async (_, hre) => { + const { network } = hre + // const [deployer] = await ethers.getSigners() + const deployer = await getDefenderSigner() + + if (network.name !== "mainnet") throw Error("Invalid network") + + await deployContract(new Poker__factory(deployer), "Poker") +}) + +task("vault-balance", "Pokes accounts that are over boosted").setAction(async (taskArgs) => { + const signer = await getDefenderSigner(taskArgs.speed) + + const vaultAddress = "0xf38522f63f40f9dd81abafd2b8efc2ec958a3016" + const accountAddress = "0x25953c127efd1e15f4d2be82b753d49b12d626d7" + const blockNumber = 12449878 + const boostVault = BoostedSavingsVault__factory.connect(vaultAddress, signer) + const balance = await boostVault.balanceOf(accountAddress, { + blockTag: blockNumber, + }) + console.log(`Block number ${blockNumber}, vault ${vaultAddress}, account ${accountAddress}, balance ${formatUnits(balance)}`) +}) + module.exports = {} From f7dbdadf7fa3f3533f63ac37ce77e9e61c2a83c4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 May 2021 17:12:48 +1000 Subject: [PATCH 4/7] chore: moved impersonate into test utils --- test-fork/buy-and-make/recipient-upgrade.spec.ts | 11 ++--------- test-fork/feeders/feeders-deployment.spec.ts | 14 ++------------ test-fork/feeders/feeders-musd-cream.spec.ts | 10 +--------- test-fork/mUSD/aave2-migration.spec.ts | 10 +--------- test-utils/fork.ts | 11 +++++++++++ 5 files changed, 17 insertions(+), 39 deletions(-) create mode 100644 test-utils/fork.ts diff --git a/test-fork/buy-and-make/recipient-upgrade.spec.ts b/test-fork/buy-and-make/recipient-upgrade.spec.ts index 72b170f4..1441e424 100644 --- a/test-fork/buy-and-make/recipient-upgrade.spec.ts +++ b/test-fork/buy-and-make/recipient-upgrade.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai" import { ONE_DAY, ONE_WEEK } from "@utils/constants" import { Signer, ContractFactory, Contract } from "ethers" -import { ethers, network } from "hardhat" +import { network } from "hardhat" import { formatEther, keccak256, toUtf8Bytes } from "ethers/lib/utils" import { increaseTime } from "@utils/time" import { @@ -23,6 +23,7 @@ import { } from "types/generated" import { simpleToExactAmount, BN } from "@utils/math" import { abi as SavingsManagerAbi, bytecode as SavingsManagerBytecode } from "./SavingsManager.json" +import { impersonate } from "@utils/fork" // Accounts that are impersonated const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" @@ -31,14 +32,6 @@ const mUsdWhaleAddress = "0x6595732468A241312bc307F327bA0D64F02b3c20" const balWhale = "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be" const nullAddr = "0xAf40dA2DcE68Bf82bd4C5eE7dA22B2F7bb7ba265" -// impersonates a specific account -const impersonate = async (addr): Promise => { - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [addr], - }) - return ethers.provider.getSigner(addr) -} interface Config { oldRecipient: string nexus: string diff --git a/test-fork/feeders/feeders-deployment.spec.ts b/test-fork/feeders/feeders-deployment.spec.ts index e4b08043..8893bdee 100644 --- a/test-fork/feeders/feeders-deployment.spec.ts +++ b/test-fork/feeders/feeders-deployment.spec.ts @@ -21,19 +21,11 @@ import { InterestValidator__factory, } from "types/generated" import { simpleToExactAmount, BN } from "@utils/math" +import { impersonate } from "@utils/fork" // Accounts that are impersonated const deployerAddress = "0x19F12C947D25Ff8a3b748829D8001cA09a28D46d" -// impersonates a specific account -const impersonate = async (addr): Promise => { - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [addr], - }) - return ethers.provider.getSigner(addr) -} - interface CommonAddresses { nexus: string proxyAdmin: string @@ -165,9 +157,7 @@ const deployFeederPool = async (sender: Signer, addresses: CommonAddresses, feed const mint = async (sender: Signer, bAssets: DeployedFasset[], feederData: FeederData) => { // e.e. $4e18 * 1e18 / 1e18 = 4e18 // e.g. 4e18 * 1e18 / 5e22 = 8e13 or 0.00008 - const scaledTestQty = simpleToExactAmount(4) - .mul(simpleToExactAmount(1)) - .div(feederData.priceCoeff) + const scaledTestQty = simpleToExactAmount(4).mul(simpleToExactAmount(1)).div(feederData.priceCoeff) // Approve spending const approvals: BN[] = [] diff --git a/test-fork/feeders/feeders-musd-cream.spec.ts b/test-fork/feeders/feeders-musd-cream.spec.ts index fe68b14b..c5ee4aec 100644 --- a/test-fork/feeders/feeders-musd-cream.spec.ts +++ b/test-fork/feeders/feeders-musd-cream.spec.ts @@ -1,3 +1,4 @@ +import { impersonate } from "@utils/fork" import { BN, simpleToExactAmount } from "@utils/math" import { expect } from "chai" import { Signer } from "ethers" @@ -25,15 +26,6 @@ const liquidatorAddress = "0xe595D67181D701A5356e010D9a58EB9A341f1DbD" // Not sure why this is 2**96 - 1 and not 2**256 - 1 for CREAM const safeInfinity = BN.from(2).pow(96).sub(1) -// impersonates a specific account -const impersonate = async (addr): Promise => { - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [addr], - }) - return ethers.provider.getSigner(addr) -} - context("mUSD Feeder Pool integration to CREAM", () => { let governor: Signer let deployer: Signer diff --git a/test-fork/mUSD/aave2-migration.spec.ts b/test-fork/mUSD/aave2-migration.spec.ts index 7e9e8249..6cf2cd11 100644 --- a/test-fork/mUSD/aave2-migration.spec.ts +++ b/test-fork/mUSD/aave2-migration.spec.ts @@ -1,5 +1,6 @@ import { formatUnits } from "@ethersproject/units" import { ONE_DAY } from "@utils/constants" +import { impersonate } from "@utils/fork" import { BN, simpleToExactAmount } from "@utils/math" import { increaseTime } from "@utils/time" import { expect } from "chai" @@ -53,15 +54,6 @@ const cDaiAddress = "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643" const safeInfinity = BN.from(2).pow(256).sub(1) -// impersonates a specific account -const impersonate = async (addr): Promise => { - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [addr], - }) - return ethers.provider.getSigner(addr) -} - context("DAI and WBTC migration to integration that can claim stkAave", () => { let governor: Signer let deployer: Signer diff --git a/test-utils/fork.ts b/test-utils/fork.ts new file mode 100644 index 00000000..1321c0f3 --- /dev/null +++ b/test-utils/fork.ts @@ -0,0 +1,11 @@ +import { Signer } from "ethers" +import { ethers, network } from "hardhat" + +// impersonates a specific account +export const impersonate = async (addr: string): Promise => { + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [addr], + }) + return ethers.provider.getSigner(addr) +} From 188bcfb01aa6d38670bd11aadc8e8cdee0569fb8 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 May 2021 00:21:07 +1000 Subject: [PATCH 5/7] chore: optimized gas of Poker contract --- contracts/savings/Poker.sol | 15 ++++-- hardhat-fork.config.ts | 6 +-- tasks/poker.ts | 39 +++++++++----- test-fork/savings/poker.spec.ts | 96 +++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 test-fork/savings/poker.spec.ts diff --git a/contracts/savings/Poker.sol b/contracts/savings/Poker.sol index 2b6c5029..240d09a4 100644 --- a/contracts/savings/Poker.sol +++ b/contracts/savings/Poker.sol @@ -23,14 +23,19 @@ contract Poker { * @dev For each boosted vault, poke all the over boosted accounts. * @param _vaultAccounts An array of PokeVaultAccounts structs */ - function poker(PokeVaultAccounts[] calldata _vaultAccounts) external { + function poke(PokeVaultAccounts[] memory _vaultAccounts) external { uint vaultCount = _vaultAccounts.length; for(uint i = 0; i < vaultCount; i++) { - IBoostedVaultWithLockup boostVault = IBoostedVaultWithLockup(_vaultAccounts[i].boostVault); + PokeVaultAccounts memory vaultAccounts = _vaultAccounts[i]; + address boostVaultAddress = vaultAccounts.boostVault; + require(boostVaultAddress != address(0), "blank vault address"); + IBoostedVaultWithLockup boostVault = IBoostedVaultWithLockup(boostVaultAddress); - uint accountsLength = _vaultAccounts[i].accounts.length; - for(uint j = 0; i < accountsLength; j++) { - boostVault.pokeBoost(_vaultAccounts[i].accounts[j]); + uint accountsLength = vaultAccounts.accounts.length; + for(uint j = 0; j < accountsLength; j++) { + address accountAddress = vaultAccounts.accounts[j]; + require(accountAddress != address(0), "blank address"); + boostVault.pokeBoost(accountAddress); } } } diff --git a/hardhat-fork.config.ts b/hardhat-fork.config.ts index b7cec353..d8960aa7 100644 --- a/hardhat-fork.config.ts +++ b/hardhat-fork.config.ts @@ -6,11 +6,11 @@ export default { ...hardhatConfig.networks, hardhat: { allowUnlimitedContractSize: false, - blockGasLimit: 9000000, - gasPrice: 151000000000, + blockGasLimit: 15000000, + gasPrice: 52000000000, forking: { url: process.env.NODE_URL || "", - blockNumber: 12205156, + blockNumber: 12450700, }, }, }, diff --git a/tasks/poker.ts b/tasks/poker.ts index 938f0592..be8fecf5 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -17,6 +17,8 @@ const maxBoost = simpleToExactAmount(4, 18) const minBoost = simpleToExactAmount(1, 18) const floor = simpleToExactAmount(95, 16) +const pokerAddress = "0x13CCB28f9Bd369B321844c528c0C1C288Ac1387E" + const calcBoost = (raw: BN, vMTA: BN, priceCoefficient: BN, boostCoeff: BN, decimals = 18): BN => { // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) const scaledBalance = raw.mul(priceCoefficient).div(simpleToExactAmount(1, decimals)) @@ -52,11 +54,15 @@ const getAccountBalanceMap = async (accounts: string[], tokenAddress: string, si task("over-boost", "Pokes accounts that are over boosted") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .addOptionalParam("update", "Will send a poke transactions to the Poker contract", false, types.boolean) + .addOptionalParam("minMtaDiff", "Min amount of vMTA over boosted", 500, types.int) .setAction(async (taskArgs) => { + const minMtaDiff = simpleToExactAmount(taskArgs.minMtaDiff) const signer = await getDefenderSigner(taskArgs.speed) + // const [signer] = await ethers.getSigners() + // const signer = await impersonate("0x2f2Db75C5276481E2B018Ac03e968af7763Ed118") const gqlClient = new GraphQLClient("https://api.thegraph.com/subgraphs/name/mstable/mstable-feeder-pools") - const query = gql` { boostedSavingsVaults { @@ -95,9 +101,11 @@ task("over-boost", "Pokes accounts that are over boosted") const vaultAccounts = gqlData.boostedSavingsVaults.map((vault) => vault.accounts.map((account) => account.account.id)) const accountsWithDuplicates = vaultAccounts.flat() const accountsUnique = [...new Set(accountsWithDuplicates)] - - const vMtcBalancesMap = await getAccountBalanceMap(accountsUnique, MTA.saving, signer) - + const vMtaBalancesMap = await getAccountBalanceMap(accountsUnique, MTA.saving, signer) + const pokeVaultAccounts: { + boostVault: string + accounts: string[] + }[] = [] // For each Boosted Vault for (const vault of gqlData.boostedSavingsVaults) { const boostVault = BoostedSavingsVault__factory.connect(vault.id, signer) @@ -105,20 +113,19 @@ task("over-boost", "Pokes accounts that are over boosted") const boostCoeff = await boostVault.boostCoeff() const overBoosted: any[] = [] - console.log( `\nVault with id ${vault.id} for token ${vault.stakingToken.symbol}, ${vault.accounts.length} accounts, price coeff ${priceCoeff}, boost coeff ${boostCoeff}`, ) console.log("Account, Raw Balance, Boosted Balance, vMTA balance, vMTA diff, Boost Actual, Boost Expected, Boost Diff") - vault.accounts.forEach((account, i) => { const boostActual = BN.from(account.boostedBalance).mul(1000).div(account.rawBalance).toNumber() - const boostExpected = calcBoost(BN.from(account.rawBalance), vMtcBalancesMap[account.account.id], priceCoeff, boostCoeff) + const boostExpected = calcBoost(BN.from(account.rawBalance), vMtaBalancesMap[account.account.id], priceCoeff, boostCoeff) .div(BN.from(10).pow(15)) .toNumber() const boostDiff = boostActual - boostExpected - const vMtaExtra = vMtcBalancesMap[account.account.id].mul(boostDiff).div(1000) - if (vMtaExtra.gt(simpleToExactAmount(100))) { + // Calculate how much extra MTA is being distributed = vMTA balance * (actual boost - expected boost) + const vMtaExtra = vMtaBalancesMap[account.account.id].mul(boostDiff).div(1000) + if (vMtaExtra.gt(minMtaDiff)) { overBoosted.push({ ...account, boostActual, @@ -129,20 +136,28 @@ task("over-boost", "Pokes accounts that are over boosted") } console.log( `${account.account.id}, ${formatUnits(account.rawBalance)}, ${formatUnits(account.boostedBalance)}, ${formatUnits( - vMtcBalancesMap[account.account.id], + vMtaBalancesMap[account.account.id], )}, ${formatUnits(vMtaExtra)}, ${formatUnits(boostActual, 3)}, ${formatUnits(boostExpected, 3)}, ${formatUnits( boostDiff, 3, )}`, ) }) - - console.log(`${overBoosted.length} of ${vault.accounts.length} over boosted`) + console.log(`${overBoosted.length} of ${vault.accounts.length} over boosted for ${vault.id}`) overBoosted.forEach((account) => { console.log(`${account.account.id} ${formatUnits(account.boostDiff, 3)}, ${formatUnits(account.vMtaExtra)}`) }) const pokeAccounts = overBoosted.map((account) => account.account.id) console.log(pokeAccounts) + pokeVaultAccounts.push({ + boostVault: vault.id, + accounts: pokeAccounts, + }) + if (taskArgs.update) { + const poker = Poker__factory.connect(pokerAddress, signer) + const tx = await poker.poke(pokeVaultAccounts) + await logTxDetails(tx, "poke Poker") + } } }) diff --git a/test-fork/savings/poker.spec.ts b/test-fork/savings/poker.spec.ts new file mode 100644 index 00000000..b0d7167b --- /dev/null +++ b/test-fork/savings/poker.spec.ts @@ -0,0 +1,96 @@ +import { impersonate } from "@utils/fork" +import { Signer } from "ethers" +import { ethers, network } from "hardhat" +import { deployContract, logTxDetails } from "tasks/utils/deploy-utils" +import { Poker, Poker__factory } from "types/generated" + +const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" + +context("Boosted vault poker", () => { + let deployer: Signer + let poker: Poker + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 12450000, + }, + }, + ], + }) + deployer = await impersonate(deployerAddress) + }) + it("Test connectivity", async () => { + const currentBlock = await ethers.provider.getBlockNumber() + console.log(`Current block ${currentBlock}`) + const startEther = await deployer.getBalance() + console.log(`Deployer ${deployerAddress} has ${startEther} Ether`) + }) + it("Deploy Poker", async () => { + poker = await deployContract(new Poker__factory(deployer), "Poker") + }) + it("Poke single vault", async () => { + const tx = await poker.poke([ + { + boostVault: "0x760ea8cfdcc4e78d8b9ca3088ecd460246dc0731", + accounts: ["0x69e0e2b3d523d3b247d798a49c3fa022a46dd6bd", "0x7e849911b62b91eb3623811a42b9820a4a78755b"], + }, + ]) + await logTxDetails(tx, "poke") + }) + it("Poke all vaults", async () => { + const tx = await poker.poke([ + { + boostVault: "0x760ea8cfdcc4e78d8b9ca3088ecd460246dc0731", + accounts: [ + "0x69e0e2b3d523d3b247d798a49c3fa022a46dd6bd", + "0x7e849911b62b91eb3623811a42b9820a4a78755b", + "0x8d0f5678557192e23d1da1c689e40f25c063eaa5", + "0xb83035f4415233e8765d8a1870852a01dae783f3", + "0xd6293058080c5f03c5c8b954ea87a5cf2d57d74c", + ], + }, + { + boostVault: "0xadeedd3e5768f7882572ad91065f93ba88343c99", + accounts: [ + "0x25953c127efd1e15f4d2be82b753d49b12d626d7", + "0x3841ef91d7e7af21cd2b6017a43f906a99b52bde", + "0x4630914247bfabf1159cfeae827c9597743661bd", + "0x8d0f5678557192e23d1da1c689e40f25c063eaa5", + "0xaa6ad7089a5ce90b36bd2a839acdca240f3e51c8", + "0xf794cf2d946bc6ee6ed905f47db211ebd451aa5f", + "0xf7f502609de883a240536832e7917db9ee802990", + ], + }, + { + boostVault: "0xd124b55f70d374f58455c8aedf308e52cf2a6207", + accounts: [ + "0x4630914247bfabf1159cfeae827c9597743661bd", + "0x7e849911b62b91eb3623811a42b9820a4a78755b", + "0x8d0f5678557192e23d1da1c689e40f25c063eaa5", + "0x9b3f49a186670194f625199b822fcbdfd3feacf7", + "0xaa6ad7089a5ce90b36bd2a839acdca240f3e51c8", + "0xaf7855c1019c6c3d8a4baf8585a496cdafece395", + "0xb83035f4415233e8765d8a1870852a01dae783f3", + "0xdc1f6fd4e237d86d30ae62ef0fbf6412eb07ec36", + "0xe7a8ea7dfa061c0ac7ade4134e597d073600ce53", + "0xf695289caf65ff991ace9957873d2913bcfb321d", + "0xf7f502609de883a240536832e7917db9ee802990", + ], + }, + { + boostVault: "0xf38522f63f40f9dd81abafd2b8efc2ec958a3016", + accounts: ["0xdcd4a180cb5bca150b6b9b2f48a043b3640ed6ed", "0xf6853c77a2452576eae5af424975a101ffc47308"], + }, + { + boostVault: "0xf65d53aa6e2e4a5f4f026e73cb3e22c22d75e35c", + accounts: ["0x25953c127efd1e15f4d2be82b753d49b12d626d7", "0x7e849911b62b91eb3623811a42b9820a4a78755b"], + }, + ]) + await logTxDetails(tx, "poke") + }) +}) From a573fe74e012f80969b8e9b54bffa1792f648e50 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 May 2021 01:15:39 +1000 Subject: [PATCH 6/7] fix: over-boost task --- hardhat-fork.config.ts | 2 +- tasks/poker.ts | 14 +++++++------- tasks/utils/defender-utils.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/hardhat-fork.config.ts b/hardhat-fork.config.ts index d8960aa7..622af437 100644 --- a/hardhat-fork.config.ts +++ b/hardhat-fork.config.ts @@ -10,7 +10,7 @@ export default { gasPrice: 52000000000, forking: { url: process.env.NODE_URL || "", - blockNumber: 12450700, + blockNumber: 12452435, }, }, }, diff --git a/tasks/poker.ts b/tasks/poker.ts index be8fecf5..6db8bfdf 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -17,7 +17,7 @@ const maxBoost = simpleToExactAmount(4, 18) const minBoost = simpleToExactAmount(1, 18) const floor = simpleToExactAmount(95, 16) -const pokerAddress = "0x13CCB28f9Bd369B321844c528c0C1C288Ac1387E" +const pokerAddress = "0x8E1Fd7F5ea7f7760a83222d3d470dFBf8493A03F" const calcBoost = (raw: BN, vMTA: BN, priceCoefficient: BN, boostCoeff: BN, decimals = 18): BN => { // min(m, max(d, (d * 0.95) + c * min(vMTA, f) / USD^b)) @@ -54,7 +54,7 @@ const getAccountBalanceMap = async (accounts: string[], tokenAddress: string, si task("over-boost", "Pokes accounts that are over boosted") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) - .addOptionalParam("update", "Will send a poke transactions to the Poker contract", false, types.boolean) + .addFlag("update", "Will send a poke transactions to the Poker contract") .addOptionalParam("minMtaDiff", "Min amount of vMTA over boosted", 500, types.int) .setAction(async (taskArgs) => { const minMtaDiff = simpleToExactAmount(taskArgs.minMtaDiff) @@ -153,11 +153,11 @@ task("over-boost", "Pokes accounts that are over boosted") boostVault: vault.id, accounts: pokeAccounts, }) - if (taskArgs.update) { - const poker = Poker__factory.connect(pokerAddress, signer) - const tx = await poker.poke(pokeVaultAccounts) - await logTxDetails(tx, "poke Poker") - } + } + if (taskArgs.update) { + const poker = Poker__factory.connect(pokerAddress, signer) + const tx = await poker.poke(pokeVaultAccounts) + await logTxDetails(tx, "poke Poker") } }) diff --git a/tasks/utils/defender-utils.ts b/tasks/utils/defender-utils.ts index 7262fac9..fb6f3214 100644 --- a/tasks/utils/defender-utils.ts +++ b/tasks/utils/defender-utils.ts @@ -2,7 +2,7 @@ import { Speed } from "defender-relay-client" import { Signer } from "ethers" import { DefenderRelayProvider, DefenderRelaySigner } from "defender-relay-client/lib/ethers" -export const getDefenderSigner = async (speed: Speed = "average"): Promise => { +export const getDefenderSigner = async (speed: Speed = "fast"): Promise => { if (!process.env.DEFENDER_API_KEY || !process.env.DEFENDER_API_SECRET) { console.error(`Defender env vars DEFENDER_API_KEY and/or DEFENDER_API_SECRET have not been set`) process.exit(1) From 5ac556370af5cf730625b1e170f4a072244e6cae Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 May 2021 12:53:50 +1000 Subject: [PATCH 7/7] Chore: removed vault-balance HH task --- tasks/poker.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tasks/poker.ts b/tasks/poker.ts index 6db8bfdf..41c8cf89 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -171,17 +171,4 @@ task("deployPoker", "Deploys the Poker contract").setAction(async (_, hre) => { await deployContract(new Poker__factory(deployer), "Poker") }) -task("vault-balance", "Pokes accounts that are over boosted").setAction(async (taskArgs) => { - const signer = await getDefenderSigner(taskArgs.speed) - - const vaultAddress = "0xf38522f63f40f9dd81abafd2b8efc2ec958a3016" - const accountAddress = "0x25953c127efd1e15f4d2be82b753d49b12d626d7" - const blockNumber = 12449878 - const boostVault = BoostedSavingsVault__factory.connect(vaultAddress, signer) - const balance = await boostVault.balanceOf(accountAddress, { - blockTag: blockNumber, - }) - console.log(`Block number ${blockNumber}, vault ${vaultAddress}, account ${accountAddress}, balance ${formatUnits(balance)}`) -}) - module.exports = {}