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/liquidator/Liquidator.sol b/contracts/masset/liquidator/Liquidator.sol index 2bd02bfb..2378a063 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(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/AbstractIntegration.sol b/contracts/masset/peripheral/AbstractIntegration.sol index 5c534106..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 @@ -34,11 +31,12 @@ 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) - 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 new file mode 100644 index 00000000..c87320a6 --- /dev/null +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -0,0 +1,277 @@ +// 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 { 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"; + +/** + * @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 + 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 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 + IAlchemixStakingPools public immutable stakingPools; + /// @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. + */ + modifier onlyLP() { + require(msg.sender == lpAddress, "Only the LP can execute"); + _; + } + + /** + * @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 _bAsset base asset to be deposited to Alchemix's staking pool. eg alUSD + */ + constructor( + address _nexus, + address _lp, + address _rewardToken, + 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 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 { + _approveContracts(); + } + + /*************************************** + ADMIN + ****************************************/ + + /** + * @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 reapproveContracts() external onlyGovernor { + _approveContracts(); + } + + function _approveContracts() internal { + // Approve Alchemix staking pools contract to transfer bAssets for deposits. + MassetHelpers.safeInfiniteApprove(bAsset, address(stakingPools)); + + // 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); + } + + /*************************************** + CORE + ****************************************/ + + /** + * @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 + * @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"); + require(_bAsset == bAsset, "Invalid 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); + } + + /** + * @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 + * @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); + } + + /** + * @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 + * @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(_receiver != address(0), "Must specify recipient"); + require(_bAsset == bAsset, "Invalid bAsset"); + require(_totalAmount > 0, "Must withdraw something"); + + 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); + } + + /** + * @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 + */ + function withdrawRaw( + address _receiver, + address _bAsset, + uint256 _amount + ) external override onlyLP nonReentrant { + require(_receiver != address(0), "Must specify recipient"); + require(_bAsset == bAsset, "Invalid bAsset"); + require(_amount > 0, "Must withdraw something"); + + IERC20(_bAsset).safeTransfer(_receiver, _amount); + + emit Withdrawal(_bAsset, address(0), _amount); + } + + /** + * @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) { + require(_bAsset == bAsset, "Invalid bAsset"); + balance = stakingPools.getStakeTotalDeposited(address(this), poolId); + } + + /*************************************** + Liquidation + ****************************************/ + + /** + * @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 claimRewards() external { + stakingPools.claim(poolId); + } + + /*************************************** + HELPERS + ****************************************/ + + /** + * @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/peripheral/Alchemix/IAlchemixStakingPools.sol b/contracts/peripheral/Alchemix/IAlchemixStakingPools.sol new file mode 100644 index 00000000..75db0135 --- /dev/null +++ b/contracts/peripheral/Alchemix/IAlchemixStakingPools.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 IAlchemixStakingPools { + 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/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/contracts/rewards/boosted-staking/BoostedDualVault.sol b/contracts/rewards/boosted-staking/BoostedDualVault.sol index 9d47cc3e..6d9f78fe 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 @@ -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/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/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) diff --git a/tasks/deployFeeders.ts b/tasks/deployFeeders.ts index 9ff81999..aa9771b0 100644 --- a/tasks/deployFeeders.ts +++ b/tasks/deployFeeders.ts @@ -3,175 +3,131 @@ 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, + AlchemixIntegration, + AlchemixIntegration__factory, } 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 { 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" -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") - .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "average", types.string) +import { deployFeederPool, deployVault, FeederData, VaultData } from "./utils/feederUtils" +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) + .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'", "fast", 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.symbol === taskArgs.masset) + if (!mAsset) throw Error(`Could not find mAsset token with symbol ${taskArgs.masset}`) + 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}`) + 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("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) + .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 mUSD, PmUSD, MTA, GUSD, alUSD", + 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 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 > 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 && 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 + // 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 && 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}`) + + const dualRewardToken = tokens.find((t) => t.symbol === taskArgs.dualRewardToken) + + const vaultData: VaultData = { + boosted: taskArgs.boosted, + name: taskArgs.name, + symbol: taskArgs.symbol, + priceCoeff: simpleToExactAmount(taskArgs.price), + stakingToken: stakingTokenAddress, + rewardToken: rewardToken.address, + dualRewardToken: dualRewardToken?.address, } - // 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..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, @@ -20,12 +21,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 +54,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 +82,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 +93,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 +104,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 +119,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) @@ -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 = {} 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..dc186770 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 @@ -163,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 95206a10..dfc1e88d 100644 --- a/tasks/ops.ts +++ b/tasks/ops.ts @@ -10,11 +10,13 @@ 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 { 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 { 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 +24,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 +54,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 +63,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 +84,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, @@ -113,16 +119,18 @@ 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) .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) @@ -137,6 +145,33 @@ 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("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}`) + + 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 ${formatUnits(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") .addOptionalParam("block", "Block number to compare rates at. (default: current block)", 0, types.int) .setAction(async (taskArgs, { ethers }) => { @@ -182,4 +217,92 @@ 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`) + }) + +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/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/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..698080c2 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: string + rewardToken: string + dualRewardToken?: string +} 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,129 @@ 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, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken, + vaultData.dualRewardToken, + ]) + } else { + vault = await deployContract(new BoostedVault__factory(signer), "BoostedVault", [ + getChainAddress("Nexus", chain), + vaultData.stakingToken, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken, + ]) + } + } else if (vaultData.dualRewardToken) { + vault = await deployContract( + new StakingRewardsWithPlatformToken__factory(signer), + "StakingRewardsWithPlatformToken", + [getChainAddress("Nexus", chain), vaultData.stakingToken, vaultData.rewardToken, vaultData.dualRewardToken], ) + } else { + vault = await deployContract(new StakingRewards__factory(signer), "StakingRewards", [ + getChainAddress("Nexus", chain), + vaultData.stakingToken, + getChainAddress("BoostDirector", chain), + vaultData.priceCoeff, + boostCoeff, + vaultData.rewardToken, + ]) } - // 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 +265,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..e2571760 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,17 @@ export const contractNames = [ "BasketManager", // Legacy mUSD contract "AaveIncentivesController", "AaveLendingPoolAddressProvider", + "AlchemixStakingPool", "QuickSwapRouter", + "UniswapRouterV3", + "UniswapQuoterV3", + "UniswapEthToken", "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 +74,23 @@ export const getNetworkAddress = (contractName: ContractNames, networkName = "ma return "0xf1049aeD858C4eAd6df1de4dbE63EF607CfF3262" case "BasketManager": return "0x66126B4aA2a1C07536Ef8E5e8bD4EfDA1FdEA96D" + case "AaveIncentivesController": + return "0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5" + case "AaveLendingPoolAddressProvider": + return "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5" + case "AlchemixStakingPool": + return "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" + case "UniswapRouterV3": + return "0xE592427A0AEce92De3Edee1F18E0157C05861564" + case "UniswapQuoterV3": + return "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6" + case "UniswapEthToken": + return "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 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 +125,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 +142,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 +152,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/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 32d58fa6..e9066110 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, @@ -722,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) @@ -740,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)}`) @@ -752,9 +757,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,12 +788,13 @@ 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 }) 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..5675c5e4 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: "0x4eaa01974B6594C0Ee62fFd7FEE56CF11E6af936", + integrator: "0xd658d5fDe0917CdC9b10cAadf10E20d942572a7B", + vault: "0x0997dDdc038c8A958a3A3d00425C16f8ECa87deb", + 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,27 @@ 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 = [ + MTA, + PMTA, + 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 new file mode 100644 index 00000000..0d0086c4 --- /dev/null +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -0,0 +1,424 @@ +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" +import { increaseTime } from "@utils/time" +import { expect } from "chai" +import { Signer, constants } from "ethers" +import { ethers, network } from "hardhat" +import { deployContract } from "tasks/utils/deploy-utils" +import { deployFeederPool, deployVault, FeederData, VaultData } from "tasks/utils/feederUtils" +import { getChainAddress } from "tasks/utils/networkAddressFactory" +import { AAVE, ALCX, alUSD, Chain, COMP, MTA, mUSD, stkAAVE } from "tasks/utils/tokens" +import { + AlchemixIntegration, + BoostedVault, + DelayedProxyAdmin, + DelayedProxyAdmin__factory, + FeederPool, + FeederPool__factory, + 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 { 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" +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) +const uniswapEthToken = getChainAddress("UniswapEthToken", Chain.mainnet) + +context("alUSD Feeder Pool integration to Alchemix", () => { + let admin: Signer + let deployer: Signer + let governor: Signer + let ethWhale: Signer + let mUsdWhale: Signer + let alUsdWhale: Signer + let fundManager: Signer + let delayedProxyAdmin: DelayedProxyAdmin + let alUsdFp: FeederPool + 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) + const approveAmount = firstMintAmount.add(secondMintAmount) + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 12810000, // After Feeder Pool deployer + }, + }, + ], + }) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + admin = await impersonate(delayedProxyAdminAddress) + 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( + [alUsdWhaleAddress, governorAddress, mUsdWhaleAddress].map((recipient) => + ethWhale.sendTransaction({ + to: recipient, + value: simpleToExactAmount(10), + }), + ), + ) + + 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) + 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() + console.log(`Current block ${currentBlock}`) + const startEther = await deployer.getBalance() + console.log(`Deployer ${deployerAddress} has ${startEther} Ether`) + }) + it("deploy alUSD Feeder Pool", async () => { + const config = { + a: BN.from(50), + 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 = 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) + }) + 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 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) + 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(approveAmount) + expect(await musdToken.balanceOf(alUsdWhaleAddress), "alUsd whale mUSD bal before").gte(approveAmount) + + await alUsdFp + .connect(alUsdWhale) + .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(firstMintAmount), + ) + 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("Boosted vault for fPmUSD/alUSD Feeder Pool", () => { + it("deploy boosted staking vault", async () => { + const vaultData: VaultData = { + boosted: true, + name: "v-mUSD/alUSD fPool Vault", + symbol: "v-fPmUSD/alUSD", + priceCoeff: simpleToExactAmount(1), + stakingToken: alUsdFp.address, + rewardToken: MTA.address, + } + + 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("stake fPmUSD/alUSD in vault", async () => { + const stakeAmount = simpleToExactAmount(1000) + 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", () => { + 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 () => { + 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) + + await alUsdFp.connect(governor).migrateBassets([alusdToken.address], alchemixIntegration.address) + + // 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 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) + + await alUsdFp + .connect(alUsdWhale) + .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(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(mUsdBassetBefore.vaultData.vaultBalance.add(secondMintAmount).sub(cacheAmount)) + 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.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)) + expect(await alcxToken.balanceOf(alchemixIntegration.address), "integration ALCX bal before").to.eq(0) + + 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(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(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("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("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]) + await liquidator.createLiquidation( + alchemixIntegration.address, + ALCX.address, + alUSD.address, + uniswapPath.encoded, + uniswapPath.encodedReversed, + simpleToExactAmount(5000), + 200, + ZERO_ADDRESS, + false, + ) + }) + it("Claim accrued ALCX using integration contract", async () => { + await increaseTime(ONE_WEEK) + + const unclaimedAlcxBefore = await alchemixStakingPools.getStakeTotalUnclaimed(alchemixIntegration.address, poolId) + 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) + 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.skip("trigger ALCX liquidation", async () => { + await liquidator.triggerLiquidation(alchemixIntegration.address) + }) + // liquidate COMP + // claim stkAAVE + // liquidate stkAAVE + }) +}) 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") })