Skip to content

Commit

Permalink
post launch and mBTC snap task (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
naddison36 committed Feb 14, 2021
1 parent 7b29b58 commit cabb395
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 6 deletions.
32 changes: 32 additions & 0 deletions README.md
Expand Up @@ -118,3 +118,35 @@ Codebase rules are enforced through a passing [CI](https://circleci.com) (visibl

[1]: https://github.com/nvm-sh/nvm
[2]: https://github.com/trufflesuite/ganache-cli

### Command Line Interface

[Hardhat Tasks](https://hardhat.org/guides/create-task.html) are used for command line interactions with the mStable contracts. The tasks can be found in the [tasks](./tasks) folder.

A separate Hardhat config file [tasks.config.ts](./tasks.config.ts) is used for task config. This inherits from the main Hardhat config file [hardhat.config.ts](./hardhat.config.ts). This avoids circular dependencies when the repository needs to be compiled before the Typechain artifacts have been generated. This means the `--config tasks.config.ts` Hardhat option needs to be used to run the mStable tasks.

Config your network. If you are just using readonly tasks like `mBTC-snap` you don't need to have a signer with Ether in it so the default Hardhat test account is ok to use. For safety, the mainnet config is not committed to the repository to avoid accidentally running tasks against mainnet.

```
mainnet: {
url: process.env.NODE_URL || "",
accounts: {
mnemonic: "test test test test test test test test test test test junk",
},
},
```

**Never commit mainnet private keys, mnemonics or provider URLs to the repository.**

Examples of using the Hardhat tasks

```zsh
# List all Hardhat tasks
hh --config tasks.config.ts

# Set the provider url
export NODE_URL=https://mainnet.infura.io/v3/yourApiKey

# To run the mBTC-snap task against mainnet
yarn task mBTC-snap --network mainnet
```
10 changes: 10 additions & 0 deletions contracts/z_mocks/shared/DeadToken.sol
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.0;

contract DeadToken {
uint8 private _decimals = 18;

function decimals() public view returns (uint8) {
return _decimals;
}
}
1 change: 1 addition & 0 deletions tasks.config.ts
@@ -1,4 +1,5 @@
import { hardhatConfig } from "./hardhat.config"
import "./tasks/deployMbtc"
import "./tasks/mBTC"

export default hardhatConfig
51 changes: 51 additions & 0 deletions tasks/deployMbtc.ts
Expand Up @@ -445,4 +445,55 @@ task("deployMBTC-mainnet", "Deploys the mBTC contracts to Mainnet").setAction(as
// - Add mBTC savingsContract to SavingsManager to enable interest collection
})

task("initMBTC", "Initializes the mBTC and imBTC implementations").setAction(async (_, hre) => {
const { ethers, network } = hre

const [deployer] = await ethers.getSigners()

console.log(`Connecting using ${await deployer.getAddress()} and url ${network.name}`)

const addresses = {
mBtcManager: "0x1E91F826fa8aA4fa4D3F595898AF3A64dd188848",
mBtcImpl: "0x69AD1387dA6b2Ab2eA4bF2BEE68246bc042B587f",
imBtcImpl: "0x1C728F1bda86CD8d19f56E36eb9e24ED3E572A39",
deadToken: "0xB68dEfcA27e80cEb9bCC201fE28edaDc508Ec15b",
}

// mBTC Implementation
const linkedAddress = {
__$1a38b0db2bd175b310a9a3f8697d44eb75$__: addresses.mBtcManager,
}
const mBtcImpl = await new Masset__factory(linkedAddress, deployer).attach(addresses.mBtcImpl)
const tx1 = await mBtcImpl.initialize(
"DEAD",
"DEAD",
DEAD_ADDRESS,
[
{
addr: addresses.deadToken,
integrator: ZERO_ADDRESS,
hasTxFee: false,
status: 0,
},
],
{
a: 100,
limits: {
min: simpleToExactAmount(100, 16),
max: simpleToExactAmount(100, 16),
},
},
)
console.log(`mBTC impl initialize tx ${tx1.hash}`)
const receipt1 = await tx1.wait()
console.log(`mBTC tx mined status ${receipt1.status} used ${receipt1.gasUsed} gas`)

// imBTC Savings Contract
const imBtcImpl = await new SavingsContract__factory(deployer).attach(addresses.imBtcImpl)
const tx2 = await imBtcImpl.initialize(DEAD_ADDRESS, "DEAD", "DEAD")
console.log(`imBTC impl initialize tx ${tx2.hash}`)
const receipt2 = await tx2.wait()
console.log(`imBTC tx mined status ${receipt2.status} used ${receipt2.gasUsed} gas`)
})

module.exports = {}
217 changes: 217 additions & 0 deletions tasks/mBTC.ts
@@ -0,0 +1,217 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-console */
import { btcBassets, capFactor, contracts, getBassetFromAddress, startingCap } from "@utils/btcConstants"
import { fullScale, ONE_DAY } from "@utils/constants"
import { applyDecimals, applyRatio, BN, simpleToExactAmount } from "@utils/math"
import { Signer } from "ethers"
import { formatUnits } from "ethers/lib/utils"
import { task } from "hardhat/config"
import { InvariantValidator__factory, Masset, Masset__factory } from "types/generated"

interface TxSummary {
total: BN
fees: BN
}

const getTvlCap = async (signer: Signer): Promise<BN> => {
const validator = await new InvariantValidator__factory(signer).attach(contracts.mainnet.InvariantValidator)
const tvlStartTime = await validator.startTime()
const weeksSinceLaunch = BN.from(Date.now()).div(1000).sub(tvlStartTime).mul(fullScale).div(604800)
// // e.g. 1e19 + (15e18 * 2.04e36) = 1e19 + 3.06e55
// // startingCap + (capFactor * weeksSinceLaunch**2 / 1e36);
return startingCap.add(capFactor.mul(weeksSinceLaunch.pow(2)).div(fullScale.pow(2)))
}

const getBasket = async (mBtc: Masset, signer: Signer) => {
const tvlCap = await getTvlCap(signer)

const bAssets = await mBtc.getBassets()
const bAssetTotals: BN[] = []
let totalBassets = BN.from(0)
btcBassets.forEach((_, i) => {
const scaledBassetQuantity = applyRatio(bAssets[1][i].vaultBalance, bAssets[1][i].ratio)
bAssetTotals.push(scaledBassetQuantity)
totalBassets = totalBassets.add(scaledBassetQuantity)
})

console.log("\nmBTC basket")
btcBassets.forEach((bAsset, i) => {
const percentage = bAssetTotals[i].mul(100).div(totalBassets)
console.log(`${bAsset.symbol.padEnd(7)} ${formatUnits(bAssetTotals[i]).padEnd(20)} ${percentage.toString().padStart(2)}%`)
})
const surplus = await mBtc.surplus()
console.log(`Surplus ${formatUnits(surplus)}`)
const tvlCapPercentage = totalBassets.mul(100).div(tvlCap)
console.log(`Total ${formatUnits(totalBassets).padStart(21)}`)
console.log(`TVL cap ${formatUnits(tvlCap).padStart(21)} ${tvlCapPercentage}%`)
}

const getSwapRates = async (mBTC: Masset) => {
console.log("\nSwap rates")
for (const inputToken of btcBassets) {
for (const outputToken of btcBassets) {
if (inputToken.symbol !== outputToken.symbol) {
const inputAddress = contracts.mainnet[inputToken.symbol]
const outputAddress = contracts.mainnet[outputToken.symbol]
try {
const inputStr = "0.1"
const input = simpleToExactAmount(inputStr, inputToken.decimals)
const output = await mBTC.getSwapOutput(inputAddress, outputAddress, input)
const scaledInput = applyDecimals(input, inputToken.decimals)
const scaledOutput = applyDecimals(output, outputToken.decimals)
const percent = scaledOutput.sub(scaledInput).mul(1000).div(scaledInput)
console.log(
`${inputStr} ${inputToken.symbol.padEnd(6)} -> ${outputToken.symbol.padEnd(6)} ${formatUnits(
output,
outputToken.decimals,
).padEnd(21)} ${percent.toString().padStart(4)}bps`,
)
} catch (err) {
console.error(`${inputToken.symbol} -> ${outputToken.symbol} ${err.message}`)
}
}
}
}
}

const getBalances = async (mBTC: Masset) => {
const mBtcBalance = await mBTC.totalSupply()
const savingBalance = await mBTC.balanceOf(contracts.mainnet.imBTC)
const sushiPoolBalance = await mBTC.balanceOf(contracts.mainnet.sushiPool)
const mStableFundManagerBalance = await mBTC.balanceOf(contracts.mainnet.fundManager)
const otherBalances = mBtcBalance.sub(savingBalance).sub(sushiPoolBalance).sub(mStableFundManagerBalance)

console.log("\nmBTC Holders")
console.log(`imBTC ${formatUnits(savingBalance).padEnd(20)} ${savingBalance.mul(100).div(mBtcBalance)}%`)
console.log(`Sushi Pool ${formatUnits(sushiPoolBalance).padEnd(20)} ${sushiPoolBalance.mul(100).div(mBtcBalance)}%`)
console.log(
`mStable Fund Manager ${formatUnits(mStableFundManagerBalance).padEnd(20)} ${mStableFundManagerBalance.mul(100).div(mBtcBalance)}%`,
)
console.log(`Others ${formatUnits(otherBalances).padEnd(20)} ${otherBalances.mul(100).div(mBtcBalance)}%`)
}

const getMints = async (mBTC: Masset, currentBlock: number): Promise<TxSummary> => {
const filter = await mBTC.filters.Minted(null, null, null, null, null)
const logs = await mBTC.queryFilter(filter, currentBlock - ONE_DAY.toNumber())

console.log("\nMints in last 24 hours")
console.log("Block#\t Minter\t\t\t\t\t bAsset Masset Quantity")
let total = BN.from(0)
let count = 0
logs.forEach((log) => {
const inputBasset = getBassetFromAddress(log.args.input)
console.log(`${log.blockNumber} ${log.args.minter} ${inputBasset.symbol.padEnd(6)} ${formatUnits(log.args.mAssetQuantity)}`)
total = total.add(log.args.mAssetQuantity)
count += 1
})
console.log(`Count ${count}, Total ${formatUnits(total)}`)
return {
total,
fees: BN.from(0),
}
}

const getRedemptions = async (mBTC: Masset, currentBlock: number): Promise<TxSummary> => {
const filter = await mBTC.filters.Redeemed(null, null, null, null, null, null)
const logs = await mBTC.queryFilter(filter, currentBlock - ONE_DAY.toNumber())

console.log("\nRedemptions in last 24 hours")
console.log("Block#\t Redeemer\t\t\t\t bAsset Masset Quantity\tFee")
let total = BN.from(0)
let fees = BN.from(0)
let count = 0
logs.forEach((log) => {
const outputBasset = getBassetFromAddress(log.args.output)
console.log(
`${log.blockNumber} ${log.args.redeemer} ${outputBasset.symbol.padEnd(6)} ${formatUnits(log.args.mAssetQuantity)} ${formatUnits(
log.args.scaledFee,
)}`,
)
total = total.add(log.args.mAssetQuantity)
fees = fees.add(log.args.scaledFee)
count += 1
})
console.log(`Count ${count}, Total ${formatUnits(total)}`)

return {
total,
fees,
}
}

const getSwaps = async (mBTC: Masset, currentBlock: number): Promise<TxSummary> => {
const filter = await mBTC.filters.Swapped(null, null, null, null, null, null)
const logs = await mBTC.queryFilter(filter, currentBlock - ONE_DAY.toNumber())

console.log("\nSwaps in last 24 hours")
console.log("Block#\t Swapper\t\t\t\t Input Output Output Quantity\tFee")
// Scaled bAsset quantities
let total = BN.from(0)
let fees = BN.from(0)
let count = 0
logs.forEach((log) => {
const inputBasset = getBassetFromAddress(log.args.input)
const outputBasset = getBassetFromAddress(log.args.output)
console.log(
`${log.blockNumber} ${log.args.swapper} ${inputBasset.symbol.padEnd(6)} ${outputBasset.symbol.padEnd(6)} ${formatUnits(
log.args.outputAmount,
outputBasset.decimals,
).padEnd(21)} ${formatUnits(log.args.scaledFee)}`,
)
total = total.add(applyDecimals(log.args.outputAmount, outputBasset.decimals))
fees = fees.add(log.args.scaledFee)
count += 1
})
console.log(`Count ${count}, Total ${formatUnits(total)}`)

return {
total,
fees,
}
}

const outputFees = (mints: TxSummary, swaps: TxSummary, redeems: TxSummary, totalSupply: BN) => {
const totalFees = redeems.fees.add(swaps.fees)
const totalTotals = mints.total.add(redeems.total).add(swaps.total)
console.log("\nFees in the last 24 hours")
console.log(" mBTC Volume\t Fees\t\t Fee %")
console.log(
`Mints ${formatUnits(mints.total).padEnd(22)} ${formatUnits(mints.fees).padEnd(20)} ${mints.fees.mul(100).div(totalFees)}%`,
)
console.log(
`Redeem ${formatUnits(redeems.total).padEnd(22)} ${formatUnits(redeems.fees).padEnd(20)} ${redeems.fees.mul(100).div(totalFees)}%`,
)
console.log(
`Swap ${formatUnits(swaps.total).padEnd(22)} ${formatUnits(swaps.fees).padEnd(20)} ${swaps.fees.mul(100).div(totalFees)}%`,
)
const totalApy = totalFees.mul(36500).div(totalSupply)
console.log(`Total ${formatUnits(totalTotals).padEnd(22)} ${formatUnits(totalFees).padEnd(20)} APY ${totalApy}%`)
}

task("mBTC-snap", "Get the latest data from the mBTC contracts").setAction(async (_, hre) => {
const { ethers } = hre

const [signer] = await ethers.getSigners()

const linkedAddress = {
__$1a38b0db2bd175b310a9a3f8697d44eb75$__: contracts.mainnet.Manager,
}
const mBtc = await new Masset__factory(linkedAddress, signer).attach(contracts.mainnet.mBTC)

const currentBlock = await hre.ethers.provider.getBlockNumber()
console.log(`Latest block ${currentBlock}, ${new Date().toUTCString()}`)

await getBasket(mBtc, signer)
await getBalances(mBtc)
await getSwapRates(mBtc)

const mintSummary = await getMints(mBtc, currentBlock)
const redeemSummary = await getRedemptions(mBtc, currentBlock)
const swapSummary = await getSwaps(mBtc, currentBlock)

const totalSupply = await mBtc.totalSupply()
outputFees(mintSummary, swapSummary, redeemSummary, totalSupply)
})

module.exports = {}
43 changes: 40 additions & 3 deletions test-utils/btcConstants.ts
Expand Up @@ -35,7 +35,7 @@ export const btcBassets: Bassets[] = [
{
name: "Ren BTC",
symbol: "renBTC",
decimals: 18,
decimals: 8,
integrator: ZERO_ADDRESS,
txFee: false,
initialMint: 11000,
Expand All @@ -50,10 +50,47 @@ export const btcBassets: Bassets[] = [
},
{
name: "Wrapped BTC",
symbol: "wBTC",
decimals: 18,
symbol: "WBTC",
decimals: 8,
integrator: ZERO_ADDRESS,
txFee: false,
initialMint: 43000,
},
]

export const contracts = {
mainnet: {
// BTC tokens
renBTC: "0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D",
sBTC: "0xfE18be6b3Bd88A2D2A7f928d00292E7a9963CfC6",
WBTC: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",

// mBTC contracts
mBTC: "0x945Facb997494CC2570096c74b5F66A3507330a1",
imBTC: "0x17d8CBB6Bce8cEE970a4027d1198F6700A7a6c24",
Manager: "0x1E91F826fa8aA4fa4D3F595898AF3A64dd188848",
InvariantValidator: "0xd36050B5F28126b5292B59128ED25E489a0f2F3f",

// Sushi
sushiPool: "0xf5A434FbAA1C00b33Ea141122603C43dE86cc9FE",

fundManager: "0xD8baE7d96df905E46718B6ceE3410F535e11bF20",
},
ropsten: {
// BTC tokens
renBTC: "0xf297a737f46f78cc07b810E544bB0f282C53a4a1",
sBTC: "0x4F85915Ef4409b953aAa70cC0169Cb48fC909C4d",
WBTC: "0x6f19A562A32EC6d6404BeaA8C218C425cA73c451",

// mBTC contracts
mBTC: "0x4A677A48A790f26eac4c97f495E537558Abf6A79",
imBTC: "0xBfe31D984d688628d06Ae2Da1D640Cf5D9e242A5",
},
}

export const getBassetFromAddress = (address: string, network = "mainnet"): Bassets => {
const contract = Object.entries(contracts[network]).find((c) => c[1] === address)
if (!contract) return undefined
const symbol = contract[0]
return btcBassets.find((btcBasset) => btcBasset.symbol === symbol)
}

0 comments on commit cabb395

Please sign in to comment.