From d48cd7355236de380f56ed745b3fa5f9eec4f150 Mon Sep 17 00:00:00 2001 From: James Lefrere Date: Wed, 7 Jul 2021 13:31:04 +0200 Subject: [PATCH 01/21] WIP: Add Alchemix Integration - Add `AlchemixIntegration` contract - Add fork tests for alUSD FP --- .../masset/peripheral/AlchemixIntegration.sol | 256 +++++++++ .../Alchemix/AlchemixStakingPools.json | 521 ++++++++++++++++++ test-fork/feeders/feeders-musd-alusd.spec.ts | 162 ++++++ 3 files changed, 939 insertions(+) create mode 100644 contracts/masset/peripheral/AlchemixIntegration.sol create mode 100644 contracts/peripheral/Alchemix/AlchemixStakingPools.json create mode 100644 test-fork/feeders/feeders-musd-alusd.spec.ts diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol new file mode 100644 index 00000000..e9f24ae5 --- /dev/null +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MassetHelpers } from "../../shared/MassetHelpers.sol"; +import { AbstractIntegration } from "./AbstractIntegration.sol"; + +interface IStakingPools { + function claim(uint256 _poolId) external; + function deposit(uint256 _poolId, uint256 _depositAmount) external; + function exit(uint256 _poolId) external; + function getStakeTotalDeposited(address _account, uint256 _poolId) external view returns (uint256); + function getStakeTotalUnclaimed(address _account, uint256 _poolId) external view returns (uint256); + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + function getPoolToken(uint256 _poolId) external view returns (address); + function reward() external view returns (address); + function tokenPoolIds(address _token) external view returns (uint256); + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; +} + +/** + * @title AlchemixIntegration + * @author mStable + * @notice A simple connection to farm ALCX rewards with the Alchemix alUSD pool + * @dev VERSION: 1.0 + * DATE: 2021-07-02 + */ +contract AlchemixIntegration is AbstractIntegration { + using SafeERC20 for IERC20; + + event SkippedWithdrawal(address bAsset, uint256 amount); + event RewardTokenApproved(address rewardToken, address account); + + address public immutable rewardToken; + + IStakingPools private immutable stakingPools; + + /** + * @param _nexus Address of the Nexus + * @param _lp Address of liquidity provider. eg mAsset or feeder pool + * @param _rewardToken Reward token, if any. eg ALCX + * @param _stakingPools Alchemix StakingPools contract address + */ + constructor( + address _nexus, + address _lp, + address _rewardToken, + address _stakingPools + ) AbstractIntegration(_nexus, _lp) { + rewardToken = _rewardToken; + stakingPools = IStakingPools(_stakingPools); + } + + /*************************************** + ADMIN + ****************************************/ + + /** + * @dev Approves Liquidator to spend reward tokens + */ + function approveRewardToken() external onlyGovernor { + address liquidator = nexus.getModule(keccak256("Liquidator")); + require(liquidator != address(0), "Liquidator address is zero"); + + MassetHelpers.safeInfiniteApprove(rewardToken, liquidator); + + emit RewardTokenApproved(rewardToken, liquidator); + } + + /** + @dev Claims any accrued rewardToken for a given bAsset staked + */ + function claim(address _bAsset) external onlyGovernor { + uint256 poolId = _getPoolIdFor(_bAsset); + stakingPools.claim(poolId); + } + + /*************************************** + CORE + ****************************************/ + + /** + * @dev Deposit a quantity of bAsset into the platform. Credited cTokens + * remain here in the vault. Can only be called by whitelisted addresses + * (mAsset and corresponding BasketManager) + * @param _bAsset Address for the bAsset + * @param _amount Units of bAsset to deposit + * @param isTokenFeeCharged Flag that signals if an xfer fee is charged on bAsset + * @return quantityDeposited Quantity of bAsset that entered the platform + */ + function deposit( + address _bAsset, + uint256 _amount, + bool isTokenFeeCharged + ) external override onlyLP nonReentrant returns (uint256 quantityDeposited) { + require(_amount > 0, "Must deposit something"); + + uint256 poolId = _getPoolIdFor(_bAsset); + + quantityDeposited = _amount; + + if (isTokenFeeCharged) { + // If we charge a fee, account for it + uint256 prevBal = this.checkBalance(_bAsset); + stakingPools.deposit(poolId, _amount); + uint256 newBal = this.checkBalance(_bAsset); + quantityDeposited = _min(quantityDeposited, newBal - prevBal); + } else { + // Else just deposit the amount + stakingPools.deposit(poolId, _amount); + } + + emit Deposit(_bAsset, address(stakingPools), quantityDeposited); + } + + /** + * @dev Withdraw a quantity of bAsset from Compound + * @param _receiver Address to which the withdrawn bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + * @param _hasTxFee Is the bAsset known to have a tx fee? + */ + function withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + bool _hasTxFee + ) external override onlyLP nonReentrant { + _withdraw(_receiver, _bAsset, _amount, _amount, _hasTxFee); + } + + /** + * @dev Withdraw a quantity of bAsset from Compound + * @param _receiver Address to which the withdrawn bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + * @param _totalAmount Total units to pull from lending platform + * @param _hasTxFee Is the bAsset known to have a tx fee? + */ + function withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + uint256 _totalAmount, + bool _hasTxFee + ) external override onlyLP nonReentrant { + _withdraw(_receiver, _bAsset, _amount, _totalAmount, _hasTxFee); + } + + function _withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + uint256 _totalAmount, + bool _hasTxFee + ) internal { + require(_totalAmount > 0, "Must withdraw something"); + require(_receiver != address(0), "Must specify recipient"); + + uint256 poolId = _getPoolIdFor(_bAsset); + + uint256 userWithdrawal = _amount; + + if (_hasTxFee) { + require(_amount == _totalAmount, "Cache inactive with tx fee"); + IERC20 b = IERC20(_bAsset); + uint256 prevBal = b.balanceOf(address(this)); + stakingPools.withdraw(poolId, _amount); + uint256 newBal = b.balanceOf(address(this)); + userWithdrawal = _min(userWithdrawal, newBal - prevBal); + } else { + // Redeem Underlying bAsset amount + stakingPools.withdraw(poolId, _totalAmount); + } + + // Send redeemed bAsset to the receiver + IERC20(_bAsset).safeTransfer(_receiver, userWithdrawal); + + emit PlatformWithdrawal(_bAsset, address(stakingPools), _totalAmount, _amount); + } + + /** + * @dev Withdraw a quantity of bAsset from the cache. + * @param _receiver Address to which the bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + */ + function withdrawRaw( + address _receiver, + address _bAsset, + uint256 _amount + ) external override onlyLP nonReentrant { + require(_amount > 0, "Must withdraw something"); + require(_receiver != address(0), "Must specify recipient"); + + IERC20(_bAsset).safeTransfer(_receiver, _amount); + + emit Withdrawal(_bAsset, address(0), _amount); + } + + /** + * @dev Get the total bAsset value held in the platform + * @param _bAsset Address of the bAsset + * @return balance Total value of the bAsset in the platform + */ + function checkBalance(address _bAsset) external view override returns (uint256 balance) { + uint256 poolId = _getPoolIdFor(_bAsset); + return stakingPools.getStakeTotalDeposited(address(this), poolId); + } + + /*************************************** + APPROVALS + ****************************************/ + + /** + * @dev Re-approve the spending of all bAssets by their corresponding cToken, + * if for some reason is it necessary. Only callable through Governance. + */ + function reApproveAllTokens() external onlyGovernor { + uint256 bAssetCount = bAssetsMapped.length; + for (uint256 i = 0; i < bAssetCount; i++) { + address bAsset = bAssetsMapped[i]; + address cToken = bAssetToPToken[bAsset]; + MassetHelpers.safeInfiniteApprove(bAsset, cToken); + } + } + + /** + FIXME do we need this? + * @dev Internal method to respond to the addition of new bAsset / cTokens + * We need to approve the cToken and give it permission to spend the bAsset + * @param _bAsset Address of the bAsset to approve + * @param _cToken This cToken has the approval approval + */ + function _abstractSetPToken(address _bAsset, address _cToken) internal override { + // approve the pool to spend the bAsset + MassetHelpers.safeInfiniteApprove(_bAsset, _cToken); + } + + /*************************************** + HELPERS + ****************************************/ + + /** + * @dev Get the cToken wrapped in the ICERC20 interface for this bAsset. + * Fails if the pToken doesn't exist in our mappings. + * @param _bAsset Address of the bAsset + * @return poolId Corresponding Alchemix StakingPools poolId + */ + function _getPoolIdFor(address _bAsset) internal view returns (uint256 poolId) { + poolId = stakingPools.tokenPoolIds(_bAsset); + require(poolId > 0, "Asset not supported on Alchemix"); + } +} diff --git a/contracts/peripheral/Alchemix/AlchemixStakingPools.json b/contracts/peripheral/Alchemix/AlchemixStakingPools.json new file mode 100644 index 00000000..19b32291 --- /dev/null +++ b/contracts/peripheral/Alchemix/AlchemixStakingPools.json @@ -0,0 +1,521 @@ +{ + "contractName": "StakingPools", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IMintableERC20", + "name": "_reward", + "type": "address" + }, + { + "internalType": "address", + "name": "_governance", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "governance", + "type": "address" + } + ], + "name": "GovernanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pendingGovernance", + "type": "address" + } + ], + "name": "PendingGovernanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardWeight", + "type": "uint256" + } + ], + "name": "PoolRewardWeightUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "rewardRate", + "type": "uint256" + } + ], + "name": "RewardRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_token", + "type": "address" + } + ], + "name": "createPool", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_depositAmount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "exit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolRewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolRewardWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolTotalDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getStakeTotalDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getStakeTotalUnclaimed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingGovernance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reward", + "outputs": [ + { + "internalType": "contract IMintableERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_pendingGovernance", + "type": "address" + } + ], + "name": "setPendingGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rewardRate", + "type": "uint256" + } + ], + "name": "setRewardRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_rewardWeights", + "type": "uint256[]" + } + ], + "name": "setRewardWeights", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "tokenPoolIds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalRewardWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/test-fork/feeders/feeders-musd-alusd.spec.ts b/test-fork/feeders/feeders-musd-alusd.spec.ts new file mode 100644 index 00000000..f6751cdd --- /dev/null +++ b/test-fork/feeders/feeders-musd-alusd.spec.ts @@ -0,0 +1,162 @@ +import { impersonate } from "@utils/fork" +import { BN, simpleToExactAmount } from "@utils/math" +import { expect } from "chai" +import { Signer, constants } from "ethers" +import { ethers, network } from "hardhat" +import { deployContract } from "tasks/utils/deploy-utils" +import { FeederPool, FeederPool__factory, IERC20, IERC20__factory, Masset__factory } from "types/generated" +import { AlchemixIntegration } from "types/generated/AlchemixIntegration" +import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" + +const governorAddress = "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" +const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" +const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" +const mUsdWhaleAddress = "0x69E0E2b3d523D3b247d798a49C3fa022a46DD6bd" +const alUsdWhaleAddress = "0xf9a0106251467fff1ff03e8609aa74fc55a2a45e" + +const nexusAddress = "0xafce80b19a8ce13dec0739a1aab7a028d6845eb3" +const mUsdAddress = "0xe2f2a5c287993345a840db3b0845fbc70f5935a5" +const alUsdAddress = "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9" +const alcxAddress = "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF" +const stakingPoolsAddress = "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" +const liquidatorAddress = "0xe595D67181D701A5356e010D9a58EB9A341f1DbD" + +context("mUSD Feeder Pool integration to Alchemix", () => { + let governor: Signer + let deployer: Signer + let ethWhale: Signer + let mUsdWhale: Signer + let alUsdWhale: Signer + let alUsdFp: FeederPool + let mUsd: IERC20 + let alUsd: IERC20 + let alcxToken: IERC20 + let alchemixIntegration: AlchemixIntegration + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 12779756, + }, + }, + ], + }) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + ethWhale = await impersonate(ethWhaleAddress) + mUsdWhale = await impersonate(mUsdWhaleAddress) + alUsdWhale = await impersonate(alUsdWhaleAddress) + + // send some Ether to addresses that need it + await Promise.all( + [alUsdWhaleAddress, governorAddress, mUsdWhaleAddress].map((recipient) => + ethWhale.sendTransaction({ + to: recipient, + value: simpleToExactAmount(10), + }), + ), + ) + + mUsd = await IERC20__factory.connect(mUsdAddress, deployer) + alUsd = await IERC20__factory.connect(alUsdAddress, deployer) + alcxToken = await IERC20__factory.connect(alcxAddress, deployer) + }) + 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 and initialize integration contract", async () => { + alUsdFp = await deployContract( + new FeederPool__factory( + { + __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: "0x90aE544E8cc76d2867987Ee4f5456C02C50aBd8B", // FeederManager + __$7791d1d5b7ea16da359ce352a2ac3a881c$__: "0x2837C77527c37d61D9763F53005211dACB4125dE", // FeederLogic + }, + deployer, + ), + "alUSD/mUSD Feeder Pool", + [nexusAddress, mUsdAddress], + ) + + const mpAssets = (await Masset__factory.connect(mUsdAddress, deployer).getBassets())[0].map((p) => p[0]) + + await alUsdFp.initialize( + "Feeder Pool mUSD/alUSD", + "fP-mUSD/alUSD", + { addr: mUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, + { addr: alUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, + mpAssets, + { + a: BN.from(225), + limits: { + min: simpleToExactAmount(10, 16), + max: simpleToExactAmount(90, 16), + }, + }, + ) + + alchemixIntegration = await deployContract( + new AlchemixIntegration__factory(deployer), + "Alchemix alUSD Integration", + [nexusAddress, alUsdFp.address, alcxAddress, stakingPoolsAddress], + ) + expect(alchemixIntegration.address).to.length(42) + + await alchemixIntegration.initialize([alUsdAddress], [alcxAddress]) + }) + it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) + + // This will be done via the delayedProxyAdmin on mainnet + await alchemixIntegration.connect(governor).approveRewardToken() + + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) + }) + it("Mint some mUSD/alUSD in the Feeder Pool", async () => { + const alUsdBassetBefore = await alUsdFp.getBasset(alUsdAddress) + const mUsdBassetBefore = await alUsdFp.getBasset(mUsdAddress) + + expect(await alUsd.balanceOf(alUsdFp.address)).to.eq(0) + expect(await mUsd.balanceOf(alUsdFp.address)).to.eq(0) + + const mintAmount = simpleToExactAmount(10000) + + // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) + await mUsd.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) + expect(await mUsd.balanceOf(alUsdWhaleAddress)).to.gte(mintAmount) + + await alUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + await mUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + expect(await alUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) + expect(await mUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) + + await alUsdFp.connect(alUsdWhale).mintMulti([alUsdAddress, mUsdAddress], [mintAmount, mintAmount], 0, alUsdWhaleAddress) + + const alUsdBassetAfter = await alUsdFp.getBasset(alUsdAddress) + const mUsdBassetAfter = await alUsdFp.getBasset(mUsdAddress) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( + alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), + ) + expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount)) + }) + it("Migrate alUSD to the Alchemix integration", async () => { + expect(await alUsd.balanceOf(alUsdFp.address), "Some alUSD in Feeder Pool").to.gt(0) + expect(await alUsd.balanceOf(alchemixIntegration.address), "No alUSD in Integration contract").to.eq(0) + const alUsdInFpBefore = await alUsd.balanceOf(alUsdFp.address) + + // Migrate the alUSD + await alUsdFp.migrateBassets([alUsdAddress], alchemixIntegration.address) + + // All alUSD in the FP should have moved to the integration contract + expect(await alUsd.balanceOf(alchemixIntegration.address), "All alUSD in FP migrated to Integration").to.eq(alUsdInFpBefore) + expect(await alUsd.balanceOf(alUsdFp.address), "No more alUSD in Feeder Pool").to.eq(0) + }) + // Mint alUSD in FP, test integration/FP balances, wait for a block or two, claim rewards + // Liquidate rewards and test output/balances +}) From fdb296b87fb090ff19bfbbbcb005b3beee5335a6 Mon Sep 17 00:00:00 2001 From: alsco77 Date: Thu, 8 Jul 2021 15:25:06 +0100 Subject: [PATCH 02/21] ci: Bump CI --- .../masset/peripheral/AlchemixIntegration.sol | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index e9f24ae5..434555b0 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -8,15 +8,31 @@ import { AbstractIntegration } from "./AbstractIntegration.sol"; interface IStakingPools { function claim(uint256 _poolId) external; + function deposit(uint256 _poolId, uint256 _depositAmount) external; + function exit(uint256 _poolId) external; - function getStakeTotalDeposited(address _account, uint256 _poolId) external view returns (uint256); - function getStakeTotalUnclaimed(address _account, uint256 _poolId) external view returns (uint256); + + function getStakeTotalDeposited(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getStakeTotalUnclaimed(address _account, uint256 _poolId) + external + view + returns (uint256); + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + function getPoolToken(uint256 _poolId) external view returns (address); + function reward() external view returns (address); + function tokenPoolIds(address _token) external view returns (uint256); + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; } @@ -70,7 +86,7 @@ contract AlchemixIntegration is AbstractIntegration { } /** - @dev Claims any accrued rewardToken for a given bAsset staked + * @dev Claims any accrued rewardToken for a given bAsset staked */ function claim(address _bAsset) external onlyGovernor { uint256 poolId = _getPoolIdFor(_bAsset); From 336dee6f728bdd012cfa2bda513a7749a5aceb76 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 9 Jul 2021 23:26:05 +1000 Subject: [PATCH 03/21] chore: standardized Alchemix integration --- .../masset/peripheral/AlchemixIntegration.sol | 79 +-- .../Alchemix/AlchemixStakingPools.json | 521 ------------------ .../Alchemix/IAlchemixStakingPool.sol | 36 ++ ....spec.ts => feeders-musd-alchemix.spec.ts} | 2 +- 4 files changed, 66 insertions(+), 572 deletions(-) delete mode 100644 contracts/peripheral/Alchemix/AlchemixStakingPools.json create mode 100644 contracts/peripheral/Alchemix/IAlchemixStakingPool.sol rename test-fork/feeders/{feeders-musd-alusd.spec.ts => feeders-musd-alchemix.spec.ts} (98%) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index 434555b0..a00cc97b 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -1,40 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.2; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IAlchemixStakingPool } from "../../peripheral/Alchemix/IAlchemixStakingPool.sol"; import { MassetHelpers } from "../../shared/MassetHelpers.sol"; import { AbstractIntegration } from "./AbstractIntegration.sol"; - -interface IStakingPools { - function claim(uint256 _poolId) external; - - function deposit(uint256 _poolId, uint256 _depositAmount) external; - - function exit(uint256 _poolId) external; - - function getStakeTotalDeposited(address _account, uint256 _poolId) - external - view - returns (uint256); - - function getStakeTotalUnclaimed(address _account, uint256 _poolId) - external - view - returns (uint256); - - function getPoolRewardRate(uint256 _poolId) external view returns (uint256); - - function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); - - function getPoolToken(uint256 _poolId) external view returns (address); - - function reward() external view returns (address); - - function tokenPoolIds(address _token) external view returns (uint256); - - function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; -} +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title AlchemixIntegration @@ -48,25 +19,28 @@ contract AlchemixIntegration is AbstractIntegration { event SkippedWithdrawal(address bAsset, uint256 amount); event RewardTokenApproved(address rewardToken, address account); + event RewardsClaimed(); address public immutable rewardToken; - IStakingPools private immutable stakingPools; + IAlchemixStakingPool private immutable stakingPool; /** * @param _nexus Address of the Nexus * @param _lp Address of liquidity provider. eg mAsset or feeder pool * @param _rewardToken Reward token, if any. eg ALCX - * @param _stakingPools Alchemix StakingPools contract address + * @param _stakingPool Alchemix StakingPools contract address */ constructor( address _nexus, address _lp, address _rewardToken, - address _stakingPools + address _stakingPool ) AbstractIntegration(_nexus, _lp) { + require(_rewardToken != address(0), "Invalid reward token"); + require(_stakingPool != address(0), "Invalid staking pool"); rewardToken = _rewardToken; - stakingPools = IStakingPools(_stakingPools); + stakingPool = IAlchemixStakingPool(_stakingPool); } /*************************************** @@ -88,9 +62,14 @@ contract AlchemixIntegration is AbstractIntegration { /** * @dev Claims any accrued rewardToken for a given bAsset staked */ - function claim(address _bAsset) external onlyGovernor { - uint256 poolId = _getPoolIdFor(_bAsset); - stakingPools.claim(poolId); + function claimRewards(address _bAsset) external onlyGovernor { + uint256 len = bAssetsMapped.length; + for (uint256 i = 0; i < len; i++) { + uint256 poolId = _getPoolIdFor(bAssetsMapped[i]); + stakingPool.claim(poolId); + } + + emit RewardsClaimed(); } /*************************************** @@ -120,19 +99,19 @@ contract AlchemixIntegration is AbstractIntegration { if (isTokenFeeCharged) { // If we charge a fee, account for it uint256 prevBal = this.checkBalance(_bAsset); - stakingPools.deposit(poolId, _amount); + stakingPool.deposit(poolId, _amount); uint256 newBal = this.checkBalance(_bAsset); quantityDeposited = _min(quantityDeposited, newBal - prevBal); } else { // Else just deposit the amount - stakingPools.deposit(poolId, _amount); + stakingPool.deposit(poolId, _amount); } - emit Deposit(_bAsset, address(stakingPools), quantityDeposited); + emit Deposit(_bAsset, address(stakingPool), quantityDeposited); } /** - * @dev Withdraw a quantity of bAsset from Compound + * @dev Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -148,7 +127,7 @@ contract AlchemixIntegration is AbstractIntegration { } /** - * @dev Withdraw a quantity of bAsset from Compound + * @dev Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -183,18 +162,18 @@ contract AlchemixIntegration is AbstractIntegration { require(_amount == _totalAmount, "Cache inactive with tx fee"); IERC20 b = IERC20(_bAsset); uint256 prevBal = b.balanceOf(address(this)); - stakingPools.withdraw(poolId, _amount); + stakingPool.withdraw(poolId, _amount); uint256 newBal = b.balanceOf(address(this)); userWithdrawal = _min(userWithdrawal, newBal - prevBal); } else { // Redeem Underlying bAsset amount - stakingPools.withdraw(poolId, _totalAmount); + stakingPool.withdraw(poolId, _totalAmount); } // Send redeemed bAsset to the receiver IERC20(_bAsset).safeTransfer(_receiver, userWithdrawal); - emit PlatformWithdrawal(_bAsset, address(stakingPools), _totalAmount, _amount); + emit PlatformWithdrawal(_bAsset, address(stakingPool), _totalAmount, _amount); } /** @@ -223,7 +202,7 @@ contract AlchemixIntegration is AbstractIntegration { */ function checkBalance(address _bAsset) external view override returns (uint256 balance) { uint256 poolId = _getPoolIdFor(_bAsset); - return stakingPools.getStakeTotalDeposited(address(this), poolId); + balance = stakingPool.getStakeTotalDeposited(address(this), poolId); } /*************************************** @@ -260,13 +239,13 @@ contract AlchemixIntegration is AbstractIntegration { ****************************************/ /** - * @dev Get the cToken wrapped in the ICERC20 interface for this bAsset. + * @dev Get the Alchemix pool id for a bAsset. * Fails if the pToken doesn't exist in our mappings. * @param _bAsset Address of the bAsset * @return poolId Corresponding Alchemix StakingPools poolId */ function _getPoolIdFor(address _bAsset) internal view returns (uint256 poolId) { - poolId = stakingPools.tokenPoolIds(_bAsset); + poolId = stakingPool.tokenPoolIds(_bAsset); require(poolId > 0, "Asset not supported on Alchemix"); } } diff --git a/contracts/peripheral/Alchemix/AlchemixStakingPools.json b/contracts/peripheral/Alchemix/AlchemixStakingPools.json deleted file mode 100644 index 19b32291..00000000 --- a/contracts/peripheral/Alchemix/AlchemixStakingPools.json +++ /dev/null @@ -1,521 +0,0 @@ -{ - "contractName": "StakingPools", - "abi": [ - { - "inputs": [ - { - "internalType": "contract IMintableERC20", - "name": "_reward", - "type": "address" - }, - { - "internalType": "address", - "name": "_governance", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "governance", - "type": "address" - } - ], - "name": "GovernanceUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "pendingGovernance", - "type": "address" - } - ], - "name": "PendingGovernanceUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "contract IERC20", - "name": "token", - "type": "address" - } - ], - "name": "PoolCreated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "rewardWeight", - "type": "uint256" - } - ], - "name": "PoolRewardWeightUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "rewardRate", - "type": "uint256" - } - ], - "name": "RewardRateUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensClaimed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensDeposited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensWithdrawn", - "type": "event" - }, - { - "inputs": [], - "name": "acceptGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "claim", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "_token", - "type": "address" - } - ], - "name": "createPool", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_depositAmount", - "type": "uint256" - } - ], - "name": "deposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "exit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolRewardRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolRewardWeight", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolToken", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolTotalDeposited", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getStakeTotalDeposited", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getStakeTotalUnclaimed", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "governance", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingGovernance", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "poolCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "reward", - "outputs": [ - { - "internalType": "contract IMintableERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_pendingGovernance", - "type": "address" - } - ], - "name": "setPendingGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_rewardRate", - "type": "uint256" - } - ], - "name": "setRewardRate", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256[]", - "name": "_rewardWeights", - "type": "uint256[]" - } - ], - "name": "setRewardWeights", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "name": "tokenPoolIds", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalRewardWeight", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_withdrawAmount", - "type": "uint256" - } - ], - "name": "withdraw", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ] -} \ No newline at end of file diff --git a/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol b/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol new file mode 100644 index 00000000..ee463033 --- /dev/null +++ b/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +/** + * @dev Alchemix Staking Pool + * Source: https://github.com/alchemix-finance/alchemix-protocol/blob/master/contracts/StakingPools.sol + */ +interface IAlchemixStakingPool { + function claim(uint256 _poolId) external; + + function deposit(uint256 _poolId, uint256 _depositAmount) external; + + function exit(uint256 _poolId) external; + + function getStakeTotalDeposited(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getStakeTotalUnclaimed(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + + function getPoolToken(uint256 _poolId) external view returns (address); + + function reward() external view returns (address); + + function tokenPoolIds(address _token) external view returns (uint256); + + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; +} diff --git a/test-fork/feeders/feeders-musd-alusd.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts similarity index 98% rename from test-fork/feeders/feeders-musd-alusd.spec.ts rename to test-fork/feeders/feeders-musd-alchemix.spec.ts index f6751cdd..349118ad 100644 --- a/test-fork/feeders/feeders-musd-alusd.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -151,7 +151,7 @@ context("mUSD Feeder Pool integration to Alchemix", () => { const alUsdInFpBefore = await alUsd.balanceOf(alUsdFp.address) // Migrate the alUSD - await alUsdFp.migrateBassets([alUsdAddress], alchemixIntegration.address) + await alUsdFp.connect(governor).migrateBassets([alUsdAddress], alchemixIntegration.address) // All alUSD in the FP should have moved to the integration contract expect(await alUsd.balanceOf(alchemixIntegration.address), "All alUSD in FP migrated to Integration").to.eq(alUsdInFpBefore) From 2afb8a79604ab3ab6ebe0f4d5a747fe0a80f419f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Sun, 11 Jul 2021 00:45:55 +1000 Subject: [PATCH 04/21] fix: Alchemix integration contract and tests --- .../masset/peripheral/AbstractIntegration.sol | 3 +- .../masset/peripheral/AlchemixIntegration.sol | 111 ++-- ...kingPool.sol => IAlchemixStakingPools.sol} | 2 +- tasks/deployFeeders.ts | 239 +++---- tasks/deployPolygon.ts | 16 +- tasks/feeder.ts | 22 +- tasks/mBTC.ts | 7 +- tasks/mUSD.ts | 7 +- tasks/ops.ts | 18 +- tasks/utils/deploy-utils.ts | 2 +- tasks/utils/feederUtils.ts | 608 +++++++++--------- tasks/utils/networkAddressFactory.ts | 41 +- tasks/utils/snap-utils.ts | 2 +- tasks/utils/storage-utils.ts | 11 +- tasks/utils/tokens.ts | 43 +- .../feeders/feeders-musd-alchemix.spec.ts | 185 ++++-- 16 files changed, 695 insertions(+), 622 deletions(-) rename contracts/peripheral/Alchemix/{IAlchemixStakingPool.sol => IAlchemixStakingPools.sol} (96%) diff --git a/contracts/masset/peripheral/AbstractIntegration.sol b/contracts/masset/peripheral/AbstractIntegration.sol index 5c534106..8eaa0b22 100644 --- a/contracts/masset/peripheral/AbstractIntegration.sol +++ b/contracts/masset/peripheral/AbstractIntegration.sol @@ -34,7 +34,8 @@ abstract contract AbstractIntegration is uint256 userAmount ); - // LP has write access + /// @notice mAsset or Feeder Pool using the integration. eg fPmUSD/alUSD + /// @dev LP has write access address public immutable lpAddress; // bAsset => pToken (Platform Specific Token Address) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index a00cc97b..abde1a6c 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.2; -import { IAlchemixStakingPool } from "../../peripheral/Alchemix/IAlchemixStakingPool.sol"; +import { IAlchemixStakingPools } from "../../peripheral/Alchemix/IAlchemixStakingPools.sol"; import { MassetHelpers } from "../../shared/MassetHelpers.sol"; import { AbstractIntegration } from "./AbstractIntegration.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -17,36 +17,78 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s contract AlchemixIntegration is AbstractIntegration { using SafeERC20 for IERC20; + event AssetAdded(address _bAsset, uint256 poolId); event SkippedWithdrawal(address bAsset, uint256 amount); event RewardTokenApproved(address rewardToken, address account); event RewardsClaimed(); + /// @notice token the staking rewards are accrued and claimed in. address public immutable rewardToken; + /// @notice Alchemix's StakingPools contract + IAlchemixStakingPools public immutable stakingPools; - IAlchemixStakingPool private immutable stakingPool; + /// @notice bAsset => Alchemix pool id + mapping(address => uint256) public bAssetToPoolId; /** * @param _nexus Address of the Nexus * @param _lp Address of liquidity provider. eg mAsset or feeder pool * @param _rewardToken Reward token, if any. eg ALCX - * @param _stakingPool Alchemix StakingPools contract address + * @param _stakingPools Alchemix StakingPools contract address */ constructor( address _nexus, address _lp, address _rewardToken, - address _stakingPool + address _stakingPools ) AbstractIntegration(_nexus, _lp) { require(_rewardToken != address(0), "Invalid reward token"); - require(_stakingPool != address(0), "Invalid staking pool"); + require(_stakingPools != address(0), "Invalid staking pools"); rewardToken = _rewardToken; - stakingPool = IAlchemixStakingPool(_stakingPool); + stakingPools = IAlchemixStakingPools(_stakingPools); + } + + /** + * @dev Simple initializer to add the first bAssets + * @param _bAssets array of base assets that can be staked in an Alchemix staking pool. eg alUSD + */ + function initialize(address[] calldata _bAssets) + public + initializer + { + uint256 len = _bAssets.length; + for (uint256 i = 0; i < len; i++) { + address bAsset = _bAssets[i]; + _addAsset(bAsset); + } } /*************************************** ADMIN ****************************************/ + /** + * @dev add another asset that can be staked in an Alchemix staking pool. + * This method can only be called by the system Governor + * @param _bAsset Address for the bAsset + */ + function addAsset(address _bAsset) external onlyGovernor { + _addAsset(_bAsset); + } + + function _addAsset(address _bAsset) internal { + require(_bAsset != address(0), "Invalid addresses"); + + uint256 poolId = _getPoolIdFor(_bAsset); + bAssetToPoolId[_bAsset] = poolId; + bAssetsMapped.push(_bAsset); + + // approve staking pools contract to transfer bAssets on deposits + MassetHelpers.safeInfiniteApprove(_bAsset, address(stakingPools)); + + emit AssetAdded(_bAsset, poolId); + } + /** * @dev Approves Liquidator to spend reward tokens */ @@ -60,13 +102,13 @@ contract AlchemixIntegration is AbstractIntegration { } /** - * @dev Claims any accrued rewardToken for a given bAsset staked + * @dev Claims any accrued reward tokens for all the bAssets */ - function claimRewards(address _bAsset) external onlyGovernor { + function claimRewards() external onlyGovernor { uint256 len = bAssetsMapped.length; for (uint256 i = 0; i < len; i++) { - uint256 poolId = _getPoolIdFor(bAssetsMapped[i]); - stakingPool.claim(poolId); + uint256 poolId = bAssetToPoolId[bAssetsMapped[i]]; + stakingPools.claim(poolId); } emit RewardsClaimed(); @@ -92,22 +134,22 @@ contract AlchemixIntegration is AbstractIntegration { ) external override onlyLP nonReentrant returns (uint256 quantityDeposited) { require(_amount > 0, "Must deposit something"); - uint256 poolId = _getPoolIdFor(_bAsset); + uint256 poolId = bAssetToPoolId[_bAsset]; quantityDeposited = _amount; if (isTokenFeeCharged) { // If we charge a fee, account for it uint256 prevBal = this.checkBalance(_bAsset); - stakingPool.deposit(poolId, _amount); + stakingPools.deposit(poolId, _amount); uint256 newBal = this.checkBalance(_bAsset); quantityDeposited = _min(quantityDeposited, newBal - prevBal); } else { // Else just deposit the amount - stakingPool.deposit(poolId, _amount); + stakingPools.deposit(poolId, _amount); } - emit Deposit(_bAsset, address(stakingPool), quantityDeposited); + emit Deposit(_bAsset, address(stakingPools), quantityDeposited); } /** @@ -154,7 +196,7 @@ contract AlchemixIntegration is AbstractIntegration { require(_totalAmount > 0, "Must withdraw something"); require(_receiver != address(0), "Must specify recipient"); - uint256 poolId = _getPoolIdFor(_bAsset); + uint256 poolId = bAssetToPoolId[_bAsset]; uint256 userWithdrawal = _amount; @@ -162,18 +204,18 @@ contract AlchemixIntegration is AbstractIntegration { require(_amount == _totalAmount, "Cache inactive with tx fee"); IERC20 b = IERC20(_bAsset); uint256 prevBal = b.balanceOf(address(this)); - stakingPool.withdraw(poolId, _amount); + stakingPools.withdraw(poolId, _amount); uint256 newBal = b.balanceOf(address(this)); userWithdrawal = _min(userWithdrawal, newBal - prevBal); } else { // Redeem Underlying bAsset amount - stakingPool.withdraw(poolId, _totalAmount); + stakingPools.withdraw(poolId, _totalAmount); } // Send redeemed bAsset to the receiver IERC20(_bAsset).safeTransfer(_receiver, userWithdrawal); - emit PlatformWithdrawal(_bAsset, address(stakingPool), _totalAmount, _amount); + emit PlatformWithdrawal(_bAsset, address(stakingPools), _totalAmount, _amount); } /** @@ -201,8 +243,8 @@ contract AlchemixIntegration is AbstractIntegration { * @return balance Total value of the bAsset in the platform */ function checkBalance(address _bAsset) external view override returns (uint256 balance) { - uint256 poolId = _getPoolIdFor(_bAsset); - balance = stakingPool.getStakeTotalDeposited(address(this), poolId); + uint256 poolId = bAssetToPoolId[_bAsset]; + balance = stakingPools.getStakeTotalDeposited(address(this), poolId); } /*************************************** @@ -210,29 +252,21 @@ contract AlchemixIntegration is AbstractIntegration { ****************************************/ /** - * @dev Re-approve the spending of all bAssets by their corresponding cToken, + * @dev Re-approve the spending of all bAssets by the staking pools contract, * if for some reason is it necessary. Only callable through Governance. */ function reApproveAllTokens() external onlyGovernor { uint256 bAssetCount = bAssetsMapped.length; for (uint256 i = 0; i < bAssetCount; i++) { address bAsset = bAssetsMapped[i]; - address cToken = bAssetToPToken[bAsset]; - MassetHelpers.safeInfiniteApprove(bAsset, cToken); + MassetHelpers.safeInfiniteApprove(bAsset, address(stakingPools)); } } /** - FIXME do we need this? - * @dev Internal method to respond to the addition of new bAsset / cTokens - * We need to approve the cToken and give it permission to spend the bAsset - * @param _bAsset Address of the bAsset to approve - * @param _cToken This cToken has the approval approval + * @dev Not used by the Alchemix integration but has to be implemented */ - function _abstractSetPToken(address _bAsset, address _cToken) internal override { - // approve the pool to spend the bAsset - MassetHelpers.safeInfiniteApprove(_bAsset, _cToken); - } + function _abstractSetPToken(address _unused1, address _unused2) internal override {} /*************************************** HELPERS @@ -240,12 +274,13 @@ contract AlchemixIntegration is AbstractIntegration { /** * @dev Get the Alchemix pool id for a bAsset. - * Fails if the pToken doesn't exist in our mappings. - * @param _bAsset Address of the bAsset - * @return poolId Corresponding Alchemix StakingPools poolId + * @param _asset Address of the integrated asset + * @return poolId Corresponding Alchemix staking pool identifier */ - function _getPoolIdFor(address _bAsset) internal view returns (uint256 poolId) { - poolId = stakingPool.tokenPoolIds(_bAsset); - require(poolId > 0, "Asset not supported on Alchemix"); + function _getPoolIdFor(address _asset) internal view returns (uint256 poolId) { + poolId = stakingPools.tokenPoolIds(_asset); + require(poolId >= 1, "Asset not supported on Alchemix"); + // Take one off the poolId + poolId = poolId - 1; } } diff --git a/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol b/contracts/peripheral/Alchemix/IAlchemixStakingPools.sol similarity index 96% rename from contracts/peripheral/Alchemix/IAlchemixStakingPool.sol rename to contracts/peripheral/Alchemix/IAlchemixStakingPools.sol index ee463033..75db0135 100644 --- a/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol +++ b/contracts/peripheral/Alchemix/IAlchemixStakingPools.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.2; * @dev Alchemix Staking Pool * Source: https://github.com/alchemix-finance/alchemix-protocol/blob/master/contracts/StakingPools.sol */ -interface IAlchemixStakingPool { +interface IAlchemixStakingPools { function claim(uint256 _poolId) external; function deposit(uint256 _poolId, uint256 _depositAmount) external; diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index 9ff81999..380f560a 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -3,175 +3,94 @@ import "ts-node/register" import "tsconfig-paths/register" -import { DEAD_ADDRESS, ZERO_ADDRESS } from "@utils/constants" import { task, types } from "hardhat/config" -import { - FeederPool__factory, - FeederLogic__factory, - MockERC20__factory, - CompoundIntegration__factory, - CompoundIntegration, - RewardsDistributor__factory, - RewardsDistributor, -} from "types/generated" -import { simpleToExactAmount, BN } from "@utils/math" -import { BUSD, CREAM, cyMUSD, FRAX, GUSD, MFRAX, MmUSD, MTA, mUSD, PFRAX, PMTA, PmUSD } from "./utils/tokens" +import { FeederPool__factory, CompoundIntegration__factory, CompoundIntegration } from "types/generated" +import { simpleToExactAmount } from "@utils/math" +import { BUSD, CREAM, cyMUSD, GUSD, mUSD, tokens } from "./utils/tokens" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { getSigner } from "./utils/defender-utils" -import { CommonAddresses, deployBoostedFeederPools, Pair } from "./utils/feederUtils" - -task("fSize", "Gets the bytecode size of the FeederPool.sol contract").setAction(async (_, { ethers }) => { - const deployer = await getSigner(ethers) - const linkedAddress = { - __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: DEAD_ADDRESS, - __$7791d1d5b7ea16da359ce352a2ac3a881c$__: DEAD_ADDRESS, - } - // Implementation - const feederPoolFactory = new FeederPool__factory(linkedAddress, deployer) - let size = feederPoolFactory.bytecode.length / 2 / 1000 - if (size > 24.576) { - console.error(`FeederPool size is ${size} kb: ${size - 24.576} kb too big`) - } else { - console.log(`FeederPool = ${size} kb`) - } - - const logic = await new FeederLogic__factory(deployer) - size = logic.bytecode.length / 2 / 1000 - console.log(`FeederLogic = ${size} kb`) - - // External linked library - const manager = await ethers.getContractFactory("FeederManager") - size = manager.bytecode.length / 2 / 1000 - console.log(`FeederManager = ${size} kb`) -}) - -task("deployBoostedFeeder", "Deploys feeder pools with vMTA boost") +import { deployFeederPool, deployVault, FeederData, VaultData } from "./utils/feederUtils" +import { getChain } from "./utils/networkAddressFactory" + +task("deployFeederPool", "Deploy Feeder Pool") + .addParam("masset", "Token symbol of mAsset. eg mUSD or PmUSD for Polygon", "mUSD", types.string) + .addParam("fasset", "Token symbol of Feeder Pool asset. eg GUSD, WBTC, PFRAX for Polygon", "alUSD", types.string) + .addOptionalParam("a", "Amplitude coefficient (A)", 100, types.int) + .addOptionalParam("min", "Minimum asset weight of the basket as a percentage. eg 10 for 10% of the basket.", 10, types.int) + .addOptionalParam("max", "Maximum asset weight of the basket as a percentage. eg 90 for 90% of the basket.", 90, types.int) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) .setAction(async (taskArgs, { hardhatArguments, ethers, network }) => { - const deployer = await getSigner(ethers, taskArgs.speed) - - let addresses: CommonAddresses - const pairs: Pair[] = [] - if (network.name === "mainnet" || hardhatArguments.config === "tasks-fork.config.ts") { - addresses = { - mta: MTA.address, - staking: MTA.savings, // vMTA - nexus: "0xafce80b19a8ce13dec0739a1aab7a028d6845eb3", - proxyAdmin: "0x5c8eb57b44c1c6391fc7a8a0cf44d26896f92386", - rewardsDistributor: "0x04dfdfa471b79cc9e6e8c355e6c71f8ec4916c50", - aave: "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", - boostDirector: "0x8892d7A5e018cdDB631F4733B5C1654e9dE10aaF", - feederLogic: "0x2837C77527c37d61D9763F53005211dACB4125dE", - feederManager: "0x90aE544E8cc76d2867987Ee4f5456C02C50aBd8B", - feederRouter: "0xdc66115Be4eaA30FE8Ca3b262bB8E3FF889F3A35", - interestValidator: "0xf1049aeD858C4eAd6df1de4dbE63EF607CfF3262", // new version replaces 0x98c54fd8c98eaf0938c4a00e7935a66341f7ba0e - } - pairs.push({ - mAsset: mUSD, - fAsset: FRAX, - aToken: ZERO_ADDRESS, - priceCoeff: simpleToExactAmount(1), - A: BN.from(100), - }) - } else if (network.name === "polygon_mainnet" || hardhatArguments.config === "tasks-fork-polygon.config.ts") { - addresses = { - mta: PMTA.address, - nexus: "0x3C6fbB8cbfCB75ecEC5128e9f73307f2cB33f2f6", - proxyAdmin: "0xCb6E4B67f2cac15c284AB49B6a4A671cdfe66711", - rewardsDistributor: "0xC42cF11c1A8768FB8306623C6f682AE966e08f0a", - feederManager: "0xa0adbAcBc179EF9b1a9436376a590b72d1d7bfbf", - feederLogic: "0xc929E040b6C8F2fEFE6B45c6bFEB55508554F3E2", - interestValidator: "0x4A268958BC2f0173CDd8E0981C4c0a259b5cA291", - boostDirector: ZERO_ADDRESS, - } - pairs.push({ - mAsset: PmUSD, - fAsset: PFRAX, - aToken: ZERO_ADDRESS, - priceCoeff: simpleToExactAmount(1), - A: BN.from(100), - }) - } else if (network.name === "polygon_testnet" || hardhatArguments.config === "tasks-fork-polygon-testnet.config.ts") { - addresses = { - mta: PMTA.address, - nexus: "0xCB4aabDb4791B35bDc9348bb68603a68a59be28E", - proxyAdmin: "0x41E4fF04e6f931f6EA71C7138A79a5B2B994eF19", - rewardsDistributor: "0x61cFA4D69Fb52e5aA7870749d91f3ec1fDce8819", - feederManager: "0x7c290A7cdF2516Ca14A0A928E81032bE00C311b0", - feederLogic: "0x096bE47CF32A829904C3741d272620E8745F051F", - interestValidator: "0x644252F179499DF2dE22b14355f677d2b2E21509", - boostDirector: ZERO_ADDRESS, - } - pairs.push({ - mAsset: MmUSD, - fAsset: MFRAX, - aToken: ZERO_ADDRESS, - priceCoeff: simpleToExactAmount(1), - A: BN.from(100), - }) - } else if (network.name === "ropsten") { - addresses = { - mta: "0x273bc479E5C21CAA15aA8538DecBF310981d14C0", - staking: "0x77f9bf80e0947408f64faa07fd150920e6b52015", - nexus: "0xeD04Cd19f50F893792357eA53A549E23Baf3F6cB", - proxyAdmin: "0x2d369F83E9DC764a759a74e87a9Bc542a2BbfdF0", - rewardsDistributor: "0x99B62B75E3565bEAD786ddBE2642E9c40aA33465", - } - } else { - addresses = { - mta: DEAD_ADDRESS, - staking: (await new MockERC20__factory(deployer).deploy("Stake", "ST8", 18, DEAD_ADDRESS, 1)).address, - nexus: DEAD_ADDRESS, - proxyAdmin: DEAD_ADDRESS, - rewardsDistributor: DEAD_ADDRESS, - } + const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) + + const mAsset = tokens.find((t) => t.address === taskArgs.masset) + if (!mAsset) throw Error(`Could not find mAsset token with symbol ${taskArgs.masset}`) + const fAsset = tokens.find((t) => t.address === taskArgs.fasset) + if (!fAsset) throw Error(`Could not find Feeder Pool token with symbol ${taskArgs.fasset}`) + + if (taskArgs.a < 10 || taskArgs.min > 5000) throw Error(`Invalid amplitude coefficient (A) ${taskArgs.a}`) + if (taskArgs.min < 0 || taskArgs.min > 50) throw Error(`Invalid min limit ${taskArgs.min}`) + if (taskArgs.max < 50 || taskArgs.max > 100) throw Error(`Invalid max limit ${taskArgs.min}`) + + const poolData: FeederData = { + mAsset, + fAsset, + name: `${mAsset.symbol}/${fAsset.symbol} Feeder Pool`, + symbol: `fP${mAsset.symbol}/${fAsset.symbol}`, + config: { + a: taskArgs.a, + limits: { + min: simpleToExactAmount(taskArgs.min, 16), + max: simpleToExactAmount(taskArgs.max, 16), + }, + }, } - if (!addresses.rewardsDistributor) { - const fundManagerAddress = "0x437E8C54Db5C66Bb3D80D2FF156e9bfe31a017db" - const distributor = await deployContract(new RewardsDistributor__factory(deployer), "RewardsDistributor", [ - addresses.nexus, - [fundManagerAddress], - ]) - addresses.rewardsDistributor = distributor.address + // Deploy Feeder Pool + await deployFeederPool(signer, poolData, chain) + }) + +task("deployVault", "Deploy Feeder Pool with boosted dual vault") + .addParam("name", "Token name of the vault. eg mUSD/alUSD fPool Vault", undefined, types.string) + .addParam("symbol", "Token symbol of the vault. eg v-fPmUSD/alUSD", undefined, types.string) + .addParam("boosted", "Rewards are boosted by staked MTA (vMTA)", true, types.string) + .addParam( + "stakingToken", + "Symbol of token that is being staked. Feeder Pool is just the fAsset. eg imUSD, PimUSD, MTA, GUSD, alUSD", + true, + types.string, + ) + .addParam("rewardsToken", "Token symbol of reward. eg MTA or PMTA for Polygon", undefined, types.string) + .addOptionalParam("dualRewardToken", "Token symbol of second reward. eg WMATIC, ALCX, QI", undefined, types.string) + .addOptionalParam("price", "Price coefficient is the value of the mAsset in USD. eg mUSD/USD = 1, mBTC/USD", 1, types.int) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) + .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { + const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) + + if (taskArgs.name?.length < 4) throw Error(`Invalid token name ${taskArgs.name}`) + if (taskArgs.symbol?.length <= 0 || taskArgs.symbol?.length > 12) throw Error(`Invalid token name ${taskArgs.name}`) + + const stakingToken = tokens.find((t) => t.address === taskArgs.stakingToken) + if (!stakingToken) throw Error(`Could not find staking token with symbol ${taskArgs.stakingToken}`) + const rewardToken = tokens.find((t) => t.address === taskArgs.rewardToken) + if (!rewardToken) throw Error(`Could not find reward token with symbol ${taskArgs.rewardToken}`) + + if (taskArgs.price < 0 || taskArgs.price >= simpleToExactAmount(1)) throw Error(`Invalid price coefficient ${taskArgs.price}`) + + const dualRewardToken = tokens.find((t) => t.address === taskArgs.dualRewardToken) + + const vaultData: VaultData = { + boosted: taskArgs.boosted, + name: taskArgs.name, + symbol: taskArgs.symbol, + priceCoeff: taskArgs.price, + stakingToken, + rewardToken, + dualRewardToken, } - // const pairs: Pair[] = [ - // // mBTC / hBTC - // { - // mAsset: mBTC.address, - // fAsset: HBTC.address, - // aToken: ZERO_ADDRESS, - // priceCoeff: simpleToExactAmount(58000), - // A: BN.from(325), - // }, - // // mBTC / tBTC - // { - // mAsset: mBTC.address, - // fAsset: TBTC.address, - // aToken: ZERO_ADDRESS, - // priceCoeff: simpleToExactAmount(58000), - // A: BN.from(175), - // }, - // // mUSD / bUSD - // { - // mAsset: mUSD.address, - // fAsset: BUSD.address, - // aToken: BUSD.liquidityProvider, - // priceCoeff: simpleToExactAmount(1), - // A: BN.from(500), - // }, - // // mUSD / GUSD - // { - // mAsset: mUSD.address, - // fAsset: GUSD.address, - // aToken: GUSD.liquidityProvider, - // priceCoeff: simpleToExactAmount(1), - // A: BN.from(225), - // } - // ] - - await deployBoostedFeederPools(deployer, addresses, pairs) + await deployVault(signer, vaultData, chain) }) task("deployIronBank", "Deploys mUSD Iron Bank (CREAM) integration contracts for GUSD and BUSD Feeder Pools") diff --git a/tasks/deployPolygon.ts b/tasks/deployPolygon.ts index 3fb31dab..f55b4612 100644 --- a/tasks/deployPolygon.ts +++ b/tasks/deployPolygon.ts @@ -42,7 +42,7 @@ import { formatUnits } from "@ethersproject/units" import { MassetLibraryAddresses } from "types/generated/factories/Masset__factory" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { getSigner } from "./utils/defender-utils" -import { getNetworkAddress } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" import { PMTA, PmUSD, PWMATIC } from "./utils/tokens" // FIXME: this import does not work for some reason @@ -452,11 +452,12 @@ task("liquidator-snap", "Dumps the config details of the liquidator on Polygon") task("deploy-vimusd", "Deploy Polygon imUSD staking contract v-imUSD").setAction(async (_, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers) + const chain = getChain(network.name, hardhatArguments.config) - const fundManagerAddress = getNetworkAddress("FundManager", network.name, hardhatArguments.config) - const governorAddress = getNetworkAddress("Governor", network.name, hardhatArguments.config) - const nexusAddress = getNetworkAddress("Nexus", network.name, hardhatArguments.config) - const rewardsDistributorAddress = getNetworkAddress("RewardsDistributor", network.name, hardhatArguments.config) + const fundManagerAddress = getChainAddress("FundManager", chain) + const governorAddress = getChainAddress("Governor", chain) + const nexusAddress = getChainAddress("Nexus", chain) + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) const rewardsDistributor = rewardsDistributorAddress ? RewardsDistributor__factory.connect(rewardsDistributorAddress, signer) @@ -500,9 +501,10 @@ task("deploy-vimusd", "Deploy Polygon imUSD staking contract v-imUSD").setAction task("upgrade-vimusd", "Upgrade Polygon imUSD staking contract v-imUSD").setAction(async (_, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers) + const chain = getChain(network.name, hardhatArguments.config) - const nexusAddress = getNetworkAddress("Nexus", network.name, hardhatArguments.config) - const rewardsDistributorAddress = getNetworkAddress("RewardsDistributor", network.name, hardhatArguments.config) + const nexusAddress = getChainAddress("Nexus", chain) + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) const rewardsDistributor = RewardsDistributor__factory.connect(rewardsDistributorAddress, signer) diff --git a/tasks/feeder.ts b/tasks/feeder.ts index 3f526885..6843a420 100644 --- a/tasks/feeder.ts +++ b/tasks/feeder.ts @@ -20,12 +20,12 @@ import { outputFees, getCollectedInterest, } from "./utils/snap-utils" -import { PFRAX, PmUSD, Token, tokens } from "./utils/tokens" +import { Chain, PFRAX, PmUSD, Token, tokens } from "./utils/tokens" import { btcFormatter, QuantityFormatter, usdFormatter } from "./utils/quantity-formatters" import { getSwapRates } from "./utils/rates-utils" import { getSigner } from "./utils/defender-utils" import { logTxDetails } from "./utils" -import { getNetworkAddress } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" const getBalances = async ( feederPool: Masset | FeederPool, @@ -53,12 +53,10 @@ const getBalances = async ( } } -const getFeederPool = (signer: Signer, contractAddress: string): FeederPool => { +const getFeederPool = (signer: Signer, contractAddress: string, chain = Chain.mainnet): FeederPool => { const linkedAddress = { - // FeederManager - __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: getNetworkAddress("FeederManager"), - // FeederLogic - __$7791d1d5b7ea16da359ce352a2ac3a881c$__: getNetworkAddress("FeederLogic"), + __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: getChainAddress("FeederManager", chain), + __$7791d1d5b7ea16da359ce352a2ac3a881c$__: getChainAddress("FeederLogic", chain), } const feederPoolFactory = new FeederPool__factory(linkedAddress, signer) return feederPoolFactory.attach(contractAddress) @@ -83,7 +81,8 @@ const getQuantities = (fAsset: Token, _swapSize?: number): { quantityFormatter: task("feeder-storage", "Dumps feeder contract storage data") .addOptionalParam("block", "Block number to get storage from. (default: current block)", 0, types.int) .addParam("fasset", "Token symbol of the feeder pool asset. eg HBTC, TBTC, GUSD or BUSD", undefined, types.string, false) - .setAction(async (taskArgs, { ethers }) => { + .setAction(async (taskArgs, { ethers, network, hardhatArguments }) => { + const chain = getChain(network.name, hardhatArguments.config) const fAsset = tokens.find((t) => t.symbol === taskArgs.fasset) if (!fAsset) { console.error(`Failed to find feeder pool asset with token symbol ${taskArgs.fasset}`) @@ -93,7 +92,7 @@ task("feeder-storage", "Dumps feeder contract storage data") const { blockNumber } = await getBlock(ethers, taskArgs.block) const signer = await getSigner(ethers) - const pool = getFeederPool(signer, fAsset.feederPool) + const pool = getFeederPool(signer, fAsset.feederPool, chain) await dumpTokenStorage(pool, blockNumber) await dumpFassetStorage(pool, blockNumber) @@ -104,7 +103,8 @@ task("feeder-snap", "Gets feeder transactions over a period of time") .addOptionalParam("from", "Block to query transaction events from. (default: deployment block)", 12146627, types.int) .addOptionalParam("to", "Block to query transaction events to. (default: current block)", 0, types.int) .addParam("fasset", "Token symbol of the feeder pool asset. eg HBTC, TBTC, GUSD or BUSD", undefined, types.string, false) - .setAction(async (taskArgs, { ethers, network }) => { + .setAction(async (taskArgs, { ethers, network, hardhatArguments }) => { + const chain = getChain(network.name, hardhatArguments.config) const signer = await getSigner(ethers) const { fromBlock, toBlock } = await getBlockRange(ethers, taskArgs.from, taskArgs.to) @@ -118,7 +118,7 @@ task("feeder-snap", "Gets feeder transactions over a period of time") const fpAssets = [mAsset, fAsset] const feederPool = getFeederPool(signer, fAsset.feederPool) - const savingsManagerAddress = getNetworkAddress("SavingsManager", network.name) + const savingsManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = SavingsManager__factory.connect(savingsManagerAddress, signer) const { quantityFormatter } = getQuantities(fAsset, taskArgs.swapSize) diff --git a/tasks/mBTC.ts b/tasks/mBTC.ts index 7b7d26a1..3dd9e8e4 100644 --- a/tasks/mBTC.ts +++ b/tasks/mBTC.ts @@ -24,7 +24,7 @@ import { import { Token, renBTC, sBTC, WBTC, mBTC, TBTC, HBTC } from "./utils/tokens" import { getSwapRates } from "./utils/rates-utils" import { getSigner } from "./utils/defender-utils" -import { getNetworkAddress } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" const bAssets: Token[] = [renBTC, sBTC, WBTC] @@ -53,8 +53,9 @@ task("mBTC-storage", "Dumps mBTC's storage data") task("mBTC-snap", "Get the latest data from the mBTC contracts") .addOptionalParam("from", "Block to query transaction events from. (default: deployment block)", 12094461, types.int) .addOptionalParam("to", "Block to query transaction events to. (default: current block)", 0, types.int) - .setAction(async (taskArgs, { ethers, network }) => { + .setAction(async (taskArgs, { ethers, network, hardhatArguments }) => { const signer = await getSigner(ethers) + const chain = getChain(network.name, hardhatArguments.config) let exposedValidator if (network.name !== "mainnet") { @@ -72,7 +73,7 @@ task("mBTC-snap", "Get the latest data from the mBTC contracts") } const mAsset = getMasset(signer) - const savingsManagerAddress = getNetworkAddress("SavingsManager", network.name) + const savingsManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = SavingsManager__factory.connect(savingsManagerAddress, signer) const { fromBlock, toBlock } = await getBlockRange(ethers, taskArgs.from, taskArgs.to) diff --git a/tasks/mUSD.ts b/tasks/mUSD.ts index cd1a4167..40893ab5 100644 --- a/tasks/mUSD.ts +++ b/tasks/mUSD.ts @@ -31,7 +31,7 @@ import { Token, sUSD, USDC, DAI, USDT, PUSDT, PUSDC, PDAI, mUSD, PmUSD, MmUSD, R import { usdFormatter } from "./utils/quantity-formatters" import { getSwapRates } from "./utils/rates-utils" import { getSigner } from "./utils" -import { getNetworkAddress } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" const mUsdBassets: Token[] = [sUSD, USDC, DAI, USDT] const mUsdPolygonBassets: Token[] = [PUSDC, PDAI, PUSDT] @@ -74,8 +74,9 @@ task("mUSD-storage", "Dumps mUSD's storage data") task("mUSD-snap", "Snaps mUSD") .addOptionalParam("from", "Block to query transaction events from. (default: deployment block)", 12094461, types.int) .addOptionalParam("to", "Block to query transaction events to. (default: current block)", 0, types.int) - .setAction(async (taskArgs, { ethers, network }) => { + .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers) + const chain = getChain(network.name, hardhatArguments.config) let exposedValidator if (!["mainnet", "polygon_mainnet"].includes(network.name)) { @@ -95,7 +96,7 @@ task("mUSD-snap", "Snaps mUSD") const { fromBlock, toBlock } = await getBlockRange(ethers, taskArgs.from, taskArgs.to) const mAsset = getMasset(signer, network.name, toBlock.blockNumber) - const savingsManagerAddress = getNetworkAddress("SavingsManager", network.name) + const savingsManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = SavingsManager__factory.connect(savingsManagerAddress, signer) const bAssets = network.name.includes("polygon") ? mUsdPolygonBassets : mUsdBassets diff --git a/tasks/ops.ts b/tasks/ops.ts index 95206a10..ba08da4c 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -14,7 +14,7 @@ import { simpleToExactAmount } from "@utils/math" import { PMTA, PmUSD, PWMATIC, tokens } from "./utils/tokens" import { getSigner } from "./utils/defender-utils" import { logTxDetails } from "./utils/deploy-utils" -import { getNetworkAddress } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" import { usdFormatter } from "./utils" import { getAaveTokens, getBlock, getBlockRange, getCompTokens } from "./utils/snap-utils" @@ -22,8 +22,9 @@ task("eject-stakers", "Ejects expired stakers from Meta staking contract (vMTA)" .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) - const ejectorAddress = getNetworkAddress("Ejector", network.name, hardhatArguments.config) + const ejectorAddress = getChainAddress("Ejector", chain) const ejector = IEjector__factory.connect(ejectorAddress, signer) // TODO check the last time the eject was run // Check it's been more than 7 days since the last eject has been run @@ -51,6 +52,8 @@ task("collect-interest", "Collects and streams interest from platforms") ) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { + const chain = getChain(network.name, hardhatArguments.config) + const asset = tokens.find((t) => t.symbol === taskArgs.asset) if (!asset) { console.error(`Failed to find main or feeder pool asset with token symbol ${taskArgs.asset}`) @@ -58,7 +61,7 @@ task("collect-interest", "Collects and streams interest from platforms") } const signer = await getSigner(ethers, taskArgs.speed) - const savingsManagerAddress = getNetworkAddress("SavingsManager", network.name, hardhatArguments.config) + const savingsManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = SavingsManager__factory.connect(savingsManagerAddress, signer) const lastBatchCollected = await savingsManager.lastBatchCollected(asset.address) @@ -79,17 +82,18 @@ task("polly-daily", "Runs the daily jobs against the contracts on Polygon mainne .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) const aave = PAaveIntegration__factory.connect(PmUSD.integrator, signer) const aaveTx = await aave.claimRewards({ gasLimit: 200000 }) await logTxDetails(aaveTx, "claimRewards") - const liquidatorAddress = getNetworkAddress("Liquidator", network.name, hardhatArguments.config) + const liquidatorAddress = getChainAddress("Liquidator", chain) const liquidator = PLiquidator__factory.connect(liquidatorAddress, signer) const liquidatorTx = await liquidator.triggerLiquidation(PmUSD.integrator, { gasLimit: 2000000 }) await logTxDetails(liquidatorTx, "triggerLiquidation") - const savingsManagerAddress = getNetworkAddress("SavingsManager", network.name, hardhatArguments.config) + const savingsManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = SavingsManager__factory.connect(savingsManagerAddress, signer) const savingsManagerTx = await savingsManager.collectAndStreamInterest(PmUSD.address, { gasLimit: 2000000, @@ -119,10 +123,12 @@ task("polly-dis-rewards", "Distributes MTA and WMATIC rewards to vaults on Polyg .addOptionalParam("wmaticAmount", "WMATIC tokens", 18666, types.int) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) + const mtaAmount = simpleToExactAmount(taskArgs.mtaAmount) const wmaticAmount = simpleToExactAmount(taskArgs.wmaticAmount) - const rewardsDistributorAddress = getNetworkAddress("RewardsDistributor", network.name, hardhatArguments.config) + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) const rewardsDistributor = RewardsDistributor__factory.connect(rewardsDistributorAddress, signer) const mtaToken = ERC20__factory.connect(PMTA.address, signer) diff --git a/tasks/utils/deploy-utils.ts b/tasks/utils/deploy-utils.ts index 9ef99c42..aa718df8 100644 --- a/tasks/utils/deploy-utils.ts +++ b/tasks/utils/deploy-utils.ts @@ -12,7 +12,7 @@ export const deployContract = async ( const ethUsed = contractReceipt.gasUsed.mul(contract.deployTransaction.gasPrice) const abiEncodedConstructorArgs = contract.interface.encodeDeploy(contractorArgs) console.log(`Deployed ${contractName} to ${contract.address}, gas used ${contractReceipt.gasUsed}, eth ${formatUnits(ethUsed)}`) - console.log(`ABI encoded args: ${abiEncodedConstructorArgs}`) + console.log(`ABI encoded args: ${abiEncodedConstructorArgs.slice(2)}`) return contract } diff --git a/tasks/utils/feederUtils.ts b/tasks/utils/feederUtils.ts index ffe6e204..78ec8fdb 100644 --- a/tasks/utils/feederUtils.ts +++ b/tasks/utils/feederUtils.ts @@ -1,63 +1,30 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-await-in-loop */ import { DEAD_ADDRESS, ZERO_ADDRESS } from "@utils/constants" -import { BN, simpleToExactAmount } from "@utils/math" +import { BN } from "@utils/math" import { Signer } from "ethers" -import { formatEther, formatUnits } from "ethers/lib/utils" +import { formatEther } from "ethers/lib/utils" import { FeederPool, - BoostDirector, - BoostDirector__factory, - ERC20, - FeederLogic__factory, - FeederManager__factory, BoostedVault, MockERC20__factory, FeederWrapper, - InterestValidator__factory, - FeederWrapper__factory, - AaveV2Integration__factory, MockInitializableToken__factory, AssetProxy__factory, MockERC20, FeederPool__factory, BoostedVault__factory, Masset__factory, - MV2__factory, - ExposedMasset, + BoostedDualVault, + StakingRewardsWithPlatformToken, + StakingRewards, + BoostedDualVault__factory, + StakingRewardsWithPlatformToken__factory, + StakingRewards__factory, } from "types/generated" import { deployContract, logTxDetails } from "./deploy-utils" -import { Token } from "./tokens" - -export interface CommonAddresses { - nexus: string - proxyAdmin: string - staking?: string - mta: string - rewardsDistributor?: string - aave?: string - boostDirector?: string - feederManager?: string - feederLogic?: string - feederRouter?: string - interestValidator?: string -} - -interface DeployedFasset { - integrator: string - txFee: boolean - contract: ERC20 - address: string - symbol: string -} - -export interface Pair { - mAsset: Token - fAsset: Token - aToken: string - priceCoeff: BN - A: BN -} +import { getChainAddress } from "./networkAddressFactory" +import { Chain, Token } from "./tokens" interface Config { a: BN @@ -67,25 +34,22 @@ interface Config { } } -interface FeederData { - nexus: string - proxyAdmin: string - feederManager: string - feederLogic: string - mAsset: DeployedFasset - fAsset: DeployedFasset - aToken: string +export interface FeederData { + mAsset: Token + fAsset: Token name: string symbol: string config: Config - vaultName: string - vaultSymbol: string - priceCoeff: BN - pool?: FeederPool - vault?: BoostedVault } - -const COEFF = 48 +export interface VaultData { + boosted: boolean + name: string + symbol: string + priceCoeff?: BN + stakingToken: Token + rewardToken: Token + dualRewardToken?: Token +} export const deployFasset = async ( sender: Signer, @@ -108,22 +72,26 @@ export const deployFasset = async ( return new MockERC20__factory(sender).attach(proxy.address) } -const deployFeederPool = async (signer: Signer, feederData: FeederData): Promise => { +export const deployFeederPool = async (signer: Signer, feederData: FeederData, chain = Chain.mainnet): Promise => { + const feederManagerAddress = getChainAddress("FeederManager", chain) + const feederLogicAddress = getChainAddress("FeederLogic", chain) + // Invariant Validator const linkedAddress = { - __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: feederData.feederManager, - __$7791d1d5b7ea16da359ce352a2ac3a881c$__: feederData.feederLogic, + __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: feederManagerAddress, + __$7791d1d5b7ea16da359ce352a2ac3a881c$__: feederLogicAddress, } - const feederPoolFactory = new FeederPool__factory(linkedAddress, signer) const impl = await deployContract(new FeederPool__factory(linkedAddress, signer), "FeederPool", [ - feederData.nexus, + getChainAddress("Nexus", chain), feederData.mAsset.address, ]) // Initialization Data - const bAssets = await (feederData.mAsset.contract as ExposedMasset).getBassets() + const mAsset = Masset__factory.connect(feederData.mAsset.address, signer) + const bAssets = await mAsset.getBassets() const mpAssets = bAssets.personal.map((bAsset) => bAsset.addr) + console.log(`mpAssets. count = ${mpAssets.length}, list: `, mpAssets) console.log( `Initializing FeederPool with: ${feederData.name}, ${feederData.symbol}, mAsset ${feederData.mAsset.address}, fAsset ${ @@ -137,13 +105,13 @@ const deployFeederPool = async (signer: Signer, feederData: FeederData): Promise feederData.symbol, { addr: feederData.mAsset.address, - integrator: ZERO_ADDRESS, + integrator: feederData.mAsset.integrator || ZERO_ADDRESS, hasTxFee: false, status: 0, }, { addr: feederData.fAsset.address, - integrator: feederData.fAsset.integrator, + integrator: feederData.fAsset.integrator || ZERO_ADDRESS, hasTxFee: false, status: 0, }, @@ -153,116 +121,134 @@ const deployFeederPool = async (signer: Signer, feederData: FeederData): Promise const feederPoolProxy = await deployContract(new AssetProxy__factory(signer), "Feeder Pool Proxy", [ impl.address, - feederData.proxyAdmin, + getChainAddress("DelayedProxyAdmin", chain), initializeData, ]) // Create a FeederPool contract pointing to the deployed proxy contract - return feederPoolFactory.attach(feederPoolProxy.address) + return new FeederPool__factory(linkedAddress, signer).attach(feederPoolProxy.address) } -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) - - // Approve spending - const approvals: BN[] = [] - // eslint-disable-next-line - for (const bAsset of bAssets) { - // eslint-disable-next-line - const dec = await bAsset.contract.decimals() - const approval = dec === 18 ? scaledTestQty : scaledTestQty.div(simpleToExactAmount(1, BN.from(18).sub(dec))) - approvals.push(approval) - // eslint-disable-next-line - const tx = await bAsset.contract.approve(feederData.pool.address, approval) - // eslint-disable-next-line - const receiptApprove = await tx.wait() - console.log( - // eslint-disable-next-line - `Approved FeederPool to transfer ${formatUnits(approval, dec)} ${bAsset.symbol} from ${await sender.getAddress()}. gas used ${ - receiptApprove.gasUsed - }`, +export const deployVault = async ( + signer: Signer, + vaultParams: VaultData, + chain = Chain.mainnet, +): Promise => { + const vaultData: VaultData = { + priceCoeff: BN.from(1), + ...vaultParams, + } + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) + const boostCoeff = 48 + let vault: BoostedDualVault | BoostedVault | StakingRewardsWithPlatformToken | StakingRewards + if (vaultData.boosted) { + if (vaultData.dualRewardToken) { + vault = await deployContract(new BoostedDualVault__factory(signer), "BoostedDualVault", [ + getChainAddress("Nexus", chain), + vaultData.stakingToken.vault, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken.address, + vaultData.dualRewardToken.address, + ]) + } else { + vault = await deployContract(new BoostedVault__factory(signer), "BoostedVault", [ + getChainAddress("Nexus", chain), + vaultData.stakingToken.vault, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken.address, + ]) + } + } else if (vaultData.dualRewardToken) { + vault = await deployContract( + new StakingRewardsWithPlatformToken__factory(signer), + "StakingRewardsWithPlatformToken", + [ + getChainAddress("Nexus", chain), + vaultData.stakingToken.vault, + vaultData.rewardToken.address, + vaultData.dualRewardToken.address, + ], ) + } else { + vault = await deployContract(new StakingRewards__factory(signer), "StakingRewards", [ + getChainAddress("Nexus", chain), + vaultData.stakingToken.vault, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken.address, + ]) } - // Mint - console.log( - bAssets.map(() => scaledTestQty.toString()), - await Promise.all( - bAssets.map(async (b) => (await b.contract.allowance(await sender.getAddress(), feederData.pool.address)).toString()), - ), - await Promise.all(bAssets.map(async (b) => (await b.contract.balanceOf(await sender.getAddress())).toString())), - bAssets.map((b) => b.address), - (await feederData.pool.getBassets())[0].map((b) => b[0]), - await feederData.pool.mAsset(), - ) - const tx = await feederData.pool.mintMulti( - bAssets.map((b) => b.address), - approvals, - 1, - await sender.getAddress(), - ) - const receiptMint = await tx.wait() - - // Log minted amount - const mAssetAmount = formatEther(await feederData.pool.totalSupply()) - console.log( - `Minted ${mAssetAmount} fpToken from ${formatEther(scaledTestQty)} Units for each [mAsset, fAsset]. gas used ${ - receiptMint.gasUsed - }`, - ) -} - -const deployBoostedVault = async ( - sender: Signer, - addresses: CommonAddresses, - lpToken: string, - priceCoeff: BN, - vaultName: string, - vaultSymbol: string, - depositAmt = BN.from(0), -): Promise => { - const vImpl = await deployContract( - new BoostedVault__factory(sender), - `Vault Impl with LP token ${lpToken}, director ${addresses.boostDirector}, priceCoeff ${formatEther( - priceCoeff, - )}, coeff ${COEFF}, mta: ${addresses.mta}}`, - [addresses.nexus, lpToken, addresses.boostDirector, priceCoeff, COEFF, addresses.mta], - ) - - // Data - console.log( - `Initializing Vault with: distributor: ${addresses.rewardsDistributor}, admin ${addresses.proxyAdmin}, ${vaultName}, ${vaultSymbol}`, - ) - const vData = vImpl.interface.encodeFunctionData("initialize", [addresses.rewardsDistributor, vaultName, vaultSymbol]) + const initializeData = vault.interface.encodeFunctionData("initialize", [rewardsDistributorAddress, vaultData.name, vaultData.symbol]) + const proxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) // Proxy - const vProxy = await deployContract(new AssetProxy__factory(sender), "AssetProxy for vault", [ - vImpl.address, - addresses.proxyAdmin, - vData, + const proxy = await deployContract(new AssetProxy__factory(signer), "AssetProxy for vault", [ + vault.address, + proxyAdminAddress, + initializeData, ]) - if (depositAmt.gt(0)) { - const erc20 = await new MockERC20__factory(sender).attach(lpToken) - const approveTx = await erc20.approve(vProxy.address, depositAmt) - await logTxDetails( - approveTx, - `Approving the vault deposit of ${depositAmt.toString()}. Your balance: ${( - await erc20.balanceOf(await sender.getAddress()) - ).toString()}`, - ) - - const vault = new BoostedVault__factory(sender).attach(vProxy.address) - const depositTx = await vault["stake(uint256)"](depositAmt) - await logTxDetails(depositTx, "Depositing to vault") - } - - return BoostedVault__factory.connect(vProxy.address, sender) + return vault.attach(proxy.address) } +// 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) + +// // Approve spending +// const approvals: BN[] = [] +// // eslint-disable-next-line +// for (const bAsset of bAssets) { +// // eslint-disable-next-line +// const dec = await bAsset.contract.decimals() +// const approval = dec === 18 ? scaledTestQty : scaledTestQty.div(simpleToExactAmount(1, BN.from(18).sub(dec))) +// approvals.push(approval) +// // eslint-disable-next-line +// const tx = await bAsset.contract.approve(feederData.pool.address, approval) +// // eslint-disable-next-line +// const receiptApprove = await tx.wait() +// console.log( +// // eslint-disable-next-line +// `Approved FeederPool to transfer ${formatUnits(approval, dec)} ${bAsset.symbol} from ${await sender.getAddress()}. gas used ${ +// receiptApprove.gasUsed +// }`, +// ) +// } + +// // Mint +// console.log( +// bAssets.map(() => scaledTestQty.toString()), +// await Promise.all( +// bAssets.map(async (b) => (await b.contract.allowance(await sender.getAddress(), feederData.pool.address)).toString()), +// ), +// await Promise.all(bAssets.map(async (b) => (await b.contract.balanceOf(await sender.getAddress())).toString())), +// bAssets.map((b) => b.address), +// (await feederData.pool.getBassets())[0].map((b) => b[0]), +// await feederData.pool.mAsset(), +// ) +// const tx = await feederData.pool.mintMulti( +// bAssets.map((b) => b.address), +// approvals, +// 1, +// await sender.getAddress(), +// ) +// const receiptMint = await tx.wait() + +// // Log minted amount +// const mAssetAmount = formatEther(await feederData.pool.totalSupply()) +// console.log( +// `Minted ${mAssetAmount} fpToken from ${formatEther(scaledTestQty)} Units for each [mAsset, fAsset]. gas used ${ +// receiptMint.gasUsed +// }`, +// ) +// } + const approveFeederWrapper = async ( sender: Signer, feederWrapper: FeederWrapper, @@ -284,152 +270,152 @@ const approveFeederWrapper = async ( } } -export const deployBoostedFeederPools = async (deployer: Signer, addresses: CommonAddresses, pairs: Pair[]): Promise => { - // 1. Deploy boostDirector & Libraries - const start = await deployer.getBalance() - console.log(`\n~~~~~ PHASE 1 - LIBS ~~~~~\n\n`) - let director: BoostDirector - if (!addresses.boostDirector && addresses.boostDirector !== ZERO_ADDRESS) { - director = await deployContract(new BoostDirector__factory(deployer), "BoostDirector", [addresses.nexus, addresses.staking]) - const directorInitTx = await director.initialize([]) - await logTxDetails(directorInitTx, "Initializing BoostDirector") - } else { - director = BoostDirector__factory.connect(addresses.boostDirector, deployer) - } - - const feederLibs = { - feederManager: addresses.feederManager, - feederLogic: addresses.feederLogic, - } - if (!addresses.feederManager || !addresses.feederLogic) { - const feederManager = await deployContract(new FeederManager__factory(deployer), "FeederManager") - const feederLogic = await deployContract(new FeederLogic__factory(deployer), "FeederLogic") - feederLibs.feederManager = feederManager.address - feederLibs.feederLogic = feederLogic.address - } - - // 2.2 For each fAsset - // - fetch fAsset & mAsset - const data: FeederData[] = [] - - // eslint-disable-next-line - for (const pair of pairs) { - const mAssetContract = MV2__factory.connect(pair.mAsset.address, deployer) - const fAssetContract = MockERC20__factory.connect(pair.fAsset.address, deployer) - const deployedMasset: DeployedFasset = { - integrator: ZERO_ADDRESS, - txFee: false, - contract: mAssetContract, - address: pair.mAsset.address, - symbol: pair.mAsset.symbol, - } - const deployedFasset: DeployedFasset = { - integrator: ZERO_ADDRESS, - txFee: false, - contract: fAssetContract, - address: pair.fAsset.address, - symbol: pair.fAsset.symbol, - } - data.push({ - ...feederLibs, - nexus: addresses.nexus, - proxyAdmin: addresses.proxyAdmin, - mAsset: deployedMasset, - fAsset: deployedFasset, - aToken: pair.aToken, - name: `${deployedMasset.symbol}/${deployedFasset.symbol} Feeder Pool`, - symbol: `fP${deployedMasset.symbol}/${deployedFasset.symbol}`, - config: { - a: pair.A, - limits: { - min: simpleToExactAmount(10, 16), - max: simpleToExactAmount(90, 16), - }, - }, - vaultName: `${deployedMasset.symbol}/${deployedFasset.symbol} fPool Vault`, - vaultSymbol: `v-fP${deployedMasset.symbol}/${deployedFasset.symbol}`, - priceCoeff: pair.priceCoeff, - }) - } - // - create fPool (nexus, mAsset, name, integrator, config) - // eslint-disable-next-line - for (const poolData of data) { - console.log(`\n~~~~~ POOL ${poolData.symbol} ~~~~~\n\n`) - console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) - // Deploy Feeder Pool - const feederPool = await deployFeederPool(deployer, poolData) - poolData.pool = feederPool - - // Mint initial supply - // await mint(deployer, [poolData.mAsset, poolData.fAsset], poolData) - - // Rewards Contract - if (addresses.boostDirector) { - const bal = await feederPool.balanceOf(await deployer.getAddress()) - const vault = await deployBoostedVault( - deployer, - addresses, - poolData.pool.address, - poolData.priceCoeff, - poolData.vaultName, - poolData.vaultSymbol, - bal, - ) - poolData.vault = vault - } - } - // 3. Clean - // - initialize boostDirector with pools - console.log(`\n~~~~~ PHASE 3 - ETC ~~~~~\n\n`) - console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) - - if (!addresses.boostDirector && addresses.boostDirector !== ZERO_ADDRESS) { - const directorInitTx = await director.initialize(data.map((d) => d.vault.address)) - logTxDetails(directorInitTx, `Initializing BoostDirector for vaults: ${data.map((d) => d.vault.address)}`) - } - - // - if aToken != 0: deploy Aave integrator & initialize with fPool & aToken addr - for (const poolData of data) { - if (poolData.aToken !== ZERO_ADDRESS) { - const integration = await deployContract( - new AaveV2Integration__factory(deployer), - `integration for ${poolData.symbol} at pool ${poolData.pool.address}`, - [addresses.nexus, poolData.pool.address, addresses.aave, DEAD_ADDRESS], - ) - - const initTx = await integration.initialize([poolData.fAsset.address], [poolData.aToken]) - await logTxDetails(initTx, `Initializing pToken ${poolData.aToken} for bAsset ${poolData.fAsset.address}...`) - } - } - - // Deploy feederRouter - let feederWrapper: FeederWrapper - if (addresses.boostDirector !== ZERO_ADDRESS) { - if (!addresses.feederRouter) { - // Deploy FeederWrapper - feederWrapper = await deployContract(new FeederWrapper__factory(deployer), "FeederWrapper") - } else { - feederWrapper = FeederWrapper__factory.connect(addresses.feederRouter, deployer) - } - await approveFeederWrapper( - deployer, - feederWrapper, - data.map((d) => d.pool), - data.map((d) => d.vault), - ) - } - - // - deploy interestValidator - if (!addresses.interestValidator) { - await deployContract(new InterestValidator__factory(deployer), "InterestValidator", [addresses.nexus]) - } - - console.log(`\n~~~~~ 🥳 CONGRATS! Time for Phase 4 🥳 ~~~~~\n\n`) - // 4. Post - // - Fund small amt to vaults - // - Add InterestValidator as a module - // - Fund vaults - console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) - const end = await deployer.getBalance() - console.log("Total ETH used: ", formatUnits(end.sub(start))) -} +// export const deployBoostedFeederPools = async (deployer: Signer, addresses: CommonAddresses, pairs: Pair[]): Promise => { +// // 1. Deploy boostDirector & Libraries +// const start = await deployer.getBalance() +// console.log(`\n~~~~~ PHASE 1 - LIBS ~~~~~\n\n`) +// let director: BoostDirector +// if (!addresses.boostDirector && addresses.boostDirector !== ZERO_ADDRESS) { +// director = await deployContract(new BoostDirector__factory(deployer), "BoostDirector", [addresses.nexus, addresses.staking]) +// const directorInitTx = await director.initialize([]) +// await logTxDetails(directorInitTx, "Initializing BoostDirector") +// } else { +// director = BoostDirector__factory.connect(addresses.boostDirector, deployer) +// } + +// const feederLibs = { +// feederManager: addresses.feederManager, +// feederLogic: addresses.feederLogic, +// } +// if (!addresses.feederManager || !addresses.feederLogic) { +// const feederManager = await deployContract(new FeederManager__factory(deployer), "FeederManager") +// const feederLogic = await deployContract(new FeederLogic__factory(deployer), "FeederLogic") +// feederLibs.feederManager = feederManager.address +// feederLibs.feederLogic = feederLogic.address +// } + +// // 2.2 For each fAsset +// // - fetch fAsset & mAsset +// const data: FeederData[] = [] + +// // eslint-disable-next-line +// for (const pair of pairs) { +// const mAssetContract = MV2__factory.connect(pair.mAsset.address, deployer) +// const fAssetContract = MockERC20__factory.connect(pair.fAsset.address, deployer) +// const deployedMasset: DeployedFasset = { +// integrator: ZERO_ADDRESS, +// txFee: false, +// contract: mAssetContract, +// address: pair.mAsset.address, +// symbol: pair.mAsset.symbol, +// } +// const deployedFasset: DeployedFasset = { +// integrator: ZERO_ADDRESS, +// txFee: false, +// contract: fAssetContract, +// address: pair.fAsset.address, +// symbol: pair.fAsset.symbol, +// } +// data.push({ +// ...feederLibs, +// nexus: addresses.nexus, +// proxyAdmin: addresses.proxyAdmin, +// mAsset: deployedMasset, +// fAsset: deployedFasset, +// aToken: pair.aToken, +// name: `${deployedMasset.symbol}/${deployedFasset.symbol} Feeder Pool`, +// symbol: `fP${deployedMasset.symbol}/${deployedFasset.symbol}`, +// config: { +// a: pair.A, +// limits: { +// min: simpleToExactAmount(10, 16), +// max: simpleToExactAmount(90, 16), +// }, +// }, +// vaultName: `${deployedMasset.symbol}/${deployedFasset.symbol} fPool Vault`, +// vaultSymbol: `v-fP${deployedMasset.symbol}/${deployedFasset.symbol}`, +// priceCoeff: pair.priceCoeff, +// }) +// } +// // - create fPool (nexus, mAsset, name, integrator, config) +// // eslint-disable-next-line +// for (const poolData of data) { +// console.log(`\n~~~~~ POOL ${poolData.symbol} ~~~~~\n\n`) +// console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) +// // Deploy Feeder Pool +// const feederPool = await deployFeederPool(deployer, poolData) +// poolData.pool = feederPool + +// // Mint initial supply +// // await mint(deployer, [poolData.mAsset, poolData.fAsset], poolData) + +// // Rewards Contract +// if (addresses.boostDirector) { +// const bal = await feederPool.balanceOf(await deployer.getAddress()) +// const vault = await deployBoostedVault( +// deployer, +// addresses, +// poolData.pool.address, +// poolData.priceCoeff, +// poolData.vaultName, +// poolData.vaultSymbol, +// bal, +// ) +// poolData.vault = vault +// } +// } +// // 3. Clean +// // - initialize boostDirector with pools +// console.log(`\n~~~~~ PHASE 3 - ETC ~~~~~\n\n`) +// console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) + +// if (!addresses.boostDirector && addresses.boostDirector !== ZERO_ADDRESS) { +// const directorInitTx = await director.initialize(data.map((d) => d.vault.address)) +// logTxDetails(directorInitTx, `Initializing BoostDirector for vaults: ${data.map((d) => d.vault.address)}`) +// } + +// // - if aToken != 0: deploy Aave integrator & initialize with fPool & aToken addr +// for (const poolData of data) { +// if (poolData.aToken !== ZERO_ADDRESS) { +// const integration = await deployContract( +// new AaveV2Integration__factory(deployer), +// `integration for ${poolData.symbol} at pool ${poolData.pool.address}`, +// [addresses.nexus, poolData.pool.address, addresses.aave, DEAD_ADDRESS], +// ) + +// const initTx = await integration.initialize([poolData.fAsset.address], [poolData.aToken]) +// await logTxDetails(initTx, `Initializing pToken ${poolData.aToken} for bAsset ${poolData.fAsset.address}...`) +// } +// } + +// // Deploy feederRouter +// let feederWrapper: FeederWrapper +// if (addresses.boostDirector !== ZERO_ADDRESS) { +// if (!addresses.feederRouter) { +// // Deploy FeederWrapper +// feederWrapper = await deployContract(new FeederWrapper__factory(deployer), "FeederWrapper") +// } else { +// feederWrapper = FeederWrapper__factory.connect(addresses.feederRouter, deployer) +// } +// await approveFeederWrapper( +// deployer, +// feederWrapper, +// data.map((d) => d.pool), +// data.map((d) => d.vault), +// ) +// } + +// // - deploy interestValidator +// if (!addresses.interestValidator) { +// await deployContract(new InterestValidator__factory(deployer), "InterestValidator", [addresses.nexus]) +// } + +// console.log(`\n~~~~~ 🥳 CONGRATS! Time for Phase 4 🥳 ~~~~~\n\n`) +// // 4. Post +// // - Fund small amt to vaults +// // - Add InterestValidator as a module +// // - Fund vaults +// console.log("Remaining ETH in deployer: ", formatUnits(await deployer.getBalance())) +// const end = await deployer.getBalance() +// console.log("Total ETH used: ", formatUnits(end.sub(start))) +// } diff --git a/tasks/utils/networkAddressFactory.ts b/tasks/utils/networkAddressFactory.ts index fce478d8..61b8bea8 100644 --- a/tasks/utils/networkAddressFactory.ts +++ b/tasks/utils/networkAddressFactory.ts @@ -1,3 +1,5 @@ +import { Chain } from "./tokens" + export const contractNames = [ "Nexus", "DelayedProxyAdmin", @@ -21,13 +23,14 @@ export const contractNames = [ "BasketManager", // Legacy mUSD contract "AaveIncentivesController", "AaveLendingPoolAddressProvider", + "AlchemixStakingPool", "QuickSwapRouter", "MStableYieldSource", // Used for PoolTogether ] as const export type ContractNames = typeof contractNames[number] -export const getNetworkAddress = (contractName: ContractNames, networkName = "mainnet", hardhatConfig?: string): string => { - if (networkName === "mainnet" || hardhatConfig === "tasks-fork.config.ts") { +export const getChainAddress = (contractName: ContractNames, chain: Chain): string => { + if (chain === Chain.mainnet) { switch (contractName) { case "Nexus": return "0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3" @@ -68,11 +71,15 @@ export const getNetworkAddress = (contractName: ContractNames, networkName = "ma return "0xf1049aeD858C4eAd6df1de4dbE63EF607CfF3262" case "BasketManager": return "0x66126B4aA2a1C07536Ef8E5e8bD4EfDA1FdEA96D" + case "AaveLendingPoolAddressProvider": + return "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5" + case "AlchemixStakingPool": + return "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" case "MStableYieldSource": return "0xdB4C9f763A4B13CF2830DFe7c2854dADf5b96E99" default: } - } else if (networkName === "polygon_mainnet" || hardhatConfig === "tasks-fork-polygon.config.ts") { + } else if (chain === Chain.polygon) { switch (contractName) { case "Nexus": return "0x3C6fbB8cbfCB75ecEC5128e9f73307f2cB33f2f6" @@ -107,7 +114,7 @@ export const getNetworkAddress = (contractName: ContractNames, networkName = "ma return "0x13bA0402f5047324B4279858298F56c30EA98753" default: } - } else if (networkName === "polygon_testnet") { + } else if (chain === Chain.mumbai) { switch (contractName) { case "Nexus": return "0xCB4aabDb4791B35bDc9348bb68603a68a59be28E" @@ -124,7 +131,7 @@ export const getNetworkAddress = (contractName: ContractNames, networkName = "ma return "0xeB2A92Cc1A9dC337173B10cAbBe91ecBc805C98B" default: } - } else if (networkName === "ropsten") { + } else if (chain === Chain.ropsten) { switch (contractName) { case "Nexus": return "0xeD04Cd19f50F893792357eA53A549E23Baf3F6cB" @@ -134,7 +141,25 @@ export const getNetworkAddress = (contractName: ContractNames, networkName = "ma } } - throw Error( - `Failed to find contract address for contract name ${contractName} on the ${networkName} network and config Hardhat ${hardhatConfig}`, - ) + return undefined +} + +export const getChain = (networkName: string, hardhatConfig?: string): Chain => { + if (networkName === "mainnet" || hardhatConfig === "tasks-fork.config.ts") { + return Chain.mainnet + } + if (networkName === "polygon_mainnet" || hardhatConfig === "tasks-fork-polygon.config.ts") { + return Chain.polygon + } + if (networkName === "polygon_testnet") { + return Chain.mumbai + } + if (networkName === "ropsten") { + return Chain.ropsten + } +} + +export const getNetworkAddress = (contractName: ContractNames, networkName = "mainnet", hardhatConfig?: string): string => { + const chain = getChain(networkName, hardhatConfig) + return getChainAddress(contractName, chain) } diff --git a/tasks/utils/snap-utils.ts b/tasks/utils/snap-utils.ts index 32d58fa6..e90769a5 100644 --- a/tasks/utils/snap-utils.ts +++ b/tasks/utils/snap-utils.ts @@ -783,7 +783,7 @@ export const getAaveTokens = async (signer: Signer, toBlock: BlockInfo, quantity const cooldownStart = await stkAaveToken.stakersCooldowns(liquidatorAddress, { blockTag: toBlock.blockNumber }) const cooldownEnd = cooldownStart.add(ONE_DAY.mul(10)) const colldownEndDate = new Date(cooldownEnd.toNumber() * 1000) - console.log(`Liquidator ${quantityFormatter(liquidatorStkAaveBal)} unlock ${colldownEndDate.toUTCString()}`) + console.log(`Liquidator ${quantityFormatter(liquidatorStkAaveBal)} unlock ${colldownEndDate.toUTCString()} (${cooldownStart})`) const aaveUsdc = await quoteSwap(signer, AAVE, USDC, totalStkAave, toBlock) console.log( diff --git a/tasks/utils/storage-utils.ts b/tasks/utils/storage-utils.ts index 10aa0c72..5ffc5e3f 100644 --- a/tasks/utils/storage-utils.ts +++ b/tasks/utils/storage-utils.ts @@ -4,8 +4,9 @@ import { FeederPool, Masset, MV1, MV2 } from "types/generated" import { BasketManager__factory } from "types/generated/factories/BasketManager__factory" import { MusdEth } from "types/generated/MusdEth" import { MusdLegacy } from "types/generated/MusdLegacy" -import { getNetworkAddress } from "./networkAddressFactory" +import { getChainAddress } from "./networkAddressFactory" import { isFeederPool, isMusdEth, isMusdLegacy } from "./snap-utils" +import { Chain } from "./tokens" // Get mAsset token storage variables export const dumpTokenStorage = async (token: Masset | MusdEth | MusdLegacy | FeederPool, toBlock: number): Promise => { @@ -19,7 +20,11 @@ export const dumpTokenStorage = async (token: Masset | MusdEth | MusdLegacy | Fe } // Get bAsset storage variables -export const dumpBassetStorage = async (mAsset: Masset | MusdEth | MusdLegacy | MV1 | MV2, block: number): Promise => { +export const dumpBassetStorage = async ( + mAsset: Masset | MusdEth | MusdLegacy | MV1 | MV2, + block: number, + chain = Chain.mainnet, +): Promise => { const override = { blockTag: block, } @@ -40,7 +45,7 @@ export const dumpBassetStorage = async (mAsset: Masset | MusdEth | MusdLegacy | }) } else { // Before the mUSD upgrade to MusdV3 where the bAssets were in a separate Basket Manager contract - const basketManagerAddress = getNetworkAddress("BasketManager") + const basketManagerAddress = getChainAddress("BasketManager", chain) const basketManager = BasketManager__factory.connect(basketManagerAddress, mAsset.signer) const basket = await basketManager.getBassets(override) let i = 0 diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index a9b29b47..075e53eb 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -205,6 +205,26 @@ export const MFRAX: Token = { parent: "MmUSD", } +// Alchemix +export const alUSD: Token = { + symbol: "alUSD", + address: "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", + feederPool: undefined, // TODO add after deployment + integrator: undefined, // TODO add after deployment + vault: undefined, // TODO add after deployment + chain: Chain.mainnet, + decimals: 18, + quantityFormatter: "USD", + parent: "mUSD", +} +export const ALCX: Token = { + symbol: "ALCX", + address: "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF", + chain: Chain.mainnet, + decimals: 18, + quantityFormatter: "USD", +} + // BTC export const renBTC: Token = { symbol: "renBTC", @@ -321,4 +341,25 @@ export const cyMUSD: Token = { quantityFormatter: "USD", } -export const tokens = [mUSD, mBTC, sUSD, USDC, USDT, DAI, GUSD, BUSD, renBTC, sBTC, WBTC, HBTC, TBTC, PFRAX, PmUSD, PUSDC, PUSDT, PDAI] +export const tokens = [ + mUSD, + mBTC, + sUSD, + USDC, + USDT, + DAI, + GUSD, + BUSD, + renBTC, + sBTC, + WBTC, + HBTC, + TBTC, + alUSD, + ALCX, + PFRAX, + PmUSD, + PUSDC, + PUSDT, + PDAI, +] diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 349118ad..8cf25f6e 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -1,12 +1,20 @@ +import { ONE_WEEK } from "@utils/constants" import { impersonate } from "@utils/fork" import { BN, simpleToExactAmount } from "@utils/math" +import { increaseTime } from "@utils/time" import { expect } from "chai" import { Signer, constants } from "ethers" +import { ProvidedRequiredArgumentsOnDirectivesRule } from "graphql/validation/rules/ProvidedRequiredArgumentsRule" import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" -import { FeederPool, FeederPool__factory, IERC20, IERC20__factory, Masset__factory } from "types/generated" +import { deployFeederPool, FeederData } from "tasks/utils/feederUtils" +import { getChainAddress } from "tasks/utils/networkAddressFactory" +import { ALCX, alUSD, Chain, mUSD } from "tasks/utils/tokens" +import { FeederPool, IERC20, IERC20__factory } from "types/generated" import { AlchemixIntegration } from "types/generated/AlchemixIntegration" import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" +import { IAlchemixStakingPools__factory } from "types/generated/factories/IAlchemixStakingPools__factory" +import { IAlchemixStakingPools } from "types/generated/IAlchemixStakingPools" const governorAddress = "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" @@ -14,14 +22,12 @@ const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" const mUsdWhaleAddress = "0x69E0E2b3d523D3b247d798a49C3fa022a46DD6bd" const alUsdWhaleAddress = "0xf9a0106251467fff1ff03e8609aa74fc55a2a45e" -const nexusAddress = "0xafce80b19a8ce13dec0739a1aab7a028d6845eb3" -const mUsdAddress = "0xe2f2a5c287993345a840db3b0845fbc70f5935a5" -const alUsdAddress = "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9" -const alcxAddress = "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF" -const stakingPoolsAddress = "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" -const liquidatorAddress = "0xe595D67181D701A5356e010D9a58EB9A341f1DbD" +const chain = Chain.mainnet +const nexusAddress = getChainAddress("Nexus", chain) +const liquidatorAddress = getChainAddress("Liquidator", chain) +const alchemixStakingPoolsAddress = getChainAddress("AlchemixStakingPool", chain) -context("mUSD Feeder Pool integration to Alchemix", () => { +context("alUSD Feeder Pool integration to Alchemix", () => { let governor: Signer let deployer: Signer let ethWhale: Signer @@ -32,6 +38,10 @@ context("mUSD Feeder Pool integration to Alchemix", () => { let alUsd: IERC20 let alcxToken: IERC20 let alchemixIntegration: AlchemixIntegration + let alchemixStakingPools: IAlchemixStakingPools + let poolId: BN + + const mintAmount = simpleToExactAmount(10000) before("reset block number", async () => { await network.provider.request({ @@ -61,9 +71,11 @@ context("mUSD Feeder Pool integration to Alchemix", () => { ), ) - mUsd = await IERC20__factory.connect(mUsdAddress, deployer) - alUsd = await IERC20__factory.connect(alUsdAddress, deployer) - alcxToken = await IERC20__factory.connect(alcxAddress, deployer) + mUsd = await IERC20__factory.connect(mUSD.address, deployer) + alUsd = await IERC20__factory.connect(alUSD.address, deployer) + alcxToken = await IERC20__factory.connect(ALCX.address, deployer) + alchemixStakingPools = await IAlchemixStakingPools__factory.connect(alchemixStakingPoolsAddress, deployer) + poolId = (await alchemixStakingPools.tokenPoolIds(alUSD.address)).sub(1) }) it("Test connectivity", async () => { const currentBlock = await ethers.provider.getBlockNumber() @@ -71,44 +83,46 @@ context("mUSD Feeder Pool integration to Alchemix", () => { const startEther = await deployer.getBalance() console.log(`Deployer ${deployerAddress} has ${startEther} Ether`) }) - it("deploy and initialize integration contract", async () => { - alUsdFp = await deployContract( - new FeederPool__factory( - { - __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: "0x90aE544E8cc76d2867987Ee4f5456C02C50aBd8B", // FeederManager - __$7791d1d5b7ea16da359ce352a2ac3a881c$__: "0x2837C77527c37d61D9763F53005211dACB4125dE", // FeederLogic - }, - deployer, - ), - "alUSD/mUSD Feeder Pool", - [nexusAddress, mUsdAddress], - ) - - const mpAssets = (await Masset__factory.connect(mUsdAddress, deployer).getBassets())[0].map((p) => p[0]) - - await alUsdFp.initialize( - "Feeder Pool mUSD/alUSD", - "fP-mUSD/alUSD", - { addr: mUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, - { addr: alUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, - mpAssets, - { - a: BN.from(225), - limits: { - min: simpleToExactAmount(10, 16), - max: simpleToExactAmount(90, 16), - }, + it("deploy alUSD Feeder Pool", async () => { + const config = { + a: BN.from(225), + limits: { + min: simpleToExactAmount(10, 16), + max: simpleToExactAmount(90, 16), }, - ) - + } + const fpData: FeederData = { + mAsset: mUSD, + fAsset: alUSD, + name: "mUSD/alUSD Feeder Pool", + symbol: "fPmUSD/alUSD", + config, + } + + alUsdFp = await deployFeederPool(deployer, fpData, chain) + + expect(await alUsdFp.name(), "name").to.eq(fpData.name) + expect(await alUsdFp.symbol(), "symbol").to.eq(fpData.symbol) + }) + it("deploy Alchemix integration", async () => { alchemixIntegration = await deployContract( new AlchemixIntegration__factory(deployer), "Alchemix alUSD Integration", - [nexusAddress, alUsdFp.address, alcxAddress, stakingPoolsAddress], + [nexusAddress, alUsdFp.address, ALCX.address, alchemixStakingPoolsAddress], ) - expect(alchemixIntegration.address).to.length(42) - await alchemixIntegration.initialize([alUsdAddress], [alcxAddress]) + expect(await alchemixIntegration.nexus(), "nexus").to.eq(nexusAddress) + expect(await alchemixIntegration.lpAddress(), "lp (feeder pool)").to.eq(alUsdFp.address) + expect(await alchemixIntegration.rewardToken(), "rewards token").to.eq(ALCX.address) + expect(await alchemixIntegration.stakingPools(), "Alchemix staking pools").to.eq(alchemixStakingPoolsAddress) + + await alchemixIntegration["initialize(address[])"]([alUSD.address]) + + expect(await alchemixIntegration.bAssetToPoolId(alUSD.address)).to.eq(0) + }) + it("Migrate alUSD Feeder Pool to the Alchemix integration", async () => { + // Migrate the alUSD + await alUsdFp.connect(governor).migrateBassets([alUsd.address], alchemixIntegration.address) }) it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) @@ -119,43 +133,80 @@ context("mUSD Feeder Pool integration to Alchemix", () => { expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) }) it("Mint some mUSD/alUSD in the Feeder Pool", async () => { - const alUsdBassetBefore = await alUsdFp.getBasset(alUsdAddress) - const mUsdBassetBefore = await alUsdFp.getBasset(mUsdAddress) - - expect(await alUsd.balanceOf(alUsdFp.address)).to.eq(0) - expect(await mUsd.balanceOf(alUsdFp.address)).to.eq(0) - - const mintAmount = simpleToExactAmount(10000) + const alUsdBassetBefore = await alUsdFp.getBasset(alUsd.address) + const mUsdBassetBefore = await alUsdFp.getBasset(mUSD.address) + + expect(await alUsd.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) + expect(await mUsd.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited before", + ).to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.eq(0) // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) await mUsd.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) - expect(await mUsd.balanceOf(alUsdWhaleAddress)).to.gte(mintAmount) + expect(await mUsd.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) await alUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) await mUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) - expect(await alUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) - expect(await mUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) + expect(await alUsd.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq(constants.MaxUint256) + expect(await mUsd.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) + expect(await alUsd.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) + expect(await mUsd.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) - await alUsdFp.connect(alUsdWhale).mintMulti([alUsdAddress, mUsdAddress], [mintAmount, mintAmount], 0, alUsdWhaleAddress) + await alUsdFp + .connect(alUsdWhale) + .mintMulti([alUsd.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) - const alUsdBassetAfter = await alUsdFp.getBasset(alUsdAddress) - const mUsdBassetAfter = await alUsdFp.getBasset(mUsdAddress) + const alUsdBassetAfter = await alUsdFp.getBasset(alUsd.address) + const mUsdBassetAfter = await alUsdFp.getBasset(mUSD.address) expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), ) expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount)) - }) - it("Migrate alUSD to the Alchemix integration", async () => { - expect(await alUsd.balanceOf(alUsdFp.address), "Some alUSD in Feeder Pool").to.gt(0) - expect(await alUsd.balanceOf(alchemixIntegration.address), "No alUSD in Integration contract").to.eq(0) - const alUsdInFpBefore = await alUsd.balanceOf(alUsdFp.address) - - // Migrate the alUSD - await alUsdFp.connect(governor).migrateBassets([alUsdAddress], alchemixIntegration.address) - // All alUSD in the FP should have moved to the integration contract - expect(await alUsd.balanceOf(alchemixIntegration.address), "All alUSD in FP migrated to Integration").to.eq(alUsdInFpBefore) - expect(await alUsd.balanceOf(alUsdFp.address), "No more alUSD in Feeder Pool").to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.eq(0) + }) + it("accrue ALCX", async () => { + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.eq(0) + + await increaseTime(ONE_WEEK) + + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.gt(0) + }) + it("redeem a lot of alUSD", async () => { + const redeemAmount = simpleToExactAmount(8000) + await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], mintAmount, alUsdWhaleAddress) + + const alUsdBassetAfter = await alUsdFp.getBasset(alUsd.address) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(mintAmount.sub(redeemAmount)) + const integrationAlusdBalance = await alUsd.balanceOf(alchemixIntegration.address) + expect(integrationAlusdBalance, "alUSD in cache").to.gt(0) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount.sub(redeemAmount).sub(integrationAlusdBalance)) }) // Mint alUSD in FP, test integration/FP balances, wait for a block or two, claim rewards // Liquidate rewards and test output/balances From c1d32e524e5da302e8f64fccfa41c34d64565891 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Sun, 11 Jul 2021 16:27:46 +1000 Subject: [PATCH 05/21] chore: AlchemixIntegration refactored to not use the AbstractIntegration --- contracts/interfaces/IPlatformIntegration.sol | 5 -- .../masset/peripheral/AbstractIntegration.sol | 7 +-- .../masset/peripheral/AlchemixIntegration.sol | 49 +++++++++++++++---- .../masset/MaliciousAaveIntegration.sol | 2 +- .../masset/MockPlatformIntegration.sol | 2 +- .../feeders/feeders-musd-alchemix.spec.ts | 3 +- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/contracts/interfaces/IPlatformIntegration.sol b/contracts/interfaces/IPlatformIntegration.sol index 27856799..73653c94 100644 --- a/contracts/interfaces/IPlatformIntegration.sol +++ b/contracts/interfaces/IPlatformIntegration.sol @@ -50,9 +50,4 @@ interface IPlatformIntegration { * @dev Returns the current balance of the given bAsset */ function checkBalance(address _bAsset) external returns (uint256 balance); - - /** - * @dev Returns the pToken - */ - function bAssetToPToken(address _bAsset) external returns (address pToken); } diff --git a/contracts/masset/peripheral/AbstractIntegration.sol b/contracts/masset/peripheral/AbstractIntegration.sol index 8eaa0b22..d962fcdf 100644 --- a/contracts/masset/peripheral/AbstractIntegration.sol +++ b/contracts/masset/peripheral/AbstractIntegration.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.2; -// Internal import { IPlatformIntegration } from "../../interfaces/IPlatformIntegration.sol"; -import { Initializable } from "@openzeppelin/contracts/utils/Initializable.sol"; - -// Libs import { ImmutableModule } from "../../shared/ImmutableModule.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import { Initializable } from "@openzeppelin/contracts/utils/Initializable.sol"; /** * @title AbstractIntegration @@ -39,7 +36,7 @@ abstract contract AbstractIntegration is address public immutable lpAddress; // bAsset => pToken (Platform Specific Token Address) - mapping(address => address) public override bAssetToPToken; + mapping(address => address) public bAssetToPToken; // Full list of all bAssets supported here address[] internal bAssetsMapped; diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index abde1a6c..40a47481 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.2; +import { IPlatformIntegration } from "../../interfaces/IPlatformIntegration.sol"; import { IAlchemixStakingPools } from "../../peripheral/Alchemix/IAlchemixStakingPools.sol"; +import { Initializable } from "@openzeppelin/contracts/utils/Initializable.sol"; +import { ImmutableModule } from "../../shared/ImmutableModule.sol"; import { MassetHelpers } from "../../shared/MassetHelpers.sol"; -import { AbstractIntegration } from "./AbstractIntegration.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -14,14 +17,28 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s * @dev VERSION: 1.0 * DATE: 2021-07-02 */ -contract AlchemixIntegration is AbstractIntegration { +contract AlchemixIntegration is + IPlatformIntegration, + Initializable, + ImmutableModule, + ReentrancyGuard { using SafeERC20 for IERC20; + event Deposit(address indexed _bAsset, address _pToken, uint256 _amount); + event Withdrawal(address indexed _bAsset, address _pToken, uint256 _amount); + event PlatformWithdrawal( + address indexed bAsset, + address pToken, + uint256 totalAmount, + uint256 userAmount + ); event AssetAdded(address _bAsset, uint256 poolId); - event SkippedWithdrawal(address bAsset, uint256 amount); event RewardTokenApproved(address rewardToken, address account); event RewardsClaimed(); + /// @notice mAsset or Feeder Pool using the integration. eg fPmUSD/alUSD + /// @dev LP has write access + address public immutable lpAddress; /// @notice token the staking rewards are accrued and claimed in. address public immutable rewardToken; /// @notice Alchemix's StakingPools contract @@ -29,6 +46,16 @@ contract AlchemixIntegration is AbstractIntegration { /// @notice bAsset => Alchemix pool id mapping(address => uint256) public bAssetToPoolId; + /// @notice Full list of all bAssets supported here + address[] public bAssetsMapped; + + /** + * @dev Modifier to allow function calls only from the Governor. + */ + modifier onlyLP() { + require(msg.sender == lpAddress, "Only the LP can execute"); + _; + } /** * @param _nexus Address of the Nexus @@ -41,9 +68,11 @@ contract AlchemixIntegration is AbstractIntegration { address _lp, address _rewardToken, address _stakingPools - ) AbstractIntegration(_nexus, _lp) { + ) ImmutableModule(_nexus) { + require(_lp != address(0), "Invalid LP address"); require(_rewardToken != address(0), "Invalid reward token"); require(_stakingPools != address(0), "Invalid staking pools"); + lpAddress = _lp; rewardToken = _rewardToken; stakingPools = IAlchemixStakingPools(_stakingPools); } @@ -263,11 +292,6 @@ contract AlchemixIntegration is AbstractIntegration { } } - /** - * @dev Not used by the Alchemix integration but has to be implemented - */ - function _abstractSetPToken(address _unused1, address _unused2) internal override {} - /*************************************** HELPERS ****************************************/ @@ -283,4 +307,11 @@ contract AlchemixIntegration is AbstractIntegration { // Take one off the poolId poolId = poolId - 1; } + + /** + * @dev Simple helper func to get the min of two values + */ + function _min(uint256 x, uint256 y) internal pure returns (uint256) { + return x > y ? y : x; + } } diff --git a/contracts/z_mocks/masset/MaliciousAaveIntegration.sol b/contracts/z_mocks/masset/MaliciousAaveIntegration.sol index aef1600b..5327a96a 100644 --- a/contracts/z_mocks/masset/MaliciousAaveIntegration.sol +++ b/contracts/z_mocks/masset/MaliciousAaveIntegration.sol @@ -31,7 +31,7 @@ contract MaliciousAaveIntegration is address public platformAddress; // bAsset => pToken (Platform Specific Token Address) - mapping(address => address) public override bAssetToPToken; + mapping(address => address) public bAssetToPToken; // Full list of all bAssets supported here address[] internal bAssetsMapped; diff --git a/contracts/z_mocks/masset/MockPlatformIntegration.sol b/contracts/z_mocks/masset/MockPlatformIntegration.sol index fcbae0c7..2f927cab 100644 --- a/contracts/z_mocks/masset/MockPlatformIntegration.sol +++ b/contracts/z_mocks/masset/MockPlatformIntegration.sol @@ -34,7 +34,7 @@ contract MockPlatformIntegration is IPlatformIntegration, ImmutableModule { address public platformAddress; // bAsset => pToken (Platform Specific Token Address) - mapping(address => address) public override bAssetToPToken; + mapping(address => address) public bAssetToPToken; // Full list of all bAssets supported here address[] internal bAssetsMapped; diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 8cf25f6e..7779417e 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -4,7 +4,6 @@ import { BN, simpleToExactAmount } from "@utils/math" import { increaseTime } from "@utils/time" import { expect } from "chai" import { Signer, constants } from "ethers" -import { ProvidedRequiredArgumentsOnDirectivesRule } from "graphql/validation/rules/ProvidedRequiredArgumentsRule" import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" import { deployFeederPool, FeederData } from "tasks/utils/feederUtils" @@ -116,7 +115,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await alchemixIntegration.rewardToken(), "rewards token").to.eq(ALCX.address) expect(await alchemixIntegration.stakingPools(), "Alchemix staking pools").to.eq(alchemixStakingPoolsAddress) - await alchemixIntegration["initialize(address[])"]([alUSD.address]) + await alchemixIntegration.initialize([alUSD.address]) expect(await alchemixIntegration.bAssetToPoolId(alUSD.address)).to.eq(0) }) From afc01e7f7b9e07c82900d526da2f9f0733cd27d8 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 00:28:09 +1000 Subject: [PATCH 06/21] feat: add ALCX to liquidator for alUSD Feeder Pool chore: refactor of Alchemix integration --- contracts/masset/liquidator/Liquidator.sol | 63 ++++--- .../masset/peripheral/AlchemixIntegration.sol | 134 +++++-------- tasks/utils/networkAddressFactory.ts | 9 + .../feeders/feeders-musd-alchemix.spec.ts | 176 ++++++++++++++---- 4 files changed, 241 insertions(+), 141 deletions(-) diff --git a/contracts/masset/liquidator/Liquidator.sol b/contracts/masset/liquidator/Liquidator.sol index 2bd02bfb..0c12ff57 100644 --- a/contracts/masset/liquidator/Liquidator.sol +++ b/contracts/masset/liquidator/Liquidator.sol @@ -62,6 +62,8 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM IUniswapV3Quoter public immutable uniswapQuoter; /// @notice Compound Token (COMP) address address public immutable compToken; + /// @notice Alchemix (ALCX) address + address public immutable alchemixToken; // No longer used struct DeprecatedLiquidation { @@ -91,7 +93,8 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM address _aaveToken, address _uniswapRouter, address _uniswapQuoter, - address _compToken + address _compToken, + address _alchemixToken ) ImmutableModule(_nexus) { require(_stkAave != address(0), "Invalid stkAAVE address"); stkAave = _stkAave; @@ -107,15 +110,19 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM require(_compToken != address(0), "Invalid COMP address"); compToken = _compToken; + + require(_alchemixToken != address(0), "Invalid ALCX address"); + alchemixToken = _alchemixToken; } /** - * @notice Liquidator approves Uniswap to transfer Aave and COMP tokens + * @notice Liquidator approves Uniswap to transfer Aave, COMP and ALCX tokens * @dev to be called via the proxy proposeUpgrade function, not the constructor. */ function upgrade() external { - IERC20(aaveToken).safeApprove(address(uniswapRouter), type(uint256).max); - IERC20(compToken).safeApprove(address(uniswapRouter), type(uint256).max); + // IERC20(aaveToken).safeApprove(address(uniswapRouter), type(uint256).max); + // IERC20(compToken).safeApprove(address(uniswapRouter), type(uint256).max); + IERC20(alchemixToken).safeApprove(address(uniswapRouter), type(uint256).max); } /*************************************** @@ -125,8 +132,8 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM /** * @notice Create a liquidation * @param _integration The integration contract address from which to receive sellToken - * @param _sellToken Token harvested from the integration contract. eg COMP or stkAave. - * @param _bAsset The asset to buy on Uniswap. eg USDC or WBTC + * @param _sellToken Token harvested from the integration contract. eg COMP, stkAave or ALCX. + * @param _bAsset The asset to buy on Uniswap. eg USDC, WBTC, GUSD or alUSD * @param _uniswapPath The Uniswap V3 bytes encoded path. * @param _trancheAmount The max amount of bAsset units to buy in each weekly tranche. * @param _minReturn Minimum exact amount of bAsset to get for each (whole) sellToken unit @@ -272,15 +279,18 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM /** * @notice Triggers a liquidation, flow (once per week): - * - Sells $COMP for $USDC (or other) on Uniswap (up to trancheAmount) - * - Mint mUSD using USDC - * - Send to SavingsManager + * - transfer sell token from integration to liquidator. eg COMP or ALCX + * - Swap sell token for bAsset on Uniswap (up to trancheAmount). eg + * - COMP for USDC + * - ALCX for alUSD + * - If bAsset in mAsset. eg USDC in mUSD + * - Mint mAsset using bAsset. eg mint mUSD using USDC. + * - Deposit mAsset to Savings Manager. eg deposit mUSD + * - else bAsset in Feeder Pool. eg alUSD in fPmUSD/alUSD. + * - Transfer bAsset to integration contract. eg transfer alUSD * @param _integration Integration for which to trigger liquidation */ function triggerLiquidation(address _integration) external override { - // solium-disable-next-line security/no-tx-origin - require(tx.origin == msg.sender, "Must be EOA"); - Liquidation memory liquidation = liquidations[_integration]; address bAsset = liquidation.bAsset; @@ -333,15 +343,27 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM ); uniswapRouter.exactInput(param); - // 4. Mint mAsset using purchased bAsset address mAsset = liquidation.mAsset; - uint256 minted = _mint(bAsset, mAsset); - - // 5. Send to SavingsManager - address savings = _savingsManager(); - ISavingsManager(savings).depositLiquidation(mAsset, minted); + // If the integration contract is connected to a mAsset like mUSD or mBTC + if (mAsset != address(0)) { + // 4a. Mint mAsset using purchased bAsset + uint256 minted = _mint(bAsset, mAsset); - emit Liquidated(sellToken, mAsset, minted, bAsset); + // 5a. Send to SavingsManager + address savings = _savingsManager(); + ISavingsManager(savings).depositLiquidation(mAsset, minted); + + emit Liquidated(sellToken, mAsset, minted, bAsset); + } else { + // If a feeder pool like alUSD + // 4b. transfer bAsset directly to the integration contract. + // this will then increase the boosted savings vault price. + IERC20 bAssetToken = IERC20(bAsset); + uint256 bAssetBal = bAssetToken.balanceOf(address(this)); + bAssetToken.transfer(_integration, bAssetBal); + + emit Liquidated(aaveToken, mAsset, bAssetBal, bAsset); + } } /** @@ -350,9 +372,6 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM * Can only claim more stkAave if the last claim's unstake window has ended. */ function claimStakedAave() external override { - // solium-disable-next-line security/no-tx-origin - require(tx.origin == msg.sender, "Must be EOA"); - // If the last claim has not yet been liquidated uint256 totalAaveBalanceMemory = totalAaveBalance; if (totalAaveBalanceMemory > 0) { diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index 40a47481..d129d345 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.2; import { IPlatformIntegration } from "../../interfaces/IPlatformIntegration.sol"; import { IAlchemixStakingPools } from "../../peripheral/Alchemix/IAlchemixStakingPools.sol"; -import { Initializable } from "@openzeppelin/contracts/utils/Initializable.sol"; import { ImmutableModule } from "../../shared/ImmutableModule.sol"; import { MassetHelpers } from "../../shared/MassetHelpers.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Initializable } from "@openzeppelin/contracts/utils/Initializable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** @@ -32,8 +32,6 @@ contract AlchemixIntegration is uint256 totalAmount, uint256 userAmount ); - event AssetAdded(address _bAsset, uint256 poolId); - event RewardTokenApproved(address rewardToken, address account); event RewardsClaimed(); /// @notice mAsset or Feeder Pool using the integration. eg fPmUSD/alUSD @@ -43,11 +41,10 @@ contract AlchemixIntegration is address public immutable rewardToken; /// @notice Alchemix's StakingPools contract IAlchemixStakingPools public immutable stakingPools; - - /// @notice bAsset => Alchemix pool id - mapping(address => uint256) public bAssetToPoolId; - /// @notice Full list of all bAssets supported here - address[] public bAssetsMapped; + /// @notice base asset that is integrated to Alchemix staking pool. eg alUSD + address public immutable bAsset; + /// @notice Alchemix pool identifier for bAsset deposits. eg pool id 0 for alUSD + uint256 public immutable poolId; /** * @dev Modifier to allow function calls only from the Governor. @@ -62,34 +59,40 @@ contract AlchemixIntegration is * @param _lp Address of liquidity provider. eg mAsset or feeder pool * @param _rewardToken Reward token, if any. eg ALCX * @param _stakingPools Alchemix StakingPools contract address + * @param _bAsset base asset to be deposited to Alchemix's staking pool. eg alUSD */ constructor( address _nexus, address _lp, address _rewardToken, - address _stakingPools + address _stakingPools, + address _bAsset ) ImmutableModule(_nexus) { require(_lp != address(0), "Invalid LP address"); require(_rewardToken != address(0), "Invalid reward token"); require(_stakingPools != address(0), "Invalid staking pools"); + require(_bAsset != address(0), "Invalid bAsset address"); + lpAddress = _lp; rewardToken = _rewardToken; stakingPools = IAlchemixStakingPools(_stakingPools); + bAsset = _bAsset; + + uint256 offsetPoolId = IAlchemixStakingPools(_stakingPools).tokenPoolIds(_bAsset); + require(offsetPoolId >= 1, "bAsset can not be farmed"); + // Take one off the poolId + poolId = offsetPoolId - 1; } /** - * @dev Simple initializer to add the first bAssets - * @param _bAssets array of base assets that can be staked in an Alchemix staking pool. eg alUSD + * @dev Approve the spending of the bAsset by Alchemix's StakingPools contract, + * and the spending of the reward token by mStable's Liquidator contract */ - function initialize(address[] calldata _bAssets) + function initialize() public initializer { - uint256 len = _bAssets.length; - for (uint256 i = 0; i < len; i++) { - address bAsset = _bAssets[i]; - _addAsset(bAsset); - } + _approveContracts(); } /*************************************** @@ -97,50 +100,23 @@ contract AlchemixIntegration is ****************************************/ /** - * @dev add another asset that can be staked in an Alchemix staking pool. - * This method can only be called by the system Governor - * @param _bAsset Address for the bAsset + * @dev Re-approve the spending of the bAsset by Alchemix's StakingPools contract, + * and the spending of the reward token by mStable's Liquidator contract + * if for some reason is it necessary. Only callable through Governance. */ - function addAsset(address _bAsset) external onlyGovernor { - _addAsset(_bAsset); + function reapproveContracts() external onlyGovernor { + _approveContracts(); } - function _addAsset(address _bAsset) internal { - require(_bAsset != address(0), "Invalid addresses"); - - uint256 poolId = _getPoolIdFor(_bAsset); - bAssetToPoolId[_bAsset] = poolId; - bAssetsMapped.push(_bAsset); + function _approveContracts() internal { + // Approve Alchemix staking pools contract to transfer bAssets for deposits. + MassetHelpers.safeInfiniteApprove(bAsset, address(stakingPools)); - // approve staking pools contract to transfer bAssets on deposits - MassetHelpers.safeInfiniteApprove(_bAsset, address(stakingPools)); - - emit AssetAdded(_bAsset, poolId); - } - - /** - * @dev Approves Liquidator to spend reward tokens - */ - function approveRewardToken() external onlyGovernor { + // Approve Liquidator to transfer reward token when claiming rewards. address liquidator = nexus.getModule(keccak256("Liquidator")); require(liquidator != address(0), "Liquidator address is zero"); MassetHelpers.safeInfiniteApprove(rewardToken, liquidator); - - emit RewardTokenApproved(rewardToken, liquidator); - } - - /** - * @dev Claims any accrued reward tokens for all the bAssets - */ - function claimRewards() external onlyGovernor { - uint256 len = bAssetsMapped.length; - for (uint256 i = 0; i < len; i++) { - uint256 poolId = bAssetToPoolId[bAssetsMapped[i]]; - stakingPools.claim(poolId); - } - - emit RewardsClaimed(); } /*************************************** @@ -148,7 +124,7 @@ contract AlchemixIntegration is ****************************************/ /** - * @dev Deposit a quantity of bAsset into the platform. Credited cTokens + * @notice Deposit a quantity of bAsset into the platform. Credited cTokens * remain here in the vault. Can only be called by whitelisted addresses * (mAsset and corresponding BasketManager) * @param _bAsset Address for the bAsset @@ -162,8 +138,7 @@ contract AlchemixIntegration is bool isTokenFeeCharged ) external override onlyLP nonReentrant returns (uint256 quantityDeposited) { require(_amount > 0, "Must deposit something"); - - uint256 poolId = bAssetToPoolId[_bAsset]; + require(_bAsset == bAsset, "Invalid bAsset"); quantityDeposited = _amount; @@ -182,7 +157,7 @@ contract AlchemixIntegration is } /** - * @dev Withdraw a quantity of bAsset from Alchemix + * @notice Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -198,7 +173,7 @@ contract AlchemixIntegration is } /** - * @dev Withdraw a quantity of bAsset from Alchemix + * @notice Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -222,10 +197,9 @@ contract AlchemixIntegration is uint256 _totalAmount, bool _hasTxFee ) internal { - require(_totalAmount > 0, "Must withdraw something"); require(_receiver != address(0), "Must specify recipient"); - - uint256 poolId = bAssetToPoolId[_bAsset]; + require(_bAsset == bAsset, "Invalid bAsset"); + require(_totalAmount > 0, "Must withdraw something"); uint256 userWithdrawal = _amount; @@ -248,7 +222,7 @@ contract AlchemixIntegration is } /** - * @dev Withdraw a quantity of bAsset from the cache. + * @notice Withdraw a quantity of bAsset from the cache. * @param _receiver Address to which the bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -258,8 +232,9 @@ contract AlchemixIntegration is address _bAsset, uint256 _amount ) external override onlyLP nonReentrant { - require(_amount > 0, "Must withdraw something"); require(_receiver != address(0), "Must specify recipient"); + require(_bAsset == bAsset, "Invalid bAsset"); + require(_amount > 0, "Must withdraw something"); IERC20(_bAsset).safeTransfer(_receiver, _amount); @@ -267,47 +242,34 @@ contract AlchemixIntegration is } /** - * @dev Get the total bAsset value held in the platform + * @notice Get the total bAsset value held in the platform * @param _bAsset Address of the bAsset * @return balance Total value of the bAsset in the platform */ function checkBalance(address _bAsset) external view override returns (uint256 balance) { - uint256 poolId = bAssetToPoolId[_bAsset]; + require(_bAsset == bAsset, "Invalid bAsset"); balance = stakingPools.getStakeTotalDeposited(address(this), poolId); } /*************************************** - APPROVALS + Liquidation ****************************************/ /** - * @dev Re-approve the spending of all bAssets by the staking pools contract, - * if for some reason is it necessary. Only callable through Governance. + * @notice Claims any accrued reward tokens from the Alchemix staking pool. + * eg ALCX tokens from the alUSD deposits. + * Claimed rewards are sent to this integration contract. + * @dev The Alchemix StakingPools will emit event + * TokensClaimed(user, poolId, amount) */ - function reApproveAllTokens() external onlyGovernor { - uint256 bAssetCount = bAssetsMapped.length; - for (uint256 i = 0; i < bAssetCount; i++) { - address bAsset = bAssetsMapped[i]; - MassetHelpers.safeInfiniteApprove(bAsset, address(stakingPools)); - } + function claimRewards() external { + stakingPools.claim(poolId); } /*************************************** HELPERS ****************************************/ - /** - * @dev Get the Alchemix pool id for a bAsset. - * @param _asset Address of the integrated asset - * @return poolId Corresponding Alchemix staking pool identifier - */ - function _getPoolIdFor(address _asset) internal view returns (uint256 poolId) { - poolId = stakingPools.tokenPoolIds(_asset); - require(poolId >= 1, "Asset not supported on Alchemix"); - // Take one off the poolId - poolId = poolId - 1; - } - /** * @dev Simple helper func to get the min of two values */ diff --git a/tasks/utils/networkAddressFactory.ts b/tasks/utils/networkAddressFactory.ts index 61b8bea8..558073c0 100644 --- a/tasks/utils/networkAddressFactory.ts +++ b/tasks/utils/networkAddressFactory.ts @@ -25,6 +25,9 @@ export const contractNames = [ "AaveLendingPoolAddressProvider", "AlchemixStakingPool", "QuickSwapRouter", + "UniswapRouterV3", + "UniswapQuoterV3", + "UniswapEthToken", "MStableYieldSource", // Used for PoolTogether ] as const export type ContractNames = typeof contractNames[number] @@ -75,6 +78,12 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri return "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5" case "AlchemixStakingPool": return "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" + case "UniswapRouterV3": + return "0xE592427A0AEce92De3Edee1F18E0157C05861564" + case "UniswapQuoterV3": + return "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6" + case "UniswapEthToken": + return "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" case "MStableYieldSource": return "0xdB4C9f763A4B13CF2830DFe7c2854dADf5b96E99" default: diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 7779417e..d57507bc 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -1,6 +1,8 @@ -import { ONE_WEEK } from "@utils/constants" +import { assertBNClose } from "@utils/assertions" +import { MAX_UINT256, ONE_WEEK, ZERO_ADDRESS } from "@utils/constants" import { impersonate } from "@utils/fork" import { BN, simpleToExactAmount } from "@utils/math" +import { encodeUniswapPath } from "@utils/peripheral/uniswap" import { increaseTime } from "@utils/time" import { expect } from "chai" import { Signer, constants } from "ethers" @@ -8,9 +10,16 @@ import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" import { deployFeederPool, FeederData } from "tasks/utils/feederUtils" import { getChainAddress } from "tasks/utils/networkAddressFactory" -import { ALCX, alUSD, Chain, mUSD } from "tasks/utils/tokens" -import { FeederPool, IERC20, IERC20__factory } from "types/generated" -import { AlchemixIntegration } from "types/generated/AlchemixIntegration" +import { AAVE, ALCX, alUSD, Chain, COMP, mUSD, stkAAVE } from "tasks/utils/tokens" +import { + AlchemixIntegration, + FeederPool, + IERC20, + IERC20__factory, + Liquidator, + LiquidatorProxy__factory, + Liquidator__factory, +} from "types/generated" import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" import { IAlchemixStakingPools__factory } from "types/generated/factories/IAlchemixStakingPools__factory" import { IAlchemixStakingPools } from "types/generated/IAlchemixStakingPools" @@ -25,6 +34,9 @@ const chain = Chain.mainnet const nexusAddress = getChainAddress("Nexus", chain) const liquidatorAddress = getChainAddress("Liquidator", chain) const alchemixStakingPoolsAddress = getChainAddress("AlchemixStakingPool", chain) +const uniswapRouterAddress = getChainAddress("UniswapRouterV3", chain) +const uniswapQuoterAddress = getChainAddress("UniswapQuoterV3", chain) +const uniswapEthToken = getChainAddress("UniswapEthToken", Chain.mainnet) context("alUSD Feeder Pool integration to Alchemix", () => { let governor: Signer @@ -33,12 +45,13 @@ context("alUSD Feeder Pool integration to Alchemix", () => { let mUsdWhale: Signer let alUsdWhale: Signer let alUsdFp: FeederPool - let mUsd: IERC20 - let alUsd: IERC20 + let musdToken: IERC20 + let alusdToken: IERC20 let alcxToken: IERC20 let alchemixIntegration: AlchemixIntegration let alchemixStakingPools: IAlchemixStakingPools let poolId: BN + let liquidator: Liquidator const mintAmount = simpleToExactAmount(10000) @@ -70,11 +83,12 @@ context("alUSD Feeder Pool integration to Alchemix", () => { ), ) - mUsd = await IERC20__factory.connect(mUSD.address, deployer) - alUsd = await IERC20__factory.connect(alUSD.address, deployer) + musdToken = await IERC20__factory.connect(mUSD.address, deployer) + alusdToken = await IERC20__factory.connect(alUSD.address, deployer) alcxToken = await IERC20__factory.connect(ALCX.address, deployer) alchemixStakingPools = await IAlchemixStakingPools__factory.connect(alchemixStakingPoolsAddress, deployer) poolId = (await alchemixStakingPools.tokenPoolIds(alUSD.address)).sub(1) + liquidator = await Liquidator__factory.connect(liquidatorAddress, governor) }) it("Test connectivity", async () => { const currentBlock = await ethers.provider.getBlockNumber() @@ -84,7 +98,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { }) it("deploy alUSD Feeder Pool", async () => { const config = { - a: BN.from(225), + a: BN.from(60), limits: { min: simpleToExactAmount(10, 16), max: simpleToExactAmount(90, 16), @@ -107,36 +121,51 @@ context("alUSD Feeder Pool integration to Alchemix", () => { alchemixIntegration = await deployContract( new AlchemixIntegration__factory(deployer), "Alchemix alUSD Integration", - [nexusAddress, alUsdFp.address, ALCX.address, alchemixStakingPoolsAddress], + [nexusAddress, alUsdFp.address, ALCX.address, alchemixStakingPoolsAddress, alUSD.address], ) expect(await alchemixIntegration.nexus(), "nexus").to.eq(nexusAddress) expect(await alchemixIntegration.lpAddress(), "lp (feeder pool)").to.eq(alUsdFp.address) expect(await alchemixIntegration.rewardToken(), "rewards token").to.eq(ALCX.address) expect(await alchemixIntegration.stakingPools(), "Alchemix staking pools").to.eq(alchemixStakingPoolsAddress) + expect(await alchemixIntegration.poolId(), "pool id").to.eq(0) + expect(await alchemixIntegration.bAsset(), "bAsset").to.eq(alUSD.address) + }) + it("initialize Alchemix integration", async () => { + expect( + await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), + "integration alUSD allowance before", + ).to.eq(0) + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance before").to.eq(0) - await alchemixIntegration.initialize([alUSD.address]) + await alchemixIntegration.initialize() - expect(await alchemixIntegration.bAssetToPoolId(alUSD.address)).to.eq(0) + expect( + await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), + "integration alUSD allowance after", + ).to.eq(MAX_UINT256) + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance after").to.eq( + MAX_UINT256, + ) }) it("Migrate alUSD Feeder Pool to the Alchemix integration", async () => { // Migrate the alUSD - await alUsdFp.connect(governor).migrateBassets([alUsd.address], alchemixIntegration.address) + await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) }) it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { - expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) + // expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) // This will be done via the delayedProxyAdmin on mainnet - await alchemixIntegration.connect(governor).approveRewardToken() + await alchemixIntegration.connect(governor).reapproveContracts() expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) }) it("Mint some mUSD/alUSD in the Feeder Pool", async () => { - const alUsdBassetBefore = await alUsdFp.getBasset(alUsd.address) + const alUsdBassetBefore = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetBefore = await alUsdFp.getBasset(mUSD.address) - expect(await alUsd.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) - expect(await mUsd.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) + expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) + expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) expect( await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), "integration's alUSD deposited before", @@ -147,21 +176,21 @@ context("alUSD Feeder Pool integration to Alchemix", () => { ).to.eq(0) // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) - await mUsd.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) - expect(await mUsd.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) + await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) - await alUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) - await mUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) - expect(await alUsd.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq(constants.MaxUint256) - expect(await mUsd.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) - expect(await alUsd.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) - expect(await mUsd.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) + await alusdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + await musdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + expect(await alusdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq(constants.MaxUint256) + expect(await musdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) + expect(await alusdToken.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) await alUsdFp .connect(alUsdWhale) - .mintMulti([alUsd.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) + .mintMulti([alusdToken.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) - const alUsdBassetAfter = await alUsdFp.getBasset(alUsd.address) + const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetAfter = await alUsdFp.getBasset(mUSD.address) expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), @@ -192,21 +221,102 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect( await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "integration's accrued ALCX after", - ).to.gt(0) + ).to.gt(simpleToExactAmount(1, 12)) }) it("redeem a lot of alUSD", async () => { + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.gt(simpleToExactAmount(1, 12)) const redeemAmount = simpleToExactAmount(8000) + await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], mintAmount, alUsdWhaleAddress) - const alUsdBassetAfter = await alUsdFp.getBasset(alUsd.address) + const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(mintAmount.sub(redeemAmount)) - const integrationAlusdBalance = await alUsd.balanceOf(alchemixIntegration.address) + const integrationAlusdBalance = await alusdToken.balanceOf(alchemixIntegration.address) expect(integrationAlusdBalance, "alUSD in cache").to.gt(0) expect( await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), "integration's alUSD deposited after", ).to.eq(mintAmount.sub(redeemAmount).sub(integrationAlusdBalance)) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.eq(0) + }) + it("Claim accrued ALCX rewards", async () => { + await increaseTime(ONE_WEEK) + + const unclaimedAlcxBefore = await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId) + const integrationAlusdBalanceBefore = await alusdToken.balanceOf(alchemixIntegration.address) + expect(unclaimedAlcxBefore, "unclaimed ALCX before").to.gt(simpleToExactAmount(1, 10)) + expect(integrationAlusdBalanceBefore, "integration alUSD balance before").to.gt(0) + + await alchemixIntegration.claimRewards() + + expect(await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "unclaimed ALCX after").to.eq(0) + // TODO fix these checks + // expect(await alusdToken.balanceOf(alchemixIntegration.address), "integration alUSD balance after").to.eq( + // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), + // ) + // assertBNClose( + // await alusdToken.balanceOf(alchemixIntegration.address), + // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), + // BN.from(1000), + // ) + }) + describe.skip("liquidator", () => { + let newLiquidatorImpl: Liquidator + it("deploy new liquidator", async () => { + newLiquidatorImpl = await deployContract(new Liquidator__factory(deployer), "Liquidator", [ + nexusAddress, + stkAAVE.address, + AAVE.address, + uniswapRouterAddress, + uniswapQuoterAddress, + COMP.address, + ALCX.address, + ]) + + expect(await newLiquidatorImpl.nexus(), "nexus").to.eq(nexusAddress) + expect(await newLiquidatorImpl.stkAave(), "stkAave").to.eq(stkAAVE.address) + expect(await newLiquidatorImpl.aaveToken(), "aaveToken").to.eq(AAVE.address) + expect(await newLiquidatorImpl.uniswapRouter(), "uniswapRouter").to.eq(uniswapRouterAddress) + expect(await newLiquidatorImpl.uniswapQuoter(), "uniswapQuoter").to.eq(uniswapQuoterAddress) + expect(await newLiquidatorImpl.compToken(), "compToken").to.eq(COMP.address) + expect(await newLiquidatorImpl.alchemixToken(), "alchemixToken").to.eq(ALCX.address) + }) + it("upgrade liquidator proxy", async () => { + const liquidatorProxy = LiquidatorProxy__factory.connect(liquidatorAddress, governor) + expect(liquidatorProxy.admin(), "admin before").to.eq(governorAddress) + + await liquidatorProxy.upgradeTo(newLiquidatorImpl.address) + + expect(await liquidatorProxy.implementation(), "liquidator impl address").to.eq(newLiquidatorImpl.address) + }) + it("upgrade liquidator", async () => { + await liquidator.upgrade() + }) + it("create liquidation of ALCX", async () => { + const uniswapPath = encodeUniswapPath([ALCX.address, uniswapEthToken, alUSD.address], [3000, 3000]) + await liquidator.createLiquidation( + alchemixIntegration.address, + ALCX.address, + alUSD.address, + uniswapPath.encoded, + uniswapPath.encodedReversed, + simpleToExactAmount(5000), + 200, + ZERO_ADDRESS, + false, + ) + }) + it("trigger ALCX liquidation", async () => { + await liquidator.triggerLiquidation(alchemixIntegration.address) + }) + // liquidate COMP + // claim stkAAVE + // liquidate stkAAVE }) - // Mint alUSD in FP, test integration/FP balances, wait for a block or two, claim rewards - // Liquidate rewards and test output/balances }) From c93d32af8e0ce172c3712723dfaf722bec0ca43e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 01:58:22 +1000 Subject: [PATCH 07/21] chore: fork test for alUSD fp vault --- .../boosted-staking/BoostedDualVault.sol | 2 +- tasks/deployFeeders.ts | 33 +- tasks/utils/feederUtils.ts | 27 +- tasks/utils/tokens.ts | 2 +- .../feeders/feeders-musd-alchemix.spec.ts | 290 +++++++++++------- 5 files changed, 205 insertions(+), 149 deletions(-) diff --git a/contracts/rewards/boosted-staking/BoostedDualVault.sol b/contracts/rewards/boosted-staking/BoostedDualVault.sol index 9d47cc3e..9f25c4d8 100644 --- a/contracts/rewards/boosted-staking/BoostedDualVault.sol +++ b/contracts/rewards/boosted-staking/BoostedDualVault.sol @@ -94,7 +94,7 @@ contract BoostedDualVault is /** * @param _nexus mStable system Nexus address - * @param _stakingToken token that is beinf rewarded for being staked. eg MTA, imUSD or fPmUSD/GUSD + * @param _stakingToken token that is being rewarded for being staked. eg MTA, imUSD or fPmUSD/GUSD * @param _boostDirector vMTA boost director * @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18 * @param _boostCoeff Boost coefficent using the the boost formula diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index 380f560a..1f2b2d19 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -5,7 +5,7 @@ import "tsconfig-paths/register" import { task, types } from "hardhat/config" import { FeederPool__factory, CompoundIntegration__factory, CompoundIntegration } from "types/generated" -import { simpleToExactAmount } from "@utils/math" +import { BN, simpleToExactAmount } from "@utils/math" import { BUSD, CREAM, cyMUSD, GUSD, mUSD, tokens } from "./utils/tokens" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { getSigner } from "./utils/defender-utils" @@ -18,14 +18,14 @@ task("deployFeederPool", "Deploy Feeder Pool") .addOptionalParam("a", "Amplitude coefficient (A)", 100, types.int) .addOptionalParam("min", "Minimum asset weight of the basket as a percentage. eg 10 for 10% of the basket.", 10, types.int) .addOptionalParam("max", "Maximum asset weight of the basket as a percentage. eg 90 for 90% of the basket.", 90, types.int) - .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, { hardhatArguments, ethers, network }) => { const signer = await getSigner(ethers, taskArgs.speed) const chain = getChain(network.name, hardhatArguments.config) - const mAsset = tokens.find((t) => t.address === taskArgs.masset) + const mAsset = tokens.find((t) => t.symbol === taskArgs.masset) if (!mAsset) throw Error(`Could not find mAsset token with symbol ${taskArgs.masset}`) - const fAsset = tokens.find((t) => t.address === taskArgs.fasset) + const fAsset = tokens.find((t) => t.symbol === taskArgs.fasset) if (!fAsset) throw Error(`Could not find Feeder Pool token with symbol ${taskArgs.fasset}`) if (taskArgs.a < 10 || taskArgs.min > 5000) throw Error(`Invalid amplitude coefficient (A) ${taskArgs.a}`) @@ -60,10 +60,11 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") true, types.string, ) + .addOptionalParam("stakingType", "Which token address is being staked? eg address, feeder or save", "feeder", types.string) .addParam("rewardsToken", "Token symbol of reward. eg MTA or PMTA for Polygon", undefined, types.string) .addOptionalParam("dualRewardToken", "Token symbol of second reward. eg WMATIC, ALCX, QI", undefined, types.string) .addOptionalParam("price", "Price coefficient is the value of the mAsset in USD. eg mUSD/USD = 1, mBTC/USD", 1, types.int) - .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers, taskArgs.speed) const chain = getChain(network.name, hardhatArguments.config) @@ -71,23 +72,31 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") if (taskArgs.name?.length < 4) throw Error(`Invalid token name ${taskArgs.name}`) if (taskArgs.symbol?.length <= 0 || taskArgs.symbol?.length > 12) throw Error(`Invalid token name ${taskArgs.name}`) - const stakingToken = tokens.find((t) => t.address === taskArgs.stakingToken) + const stakingToken = tokens.find((t) => t.symbol === taskArgs.stakingToken) if (!stakingToken) throw Error(`Could not find staking token with symbol ${taskArgs.stakingToken}`) - const rewardToken = tokens.find((t) => t.address === taskArgs.rewardToken) + let stakingTokenAddress = stakingToken.feederPool + if (taskArgs.stakingType === "address") { + stakingTokenAddress = stakingToken.address + } else if (taskArgs.stakingType === "save") { + stakingTokenAddress = stakingToken.savings + } else if (taskArgs.stakingType !== "feeder") { + throw Error(`Invalid staking type ${taskArgs.stakingType}. Must be either address, feeder or save`) + } + const rewardToken = tokens.find((t) => t.symbol === taskArgs.rewardToken) if (!rewardToken) throw Error(`Could not find reward token with symbol ${taskArgs.rewardToken}`) if (taskArgs.price < 0 || taskArgs.price >= simpleToExactAmount(1)) throw Error(`Invalid price coefficient ${taskArgs.price}`) - const dualRewardToken = tokens.find((t) => t.address === taskArgs.dualRewardToken) + const dualRewardToken = tokens.find((t) => t.symbol === taskArgs.dualRewardToken) const vaultData: VaultData = { boosted: taskArgs.boosted, name: taskArgs.name, symbol: taskArgs.symbol, - priceCoeff: taskArgs.price, - stakingToken, - rewardToken, - dualRewardToken, + priceCoeff: BN.from(taskArgs.price), + stakingToken: stakingTokenAddress, + rewardToken: rewardToken.address, + dualRewardToken: dualRewardToken.address, } await deployVault(signer, vaultData, chain) diff --git a/tasks/utils/feederUtils.ts b/tasks/utils/feederUtils.ts index 78ec8fdb..698080c2 100644 --- a/tasks/utils/feederUtils.ts +++ b/tasks/utils/feederUtils.ts @@ -46,9 +46,9 @@ export interface VaultData { name: string symbol: string priceCoeff?: BN - stakingToken: Token - rewardToken: Token - dualRewardToken?: Token + stakingToken: string + rewardToken: string + dualRewardToken?: string } export const deployFasset = async ( @@ -145,42 +145,37 @@ export const deployVault = async ( if (vaultData.dualRewardToken) { vault = await deployContract(new BoostedDualVault__factory(signer), "BoostedDualVault", [ getChainAddress("Nexus", chain), - vaultData.stakingToken.vault, + vaultData.stakingToken, getChainAddress("BoostDirector", chain), vaultData.priceCoeff, boostCoeff, - vaultData.rewardToken.address, - vaultData.dualRewardToken.address, + vaultData.rewardToken, + vaultData.dualRewardToken, ]) } else { vault = await deployContract(new BoostedVault__factory(signer), "BoostedVault", [ getChainAddress("Nexus", chain), - vaultData.stakingToken.vault, + vaultData.stakingToken, getChainAddress("BoostDirector", chain), vaultData.priceCoeff, boostCoeff, - vaultData.rewardToken.address, + vaultData.rewardToken, ]) } } else if (vaultData.dualRewardToken) { vault = await deployContract( new StakingRewardsWithPlatformToken__factory(signer), "StakingRewardsWithPlatformToken", - [ - getChainAddress("Nexus", chain), - vaultData.stakingToken.vault, - vaultData.rewardToken.address, - vaultData.dualRewardToken.address, - ], + [getChainAddress("Nexus", chain), vaultData.stakingToken, vaultData.rewardToken, vaultData.dualRewardToken], ) } else { vault = await deployContract(new StakingRewards__factory(signer), "StakingRewards", [ getChainAddress("Nexus", chain), - vaultData.stakingToken.vault, + vaultData.stakingToken, getChainAddress("BoostDirector", chain), vaultData.priceCoeff, boostCoeff, - vaultData.rewardToken.address, + vaultData.rewardToken, ]) } diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index 075e53eb..488ab39b 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -209,7 +209,7 @@ export const MFRAX: Token = { export const alUSD: Token = { symbol: "alUSD", address: "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", - feederPool: undefined, // TODO add after deployment + feederPool: "0x4eaa01974B6594C0Ee62fFd7FEE56CF11E6af936", integrator: undefined, // TODO add after deployment vault: undefined, // TODO add after deployment chain: Chain.mainnet, diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index d57507bc..9b09de5f 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -8,11 +8,12 @@ import { expect } from "chai" import { Signer, constants } from "ethers" import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" -import { deployFeederPool, FeederData } from "tasks/utils/feederUtils" +import { deployFeederPool, deployVault, FeederData, VaultData } from "tasks/utils/feederUtils" import { getChainAddress } from "tasks/utils/networkAddressFactory" -import { AAVE, ALCX, alUSD, Chain, COMP, mUSD, stkAAVE } from "tasks/utils/tokens" +import { AAVE, ALCX, alUSD, Chain, COMP, MTA, mUSD, stkAAVE } from "tasks/utils/tokens" import { AlchemixIntegration, + BoostedDualVault, FeederPool, IERC20, IERC20__factory, @@ -45,6 +46,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { let mUsdWhale: Signer let alUsdWhale: Signer let alUsdFp: FeederPool + let vault: BoostedDualVault let musdToken: IERC20 let alusdToken: IERC20 let alcxToken: IERC20 @@ -98,7 +100,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { }) it("deploy alUSD Feeder Pool", async () => { const config = { - a: BN.from(60), + a: BN.from(50), limits: { min: simpleToExactAmount(10, 16), max: simpleToExactAmount(90, 16), @@ -117,63 +119,12 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await alUsdFp.name(), "name").to.eq(fpData.name) expect(await alUsdFp.symbol(), "symbol").to.eq(fpData.symbol) }) - it("deploy Alchemix integration", async () => { - alchemixIntegration = await deployContract( - new AlchemixIntegration__factory(deployer), - "Alchemix alUSD Integration", - [nexusAddress, alUsdFp.address, ALCX.address, alchemixStakingPoolsAddress, alUSD.address], - ) - - expect(await alchemixIntegration.nexus(), "nexus").to.eq(nexusAddress) - expect(await alchemixIntegration.lpAddress(), "lp (feeder pool)").to.eq(alUsdFp.address) - expect(await alchemixIntegration.rewardToken(), "rewards token").to.eq(ALCX.address) - expect(await alchemixIntegration.stakingPools(), "Alchemix staking pools").to.eq(alchemixStakingPoolsAddress) - expect(await alchemixIntegration.poolId(), "pool id").to.eq(0) - expect(await alchemixIntegration.bAsset(), "bAsset").to.eq(alUSD.address) - }) - it("initialize Alchemix integration", async () => { - expect( - await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), - "integration alUSD allowance before", - ).to.eq(0) - expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance before").to.eq(0) - - await alchemixIntegration.initialize() - - expect( - await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), - "integration alUSD allowance after", - ).to.eq(MAX_UINT256) - expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance after").to.eq( - MAX_UINT256, - ) - }) - it("Migrate alUSD Feeder Pool to the Alchemix integration", async () => { - // Migrate the alUSD - await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) - }) - it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { - // expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) - - // This will be done via the delayedProxyAdmin on mainnet - await alchemixIntegration.connect(governor).reapproveContracts() - - expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) - }) it("Mint some mUSD/alUSD in the Feeder Pool", async () => { const alUsdBassetBefore = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetBefore = await alUsdFp.getBasset(mUSD.address) expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) - expect( - await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), - "integration's alUSD deposited before", - ).to.eq(0) - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX before", - ).to.eq(0) // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) @@ -196,75 +147,155 @@ context("alUSD Feeder Pool integration to Alchemix", () => { alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), ) expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount)) - - expect( - await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), - "integration's alUSD deposited after", - ).to.eq(mintAmount) - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX after", - ).to.eq(0) }) - it("accrue ALCX", async () => { - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX before", - ).to.eq(0) - - await increaseTime(ONE_WEEK) - - expect( - await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), - "integration's alUSD deposited after", - ).to.eq(mintAmount) - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX after", - ).to.gt(simpleToExactAmount(1, 12)) + describe("Dual Rewards Feeder Pool Vault", () => { + it("deploy vault", async () => { + const vaultData: VaultData = { + boosted: true, + name: "v-mUSD/alUSD fPool Vault", + symbol: "v-fPmUSD/alUSD", + priceCoeff: BN.from(1), + stakingToken: alUsdFp.address, + rewardToken: MTA.address, + dualRewardToken: ALCX.address, + } + + vault = (await deployVault(deployer, vaultData, chain)) as BoostedDualVault + }) }) - it("redeem a lot of alUSD", async () => { - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX before", - ).to.gt(simpleToExactAmount(1, 12)) - const redeemAmount = simpleToExactAmount(8000) + describe.skip("Integration", () => { + it("deploy Alchemix integration", async () => { + alchemixIntegration = await deployContract( + new AlchemixIntegration__factory(deployer), + "Alchemix alUSD Integration", + [nexusAddress, alUsdFp.address, ALCX.address, alchemixStakingPoolsAddress, alUSD.address], + ) + + expect(await alchemixIntegration.nexus(), "nexus").to.eq(nexusAddress) + expect(await alchemixIntegration.lpAddress(), "lp (feeder pool)").to.eq(alUsdFp.address) + expect(await alchemixIntegration.rewardToken(), "rewards token").to.eq(ALCX.address) + expect(await alchemixIntegration.stakingPools(), "Alchemix staking pools").to.eq(alchemixStakingPoolsAddress) + expect(await alchemixIntegration.poolId(), "pool id").to.eq(0) + expect(await alchemixIntegration.bAsset(), "bAsset").to.eq(alUSD.address) + }) + it("initialize Alchemix integration", async () => { + expect( + await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), + "integration alUSD allowance before", + ).to.eq(0) + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance before").to.eq(0) - await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], mintAmount, alUsdWhaleAddress) + await alchemixIntegration.initialize() - const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) - expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(mintAmount.sub(redeemAmount)) - const integrationAlusdBalance = await alusdToken.balanceOf(alchemixIntegration.address) - expect(integrationAlusdBalance, "alUSD in cache").to.gt(0) - expect( - await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), - "integration's alUSD deposited after", - ).to.eq(mintAmount.sub(redeemAmount).sub(integrationAlusdBalance)) - expect( - await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), - "integration's accrued ALCX after", - ).to.eq(0) - }) - it("Claim accrued ALCX rewards", async () => { - await increaseTime(ONE_WEEK) - - const unclaimedAlcxBefore = await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId) - const integrationAlusdBalanceBefore = await alusdToken.balanceOf(alchemixIntegration.address) - expect(unclaimedAlcxBefore, "unclaimed ALCX before").to.gt(simpleToExactAmount(1, 10)) - expect(integrationAlusdBalanceBefore, "integration alUSD balance before").to.gt(0) - - await alchemixIntegration.claimRewards() - - expect(await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "unclaimed ALCX after").to.eq(0) - // TODO fix these checks - // expect(await alusdToken.balanceOf(alchemixIntegration.address), "integration alUSD balance after").to.eq( - // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), - // ) - // assertBNClose( - // await alusdToken.balanceOf(alchemixIntegration.address), - // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), - // BN.from(1000), - // ) + expect( + await alusdToken.allowance(alchemixIntegration.address, alchemixStakingPools.address), + "integration alUSD allowance after", + ).to.eq(MAX_UINT256) + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress), "integration ALCX allowance after").to.eq( + MAX_UINT256, + ) + }) + it("Migrate alUSD Feeder Pool to the Alchemix integration", async () => { + // Migrate the alUSD + await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) + }) + it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { + // expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) + + // This will be done via the delayedProxyAdmin on mainnet + await alchemixIntegration.connect(governor).reapproveContracts() + + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) + }) + it("Mint some mUSD/alUSD in the Feeder Pool", async () => { + const alUsdBassetBefore = await alUsdFp.getBasset(alusdToken.address) + const mUsdBassetBefore = await alUsdFp.getBasset(mUSD.address) + + expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) + expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited before", + ).to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.eq(0) + + // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) + await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) + + await alusdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + await musdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + expect(await alusdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq( + constants.MaxUint256, + ) + expect(await musdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) + expect(await alusdToken.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) + + await alUsdFp + .connect(alUsdWhale) + .mintMulti([alusdToken.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) + + const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) + const mUsdBassetAfter = await alUsdFp.getBasset(mUSD.address) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( + alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), + ) + expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq( + mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), + ) + + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.eq(0) + }) + it("accrue ALCX", async () => { + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.eq(0) + + await increaseTime(ONE_WEEK) + + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.gt(simpleToExactAmount(1, 12)) + }) + it("redeem a lot of alUSD", async () => { + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX before", + ).to.gt(simpleToExactAmount(1, 12)) + const redeemAmount = simpleToExactAmount(8000) + + await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], mintAmount, alUsdWhaleAddress) + + const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(mintAmount.sub(redeemAmount)) + const integrationAlusdBalance = await alusdToken.balanceOf(alchemixIntegration.address) + expect(integrationAlusdBalance, "alUSD in cache").to.gt(0) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(mintAmount.sub(redeemAmount).sub(integrationAlusdBalance)) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.eq(0) + }) }) describe.skip("liquidator", () => { let newLiquidatorImpl: Liquidator @@ -312,6 +343,27 @@ context("alUSD Feeder Pool integration to Alchemix", () => { false, ) }) + it("Claim accrued ALCX rewards", async () => { + await increaseTime(ONE_WEEK) + + const unclaimedAlcxBefore = await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId) + const integrationAlusdBalanceBefore = await alusdToken.balanceOf(alchemixIntegration.address) + expect(unclaimedAlcxBefore, "unclaimed ALCX before").to.gt(simpleToExactAmount(1, 10)) + expect(integrationAlusdBalanceBefore, "integration alUSD balance before").to.gt(0) + + await alchemixIntegration.claimRewards() + + expect(await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "unclaimed ALCX after").to.eq(0) + // TODO fix these checks + // expect(await alusdToken.balanceOf(alchemixIntegration.address), "integration alUSD balance after").to.eq( + // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), + // ) + // assertBNClose( + // await alusdToken.balanceOf(alchemixIntegration.address), + // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), + // BN.from(1000), + // ) + }) it("trigger ALCX liquidation", async () => { await liquidator.triggerLiquidation(alchemixIntegration.address) }) From 0e544be094193dbe15795ed79f9bc59c17b9e58a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 11:41:15 +1000 Subject: [PATCH 08/21] choire: more Alchemix fork tests --- .../feeders/feeders-musd-alchemix.spec.ts | 109 ++++++++++-------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 9b09de5f..b341c28f 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -55,7 +55,9 @@ context("alUSD Feeder Pool integration to Alchemix", () => { let poolId: BN let liquidator: Liquidator - const mintAmount = simpleToExactAmount(10000) + const firstMintAmount = simpleToExactAmount(10000) + const secondMintAmount = simpleToExactAmount(2000) + const approveAmount = firstMintAmount.add(secondMintAmount) before("reset block number", async () => { await network.provider.request({ @@ -127,28 +129,35 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) - await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) - expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) + await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, approveAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(approveAmount) await alusdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) await musdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) expect(await alusdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq(constants.MaxUint256) expect(await musdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) - expect(await alusdToken.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) - expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) + expect(await alusdToken.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(approveAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(approveAmount) await alUsdFp .connect(alUsdWhale) - .mintMulti([alusdToken.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) + .mintMulti( + [alusdToken.address, mUSD.address], + [firstMintAmount, firstMintAmount], + firstMintAmount.mul(2).sub(1), + alUsdWhaleAddress, + ) const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetAfter = await alUsdFp.getBasset(mUSD.address) expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( - alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), + alUsdBassetBefore.vaultData.vaultBalance.add(firstMintAmount), + ) + expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq( + mUsdBassetBefore.vaultData.vaultBalance.add(firstMintAmount), ) - expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount)) }) - describe("Dual Rewards Feeder Pool Vault", () => { + describe.skip("Dual Rewards Feeder Pool Vault", () => { it("deploy vault", async () => { const vaultData: VaultData = { boosted: true, @@ -162,8 +171,13 @@ context("alUSD Feeder Pool integration to Alchemix", () => { vault = (await deployVault(deployer, vaultData, chain)) as BoostedDualVault }) + it("deposit to vault", async () => { + const stakeAmount = simpleToExactAmount(1000) + await alUsdFp.approve(vault.address, stakeAmount) + await vault["stake(uint256)"](stakeAmount) + }) }) - describe.skip("Integration", () => { + describe("Integration", () => { it("deploy Alchemix integration", async () => { alchemixIntegration = await deployContract( new AlchemixIntegration__factory(deployer), @@ -196,23 +210,29 @@ context("alUSD Feeder Pool integration to Alchemix", () => { ) }) it("Migrate alUSD Feeder Pool to the Alchemix integration", async () => { - // Migrate the alUSD - await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) - }) - it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { - // expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) + expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(firstMintAmount) + expect(await alusdToken.balanceOf(alchemixIntegration.address), "alUSD integration bal before").to.eq(0) + expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(firstMintAmount) - // This will be done via the delayedProxyAdmin on mainnet - await alchemixIntegration.connect(governor).reapproveContracts() + await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) - expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) + // The migration just moves the alUSD to the integration contract. It is not deposited into the staking pool yet. + expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD fp bal after").to.eq(0) + expect(await alusdToken.balanceOf(alchemixIntegration.address), "alUSD integration bal after").to.eq(firstMintAmount) + expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal after").to.eq(firstMintAmount) + expect( + await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), + "integration's alUSD deposited after", + ).to.eq(0) + expect( + await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), + "integration's accrued ALCX after", + ).to.eq(0) }) it("Mint some mUSD/alUSD in the Feeder Pool", async () => { const alUsdBassetBefore = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetBefore = await alUsdFp.getBasset(mUSD.address) - expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) - expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) expect( await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), "integration's alUSD deposited before", @@ -222,36 +242,26 @@ context("alUSD Feeder Pool integration to Alchemix", () => { "integration's accrued ALCX before", ).to.eq(0) - // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) - await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) - expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsdWhale's mUSD bal after").to.gte(mintAmount) - - await alusdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) - await musdToken.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) - expect(await alusdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's alUSD bal after").to.eq( - constants.MaxUint256, - ) - expect(await musdToken.allowance(alUsdWhaleAddress, alUsdFp.address), "alUsdWhale's mUSD bal after").to.eq(constants.MaxUint256) - expect(await alusdToken.balanceOf(alUsdWhaleAddress), "alUsd whale alUSD bal before").gte(mintAmount) - expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(mintAmount) - await alUsdFp .connect(alUsdWhale) - .mintMulti([alusdToken.address, mUSD.address], [mintAmount, mintAmount], mintAmount.sub(1), alUsdWhaleAddress) + .mintMulti( + [alusdToken.address, mUSD.address], + [secondMintAmount, secondMintAmount], + secondMintAmount.mul(2).sub(1), + alUsdWhaleAddress, + ) const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) const mUsdBassetAfter = await alUsdFp.getBasset(mUSD.address) - expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( - alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), - ) - expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq( - mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), - ) - + expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD fp bal after").to.eq(0) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance after").to.eq(approveAmount) + expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance after").to.eq(approveAmount) + const cacheAmount = simpleToExactAmount(1000) + expect(await alusdToken.balanceOf(alchemixIntegration.address), "alUSD integration bal after").to.eq(cacheAmount) expect( await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), "integration's alUSD deposited after", - ).to.eq(mintAmount) + ).to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(secondMintAmount).sub(cacheAmount)) expect( await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "integration's accrued ALCX after", @@ -265,10 +275,6 @@ context("alUSD Feeder Pool integration to Alchemix", () => { await increaseTime(ONE_WEEK) - expect( - await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), - "integration's alUSD deposited after", - ).to.eq(mintAmount) expect( await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "integration's accrued ALCX after", @@ -279,22 +285,25 @@ context("alUSD Feeder Pool integration to Alchemix", () => { await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "integration's accrued ALCX before", ).to.gt(simpleToExactAmount(1, 12)) - const redeemAmount = simpleToExactAmount(8000) + expect(await alcxToken.balanceOf(alchemixIntegration.address), "integration ALCX bal before").to.eq(0) - await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], mintAmount, alUsdWhaleAddress) + const redeemAmount = simpleToExactAmount(8000) + await alUsdFp.connect(alUsdWhale).redeemExactBassets([alUSD.address], [redeemAmount], firstMintAmount, alUsdWhaleAddress) const alUsdBassetAfter = await alUsdFp.getBasset(alusdToken.address) - expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(mintAmount.sub(redeemAmount)) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq(approveAmount.sub(redeemAmount)) const integrationAlusdBalance = await alusdToken.balanceOf(alchemixIntegration.address) expect(integrationAlusdBalance, "alUSD in cache").to.gt(0) expect( await alchemixStakingPools.getStakeTotalDeposited(alchemixIntegration.address, poolId), "integration's alUSD deposited after", - ).to.eq(mintAmount.sub(redeemAmount).sub(integrationAlusdBalance)) + ).to.eq(approveAmount.sub(redeemAmount).sub(integrationAlusdBalance)) + // The withdraw from the staking pool sends accrued ALCX rewards to the integration contract expect( await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "integration's accrued ALCX after", ).to.eq(0) + expect(await alcxToken.balanceOf(alchemixIntegration.address), "integration ALCX bal after").to.gt(simpleToExactAmount(1, 12)) }) }) describe.skip("liquidator", () => { From 6e09c87d28a8f43c069ff6c7011c06b40dd1369f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 14:00:12 +1000 Subject: [PATCH 09/21] chore: more Alchemix tests working --- tasks/deployFeeders.ts | 15 +++-- .../feeders/feeders-musd-alchemix.spec.ts | 55 ++++++++++--------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index 1f2b2d19..1a0ad9d3 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -74,14 +74,13 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") const stakingToken = tokens.find((t) => t.symbol === taskArgs.stakingToken) if (!stakingToken) throw Error(`Could not find staking token with symbol ${taskArgs.stakingToken}`) - let stakingTokenAddress = stakingToken.feederPool - if (taskArgs.stakingType === "address") { - stakingTokenAddress = stakingToken.address - } else if (taskArgs.stakingType === "save") { - stakingTokenAddress = stakingToken.savings - } else if (taskArgs.stakingType !== "feeder") { - throw Error(`Invalid staking type ${taskArgs.stakingType}. Must be either address, feeder or save`) - } + + // Staking Token is for Feeder Pool, Savings Vault or the token itself. eg + // alUSD will stake feeder pool in a v-fPmUSD/alUSD vault + // mUSD will stake savings vault in a v-imUSD vault + // MTA will stake MTA in a v-MTA vault + const stakingTokenAddress = stakingToken.feederPool || stakingToken.savings || stakingToken.address + const rewardToken = tokens.find((t) => t.symbol === taskArgs.rewardToken) if (!rewardToken) throw Error(`Could not find reward token with symbol ${taskArgs.rewardToken}`) diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index b341c28f..5e1d1e4a 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -14,6 +14,8 @@ import { AAVE, ALCX, alUSD, Chain, COMP, MTA, mUSD, stkAAVE } from "tasks/utils/ import { AlchemixIntegration, BoostedDualVault, + DelayedProxyAdmin, + DelayedProxyAdmin__factory, FeederPool, IERC20, IERC20__factory, @@ -33,6 +35,7 @@ const alUsdWhaleAddress = "0xf9a0106251467fff1ff03e8609aa74fc55a2a45e" const chain = Chain.mainnet const nexusAddress = getChainAddress("Nexus", chain) +const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) const liquidatorAddress = getChainAddress("Liquidator", chain) const alchemixStakingPoolsAddress = getChainAddress("AlchemixStakingPool", chain) const uniswapRouterAddress = getChainAddress("UniswapRouterV3", chain) @@ -40,11 +43,13 @@ const uniswapQuoterAddress = getChainAddress("UniswapQuoterV3", chain) const uniswapEthToken = getChainAddress("UniswapEthToken", Chain.mainnet) context("alUSD Feeder Pool integration to Alchemix", () => { - let governor: Signer + let admin: Signer let deployer: Signer + let governor: Signer let ethWhale: Signer let mUsdWhale: Signer let alUsdWhale: Signer + let delayedProxyAdmin: DelayedProxyAdmin let alUsdFp: FeederPool let vault: BoostedDualVault let musdToken: IERC20 @@ -73,6 +78,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { }) deployer = await impersonate(deployerAddress) governor = await impersonate(governorAddress) + admin = await impersonate(delayedProxyAdminAddress) ethWhale = await impersonate(ethWhaleAddress) mUsdWhale = await impersonate(mUsdWhaleAddress) alUsdWhale = await impersonate(alUsdWhaleAddress) @@ -87,6 +93,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { ), ) + delayedProxyAdmin = await DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) musdToken = await IERC20__factory.connect(mUSD.address, deployer) alusdToken = await IERC20__factory.connect(alUSD.address, deployer) alcxToken = await IERC20__factory.connect(ALCX.address, deployer) @@ -306,7 +313,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await alcxToken.balanceOf(alchemixIntegration.address), "integration ALCX bal after").to.gt(simpleToExactAmount(1, 12)) }) }) - describe.skip("liquidator", () => { + describe("liquidator", () => { let newLiquidatorImpl: Liquidator it("deploy new liquidator", async () => { newLiquidatorImpl = await deployContract(new Liquidator__factory(deployer), "Liquidator", [ @@ -327,16 +334,18 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await newLiquidatorImpl.compToken(), "compToken").to.eq(COMP.address) expect(await newLiquidatorImpl.alchemixToken(), "alchemixToken").to.eq(ALCX.address) }) - it("upgrade liquidator proxy", async () => { - const liquidatorProxy = LiquidatorProxy__factory.connect(liquidatorAddress, governor) - expect(liquidatorProxy.admin(), "admin before").to.eq(governorAddress) - - await liquidatorProxy.upgradeTo(newLiquidatorImpl.address) - - expect(await liquidatorProxy.implementation(), "liquidator impl address").to.eq(newLiquidatorImpl.address) - }) - it("upgrade liquidator", async () => { - await liquidator.upgrade() + it("Update the Liquidator proxy", async () => { + const liquidatorProxy = LiquidatorProxy__factory.connect(liquidatorAddress, admin) + expect(await liquidatorProxy.callStatic.admin(), "proxy admin before").to.eq(delayedProxyAdminAddress) + expect(await liquidatorProxy.callStatic.implementation(), "liquidator impl address before").to.not.eq(newLiquidatorImpl.address) + + // Update the Liquidator proxy to point to the new implementation using the delayed proxy admin + const data = newLiquidatorImpl.interface.encodeFunctionData("upgrade") + await delayedProxyAdmin.proposeUpgrade(liquidatorAddress, newLiquidatorImpl.address, data) + await increaseTime(ONE_WEEK.add(60)) + await delayedProxyAdmin.acceptUpgradeRequest(liquidatorAddress) + + expect(await liquidatorProxy.callStatic.implementation(), "liquidator impl address after").to.eq(newLiquidatorImpl.address) }) it("create liquidation of ALCX", async () => { const uniswapPath = encodeUniswapPath([ALCX.address, uniswapEthToken, alUSD.address], [3000, 3000]) @@ -352,28 +361,24 @@ context("alUSD Feeder Pool integration to Alchemix", () => { false, ) }) - it("Claim accrued ALCX rewards", async () => { + it("Claim accrued ALCX using integration contract", async () => { await increaseTime(ONE_WEEK) const unclaimedAlcxBefore = await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId) - const integrationAlusdBalanceBefore = await alusdToken.balanceOf(alchemixIntegration.address) - expect(unclaimedAlcxBefore, "unclaimed ALCX before").to.gt(simpleToExactAmount(1, 10)) - expect(integrationAlusdBalanceBefore, "integration alUSD balance before").to.gt(0) + expect(unclaimedAlcxBefore, "some ALCX before").to.gt(0) + const integrationAlcxBalanceBefore = await alcxToken.balanceOf(alchemixIntegration.address) await alchemixIntegration.claimRewards() expect(await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId), "unclaimed ALCX after").to.eq(0) - // TODO fix these checks - // expect(await alusdToken.balanceOf(alchemixIntegration.address), "integration alUSD balance after").to.eq( - // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), - // ) - // assertBNClose( - // await alusdToken.balanceOf(alchemixIntegration.address), - // integrationAlusdBalanceBefore.add(unclaimedAlcxBefore), - // BN.from(1000), + const integrationAlcxBalanceAfter = await alcxToken.balanceOf(alchemixIntegration.address) + expect(integrationAlcxBalanceAfter, "more ALCX").to.gt(integrationAlcxBalanceBefore) + // TODO why can't I get the correct amount? + // expect(await alcxToken.balanceOf(alchemixIntegration.address), "claimed ALCX").to.eq( + // integrationAlcxBalanceBefore.add(unclaimedAlcxBefore), // ) }) - it("trigger ALCX liquidation", async () => { + it.skip("trigger ALCX liquidation", async () => { await liquidator.triggerLiquidation(alchemixIntegration.address) }) // liquidate COMP From b42a4bcfaba9d6c3cf6a2b6a5d3cea58c9553c2a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 17:06:59 +1000 Subject: [PATCH 10/21] chore: more Alchemix fork tests --- contracts/rewards/RewardsDistributorEth.json | 82 +++++++++++++++++++ .../feeders/feeders-musd-alchemix.spec.ts | 64 +++++++++++---- 2 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 contracts/rewards/RewardsDistributorEth.json diff --git a/contracts/rewards/RewardsDistributorEth.json b/contracts/rewards/RewardsDistributorEth.json new file mode 100644 index 00000000..f11c47d8 --- /dev/null +++ b/contracts/rewards/RewardsDistributorEth.json @@ -0,0 +1,82 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_nexus", "type": "address" }, + { "internalType": "address[]", "name": "_fundManagers", "type": "address[]" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "funder", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "rewardToken", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "DistributedReward", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "_address", "type": "address" }], + "name": "RemovedFundManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "_address", "type": "address" }], + "name": "Whitelisted", + "type": "event" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "addFundManager", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "contract IRewardsDistributionRecipient[]", "name": "_recipients", "type": "address[]" }, + { "internalType": "uint256[]", "name": "_amounts", "type": "uint256[]" } + ], + "name": "distributeRewards", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "nexus", + "outputs": [{ "internalType": "contract INexus", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "removeFundManager", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "whitelist", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 5e1d1e4a..6a546197 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -1,5 +1,4 @@ -import { assertBNClose } from "@utils/assertions" -import { MAX_UINT256, ONE_WEEK, ZERO_ADDRESS } from "@utils/constants" +import { MAX_UINT256, ONE_DAY, ONE_WEEK, ZERO_ADDRESS } from "@utils/constants" import { impersonate } from "@utils/fork" import { BN, simpleToExactAmount } from "@utils/math" import { encodeUniswapPath } from "@utils/peripheral/uniswap" @@ -13,15 +12,18 @@ import { getChainAddress } from "tasks/utils/networkAddressFactory" import { AAVE, ALCX, alUSD, Chain, COMP, MTA, mUSD, stkAAVE } from "tasks/utils/tokens" import { AlchemixIntegration, - BoostedDualVault, + BoostedVault, DelayedProxyAdmin, DelayedProxyAdmin__factory, FeederPool, + FeederPool__factory, IERC20, IERC20__factory, Liquidator, LiquidatorProxy__factory, Liquidator__factory, + RewardsDistributorEth, + RewardsDistributorEth__factory, } from "types/generated" import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" import { IAlchemixStakingPools__factory } from "types/generated/factories/IAlchemixStakingPools__factory" @@ -32,11 +34,13 @@ const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" const mUsdWhaleAddress = "0x69E0E2b3d523D3b247d798a49C3fa022a46DD6bd" const alUsdWhaleAddress = "0xf9a0106251467fff1ff03e8609aa74fc55a2a45e" +const fundManagerAddress = "0x437e8c54db5c66bb3d80d2ff156e9bfe31a017db" const chain = Chain.mainnet const nexusAddress = getChainAddress("Nexus", chain) const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) const liquidatorAddress = getChainAddress("Liquidator", chain) +const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) const alchemixStakingPoolsAddress = getChainAddress("AlchemixStakingPool", chain) const uniswapRouterAddress = getChainAddress("UniswapRouterV3", chain) const uniswapQuoterAddress = getChainAddress("UniswapQuoterV3", chain) @@ -49,16 +53,19 @@ context("alUSD Feeder Pool integration to Alchemix", () => { let ethWhale: Signer let mUsdWhale: Signer let alUsdWhale: Signer + let fundManager: Signer let delayedProxyAdmin: DelayedProxyAdmin let alUsdFp: FeederPool - let vault: BoostedDualVault + let vault: BoostedVault let musdToken: IERC20 let alusdToken: IERC20 let alcxToken: IERC20 + let mtaToken: IERC20 let alchemixIntegration: AlchemixIntegration let alchemixStakingPools: IAlchemixStakingPools let poolId: BN let liquidator: Liquidator + let rewardsDistributor: RewardsDistributorEth const firstMintAmount = simpleToExactAmount(10000) const secondMintAmount = simpleToExactAmount(2000) @@ -71,7 +78,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { { forking: { jsonRpcUrl: process.env.NODE_URL, - blockNumber: 12779756, + blockNumber: 12810000, // After Feeder Pool deployer }, }, ], @@ -82,6 +89,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { ethWhale = await impersonate(ethWhaleAddress) mUsdWhale = await impersonate(mUsdWhaleAddress) alUsdWhale = await impersonate(alUsdWhaleAddress) + fundManager = await impersonate(fundManagerAddress) // send some Ether to addresses that need it await Promise.all( @@ -97,9 +105,11 @@ context("alUSD Feeder Pool integration to Alchemix", () => { musdToken = await IERC20__factory.connect(mUSD.address, deployer) alusdToken = await IERC20__factory.connect(alUSD.address, deployer) alcxToken = await IERC20__factory.connect(ALCX.address, deployer) + mtaToken = await IERC20__factory.connect(MTA.address, deployer) alchemixStakingPools = await IAlchemixStakingPools__factory.connect(alchemixStakingPoolsAddress, deployer) poolId = (await alchemixStakingPools.tokenPoolIds(alUSD.address)).sub(1) liquidator = await Liquidator__factory.connect(liquidatorAddress, governor) + rewardsDistributor = await RewardsDistributorEth__factory.connect(rewardsDistributorAddress, fundManager) }) it("Test connectivity", async () => { const currentBlock = await ethers.provider.getBlockNumber() @@ -122,8 +132,9 @@ context("alUSD Feeder Pool integration to Alchemix", () => { symbol: "fPmUSD/alUSD", config, } - - alUsdFp = await deployFeederPool(deployer, fpData, chain) + alUsdFp = alUSD.feederPool + ? FeederPool__factory.connect(alUSD.feederPool, deployer) + : await deployFeederPool(deployer, fpData, chain) expect(await alUsdFp.name(), "name").to.eq(fpData.name) expect(await alUsdFp.symbol(), "symbol").to.eq(fpData.symbol) @@ -134,6 +145,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(await alusdToken.balanceOf(alUsdFp.address), "alUSD bal before").to.eq(0) expect(await musdToken.balanceOf(alUsdFp.address), "mUSD bal before").to.eq(0) + expect(await alUsdFp.balanceOf(alUsdWhaleAddress), "whale fp bal before").to.eq(0) // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) await musdToken.connect(mUsdWhale).transfer(alUsdWhaleAddress, approveAmount) @@ -163,9 +175,10 @@ context("alUSD Feeder Pool integration to Alchemix", () => { expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq( mUsdBassetBefore.vaultData.vaultBalance.add(firstMintAmount), ) + expect(await alUsdFp.balanceOf(alUsdWhaleAddress), "whale fp bal after").to.eq(firstMintAmount.mul(2).add(1)) }) - describe.skip("Dual Rewards Feeder Pool Vault", () => { - it("deploy vault", async () => { + describe("Boosted vault for fPmUSD/alUSD Feeder Pool", () => { + it("deploy boosted staking vault", async () => { const vaultData: VaultData = { boosted: true, name: "v-mUSD/alUSD fPool Vault", @@ -173,15 +186,38 @@ context("alUSD Feeder Pool integration to Alchemix", () => { priceCoeff: BN.from(1), stakingToken: alUsdFp.address, rewardToken: MTA.address, - dualRewardToken: ALCX.address, } - vault = (await deployVault(deployer, vaultData, chain)) as BoostedDualVault + vault = (await deployVault(deployer, vaultData, chain)) as BoostedVault + }) + it("Distribute MTA rewards to vault", async () => { + const distributionAmount = simpleToExactAmount(20000) + const fundManagerMtaBalBefore = await mtaToken.balanceOf(fundManagerAddress) + expect(fundManagerMtaBalBefore, "fund manager mta bal before").to.gt(distributionAmount) + + await mtaToken.connect(fundManager).approve(rewardsDistributor.address, distributionAmount) + await rewardsDistributor.connect(fundManager).distributeRewards([vault.address], [distributionAmount]) + + expect(await mtaToken.balanceOf(fundManagerAddress), "fund manager mta bal before").to.eq( + fundManagerMtaBalBefore.sub(distributionAmount), + ) }) - it("deposit to vault", async () => { + it("stake fPmUSD/alUSD in vault", async () => { const stakeAmount = simpleToExactAmount(1000) - await alUsdFp.approve(vault.address, stakeAmount) - await vault["stake(uint256)"](stakeAmount) + expect(await vault.balanceOf(alUsdWhaleAddress), "whale v-fp bal before").to.eq(0) + + await alUsdFp.connect(alUsdWhale).approve(vault.address, stakeAmount) + await vault.connect(alUsdWhale)["stake(uint256)"](stakeAmount) + + expect(await vault.balanceOf(alUsdWhaleAddress), "whale v-fp bal after").to.eq(stakeAmount) + }) + it("whale claims MTA from vault", async () => { + await increaseTime(ONE_DAY.mul(5)) + expect(await mtaToken.balanceOf(alUsdWhaleAddress), "whale mta bal before").to.eq(0) + + await vault.connect(alUsdWhale).claimReward() + + expect(await mtaToken.balanceOf(alUsdWhaleAddress), "whale mta bal after").to.gt(0) }) }) describe("Integration", () => { From 57932a742cdfffeb93ab49205ebc64071b838055 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 17:40:50 +1000 Subject: [PATCH 11/21] fix: task to deploy vaults --- tasks/deployFeeders.ts | 20 ++++++++++---------- tasks/utils/tokens.ts | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index 1a0ad9d3..ac55f15e 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -53,26 +53,26 @@ task("deployFeederPool", "Deploy Feeder Pool") task("deployVault", "Deploy Feeder Pool with boosted dual vault") .addParam("name", "Token name of the vault. eg mUSD/alUSD fPool Vault", undefined, types.string) .addParam("symbol", "Token symbol of the vault. eg v-fPmUSD/alUSD", undefined, types.string) - .addParam("boosted", "Rewards are boosted by staked MTA (vMTA)", true, types.string) + .addParam("boosted", "Rewards are boosted by staked MTA (vMTA)", undefined, types.boolean) .addParam( "stakingToken", - "Symbol of token that is being staked. Feeder Pool is just the fAsset. eg imUSD, PimUSD, MTA, GUSD, alUSD", - true, + "Symbol of token that is being staked. Feeder Pool is just the fAsset. eg mUSD, PmUSD, MTA, GUSD, alUSD", + undefined, types.string, ) - .addOptionalParam("stakingType", "Which token address is being staked? eg address, feeder or save", "feeder", types.string) - .addParam("rewardsToken", "Token symbol of reward. eg MTA or PMTA for Polygon", undefined, types.string) + .addParam("rewardToken", "Token symbol of reward. eg MTA or PMTA for Polygon", undefined, types.string) .addOptionalParam("dualRewardToken", "Token symbol of second reward. eg WMATIC, ALCX, QI", undefined, types.string) .addOptionalParam("price", "Price coefficient is the value of the mAsset in USD. eg mUSD/USD = 1, mBTC/USD", 1, types.int) .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { - const signer = await getSigner(ethers, taskArgs.speed) const chain = getChain(network.name, hardhatArguments.config) + const signer = await getSigner(ethers, taskArgs.speed) if (taskArgs.name?.length < 4) throw Error(`Invalid token name ${taskArgs.name}`) - if (taskArgs.symbol?.length <= 0 || taskArgs.symbol?.length > 12) throw Error(`Invalid token name ${taskArgs.name}`) + if (taskArgs.symbol?.length <= 0 || taskArgs.symbol?.length > 14) throw Error(`Invalid token symbol ${taskArgs.name}`) + if (taskArgs.boosted === undefined) throw Error(`Invalid boolean boost ${taskArgs.boosted}`) - const stakingToken = tokens.find((t) => t.symbol === taskArgs.stakingToken) + const stakingToken = tokens.find((t) => t.symbol === taskArgs.stakingToken && t.chain === chain) if (!stakingToken) throw Error(`Could not find staking token with symbol ${taskArgs.stakingToken}`) // Staking Token is for Feeder Pool, Savings Vault or the token itself. eg @@ -81,7 +81,7 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") // MTA will stake MTA in a v-MTA vault const stakingTokenAddress = stakingToken.feederPool || stakingToken.savings || stakingToken.address - const rewardToken = tokens.find((t) => t.symbol === taskArgs.rewardToken) + const rewardToken = tokens.find((t) => t.symbol === taskArgs.rewardToken && t.chain === chain) if (!rewardToken) throw Error(`Could not find reward token with symbol ${taskArgs.rewardToken}`) if (taskArgs.price < 0 || taskArgs.price >= simpleToExactAmount(1)) throw Error(`Invalid price coefficient ${taskArgs.price}`) @@ -95,7 +95,7 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") priceCoeff: BN.from(taskArgs.price), stakingToken: stakingTokenAddress, rewardToken: rewardToken.address, - dualRewardToken: dualRewardToken.address, + dualRewardToken: dualRewardToken?.address, } await deployVault(signer, vaultData, chain) diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index 488ab39b..a644e53c 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -342,6 +342,8 @@ export const cyMUSD: Token = { } export const tokens = [ + MTA, + PMTA, mUSD, mBTC, sUSD, From 19613129e239f716a6ea9c6755344875d54fcbe5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 19:17:26 +1000 Subject: [PATCH 12/21] feat: added feeder-mint task --- tasks/feeder.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tasks/feeder.ts b/tasks/feeder.ts index 6843a420..f0b5f067 100644 --- a/tasks/feeder.ts +++ b/tasks/feeder.ts @@ -5,6 +5,7 @@ import { Signer } from "ethers" import { ERC20__factory, FeederPool, FeederPool__factory, IERC20__factory, Masset, SavingsManager__factory } from "types/generated" import { BN, simpleToExactAmount } from "@utils/math" +import { formatUnits } from "ethers/lib/utils" import { dumpConfigStorage, dumpFassetStorage, dumpTokenStorage } from "./utils/storage-utils" import { getMultiRedemptions, @@ -226,4 +227,41 @@ task("frax-post-deploy", "Mint FRAX Feeder Pool").setAction(async (_, { ethers } await logTxDetails(tx, "mint FRAX FP") }) +task("feeder-mint", "Mint some Feeder Pool tokens") + .addOptionalParam("amount", "Amount of the mAsset and fAsset to deposit", undefined, types.int) + .addParam("fasset", "Token symbol of the feeder pool asset. eg HBTC, GUSD, PFRAX or alUSD", undefined, types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { ethers, network }) => { + const chain = getChain(network.name) + const signer = await getSigner(ethers, taskArgs.speed) + const signerAddress = await signer.getAddress() + + const fAssetSymbol = taskArgs.fasset + const feederPoolToken = tokens.find((t) => t.symbol === taskArgs.fasset && t.chain === chain) + if (!feederPoolToken) throw Error(`Could not find feeder pool asset token with symbol ${fAssetSymbol}`) + if (!feederPoolToken.feederPool) throw Error(`No feeder pool configured for token ${fAssetSymbol}`) + + const mAssetSymbol = feederPoolToken.parent + if (!mAssetSymbol) throw Error(`No parent mAsset configured for feeder pool asset ${fAssetSymbol}`) + const mAssetToken = tokens.find((t) => t.symbol === mAssetSymbol && t.chain === chain) + if (!mAssetToken) throw Error(`Could not find mAsset token with symbol ${mAssetToken}`) + + const fp = FeederPool__factory.connect(feederPoolToken.feederPool, signer) + const fpSymbol = await fp.symbol() + const mAsset = ERC20__factory.connect(mAssetToken.address, signer) + const fAsset = ERC20__factory.connect(feederPoolToken.address, signer) + + const mintAmount = simpleToExactAmount(taskArgs.amount) + + // Approvals before mint + let tx = await mAsset.approve(fp.address, mintAmount) + await logTxDetails(tx, `${signerAddress} approves ${fpSymbol} to spend ${mAssetSymbol}`) + tx = await fAsset.approve(fp.address, mintAmount) + await logTxDetails(tx, `${signerAddress} approves ${fpSymbol} to spend ${feederPoolToken.symbol}`) + + // mint Feeder Pool tokens + tx = await fp.mintMulti([mAsset.address, fAsset.address], [mintAmount, mintAmount], 0, signerAddress) + logTxDetails(tx, `Mint ${fpSymbol} from ${formatUnits(mintAmount)} ${mAssetSymbol} and ${formatUnits(mintAmount)} ${fAssetSymbol}`) + }) + module.exports = {} From 8b388e554b0de9f88b77e987d12c5519e4c94a2f Mon Sep 17 00:00:00 2001 From: alsco77 Date: Mon, 12 Jul 2021 10:58:33 +0100 Subject: [PATCH 13/21] chore: Reduce BoostedDualVault bytecode size --- contracts/masset/peripheral/AlchemixIntegration.sol | 8 +++----- .../rewards/boosted-staking/BoostedDualVault.sol | 6 +++++- tasks/deployBoostedVault.ts | 11 ++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index d129d345..c87320a6 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -21,7 +21,8 @@ contract AlchemixIntegration is IPlatformIntegration, Initializable, ImmutableModule, - ReentrancyGuard { + ReentrancyGuard +{ using SafeERC20 for IERC20; event Deposit(address indexed _bAsset, address _pToken, uint256 _amount); @@ -88,10 +89,7 @@ contract AlchemixIntegration is * @dev Approve the spending of the bAsset by Alchemix's StakingPools contract, * and the spending of the reward token by mStable's Liquidator contract */ - function initialize() - public - initializer - { + function initialize() public initializer { _approveContracts(); } diff --git a/contracts/rewards/boosted-staking/BoostedDualVault.sol b/contracts/rewards/boosted-staking/BoostedDualVault.sol index 9f25c4d8..6d9f78fe 100644 --- a/contracts/rewards/boosted-staking/BoostedDualVault.sol +++ b/contracts/rewards/boosted-staking/BoostedDualVault.sol @@ -142,6 +142,11 @@ contract BoostedDualVault is * to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual). */ modifier updateReward(address _account) { + _updateReward(_account); + _; + } + + function _updateReward(address _account) internal { uint256 currentTime = block.timestamp; uint64 currentTime64 = SafeCast.toUint64(currentTime); @@ -210,7 +215,6 @@ contract BoostedDualVault is // This should only be hit once, for first staker in initialisation case userData[_account].lastAction = currentTime64; } - _; } /** @dev Updates the boost for a given address, after the rest of the function has executed */ diff --git a/tasks/deployBoostedVault.ts b/tasks/deployBoostedVault.ts index 0dcab35a..2f84eac2 100644 --- a/tasks/deployBoostedVault.ts +++ b/tasks/deployBoostedVault.ts @@ -4,7 +4,16 @@ import { task, types } from "hardhat/config" import { DEAD_ADDRESS } from "@utils/constants" import { params } from "./taskUtils" -import { AssetProxy__factory, BoostedVault__factory } from "../types/generated" +import { AssetProxy__factory, BoostedVault__factory, BoostedDualVault__factory } from "../types/generated" + +task("getBytecode-BoostedDualVault").setAction(async () => { + const size = BoostedDualVault__factory.bytecode.length / 2 / 1000 + if (size > 24.576) { + console.error(`BoostedDualVault size is ${size} kb: ${size - 24.576} kb too big`) + } else { + console.log(`BoostedDualVault = ${size} kb`) + } +}) task("BoostedVault.deploy", "Deploys a BoostedVault") .addParam("nexus", "Nexus address", undefined, params.address, false) From 9ac59011ed47da77e038215abe10eee1f21f7291 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 21:16:08 +1000 Subject: [PATCH 14/21] chore: added v-fPmUSD/alUSD vault to alUSD token config --- tasks/utils/tokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index a644e53c..10b2346a 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -211,7 +211,7 @@ export const alUSD: Token = { address: "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", feederPool: "0x4eaa01974B6594C0Ee62fFd7FEE56CF11E6af936", integrator: undefined, // TODO add after deployment - vault: undefined, // TODO add after deployment + vault: "0xe52aba1eaa4a9f7efea2e8dfd284aeaa818dc43e", chain: Chain.mainnet, decimals: 18, quantityFormatter: "USD", From 873f20909791778be07587b0fd8b4c16f6705394 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 12 Jul 2021 23:59:16 +1000 Subject: [PATCH 15/21] chore: remove addresses from tasks --- tasks/mUSD.ts | 2 +- tasks/ops.ts | 28 ++++++++++++++++++++++++++++ tasks/poker.ts | 26 ++++++++------------------ tasks/utils/networkAddressFactory.ts | 2 ++ tasks/utils/rates-utils.ts | 8 ++++---- tasks/utils/snap-utils.ts | 19 ++++++++++++------- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/tasks/mUSD.ts b/tasks/mUSD.ts index 40893ab5..dc186770 100644 --- a/tasks/mUSD.ts +++ b/tasks/mUSD.ts @@ -164,7 +164,7 @@ task("mUSD-snap", "Snaps mUSD") balances.save, ) - await snapSave(signer, network.name, toBlock.blockNumber) + await snapSave(signer, chain, toBlock.blockNumber) outputFees( mintSummary, diff --git a/tasks/ops.ts b/tasks/ops.ts index ba08da4c..8bb1344b 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -188,4 +188,32 @@ task("proxy-upgrades", "Proxy implementation changes") }) }) +task("vault-stake", "Stake into a vault") + .addParam("asset", "Symbol of the asset that has a mStable vault. eg mUSD, alUSD, MTA", undefined, types.string) + .addParam("amount", "Amount to be staked", undefined, types.int) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { ethers, network }) => { + const chain = getChain(network.name) + const signer = await getSigner(ethers, taskArgs.speed) + const signerAddress = await signer.getAddress() + + const assetSymbol = taskArgs.asset + const assetToken = tokens.find((t) => t.symbol === assetSymbol && t.chain === chain) + if (!assetToken) throw Error(`Could not find asset with symbol ${assetSymbol}`) + if (!assetToken.vault) throw Error(`No vault is configured for asset ${assetSymbol}`) + + const stakedTokenAddress = assetToken.feederPool || assetToken.savings || assetToken.address + const stakedToken = ERC20__factory.connect(stakedTokenAddress, signer) + + const vault = StakingRewards__factory.connect(assetToken.vault, signer) + + const amount = simpleToExactAmount(taskArgs.amount) + + let tx = await stakedToken.approve(vault.address, amount) + await logTxDetails(tx, `${signerAddress} approves ${assetSymbol} vault to spend ${stakedTokenAddress}`) + + tx = await vault["stake(uint256)"](amount) + await logTxDetails(tx, `${signerAddress} stakes ${amount} ${assetSymbol} in vault`) + }) + module.exports = {} diff --git a/tasks/poker.ts b/tasks/poker.ts index 3138ed68..3744ec83 100644 --- a/tasks/poker.ts +++ b/tasks/poker.ts @@ -10,6 +10,7 @@ import { task, types } from "hardhat/config" import { BoostedVault__factory, Poker, Poker__factory } from "types/generated" import { getSigner } from "./utils/defender-utils" import { deployContract, logTxDetails } from "./utils/deploy-utils" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" import { MTA, mUSD } from "./utils/tokens" const maxVMTA = simpleToExactAmount(300000, 18) @@ -17,8 +18,6 @@ const maxBoost = simpleToExactAmount(4, 18) const minBoost = simpleToExactAmount(1, 18) const floor = simpleToExactAmount(95, 16) -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)) const scaledBalance = raw.mul(priceCoefficient).div(simpleToExactAmount(1, decimals)) @@ -28,13 +27,7 @@ const calcBoost = (raw: BN, vMTA: BN, priceCoefficient: BN, boostCoeff: BN, deci let denom = parseFloat(formatUnits(scaledBalance)) denom **= 0.875 const flooredMTA = vMTA.gt(maxVMTA) ? maxVMTA : vMTA - let rhs = floor.add( - flooredMTA - .mul(boostCoeff) - .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 } @@ -62,10 +55,11 @@ task("over-boost", "Pokes accounts that are over boosted") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .addFlag("update", "Will send a poke transactions to the Poker contract") .addOptionalParam("minMtaDiff", "Min amount of vMTA over boosted. 300 = 0.3 boost", 300, types.int) - .setAction(async (taskArgs, hre) => { + .setAction(async (taskArgs, { ethers, network, hardhatArguments }) => { const minMtaDiff = taskArgs.minMtaDiff - const signer = await getSigner(hre.ethers, taskArgs.speed) + const signer = await getSigner(ethers, taskArgs.speed) // const signer = await impersonate("0x2f2Db75C5276481E2B018Ac03e968af7763Ed118") + const chain = getChain(network.name, hardhatArguments.config) const gqlClient = new GraphQLClient("https://api.thegraph.com/subgraphs/name/mstable/mstable-feeder-pools") const query = gql` @@ -123,19 +117,14 @@ task("over-boost", "Pokes accounts that are over boosted") console.log("Account, Raw Balance, Boosted Balance, Boost Balance USD, vMTA balance, Boost Actual, Boost Expected, Boost Diff") // For each account in the boosted savings vault vault.accounts.forEach((account) => { - const boostActual = BN.from(account.boostedBalance) - .mul(1000) - .div(account.rawBalance) - .toNumber() + const boostActual = BN.from(account.boostedBalance).mul(1000).div(account.rawBalance).toNumber() const accountId = account.id.split(".")[1] const boostExpected = calcBoost(BN.from(account.rawBalance), vMtaBalancesMap[accountId], priceCoeff, boostCoeff) .div(simpleToExactAmount(1, 15)) .toNumber() const boostDiff = boostActual - boostExpected // Calculate how much the boost balance is in USD = balance balance * price coefficient / 1e18 - const boostBalanceUsd = BN.from(account.boostedBalance) - .mul(priceCoeff) - .div(simpleToExactAmount(1)) + const boostBalanceUsd = BN.from(account.boostedBalance).mul(priceCoeff).div(simpleToExactAmount(1)) // Identify accounts with more than 20% over their boost and boost balance > 50,000 USD if (boostDiff > minMtaDiff && boostBalanceUsd.gt(simpleToExactAmount(50000))) { overBoosted.push({ @@ -168,6 +157,7 @@ task("over-boost", "Pokes accounts that are over boosted") }) } if (taskArgs.update) { + const pokerAddress = getChainAddress("Poker", chain) console.log(`About to poke ${pokeVaultAccounts.length} vaults`) const poker = Poker__factory.connect(pokerAddress, signer) const tx = await poker.poke(pokeVaultAccounts) diff --git a/tasks/utils/networkAddressFactory.ts b/tasks/utils/networkAddressFactory.ts index 558073c0..e2571760 100644 --- a/tasks/utils/networkAddressFactory.ts +++ b/tasks/utils/networkAddressFactory.ts @@ -74,6 +74,8 @@ export const getChainAddress = (contractName: ContractNames, chain: Chain): stri return "0xf1049aeD858C4eAd6df1de4dbE63EF607CfF3262" case "BasketManager": return "0x66126B4aA2a1C07536Ef8E5e8bD4EfDA1FdEA96D" + case "AaveIncentivesController": + return "0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5" case "AaveLendingPoolAddressProvider": return "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5" case "AlchemixStakingPool": diff --git a/tasks/utils/rates-utils.ts b/tasks/utils/rates-utils.ts index e9a7080e..d6240ba5 100644 --- a/tasks/utils/rates-utils.ts +++ b/tasks/utils/rates-utils.ts @@ -6,7 +6,7 @@ import { MusdEth } from "types/generated/MusdEth" import { MusdLegacy } from "types/generated/MusdLegacy" import { QuantityFormatter } from "./quantity-formatters" import { isMusdLegacy } from "./snap-utils" -import { Token } from "./tokens" +import { PDAI, PUSDC, PUSDT, Token } from "./tokens" export interface Balances { total: BN @@ -129,9 +129,9 @@ export const getSwapRates = async ( const curvePool = ICurve__factory.connect("0x445FE580eF8d70FF569aB36e80c647af338db351", mAsset.signer) // Just hard code the mapping for now const curveIndexMap = { - "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063": 0, // DAI - "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174": 1, // USDC - "0xc2132D05D31c914a87C6611C10748AEb04B58e8F": 2, // Tether + [PDAI.address]: 0, + [PUSDC.address]: 1, + [PUSDT.address]: 2, } const inputIndex = curveIndexMap[inputToken.address] const outputIndex = curveIndexMap[outputToken.address] diff --git a/tasks/utils/snap-utils.ts b/tasks/utils/snap-utils.ts index e90769a5..105e48ed 100644 --- a/tasks/utils/snap-utils.ts +++ b/tasks/utils/snap-utils.ts @@ -22,10 +22,10 @@ import { AaveStakedTokenV2__factory } from "types/generated/factories/AaveStaked import { Comptroller__factory } from "types/generated/factories/Comptroller__factory" import { MusdLegacy } from "types/generated/MusdLegacy" import { QuantityFormatter, usdFormatter } from "./quantity-formatters" -import { AAVE, COMP, DAI, GUSD, stkAAVE, sUSD, Token, USDC, USDT, WBTC } from "./tokens" +import { AAVE, Chain, COMP, DAI, GUSD, stkAAVE, sUSD, Token, USDC, USDT, WBTC } from "./tokens" +import { getChainAddress } from "./networkAddressFactory" const compIntegrationAddress = "0xD55684f4369040C12262949Ff78299f2BC9dB735" -const liquidatorAddress = "0xe595D67181D701A5356e010D9a58EB9A341f1DbD" const comptrollerAddress = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B" export interface TxSummary { count: number @@ -127,9 +127,8 @@ export const snapConfig = async (asset: Masset | MusdEth | MusdLegacy | FeederPo console.log(`Weights: min ${formatUnits(conf.limits.min, 16)}% max ${formatUnits(conf.limits.max, 16)}%`) } -export const snapSave = async (signer: Signer, networkName: string, toBlock: number): Promise => { - const savingManagerAddress = - networkName === "mainnet" ? "0x30647a72dc82d7fbb1123ea74716ab8a317eac19" : "0x5290Ad3d83476CA6A2b178Cd9727eE1EF72432af" +export const snapSave = async (signer: Signer, chain: Chain, toBlock: number): Promise => { + const savingManagerAddress = getChainAddress("SavingsManager", chain) const savingsManager = new SavingsContract__factory(signer).attach(savingManagerAddress) const exchangeRate = await savingsManager.exchangeRate({ blockTag: toBlock, @@ -752,9 +751,14 @@ export const getCompTokens = async (signer: Signer, toBlock: BlockInfo, quantity ) } -export const getAaveTokens = async (signer: Signer, toBlock: BlockInfo, quantityFormatter = usdFormatter): Promise => { +export const getAaveTokens = async ( + signer: Signer, + toBlock: BlockInfo, + quantityFormatter = usdFormatter, + chain = Chain.mainnet, +): Promise => { const stkAaveToken = AaveStakedTokenV2__factory.connect(stkAAVE.address, signer) - const aaveIncentivesAddress = "0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5" + const aaveIncentivesAddress = getChainAddress("AaveIncentivesController", chain) const aaveIncentives = IAaveIncentivesController__factory.connect(aaveIncentivesAddress, signer) let totalStkAave = BN.from(0) @@ -778,6 +782,7 @@ export const getAaveTokens = async (signer: Signer, toBlock: BlockInfo, quantity } // Get stkAave and AAVE in liquidity manager + const liquidatorAddress = getChainAddress("Liquidator", chain) const liquidatorStkAaveBal = await stkAaveToken.balanceOf(liquidatorAddress, { blockTag: toBlock.blockNumber }) totalStkAave = totalStkAave.add(liquidatorStkAaveBal) const cooldownStart = await stkAaveToken.stakersCooldowns(liquidatorAddress, { blockTag: toBlock.blockNumber }) From 55a2f656d662102d7b091bcabf90552c3295ed07 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 00:31:25 +1000 Subject: [PATCH 16/21] feat: added task to deploy Alchemix integration contract --- tasks/deployFeeders.ts | 36 +++++++++++++++++++++++++++++++++--- tasks/utils/snap-utils.ts | 8 +++++++- tasks/utils/tokens.ts | 2 +- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index ac55f15e..e0bca593 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -4,13 +4,20 @@ import "ts-node/register" import "tsconfig-paths/register" import { task, types } from "hardhat/config" -import { FeederPool__factory, CompoundIntegration__factory, CompoundIntegration } from "types/generated" +import { + FeederPool__factory, + CompoundIntegration__factory, + CompoundIntegration, + AlchemixIntegration, + AlchemixIntegration__factory, + FeederPool, +} from "types/generated" import { BN, simpleToExactAmount } from "@utils/math" -import { BUSD, CREAM, cyMUSD, GUSD, mUSD, tokens } from "./utils/tokens" +import { ALCX, alUSD, BUSD, CREAM, cyMUSD, GUSD, mUSD, tokens } from "./utils/tokens" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { getSigner } from "./utils/defender-utils" import { deployFeederPool, deployVault, FeederData, VaultData } from "./utils/feederUtils" -import { getChain } from "./utils/networkAddressFactory" +import { getChain, getChainAddress } from "./utils/networkAddressFactory" task("deployFeederPool", "Deploy Feeder Pool") .addParam("masset", "Token symbol of mAsset. eg mUSD or PmUSD for Polygon", "mUSD", types.string) @@ -50,6 +57,29 @@ task("deployFeederPool", "Deploy Feeder Pool") await deployFeederPool(signer, poolData, chain) }) +task("deployAlcxInt", "Deploy Alchemix integration contract for alUSD Feeder Pool") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { hardhatArguments, ethers, network }) => { + const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) + + const nexusAddress = getChainAddress("Nexus", chain) + const alchemixStakingPoolsAddress = getChainAddress("AlchemixStakingPool", chain) + + const alchemixIntegration = await deployContract( + new AlchemixIntegration__factory(signer), + "Alchemix alUSD Integration", + [nexusAddress, alUSD.feederPool, ALCX.address, alchemixStakingPoolsAddress, alUSD.address], + ) + + const tx = await alchemixIntegration.initialize() + logTxDetails(tx, "initialize Alchemix integration") + + const fp = FeederPool__factory.connect(alUSD.feederPool, signer) + const migrateData = fp.interface.encodeFunctionData("migrateBassets", [[alUSD.address], alchemixIntegration.address]) + console.log(`migrateBassets data:\n${migrateData}`) + }) + task("deployVault", "Deploy Feeder Pool with boosted dual vault") .addParam("name", "Token name of the vault. eg mUSD/alUSD fPool Vault", undefined, types.string) .addParam("symbol", "Token symbol of the vault. eg v-fPmUSD/alUSD", undefined, types.string) diff --git a/tasks/utils/snap-utils.ts b/tasks/utils/snap-utils.ts index 105e48ed..e9066110 100644 --- a/tasks/utils/snap-utils.ts +++ b/tasks/utils/snap-utils.ts @@ -721,7 +721,12 @@ export const quoteSwap = async ( return { outAmount, exchangeRate } } -export const getCompTokens = async (signer: Signer, toBlock: BlockInfo, quantityFormatter = usdFormatter): Promise => { +export const getCompTokens = async ( + signer: Signer, + toBlock: BlockInfo, + quantityFormatter = usdFormatter, + chain = Chain.mainnet, +): Promise => { const comptroller = Comptroller__factory.connect(comptrollerAddress, signer) const compToken = ERC20__factory.connect(COMP.address, signer) @@ -739,6 +744,7 @@ export const getCompTokens = async (signer: Signer, toBlock: BlockInfo, quantity console.log(`Integration ${quantityFormatter(compIntegrationBal)}`) // Get COMP in mUSD liquidator + const liquidatorAddress = getChainAddress("Liquidator", chain) const compLiquidatorBal = await compToken.balanceOf(liquidatorAddress, { blockTag: toBlock.blockNumber }) totalComp = totalComp.add(compLiquidatorBal) console.log(`Liquidator ${quantityFormatter(compLiquidatorBal)}`) diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index 10b2346a..5c756af6 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -210,7 +210,7 @@ export const alUSD: Token = { symbol: "alUSD", address: "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", feederPool: "0x4eaa01974B6594C0Ee62fFd7FEE56CF11E6af936", - integrator: undefined, // TODO add after deployment + integrator: "0xd658d5fDe0917CdC9b10cAadf10E20d942572a7B", vault: "0xe52aba1eaa4a9f7efea2e8dfd284aeaa818dc43e", chain: Chain.mainnet, decimals: 18, From 1fc2f608f1f587e7639e3a8d3b8263681acf71be Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 00:45:55 +1000 Subject: [PATCH 17/21] chore: fixed build --- contracts/masset/liquidator/Liquidator.sol | 4 ++-- test/masset/liquidator.spec.ts | 27 ++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/contracts/masset/liquidator/Liquidator.sol b/contracts/masset/liquidator/Liquidator.sol index 0c12ff57..2378a063 100644 --- a/contracts/masset/liquidator/Liquidator.sol +++ b/contracts/masset/liquidator/Liquidator.sol @@ -120,8 +120,8 @@ contract Liquidator is ILiquidator, Initializable, ModuleKeysStorage, ImmutableM * @dev to be called via the proxy proposeUpgrade function, not the constructor. */ function upgrade() external { - // IERC20(aaveToken).safeApprove(address(uniswapRouter), type(uint256).max); - // IERC20(compToken).safeApprove(address(uniswapRouter), type(uint256).max); + IERC20(aaveToken).safeApprove(address(uniswapRouter), type(uint256).max); + IERC20(compToken).safeApprove(address(uniswapRouter), type(uint256).max); IERC20(alchemixToken).safeApprove(address(uniswapRouter), type(uint256).max); } diff --git a/test/masset/liquidator.spec.ts b/test/masset/liquidator.spec.ts index 80c1ec19..2254f95f 100644 --- a/test/masset/liquidator.spec.ts +++ b/test/masset/liquidator.spec.ts @@ -43,6 +43,7 @@ describe("Liquidator", () => { let compToken: MockERC20 let aaveToken: MockERC20 let stkAaveToken: MockERC20 + let alcxToken: MockERC20 let savings: SavingsManager let uniswap: MockUniswapV3 let uniswapCompBassetPaths: EncodedPaths @@ -86,6 +87,10 @@ describe("Liquidator", () => { await compIntegration.setRewardToken(compToken.address) await compToken.connect(sa.fundManager.signer).transfer(compIntegration.address, simpleToExactAmount(10, 18)) + // Create ALCX token and assign, then approve the liquidator + alcxToken = await new MockERC20__factory(sa.default.signer).deploy("Alchemix Gov", "ALCX", 18, sa.fundManager.address, 100000000) + // TODO deploy mock ALCX rewards + // Aave tokens and integration contract aaveToken = await new MockERC20__factory(sa.default.signer).deploy("Aave Gov", "AAVE", 18, sa.fundManager.address, 100000000) stkAaveToken = await new MockStakedAave__factory(sa.default.signer).deploy(aaveToken.address, sa.fundManager.address, 100000000) @@ -112,6 +117,7 @@ describe("Liquidator", () => { uniswap.address, uniswap.address, compToken.address, + alcxToken.address, ) const data: string = impl.interface.encodeFunctionData("upgrade") const proxy = await new AssetProxy__factory(sa.default.signer).deploy(impl.address, sa.other.address, data) @@ -166,6 +172,7 @@ describe("Liquidator", () => { expect(await liquidator.uniswapQuoter(), "Uniswap Quoter").eq(uniswap.address) expect(await liquidator.stkAave(), "stkAave").eq(stkAaveToken.address) expect(await liquidator.aaveToken(), "aaveToken").eq(aaveToken.address) + expect(await liquidator.alchemixToken(), "alchemixToken").eq(alcxToken.address) }) }) @@ -214,6 +221,7 @@ describe("Liquidator", () => { uniswap.address, uniswap.address, compToken.address, + alcxToken.address, ), ).to.be.revertedWith("Invalid stkAAVE address") await expect( @@ -224,6 +232,7 @@ describe("Liquidator", () => { uniswap.address, uniswap.address, compToken.address, + alcxToken.address, ), ).to.be.revertedWith("Invalid AAVE address") await expect( @@ -234,6 +243,7 @@ describe("Liquidator", () => { ZERO_ADDRESS, uniswap.address, compToken.address, + alcxToken.address, ), ).to.be.revertedWith("Invalid Uniswap Router address") await expect( @@ -244,6 +254,7 @@ describe("Liquidator", () => { uniswap.address, ZERO_ADDRESS, compToken.address, + alcxToken.address, ), ).to.be.revertedWith("Invalid Uniswap Quoter address") await expect( @@ -254,8 +265,20 @@ describe("Liquidator", () => { uniswap.address, uniswap.address, ZERO_ADDRESS, + alcxToken.address, ), ).to.be.revertedWith("Invalid COMP address") + await expect( + new Liquidator__factory(sa.default.signer).deploy( + nexus.address, + stkAaveToken.address, + aaveToken.address, + uniswap.address, + uniswap.address, + compToken.address, + ZERO_ADDRESS, + ), + ).to.be.revertedWith("Invalid ALCX address") }) }) context("creating a new liquidation", () => { @@ -498,10 +521,6 @@ describe("Liquidator", () => { ) await compIntegration.connect(sa.governor.signer).approveRewardToken() }) - it("should fail if called via contract", async () => { - const mock = await new MockTrigger__factory(sa.default.signer).deploy() - await expect(mock.trigger(liquidator.address, compIntegration.address)).to.be.revertedWith("Must be EOA") - }) it("should fail if liquidation does not exist", async () => { await expect(liquidator.triggerLiquidation(sa.dummy2.address)).to.be.revertedWith("Liquidation does not exist") }) From 684fb448fc467a049b6dcd9d7bab35f4efd572f7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 01:04:03 +1000 Subject: [PATCH 18/21] fix: deployVault task --- tasks/deployFeeders.ts | 2 +- test-fork/feeders/feeders-musd-alchemix.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index e0bca593..f106db1e 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -122,7 +122,7 @@ task("deployVault", "Deploy Feeder Pool with boosted dual vault") boosted: taskArgs.boosted, name: taskArgs.name, symbol: taskArgs.symbol, - priceCoeff: BN.from(taskArgs.price), + priceCoeff: simpleToExactAmount(taskArgs.price), stakingToken: stakingTokenAddress, rewardToken: rewardToken.address, dualRewardToken: dualRewardToken?.address, diff --git a/test-fork/feeders/feeders-musd-alchemix.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts index 6a546197..0d0086c4 100644 --- a/test-fork/feeders/feeders-musd-alchemix.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -22,12 +22,12 @@ import { Liquidator, LiquidatorProxy__factory, Liquidator__factory, - RewardsDistributorEth, - RewardsDistributorEth__factory, } from "types/generated" import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" import { IAlchemixStakingPools__factory } from "types/generated/factories/IAlchemixStakingPools__factory" +import { RewardsDistributorEth__factory } from "types/generated/factories/RewardsDistributorEth__factory" import { IAlchemixStakingPools } from "types/generated/IAlchemixStakingPools" +import { RewardsDistributorEth } from "types/generated/RewardsDistributorEth" const governorAddress = "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" @@ -183,7 +183,7 @@ context("alUSD Feeder Pool integration to Alchemix", () => { boosted: true, name: "v-mUSD/alUSD fPool Vault", symbol: "v-fPmUSD/alUSD", - priceCoeff: BN.from(1), + priceCoeff: simpleToExactAmount(1), stakingToken: alUsdFp.address, rewardToken: MTA.address, } From 0943001ab667f5cb1f8e187b94e5f7914df9d710 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 02:31:59 +1000 Subject: [PATCH 19/21] feat: more vault hh tasks --- tasks/deployFeeders.ts | 3 +- tasks/ops.ts | 94 +++++++++++++++++++++++++++++++++++++++++- tasks/utils/tokens.ts | 2 +- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index f106db1e..aa9771b0 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -10,9 +10,8 @@ import { CompoundIntegration, AlchemixIntegration, AlchemixIntegration__factory, - FeederPool, } from "types/generated" -import { BN, simpleToExactAmount } from "@utils/math" +import { simpleToExactAmount } from "@utils/math" import { ALCX, alUSD, BUSD, CREAM, cyMUSD, GUSD, mUSD, tokens } from "./utils/tokens" import { deployContract, logTxDetails } from "./utils/deploy-utils" import { getSigner } from "./utils/defender-utils" diff --git a/tasks/ops.ts b/tasks/ops.ts index 8bb1344b..d8f146ee 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -10,8 +10,9 @@ import { ERC20__factory, AssetProxy__factory, } from "types/generated" +import { RewardsDistributorEth__factory } from "types/generated/factories/RewardsDistributorEth__factory" import { simpleToExactAmount } from "@utils/math" -import { PMTA, PmUSD, PWMATIC, tokens } from "./utils/tokens" +import { MTA, PMTA, PmUSD, PWMATIC, tokens } from "./utils/tokens" import { getSigner } from "./utils/defender-utils" import { logTxDetails } from "./utils/deploy-utils" import { getChain, getChainAddress } from "./utils/networkAddressFactory" @@ -117,7 +118,7 @@ task("polly-stake-imusd", "Stakes imUSD into the v-imUSD vault on Polygon") await logTxDetails(tx2, `stake ${usdFormatter(amount)} imUSD in v-imUSD vault`) }) -task("polly-dis-rewards", "Distributes MTA and WMATIC rewards to vaults on Polygon") +task("polly-dis-rewards", "Distributes MTA and WMATIC rewards to the imUSD vault on Polygon") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .addOptionalParam("mtaAmount", "MTA tokens", 20833, types.int) .addOptionalParam("wmaticAmount", "WMATIC tokens", 18666, types.int) @@ -143,6 +144,35 @@ task("polly-dis-rewards", "Distributes MTA and WMATIC rewards to vaults on Polyg await logTxDetails(tx3, `distributeRewards ${usdFormatter(mtaAmount)} MTA and ${usdFormatter(wmaticAmount)} WMATIC`) }) +task("dis-rewards", "Distributes MTA rewards to a vault on Mainnet") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .addParam("vaultAsset", "Symbol of asset that is staked. eg mUSD, MTA, GUSD, alUSD", undefined, types.string) + .addOptionalParam("mtaAmount", "MTA tokens", 20833, types.int) + .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { + const signer = await getSigner(ethers, taskArgs.speed) + const chain = getChain(network.name, hardhatArguments.config) + + const vaultAsset = tokens.find((t) => t.symbol === taskArgs.vaultAsset && t.chain === chain) + if (!vaultAsset) throw Error(`Could not find vault asset with symbol ${taskArgs.vaultAsset}`) + // Staking Token is for Feeder Pool, Savings Vault or the token itself. eg + // alUSD will stake feeder pool in a v-fPmUSD/alUSD vault + // mUSD will stake savings vault in a v-imUSD vault + // MTA will stake MTA in a v-MTA vault + const vaultAddress = vaultAsset.feederPool || vaultAsset.savings || vaultAsset.address + + const mtaAmount = simpleToExactAmount(taskArgs.mtaAmount) + + const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) + const rewardsDistributor = RewardsDistributorEth__factory.connect(rewardsDistributorAddress, signer) + + const mtaToken = ERC20__factory.connect(MTA.address, signer) + const tx1 = await mtaToken.approve(rewardsDistributorAddress, mtaAmount) + await logTxDetails(tx1, `Relay account approve RewardsDistributor contract to transfer ${usdFormatter(mtaAmount)} MTA`) + + const tx3 = await rewardsDistributor.distributeRewards([vaultAddress], [mtaAmount]) + await logTxDetails(tx3, `distributeRewards ${usdFormatter(mtaAmount)} MTA`) + }) + task("rewards", "Get Compound and Aave platform reward tokens") .addOptionalParam("block", "Block number to compare rates at. (default: current block)", 0, types.int) .setAction(async (taskArgs, { ethers }) => { @@ -216,4 +246,64 @@ task("vault-stake", "Stake into a vault") await logTxDetails(tx, `${signerAddress} stakes ${amount} ${assetSymbol} in vault`) }) +task("vault-withdraw", "Withdraw from a vault") + .addParam("asset", "Symbol of the asset that has a mStable vault. eg mUSD, alUSD, MTA", undefined, types.string) + .addParam("amount", "Amount to be withdrawn", undefined, types.int) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { ethers, network }) => { + const chain = getChain(network.name) + const signer = await getSigner(ethers, taskArgs.speed) + const signerAddress = await signer.getAddress() + + const assetSymbol = taskArgs.asset + const assetToken = tokens.find((t) => t.symbol === assetSymbol && t.chain === chain) + if (!assetToken) throw Error(`Could not find asset with symbol ${assetSymbol}`) + if (!assetToken.vault) throw Error(`No vault is configured for asset ${assetSymbol}`) + + const vault = StakingRewards__factory.connect(assetToken.vault, signer) + + const amount = simpleToExactAmount(taskArgs.amount) + + const tx = await vault.withdraw(amount) + await logTxDetails(tx, `${signerAddress} withdraw ${amount} ${assetSymbol} from vault`) + }) + +task("vault-exit", "Exit from vault claiming rewards") + .addParam("asset", "Symbol of the asset that has a mStable vault. eg mUSD, alUSD, MTA", undefined, types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { ethers, network }) => { + const chain = getChain(network.name) + const signer = await getSigner(ethers, taskArgs.speed) + const signerAddress = await signer.getAddress() + + const assetSymbol = taskArgs.asset + const assetToken = tokens.find((t) => t.symbol === assetSymbol && t.chain === chain) + if (!assetToken) throw Error(`Could not find asset with symbol ${assetSymbol}`) + if (!assetToken.vault) throw Error(`No vault is configured for asset ${assetSymbol}`) + + const vault = StakingRewards__factory.connect(assetToken.vault, signer) + + const tx = await vault.exit() + await logTxDetails(tx, `${signerAddress} exits ${assetSymbol} vault`) + }) + +task("vault-claim", "Claim rewards from vault") + .addParam("asset", "Symbol of the asset that has a mStable vault. eg mUSD, alUSD, MTA", undefined, types.string) + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, { ethers, network }) => { + const chain = getChain(network.name) + const signer = await getSigner(ethers, taskArgs.speed) + const signerAddress = await signer.getAddress() + + const assetSymbol = taskArgs.asset + const assetToken = tokens.find((t) => t.symbol === assetSymbol && t.chain === chain) + if (!assetToken) throw Error(`Could not find asset with symbol ${assetSymbol}`) + if (!assetToken.vault) throw Error(`No vault is configured for asset ${assetSymbol}`) + + const vault = StakingRewards__factory.connect(assetToken.vault, signer) + + const tx = await vault.claimReward() + await logTxDetails(tx, `${signerAddress} claim rewards from ${assetSymbol} vault`) + }) + module.exports = {} diff --git a/tasks/utils/tokens.ts b/tasks/utils/tokens.ts index 5c756af6..5675c5e4 100644 --- a/tasks/utils/tokens.ts +++ b/tasks/utils/tokens.ts @@ -211,7 +211,7 @@ export const alUSD: Token = { address: "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9", feederPool: "0x4eaa01974B6594C0Ee62fFd7FEE56CF11E6af936", integrator: "0xd658d5fDe0917CdC9b10cAadf10E20d942572a7B", - vault: "0xe52aba1eaa4a9f7efea2e8dfd284aeaa818dc43e", + vault: "0x0997dDdc038c8A958a3A3d00425C16f8ECa87deb", chain: Chain.mainnet, decimals: 18, quantityFormatter: "USD", From 6ddb0f08510249769bb609da0070f9fa6f6e8429 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 03:07:06 +1000 Subject: [PATCH 20/21] fix: dis-rewards task --- tasks/ops.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tasks/ops.ts b/tasks/ops.ts index d8f146ee..237b8206 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -18,6 +18,8 @@ import { logTxDetails } from "./utils/deploy-utils" import { getChain, getChainAddress } from "./utils/networkAddressFactory" import { usdFormatter } from "./utils" import { getAaveTokens, getBlock, getBlockRange, getCompTokens } from "./utils/snap-utils" +import { sign } from "crypto" +import { formatUnits } from "ethers/lib/utils" task("eject-stakers", "Ejects expired stakers from Meta staking contract (vMTA)") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) @@ -147,30 +149,28 @@ task("polly-dis-rewards", "Distributes MTA and WMATIC rewards to the imUSD vault task("dis-rewards", "Distributes MTA rewards to a vault on Mainnet") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) .addParam("vaultAsset", "Symbol of asset that is staked. eg mUSD, MTA, GUSD, alUSD", undefined, types.string) - .addOptionalParam("mtaAmount", "MTA tokens", 20833, types.int) + .addOptionalParam("amount", "MTA tokens", 20833, types.int) .setAction(async (taskArgs, { ethers, hardhatArguments, network }) => { const signer = await getSigner(ethers, taskArgs.speed) const chain = getChain(network.name, hardhatArguments.config) const vaultAsset = tokens.find((t) => t.symbol === taskArgs.vaultAsset && t.chain === chain) if (!vaultAsset) throw Error(`Could not find vault asset with symbol ${taskArgs.vaultAsset}`) - // Staking Token is for Feeder Pool, Savings Vault or the token itself. eg - // alUSD will stake feeder pool in a v-fPmUSD/alUSD vault - // mUSD will stake savings vault in a v-imUSD vault - // MTA will stake MTA in a v-MTA vault - const vaultAddress = vaultAsset.feederPool || vaultAsset.savings || vaultAsset.address - const mtaAmount = simpleToExactAmount(taskArgs.mtaAmount) + const mtaAmount = simpleToExactAmount(taskArgs.amount) const rewardsDistributorAddress = getChainAddress("RewardsDistributor", chain) const rewardsDistributor = RewardsDistributorEth__factory.connect(rewardsDistributorAddress, signer) const mtaToken = ERC20__factory.connect(MTA.address, signer) const tx1 = await mtaToken.approve(rewardsDistributorAddress, mtaAmount) - await logTxDetails(tx1, `Relay account approve RewardsDistributor contract to transfer ${usdFormatter(mtaAmount)} MTA`) + await logTxDetails(tx1, `Relay account approve RewardsDistributor contract to transfer ${formatUnits(mtaAmount)} MTA`) - const tx3 = await rewardsDistributor.distributeRewards([vaultAddress], [mtaAmount]) - await logTxDetails(tx3, `distributeRewards ${usdFormatter(mtaAmount)} MTA`) + const tx2 = await rewardsDistributor.distributeRewards([vaultAsset.vault], [mtaAmount]) + await logTxDetails( + tx2, + `distributeRewards ${formatUnits(mtaAmount)} MTA to vault with asset ${vaultAsset.symbol} and address ${vaultAsset.vault}`, + ) }) task("rewards", "Get Compound and Aave platform reward tokens") From 0790b7bcb8a2e934d02fa22ec4597c8b031c4a74 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 13 Jul 2021 03:12:41 +1000 Subject: [PATCH 21/21] chore: formatting --- tasks/ops.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks/ops.ts b/tasks/ops.ts index 237b8206..dfc1e88d 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -12,14 +12,13 @@ import { } from "types/generated" import { RewardsDistributorEth__factory } from "types/generated/factories/RewardsDistributorEth__factory" import { simpleToExactAmount } from "@utils/math" +import { formatUnits } from "ethers/lib/utils" import { MTA, PMTA, PmUSD, PWMATIC, tokens } from "./utils/tokens" import { getSigner } from "./utils/defender-utils" import { logTxDetails } from "./utils/deploy-utils" import { getChain, getChainAddress } from "./utils/networkAddressFactory" import { usdFormatter } from "./utils" import { getAaveTokens, getBlock, getBlockRange, getCompTokens } from "./utils/snap-utils" -import { sign } from "crypto" -import { formatUnits } from "ethers/lib/utils" task("eject-stakers", "Ejects expired stakers from Meta staking contract (vMTA)") .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string)