From d48cd7355236de380f56ed745b3fa5f9eec4f150 Mon Sep 17 00:00:00 2001 From: James Lefrere Date: Wed, 7 Jul 2021 13:31:04 +0200 Subject: [PATCH 1/3] WIP: Add Alchemix Integration - Add `AlchemixIntegration` contract - Add fork tests for alUSD FP --- .../masset/peripheral/AlchemixIntegration.sol | 256 +++++++++ .../Alchemix/AlchemixStakingPools.json | 521 ++++++++++++++++++ test-fork/feeders/feeders-musd-alusd.spec.ts | 162 ++++++ 3 files changed, 939 insertions(+) create mode 100644 contracts/masset/peripheral/AlchemixIntegration.sol create mode 100644 contracts/peripheral/Alchemix/AlchemixStakingPools.json create mode 100644 test-fork/feeders/feeders-musd-alusd.spec.ts diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol new file mode 100644 index 00000000..e9f24ae5 --- /dev/null +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MassetHelpers } from "../../shared/MassetHelpers.sol"; +import { AbstractIntegration } from "./AbstractIntegration.sol"; + +interface IStakingPools { + function claim(uint256 _poolId) external; + function deposit(uint256 _poolId, uint256 _depositAmount) external; + function exit(uint256 _poolId) external; + function getStakeTotalDeposited(address _account, uint256 _poolId) external view returns (uint256); + function getStakeTotalUnclaimed(address _account, uint256 _poolId) external view returns (uint256); + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + function getPoolToken(uint256 _poolId) external view returns (address); + function reward() external view returns (address); + function tokenPoolIds(address _token) external view returns (uint256); + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; +} + +/** + * @title AlchemixIntegration + * @author mStable + * @notice A simple connection to farm ALCX rewards with the Alchemix alUSD pool + * @dev VERSION: 1.0 + * DATE: 2021-07-02 + */ +contract AlchemixIntegration is AbstractIntegration { + using SafeERC20 for IERC20; + + event SkippedWithdrawal(address bAsset, uint256 amount); + event RewardTokenApproved(address rewardToken, address account); + + address public immutable rewardToken; + + IStakingPools private immutable stakingPools; + + /** + * @param _nexus Address of the Nexus + * @param _lp Address of liquidity provider. eg mAsset or feeder pool + * @param _rewardToken Reward token, if any. eg ALCX + * @param _stakingPools Alchemix StakingPools contract address + */ + constructor( + address _nexus, + address _lp, + address _rewardToken, + address _stakingPools + ) AbstractIntegration(_nexus, _lp) { + rewardToken = _rewardToken; + stakingPools = IStakingPools(_stakingPools); + } + + /*************************************** + ADMIN + ****************************************/ + + /** + * @dev Approves Liquidator to spend reward tokens + */ + function approveRewardToken() external onlyGovernor { + address liquidator = nexus.getModule(keccak256("Liquidator")); + require(liquidator != address(0), "Liquidator address is zero"); + + MassetHelpers.safeInfiniteApprove(rewardToken, liquidator); + + emit RewardTokenApproved(rewardToken, liquidator); + } + + /** + @dev Claims any accrued rewardToken for a given bAsset staked + */ + function claim(address _bAsset) external onlyGovernor { + uint256 poolId = _getPoolIdFor(_bAsset); + stakingPools.claim(poolId); + } + + /*************************************** + CORE + ****************************************/ + + /** + * @dev Deposit a quantity of bAsset into the platform. Credited cTokens + * remain here in the vault. Can only be called by whitelisted addresses + * (mAsset and corresponding BasketManager) + * @param _bAsset Address for the bAsset + * @param _amount Units of bAsset to deposit + * @param isTokenFeeCharged Flag that signals if an xfer fee is charged on bAsset + * @return quantityDeposited Quantity of bAsset that entered the platform + */ + function deposit( + address _bAsset, + uint256 _amount, + bool isTokenFeeCharged + ) external override onlyLP nonReentrant returns (uint256 quantityDeposited) { + require(_amount > 0, "Must deposit something"); + + uint256 poolId = _getPoolIdFor(_bAsset); + + quantityDeposited = _amount; + + if (isTokenFeeCharged) { + // If we charge a fee, account for it + uint256 prevBal = this.checkBalance(_bAsset); + stakingPools.deposit(poolId, _amount); + uint256 newBal = this.checkBalance(_bAsset); + quantityDeposited = _min(quantityDeposited, newBal - prevBal); + } else { + // Else just deposit the amount + stakingPools.deposit(poolId, _amount); + } + + emit Deposit(_bAsset, address(stakingPools), quantityDeposited); + } + + /** + * @dev Withdraw a quantity of bAsset from Compound + * @param _receiver Address to which the withdrawn bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + * @param _hasTxFee Is the bAsset known to have a tx fee? + */ + function withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + bool _hasTxFee + ) external override onlyLP nonReentrant { + _withdraw(_receiver, _bAsset, _amount, _amount, _hasTxFee); + } + + /** + * @dev Withdraw a quantity of bAsset from Compound + * @param _receiver Address to which the withdrawn bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + * @param _totalAmount Total units to pull from lending platform + * @param _hasTxFee Is the bAsset known to have a tx fee? + */ + function withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + uint256 _totalAmount, + bool _hasTxFee + ) external override onlyLP nonReentrant { + _withdraw(_receiver, _bAsset, _amount, _totalAmount, _hasTxFee); + } + + function _withdraw( + address _receiver, + address _bAsset, + uint256 _amount, + uint256 _totalAmount, + bool _hasTxFee + ) internal { + require(_totalAmount > 0, "Must withdraw something"); + require(_receiver != address(0), "Must specify recipient"); + + uint256 poolId = _getPoolIdFor(_bAsset); + + uint256 userWithdrawal = _amount; + + if (_hasTxFee) { + require(_amount == _totalAmount, "Cache inactive with tx fee"); + IERC20 b = IERC20(_bAsset); + uint256 prevBal = b.balanceOf(address(this)); + stakingPools.withdraw(poolId, _amount); + uint256 newBal = b.balanceOf(address(this)); + userWithdrawal = _min(userWithdrawal, newBal - prevBal); + } else { + // Redeem Underlying bAsset amount + stakingPools.withdraw(poolId, _totalAmount); + } + + // Send redeemed bAsset to the receiver + IERC20(_bAsset).safeTransfer(_receiver, userWithdrawal); + + emit PlatformWithdrawal(_bAsset, address(stakingPools), _totalAmount, _amount); + } + + /** + * @dev Withdraw a quantity of bAsset from the cache. + * @param _receiver Address to which the bAsset should be sent + * @param _bAsset Address of the bAsset + * @param _amount Units of bAsset to withdraw + */ + function withdrawRaw( + address _receiver, + address _bAsset, + uint256 _amount + ) external override onlyLP nonReentrant { + require(_amount > 0, "Must withdraw something"); + require(_receiver != address(0), "Must specify recipient"); + + IERC20(_bAsset).safeTransfer(_receiver, _amount); + + emit Withdrawal(_bAsset, address(0), _amount); + } + + /** + * @dev Get the total bAsset value held in the platform + * @param _bAsset Address of the bAsset + * @return balance Total value of the bAsset in the platform + */ + function checkBalance(address _bAsset) external view override returns (uint256 balance) { + uint256 poolId = _getPoolIdFor(_bAsset); + return stakingPools.getStakeTotalDeposited(address(this), poolId); + } + + /*************************************** + APPROVALS + ****************************************/ + + /** + * @dev Re-approve the spending of all bAssets by their corresponding cToken, + * if for some reason is it necessary. Only callable through Governance. + */ + function reApproveAllTokens() external onlyGovernor { + uint256 bAssetCount = bAssetsMapped.length; + for (uint256 i = 0; i < bAssetCount; i++) { + address bAsset = bAssetsMapped[i]; + address cToken = bAssetToPToken[bAsset]; + MassetHelpers.safeInfiniteApprove(bAsset, cToken); + } + } + + /** + FIXME do we need this? + * @dev Internal method to respond to the addition of new bAsset / cTokens + * We need to approve the cToken and give it permission to spend the bAsset + * @param _bAsset Address of the bAsset to approve + * @param _cToken This cToken has the approval approval + */ + function _abstractSetPToken(address _bAsset, address _cToken) internal override { + // approve the pool to spend the bAsset + MassetHelpers.safeInfiniteApprove(_bAsset, _cToken); + } + + /*************************************** + HELPERS + ****************************************/ + + /** + * @dev Get the cToken wrapped in the ICERC20 interface for this bAsset. + * Fails if the pToken doesn't exist in our mappings. + * @param _bAsset Address of the bAsset + * @return poolId Corresponding Alchemix StakingPools poolId + */ + function _getPoolIdFor(address _bAsset) internal view returns (uint256 poolId) { + poolId = stakingPools.tokenPoolIds(_bAsset); + require(poolId > 0, "Asset not supported on Alchemix"); + } +} diff --git a/contracts/peripheral/Alchemix/AlchemixStakingPools.json b/contracts/peripheral/Alchemix/AlchemixStakingPools.json new file mode 100644 index 00000000..19b32291 --- /dev/null +++ b/contracts/peripheral/Alchemix/AlchemixStakingPools.json @@ -0,0 +1,521 @@ +{ + "contractName": "StakingPools", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IMintableERC20", + "name": "_reward", + "type": "address" + }, + { + "internalType": "address", + "name": "_governance", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "governance", + "type": "address" + } + ], + "name": "GovernanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "pendingGovernance", + "type": "address" + } + ], + "name": "PendingGovernanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardWeight", + "type": "uint256" + } + ], + "name": "PoolRewardWeightUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "rewardRate", + "type": "uint256" + } + ], + "name": "RewardRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "poolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_token", + "type": "address" + } + ], + "name": "createPool", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_depositAmount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "exit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolRewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolRewardWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getPoolTotalDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getStakeTotalDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + } + ], + "name": "getStakeTotalUnclaimed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingGovernance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "reward", + "outputs": [ + { + "internalType": "contract IMintableERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_pendingGovernance", + "type": "address" + } + ], + "name": "setPendingGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rewardRate", + "type": "uint256" + } + ], + "name": "setRewardRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_rewardWeights", + "type": "uint256[]" + } + ], + "name": "setRewardWeights", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "tokenPoolIds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalRewardWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_poolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/test-fork/feeders/feeders-musd-alusd.spec.ts b/test-fork/feeders/feeders-musd-alusd.spec.ts new file mode 100644 index 00000000..f6751cdd --- /dev/null +++ b/test-fork/feeders/feeders-musd-alusd.spec.ts @@ -0,0 +1,162 @@ +import { impersonate } from "@utils/fork" +import { BN, simpleToExactAmount } from "@utils/math" +import { expect } from "chai" +import { Signer, constants } from "ethers" +import { ethers, network } from "hardhat" +import { deployContract } from "tasks/utils/deploy-utils" +import { FeederPool, FeederPool__factory, IERC20, IERC20__factory, Masset__factory } from "types/generated" +import { AlchemixIntegration } from "types/generated/AlchemixIntegration" +import { AlchemixIntegration__factory } from "types/generated/factories/AlchemixIntegration__factory" + +const governorAddress = "0xF6FF1F7FCEB2cE6d26687EaaB5988b445d0b94a2" +const deployerAddress = "0xb81473f20818225302b8fffb905b53d58a793d84" +const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" +const mUsdWhaleAddress = "0x69E0E2b3d523D3b247d798a49C3fa022a46DD6bd" +const alUsdWhaleAddress = "0xf9a0106251467fff1ff03e8609aa74fc55a2a45e" + +const nexusAddress = "0xafce80b19a8ce13dec0739a1aab7a028d6845eb3" +const mUsdAddress = "0xe2f2a5c287993345a840db3b0845fbc70f5935a5" +const alUsdAddress = "0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9" +const alcxAddress = "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF" +const stakingPoolsAddress = "0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa" +const liquidatorAddress = "0xe595D67181D701A5356e010D9a58EB9A341f1DbD" + +context("mUSD Feeder Pool integration to Alchemix", () => { + let governor: Signer + let deployer: Signer + let ethWhale: Signer + let mUsdWhale: Signer + let alUsdWhale: Signer + let alUsdFp: FeederPool + let mUsd: IERC20 + let alUsd: IERC20 + let alcxToken: IERC20 + let alchemixIntegration: AlchemixIntegration + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 12779756, + }, + }, + ], + }) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + ethWhale = await impersonate(ethWhaleAddress) + mUsdWhale = await impersonate(mUsdWhaleAddress) + alUsdWhale = await impersonate(alUsdWhaleAddress) + + // send some Ether to addresses that need it + await Promise.all( + [alUsdWhaleAddress, governorAddress, mUsdWhaleAddress].map((recipient) => + ethWhale.sendTransaction({ + to: recipient, + value: simpleToExactAmount(10), + }), + ), + ) + + mUsd = await IERC20__factory.connect(mUsdAddress, deployer) + alUsd = await IERC20__factory.connect(alUsdAddress, deployer) + alcxToken = await IERC20__factory.connect(alcxAddress, deployer) + }) + it("Test connectivity", async () => { + const currentBlock = await ethers.provider.getBlockNumber() + console.log(`Current block ${currentBlock}`) + const startEther = await deployer.getBalance() + console.log(`Deployer ${deployerAddress} has ${startEther} Ether`) + }) + it("deploy and initialize integration contract", async () => { + alUsdFp = await deployContract( + new FeederPool__factory( + { + __$60670dd84d06e10bb8a5ac6f99a1c0890c$__: "0x90aE544E8cc76d2867987Ee4f5456C02C50aBd8B", // FeederManager + __$7791d1d5b7ea16da359ce352a2ac3a881c$__: "0x2837C77527c37d61D9763F53005211dACB4125dE", // FeederLogic + }, + deployer, + ), + "alUSD/mUSD Feeder Pool", + [nexusAddress, mUsdAddress], + ) + + const mpAssets = (await Masset__factory.connect(mUsdAddress, deployer).getBassets())[0].map((p) => p[0]) + + await alUsdFp.initialize( + "Feeder Pool mUSD/alUSD", + "fP-mUSD/alUSD", + { addr: mUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, + { addr: alUsdAddress, integrator: constants.AddressZero, hasTxFee: false, status: 0 }, + mpAssets, + { + a: BN.from(225), + limits: { + min: simpleToExactAmount(10, 16), + max: simpleToExactAmount(90, 16), + }, + }, + ) + + alchemixIntegration = await deployContract( + new AlchemixIntegration__factory(deployer), + "Alchemix alUSD Integration", + [nexusAddress, alUsdFp.address, alcxAddress, stakingPoolsAddress], + ) + expect(alchemixIntegration.address).to.length(42) + + await alchemixIntegration.initialize([alUsdAddress], [alcxAddress]) + }) + it("Governor approves Liquidator to spend the reward (ALCX) token", async () => { + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(0) + + // This will be done via the delayedProxyAdmin on mainnet + await alchemixIntegration.connect(governor).approveRewardToken() + + expect(await alcxToken.allowance(alchemixIntegration.address, liquidatorAddress)).to.eq(constants.MaxUint256) + }) + it("Mint some mUSD/alUSD in the Feeder Pool", async () => { + const alUsdBassetBefore = await alUsdFp.getBasset(alUsdAddress) + const mUsdBassetBefore = await alUsdFp.getBasset(mUsdAddress) + + expect(await alUsd.balanceOf(alUsdFp.address)).to.eq(0) + expect(await mUsd.balanceOf(alUsdFp.address)).to.eq(0) + + const mintAmount = simpleToExactAmount(10000) + + // Transfer some mUSD to the alUSD whale so they can do a mintMulti (to get the pool started) + await mUsd.connect(mUsdWhale).transfer(alUsdWhaleAddress, mintAmount) + expect(await mUsd.balanceOf(alUsdWhaleAddress)).to.gte(mintAmount) + + await alUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + await mUsd.connect(alUsdWhale).approve(alUsdFp.address, constants.MaxUint256) + expect(await alUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) + expect(await mUsd.allowance(alUsdWhaleAddress, alUsdFp.address)).to.eq(constants.MaxUint256) + + await alUsdFp.connect(alUsdWhale).mintMulti([alUsdAddress, mUsdAddress], [mintAmount, mintAmount], 0, alUsdWhaleAddress) + + const alUsdBassetAfter = await alUsdFp.getBasset(alUsdAddress) + const mUsdBassetAfter = await alUsdFp.getBasset(mUsdAddress) + expect(alUsdBassetAfter.vaultData.vaultBalance, "alUSD vault balance").to.eq( + alUsdBassetBefore.vaultData.vaultBalance.add(mintAmount), + ) + expect(mUsdBassetAfter.vaultData.vaultBalance, "mUSD vault balance").to.eq(mUsdBassetBefore.vaultData.vaultBalance.add(mintAmount)) + }) + it("Migrate alUSD to the Alchemix integration", async () => { + expect(await alUsd.balanceOf(alUsdFp.address), "Some alUSD in Feeder Pool").to.gt(0) + expect(await alUsd.balanceOf(alchemixIntegration.address), "No alUSD in Integration contract").to.eq(0) + const alUsdInFpBefore = await alUsd.balanceOf(alUsdFp.address) + + // Migrate the alUSD + await alUsdFp.migrateBassets([alUsdAddress], alchemixIntegration.address) + + // All alUSD in the FP should have moved to the integration contract + expect(await alUsd.balanceOf(alchemixIntegration.address), "All alUSD in FP migrated to Integration").to.eq(alUsdInFpBefore) + expect(await alUsd.balanceOf(alUsdFp.address), "No more alUSD in Feeder Pool").to.eq(0) + }) + // Mint alUSD in FP, test integration/FP balances, wait for a block or two, claim rewards + // Liquidate rewards and test output/balances +}) From fdb296b87fb090ff19bfbbbcb005b3beee5335a6 Mon Sep 17 00:00:00 2001 From: alsco77 Date: Thu, 8 Jul 2021 15:25:06 +0100 Subject: [PATCH 2/3] ci: Bump CI --- .../masset/peripheral/AlchemixIntegration.sol | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index e9f24ae5..434555b0 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -8,15 +8,31 @@ import { AbstractIntegration } from "./AbstractIntegration.sol"; interface IStakingPools { function claim(uint256 _poolId) external; + function deposit(uint256 _poolId, uint256 _depositAmount) external; + function exit(uint256 _poolId) external; - function getStakeTotalDeposited(address _account, uint256 _poolId) external view returns (uint256); - function getStakeTotalUnclaimed(address _account, uint256 _poolId) external view returns (uint256); + + function getStakeTotalDeposited(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getStakeTotalUnclaimed(address _account, uint256 _poolId) + external + view + returns (uint256); + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + function getPoolToken(uint256 _poolId) external view returns (address); + function reward() external view returns (address); + function tokenPoolIds(address _token) external view returns (uint256); + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; } @@ -70,7 +86,7 @@ contract AlchemixIntegration is AbstractIntegration { } /** - @dev Claims any accrued rewardToken for a given bAsset staked + * @dev Claims any accrued rewardToken for a given bAsset staked */ function claim(address _bAsset) external onlyGovernor { uint256 poolId = _getPoolIdFor(_bAsset); From 336dee6f728bdd012cfa2bda513a7749a5aceb76 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 9 Jul 2021 23:26:05 +1000 Subject: [PATCH 3/3] chore: standardized Alchemix integration --- .../masset/peripheral/AlchemixIntegration.sol | 79 +-- .../Alchemix/AlchemixStakingPools.json | 521 ------------------ .../Alchemix/IAlchemixStakingPool.sol | 36 ++ ....spec.ts => feeders-musd-alchemix.spec.ts} | 2 +- 4 files changed, 66 insertions(+), 572 deletions(-) delete mode 100644 contracts/peripheral/Alchemix/AlchemixStakingPools.json create mode 100644 contracts/peripheral/Alchemix/IAlchemixStakingPool.sol rename test-fork/feeders/{feeders-musd-alusd.spec.ts => feeders-musd-alchemix.spec.ts} (98%) diff --git a/contracts/masset/peripheral/AlchemixIntegration.sol b/contracts/masset/peripheral/AlchemixIntegration.sol index 434555b0..a00cc97b 100644 --- a/contracts/masset/peripheral/AlchemixIntegration.sol +++ b/contracts/masset/peripheral/AlchemixIntegration.sol @@ -1,40 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.2; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IAlchemixStakingPool } from "../../peripheral/Alchemix/IAlchemixStakingPool.sol"; import { MassetHelpers } from "../../shared/MassetHelpers.sol"; import { AbstractIntegration } from "./AbstractIntegration.sol"; - -interface IStakingPools { - function claim(uint256 _poolId) external; - - function deposit(uint256 _poolId, uint256 _depositAmount) external; - - function exit(uint256 _poolId) external; - - function getStakeTotalDeposited(address _account, uint256 _poolId) - external - view - returns (uint256); - - function getStakeTotalUnclaimed(address _account, uint256 _poolId) - external - view - returns (uint256); - - function getPoolRewardRate(uint256 _poolId) external view returns (uint256); - - function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); - - function getPoolToken(uint256 _poolId) external view returns (address); - - function reward() external view returns (address); - - function tokenPoolIds(address _token) external view returns (uint256); - - function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; -} +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title AlchemixIntegration @@ -48,25 +19,28 @@ contract AlchemixIntegration is AbstractIntegration { event SkippedWithdrawal(address bAsset, uint256 amount); event RewardTokenApproved(address rewardToken, address account); + event RewardsClaimed(); address public immutable rewardToken; - IStakingPools private immutable stakingPools; + IAlchemixStakingPool private immutable stakingPool; /** * @param _nexus Address of the Nexus * @param _lp Address of liquidity provider. eg mAsset or feeder pool * @param _rewardToken Reward token, if any. eg ALCX - * @param _stakingPools Alchemix StakingPools contract address + * @param _stakingPool Alchemix StakingPools contract address */ constructor( address _nexus, address _lp, address _rewardToken, - address _stakingPools + address _stakingPool ) AbstractIntegration(_nexus, _lp) { + require(_rewardToken != address(0), "Invalid reward token"); + require(_stakingPool != address(0), "Invalid staking pool"); rewardToken = _rewardToken; - stakingPools = IStakingPools(_stakingPools); + stakingPool = IAlchemixStakingPool(_stakingPool); } /*************************************** @@ -88,9 +62,14 @@ contract AlchemixIntegration is AbstractIntegration { /** * @dev Claims any accrued rewardToken for a given bAsset staked */ - function claim(address _bAsset) external onlyGovernor { - uint256 poolId = _getPoolIdFor(_bAsset); - stakingPools.claim(poolId); + function claimRewards(address _bAsset) external onlyGovernor { + uint256 len = bAssetsMapped.length; + for (uint256 i = 0; i < len; i++) { + uint256 poolId = _getPoolIdFor(bAssetsMapped[i]); + stakingPool.claim(poolId); + } + + emit RewardsClaimed(); } /*************************************** @@ -120,19 +99,19 @@ contract AlchemixIntegration is AbstractIntegration { if (isTokenFeeCharged) { // If we charge a fee, account for it uint256 prevBal = this.checkBalance(_bAsset); - stakingPools.deposit(poolId, _amount); + stakingPool.deposit(poolId, _amount); uint256 newBal = this.checkBalance(_bAsset); quantityDeposited = _min(quantityDeposited, newBal - prevBal); } else { // Else just deposit the amount - stakingPools.deposit(poolId, _amount); + stakingPool.deposit(poolId, _amount); } - emit Deposit(_bAsset, address(stakingPools), quantityDeposited); + emit Deposit(_bAsset, address(stakingPool), quantityDeposited); } /** - * @dev Withdraw a quantity of bAsset from Compound + * @dev Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -148,7 +127,7 @@ contract AlchemixIntegration is AbstractIntegration { } /** - * @dev Withdraw a quantity of bAsset from Compound + * @dev Withdraw a quantity of bAsset from Alchemix * @param _receiver Address to which the withdrawn bAsset should be sent * @param _bAsset Address of the bAsset * @param _amount Units of bAsset to withdraw @@ -183,18 +162,18 @@ contract AlchemixIntegration is AbstractIntegration { require(_amount == _totalAmount, "Cache inactive with tx fee"); IERC20 b = IERC20(_bAsset); uint256 prevBal = b.balanceOf(address(this)); - stakingPools.withdraw(poolId, _amount); + stakingPool.withdraw(poolId, _amount); uint256 newBal = b.balanceOf(address(this)); userWithdrawal = _min(userWithdrawal, newBal - prevBal); } else { // Redeem Underlying bAsset amount - stakingPools.withdraw(poolId, _totalAmount); + stakingPool.withdraw(poolId, _totalAmount); } // Send redeemed bAsset to the receiver IERC20(_bAsset).safeTransfer(_receiver, userWithdrawal); - emit PlatformWithdrawal(_bAsset, address(stakingPools), _totalAmount, _amount); + emit PlatformWithdrawal(_bAsset, address(stakingPool), _totalAmount, _amount); } /** @@ -223,7 +202,7 @@ contract AlchemixIntegration is AbstractIntegration { */ function checkBalance(address _bAsset) external view override returns (uint256 balance) { uint256 poolId = _getPoolIdFor(_bAsset); - return stakingPools.getStakeTotalDeposited(address(this), poolId); + balance = stakingPool.getStakeTotalDeposited(address(this), poolId); } /*************************************** @@ -260,13 +239,13 @@ contract AlchemixIntegration is AbstractIntegration { ****************************************/ /** - * @dev Get the cToken wrapped in the ICERC20 interface for this bAsset. + * @dev Get the Alchemix pool id for a bAsset. * Fails if the pToken doesn't exist in our mappings. * @param _bAsset Address of the bAsset * @return poolId Corresponding Alchemix StakingPools poolId */ function _getPoolIdFor(address _bAsset) internal view returns (uint256 poolId) { - poolId = stakingPools.tokenPoolIds(_bAsset); + poolId = stakingPool.tokenPoolIds(_bAsset); require(poolId > 0, "Asset not supported on Alchemix"); } } diff --git a/contracts/peripheral/Alchemix/AlchemixStakingPools.json b/contracts/peripheral/Alchemix/AlchemixStakingPools.json deleted file mode 100644 index 19b32291..00000000 --- a/contracts/peripheral/Alchemix/AlchemixStakingPools.json +++ /dev/null @@ -1,521 +0,0 @@ -{ - "contractName": "StakingPools", - "abi": [ - { - "inputs": [ - { - "internalType": "contract IMintableERC20", - "name": "_reward", - "type": "address" - }, - { - "internalType": "address", - "name": "_governance", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "governance", - "type": "address" - } - ], - "name": "GovernanceUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "pendingGovernance", - "type": "address" - } - ], - "name": "PendingGovernanceUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "contract IERC20", - "name": "token", - "type": "address" - } - ], - "name": "PoolCreated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "rewardWeight", - "type": "uint256" - } - ], - "name": "PoolRewardWeightUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "rewardRate", - "type": "uint256" - } - ], - "name": "RewardRateUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensClaimed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensDeposited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "poolId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TokensWithdrawn", - "type": "event" - }, - { - "inputs": [], - "name": "acceptGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "claim", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "_token", - "type": "address" - } - ], - "name": "createPool", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_depositAmount", - "type": "uint256" - } - ], - "name": "deposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "exit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolRewardRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolRewardWeight", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolToken", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getPoolTotalDeposited", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getStakeTotalDeposited", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - } - ], - "name": "getStakeTotalUnclaimed", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "governance", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingGovernance", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "poolCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "reward", - "outputs": [ - { - "internalType": "contract IMintableERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_pendingGovernance", - "type": "address" - } - ], - "name": "setPendingGovernance", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_rewardRate", - "type": "uint256" - } - ], - "name": "setRewardRate", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256[]", - "name": "_rewardWeights", - "type": "uint256[]" - } - ], - "name": "setRewardWeights", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "name": "tokenPoolIds", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalRewardWeight", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_poolId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_withdrawAmount", - "type": "uint256" - } - ], - "name": "withdraw", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ] -} \ No newline at end of file diff --git a/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol b/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol new file mode 100644 index 00000000..ee463033 --- /dev/null +++ b/contracts/peripheral/Alchemix/IAlchemixStakingPool.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +/** + * @dev Alchemix Staking Pool + * Source: https://github.com/alchemix-finance/alchemix-protocol/blob/master/contracts/StakingPools.sol + */ +interface IAlchemixStakingPool { + function claim(uint256 _poolId) external; + + function deposit(uint256 _poolId, uint256 _depositAmount) external; + + function exit(uint256 _poolId) external; + + function getStakeTotalDeposited(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getStakeTotalUnclaimed(address _account, uint256 _poolId) + external + view + returns (uint256); + + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + + function getPoolToken(uint256 _poolId) external view returns (address); + + function reward() external view returns (address); + + function tokenPoolIds(address _token) external view returns (uint256); + + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; +} diff --git a/test-fork/feeders/feeders-musd-alusd.spec.ts b/test-fork/feeders/feeders-musd-alchemix.spec.ts similarity index 98% rename from test-fork/feeders/feeders-musd-alusd.spec.ts rename to test-fork/feeders/feeders-musd-alchemix.spec.ts index f6751cdd..349118ad 100644 --- a/test-fork/feeders/feeders-musd-alusd.spec.ts +++ b/test-fork/feeders/feeders-musd-alchemix.spec.ts @@ -151,7 +151,7 @@ context("mUSD Feeder Pool integration to Alchemix", () => { const alUsdInFpBefore = await alUsd.balanceOf(alUsdFp.address) // Migrate the alUSD - await alUsdFp.migrateBassets([alUsdAddress], alchemixIntegration.address) + await alUsdFp.connect(governor).migrateBassets([alUsdAddress], alchemixIntegration.address) // All alUSD in the FP should have moved to the integration contract expect(await alUsd.balanceOf(alchemixIntegration.address), "All alUSD in FP migrated to Integration").to.eq(alUsdInFpBefore)