diff --git a/.env.template b/.env.template index 7f2377b..c10603d 100644 --- a/.env.template +++ b/.env.template @@ -58,12 +58,15 @@ REPORT_GAS=false # ROLLUP_OPERATOR= # UNISWAP_ROUTER=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D +# SUSHISWAP_ROUTER=0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F # WETH=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 +# WBTC=0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 # DAI=0x6b175474e89094c44da98b954eedeac495271d0f # USDC=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 # USDT=0xdac17f958d2ee523a2206206994597c13d831ec7 # SUSD=0x57ab1ec28d129707052df4df418d58a2d46d5f51 # BUSD=0x4fabb145d64652a948d72533023f6e7a623c7c53 +# TUSD=0x0000000000085d4780B73119b644AE5ecd22b376 # COMPOUND_CETH=0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5 # COMPOUND_CDAI=0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 @@ -93,6 +96,17 @@ REPORT_GAS=false # CURVE_STETH_LPTOKEN=0x06325440D014e39736583c165C2963BA99fAf14E # CURVE_STETH_GAUGE=0x182B723a58739a9c974cFDB385ceaDb237453c28 +# IDLE_DAI_BEST_YIELD=0x3fE7940616e5Bc47b0775a0dccf6237893353bB4 +# IDLE_USDC_BEST_YIELD=0x5274891bEC421B39D23760c04A6755eCB444797C +# IDLE_USDT_BEST_YIELD=0xF34842d05A1c888Ca02769A633DF37177415C2f8 +# IDLE_SUSD_BEST_YIELD=0xF52CDcD458bf455aeD77751743180eC4A595Fd3F +# IDLE_TUSD_BEST_YIELD=0xc278041fDD8249FE4c1Aad1193876857EEa3D68c +# IDLE_WBTC_BEST_YIELD=0x8C81121B15197fA0eEaEE1DC75533419DcfD3151 +# IDLE_WETH_BEST_YIELD=0xC8E6CA6E96a326dC448307A5fDE90a0b21fd7f80 +# IDLE_DAI_RISK_ADJUSTED=0xa14eA0E11121e6E951E87c66AFe460A00BCD6A16 +# IDLE_USDC_RISK_ADJUSTED=0x3391bc034f2935eF0E1e41619445F998b2680D35 +# IDLE_USDT_RISK_ADJUSTED=0x28fAc5334C9f7262b3A3Fe707e250E01053e07b5 + # IMPERSONATED_DEPLOYER=0xab5801a7d398351b8be11c439e05c5b3259aec9b # ETH_FUNDER=0xab5801a7d398351b8be11c439e05c5b3259aec9b # DAI_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec @@ -104,6 +118,7 @@ REPORT_GAS=false # COMPOUND_COMP_FUNDER=0xbe0eb53f46cd790cd13851d5eff43d12404d33e8 # CURVE_CRV_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec # AAVE_AAVE_FUNDER=0xbe0eb53f46cd790cd13851d5eff43d12404d33e8 +# IDLE_IDLE_FUNDER=0x2bc44f6c34d9200c258ac07a56d81963d4f92c4c # DUMMY_ASSET= # DUMMY_FUNDER= diff --git a/contracts/strategies/idle/GovTokenRegistry.sol b/contracts/strategies/idle/GovTokenRegistry.sol new file mode 100644 index 0000000..7cbc95b --- /dev/null +++ b/contracts/strategies/idle/GovTokenRegistry.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract GovTokenRegistry is Ownable { + // Array of governance token addresses + // Governance tokens are ditributed by Idle finance + address[] public govTokens; + + event GovTokenRegistered(address govTokenAddress); + event GovTokenUnregistered(address govTokenAddress); + + constructor( + address _comp, + address _idle, + address _aave + ){ + govTokens.push(_comp); + govTokens.push(_idle); + govTokens.push(_aave); + } + + function getGovTokens() public view returns (address[] memory) { + return govTokens; + } + + function getGovTokensLength() public view returns (uint) { + return govTokens.length; + } + + /** + * @notice Register a governance token which can swap on sushiswap + * @param _govToken The governance token address + */ + function registerGovToken(address _govToken) external onlyOwner { + require(_govToken != address(0), "Invalid governance token"); + govTokens.push(_govToken); + + emit GovTokenRegistered(_govToken); + } + + /** + * @notice Unregister a govenance token when Idle finance does not support token + * @param _govToken The governance token address + */ + function unregisterGovToken(address _govToken) external onlyOwner { + require(_govToken != address(0), "Invalid governance token"); + for (uint i = 0; i < govTokens.length; i++) { + if (govTokens[i] == _govToken) { + govTokens[i] = govTokens[govTokens.length-1]; + delete govTokens[govTokens.length-1]; + + emit GovTokenUnregistered(_govToken); + } + } + } +} \ No newline at end of file diff --git a/contracts/strategies/idle/StrategyIdleLendingPool.sol b/contracts/strategies/idle/StrategyIdleLendingPool.sol new file mode 100644 index 0000000..db9bc90 --- /dev/null +++ b/contracts/strategies/idle/StrategyIdleLendingPool.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./GovTokenRegistry.sol"; + +import "../interfaces/IStrategy.sol"; +import "../interfaces/idle/IIdleToken.sol"; +import "../interfaces/aave/IStakedAave.sol"; +import "../interfaces/uniswap/IUniswapV2.sol"; + +/** + * Deposits ERC20 token into Idle Lending Pool V4. Holds IdleErc20(e.g. IdleDAI, IdleUSDC). + */ +contract StrategyIdleLendingPool is IStrategy, Ownable { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // Governance token registry + GovTokenRegistry govTokenRegistry; + + // The address of Idle Lending Pool(e.g. IdleDAI, IdleUSDC) + address public iToken; + + // Info of supplying erc20 token to Aave lending pool + // The symbol of the supplying token + string public symbol; + // The address of supplying token (e.g. DAI, USDT) + address public supplyToken; + // Supplying token decimals + uint8 public decimals; + + // The address of Aave StakedAave contract + address public stakedAave; + + address public weth; + address public sushiswap; + + address public controller; + + uint256 constant public FULL_ALLOC = 100000; + + constructor( + address _iToken, + string memory _symbol, + address _supplyToken, + uint8 _decimals, + address _govTokenRegistryAddress, + address _stakedAave, + address _weth, + address _sushiswap, + address _controller + ) { + iToken = _iToken; + symbol = _symbol; + supplyToken = _supplyToken; + decimals = _decimals; + govTokenRegistry = GovTokenRegistry(_govTokenRegistryAddress); + stakedAave = _stakedAave; + weth = _weth; + sushiswap = _sushiswap; + controller = _controller; + } + + /** + * @dev Require that the caller must be an EOA account to avoid flash loans. + */ + modifier onlyEOA() { + require(msg.sender == tx.origin, "Not EOA"); + _; + } + + function getAssetAddress() external view override returns (address) { + return supplyToken; + } + + function harvest() external override onlyEOA { + // Claim governance tokens without redeeming supply token + IIdleToken(iToken).redeemIdleToken(uint256(0)); + + harvestAAVE(); + swapGovTokensToSupplyToken(); + + // Deposit obtained supply token to Idle Lending Pool + uint256 obtainedSupplyTokenAmount = IERC20(supplyToken).balanceOf(address(this)); + IERC20(supplyToken).safeIncreaseAllowance(iToken, obtainedSupplyTokenAmount); + IIdleToken(iToken).mintIdleToken(obtainedSupplyTokenAmount, false, address(0)); + } + + function syncBalance() external view override returns (uint256) { + uint256 iTokenPrice = IIdleToken(iToken).tokenPrice(); + uint256 iTokenBalance = IERC20(iToken).balanceOf(address(this)); + uint256 supplyTokenBalance = iTokenBalance.mul(iTokenPrice).div(10**decimals).div(10**(18 - decimals)); + return supplyTokenBalance; + } + + function aggregateCommit(uint256 _supplyTokenAmount) external override { + require(msg.sender == controller, "Not controller"); + require(_supplyTokenAmount > 0, "Nothing to commit"); + + // Pull supply token from Controller + IERC20(supplyToken).safeTransferFrom(msg.sender, address(this), _supplyTokenAmount); + + // Deposit supply token to Idle Lending Pool + IERC20(supplyToken).safeIncreaseAllowance(iToken, _supplyTokenAmount); + IIdleToken(iToken).mintIdleToken(_supplyTokenAmount, false, address(0)); + + emit Committed(_supplyTokenAmount); + } + + function aggregateUncommit(uint256 _supplyTokenAmount) external override { + require(msg.sender == controller, "Not controller"); + require(_supplyTokenAmount > 0, "Nothing to uncommit"); + + // Redeem supply token amount + interests and claim governance tokens + // When `harvest` function is called, this contract lend obtained governance token to save gas + uint256 iTokenRedeemPrice = getRedeemPrice(); + uint256 iTokenBurnAmount = _supplyTokenAmount.mul(10**(decimals)).div(iTokenRedeemPrice).mul(10**(18 - decimals)); + IIdleToken(iToken).redeemIdleToken(iTokenBurnAmount); + + // Transfer supply token to Controller + uint256 supplyTokenBalance = IERC20(supplyToken).balanceOf(address(this)); + IERC20(supplyToken).safeTransfer(msg.sender, supplyTokenBalance); + + emit UnCommitted(_supplyTokenAmount); + } + + function setController(address _controller) external onlyOwner { + emit ControllerChanged(controller, _controller); + controller = _controller; + } + + // Refer to IdleTokenHelper.sol (https://github.com/emilianobonassi/idle-token-helper/blob/master/IdleTokenHelper.sol) + function getRedeemPrice() public view returns (uint256) { + /* + * As per https://github.com/Idle-Labs/idle-contracts/blob/ad0f18fef670ea6a4030fe600f64ece3d3ac2202/contracts/IdleTokenGovernance.sol#L878-L900 + * + * Price on minting is currentPrice + * Price on redeem must consider the fee + * + * Below the implementation of the following redeemPrice formula + * + * redeemPrice := underlyingAmount/idleTokenAmount + * + * redeemPrice = currentPrice * (1 - scaledFee * ΔP%) + * + * where: + * - scaledFee := fee/FULL_ALLOC + * - ΔP% := 0 when currentPrice < userAvgPrice (no gain) and (currentPrice-userAvgPrice)/currentPrice + * + * n.b: gain := idleTokenAmount * ΔP% * currentPrice + */ + uint256 userAvgPrice = IIdleToken(iToken).userAvgPrices(address(this)); + uint256 currentPrice = IIdleToken(iToken).tokenPrice(); + + // When no deposits userAvgPrice is 0 equiv currentPrice + // and in the case of + uint256 redeemPrice; + if (userAvgPrice == 0 || currentPrice < userAvgPrice) { + redeemPrice = currentPrice; + } else { + uint256 fee = IIdleToken(iToken).fee(); + + redeemPrice = ((currentPrice.mul(FULL_ALLOC)) + .sub( + fee.mul( + currentPrice.sub(userAvgPrice) + ) + )).div(FULL_ALLOC); + } + + return redeemPrice; + } + + function harvestAAVE() private { + // Idle finance transfer stkAAVE to this contract + // Activates the cooldown period if not already activated + uint256 stakedAaveBalance = IERC20(stakedAave).balanceOf(address(this)); + if (stakedAaveBalance > 0 && IStakedAave(stakedAave).stakersCooldowns(address(this)) == 0) { + IStakedAave(stakedAave).cooldown(); + } + + // Claims the AAVE staking rewards + uint256 stakingRewards = IStakedAave(stakedAave).getTotalRewardsBalance(address(this)); + if (stakingRewards > 0) { + IStakedAave(stakedAave).claimRewards(address(this), stakingRewards); + } + + // Redeems staked AAVE if possible + uint256 cooldownStartTimestamp = IStakedAave(stakedAave).stakersCooldowns(address(this)); + if ( + stakedAaveBalance > 0 && + block.timestamp > cooldownStartTimestamp.add(IStakedAave(stakedAave).COOLDOWN_SECONDS()) && + block.timestamp <= + cooldownStartTimestamp.add(IStakedAave(stakedAave).COOLDOWN_SECONDS()).add( + IStakedAave(stakedAave).UNSTAKE_WINDOW() + ) + ) { + IStakedAave(stakedAave).redeem(address(this), stakedAaveBalance); + } + } + + function swapGovTokensToSupplyToken() private { + uint govTokenLength = govTokenRegistry.getGovTokensLength(); + address[] memory govTokens = govTokenRegistry.getGovTokens(); + for(uint32 i = 0; i < govTokenLength; i++) { + uint256 govTokenBalance = IERC20(govTokens[i]).balanceOf(address(this)); + if (govTokenBalance > 0) { + IERC20(govTokens[i]).safeIncreaseAllowance(sushiswap, govTokenBalance); + if (supplyToken != weth) { + address[] memory paths = new address[](3); + paths[0] = govTokens[i]; + paths[1] = weth; + paths[2] = supplyToken; + + IUniswapV2(sushiswap).swapExactTokensForTokens( + govTokenBalance, + uint256(0), + paths, + address(this), + block.timestamp.add(1800) + ); + } else { + address[] memory paths = new address[](2); + paths[0] = govTokens[i]; + paths[1] = weth; + + IUniswapV2(sushiswap).swapExactTokensForTokens( + govTokenBalance, + uint256(0), + paths, + address(this), + block.timestamp.add(1800) + ); + } + } + } + } +} \ No newline at end of file diff --git a/contracts/strategies/interfaces/idle/IIdleToken.sol b/contracts/strategies/interfaces/idle/IIdleToken.sol new file mode 100644 index 0000000..fe2aa2b --- /dev/null +++ b/contracts/strategies/interfaces/idle/IIdleToken.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +interface IIdleToken { + /** + * Get currently used gov tokens + * + * @return : array of govTokens supported + */ + function getGovTokens() external view returns (address[] memory); + + /** + * Map which saves avg idleToken minting price per user + * Used in calculating redeem price + * + * @return price : price in underlying token + */ + function userAvgPrices(address user) external view returns (uint256 price); + + /** + * IdleToken price calculation, in underlying + * + * @return : price in underlying token + */ + function tokenPrice() external view returns (uint256); + + /** + * Current fee on interest gained + * + * @return fee : fee on interest gained + */ + function fee() external view returns (uint256 fee); + + /** + * Used to mint IdleTokens, given an underlying amount (eg. DAI). + * This method triggers a rebalance of the pools if _skipRebalance is set to false + * NOTE: User should 'approve' _amount of tokens before calling mintIdleToken + * NOTE 2: this method can be paused + * This method use GasTokens of this contract (if present) to get a gas discount + * + * @param _amount : amount of underlying token to be lended + * @param _referral : referral address + * @return mintedTokens : amount of IdleTokens minted + */ + function mintIdleToken(uint256 _amount, bool _skipRebalance, address _referral) external returns (uint256 mintedTokens); + + /** + * Here we calc the pool share one can withdraw given the amount of IdleToken they want to burn + * + * @param _amount : amount of IdleTokens to be burned + * @return redeemedTokens : amount of underlying tokens redeemed + */ + function redeemIdleToken(uint256 _amount) external returns (uint256 redeemedTokens); +} \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleDAIBestYield.spec.ts b/test-strategy/idle/StrategyIdleDAIBestYield.spec.ts new file mode 100644 index 0000000..b9f5554 --- /dev/null +++ b/test-strategy/idle/StrategyIdleDAIBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleDAIBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_DAI_BEST_YIELD, + 'DAI', + process.env.DAI as string, + 18, + process.env.IDLE_DAI_BEST_YIELD as string, + process.env.DAI_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleDAIRiskAdjusted.spec.ts b/test-strategy/idle/StrategyIdleDAIRiskAdjusted.spec.ts new file mode 100644 index 0000000..0ec150f --- /dev/null +++ b/test-strategy/idle/StrategyIdleDAIRiskAdjusted.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleDAIRiskAdjusted', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_DAI_RISK_ADJUSTED, + 'DAI', + process.env.DAI as string, + 18, + process.env.IDLE_DAI_RISK_ADJUSTED as string, + process.env.DAI_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleLendingPool.spec.ts b/test-strategy/idle/StrategyIdleLendingPool.spec.ts new file mode 100644 index 0000000..39ac6fb --- /dev/null +++ b/test-strategy/idle/StrategyIdleLendingPool.spec.ts @@ -0,0 +1,228 @@ +import { expect } from 'chai'; +import * as dotenv from 'dotenv'; +import { ethers, network } from 'hardhat'; + +import { getAddress } from '@ethersproject/address'; +import { formatUnits, parseEther, parseUnits } from '@ethersproject/units'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import { ERC20 } from '../../typechain/ERC20.d'; +import { ERC20__factory } from '../../typechain/factories/ERC20__factory'; +import { StrategyIdleLendingPool__factory } from '../../typechain/factories/StrategyIdleLendingPool__factory'; +import { StrategyIdleLendingPool } from '../../typechain/StrategyIdleLendingPool'; +import { GovTokenRegistry__factory } from '../../typechain/factories/GovTokenRegistry__factory'; +import { GovTokenRegistry } from '../../typechain/GovTokenRegistry'; +import { ensureBalanceAndApproval, getDeployerSigner } from '../common'; + +dotenv.config(); + +interface DeployStrategyIdleLendingPoolInfo { + strategy: StrategyIdleLendingPool; + supplyToken: ERC20; + deployerSigner: SignerWithAddress; +} + +async function deployGovTokenRegistry( + deployedAddress: string | undefined, +): Promise { + const deployerSigner = await getDeployerSigner(); + + let govTokenRegistry: GovTokenRegistry; + if(deployedAddress) { + govTokenRegistry = GovTokenRegistry__factory.connect(deployedAddress, deployerSigner); + } else { + const govTokenRegistryFactory = (await ethers.getContractFactory( + 'GovTokenRegistry' + )) as GovTokenRegistry__factory; + govTokenRegistry = await govTokenRegistryFactory + .connect(deployerSigner) + .deploy( + process.env.COMPOUND_COMP as string, + process.env.IDLE_IDLE as string, + process.env.AAVE_AAVE as string + ); + await govTokenRegistry.deployed(); + } + return govTokenRegistry; +} + +async function deployStrategyIdleLendingPool( + deployedAddress: string | undefined, + supplyTokenSymbol: string, + supplyTokenAddress: string, + supplyTokenDecimals: number, + idleTokenAddress: string, + govTokenRegistryAddress: string +): Promise { + const deployerSigner = await getDeployerSigner(); + + let strategy: StrategyIdleLendingPool; + if (deployedAddress) { + strategy = StrategyIdleLendingPool__factory.connect(deployedAddress, deployerSigner); + } else { + const strategyIdleLendingPoolFactory = (await ethers.getContractFactory( + 'StrategyIdleLendingPool' + )) as StrategyIdleLendingPool__factory; + strategy = await strategyIdleLendingPoolFactory + .connect(deployerSigner) + .deploy( + idleTokenAddress, + supplyTokenSymbol, + supplyTokenAddress, + supplyTokenDecimals, + govTokenRegistryAddress, + process.env.AAVE_STAKED_AAVE as string, + process.env.WETH as string, + process.env.SUSHISWAP_ROUTER as string, + deployerSigner.address + ); + await strategy.deployed(); + } + + const supplyToken = ERC20__factory.connect(supplyTokenAddress, deployerSigner) + + return { strategy, supplyToken, deployerSigner }; +} + +export async function testStrategyIdleLendingPool( + context: Mocha.Context, + deployedAddress: string | undefined, + supplyTokenSymbol: string, + supplyTokenAddress: string, + supplyTokenDecimals: number, + idleTokenAddress: string, + supplyTokenFunder: string +): Promise { + context.timeout(300000); + + const govTokenRegistry = await deployGovTokenRegistry(deployedAddress); + const { strategy, supplyToken, deployerSigner } = await deployStrategyIdleLendingPool( + deployedAddress, + supplyTokenSymbol, + supplyTokenAddress, + supplyTokenDecimals, + idleTokenAddress, + govTokenRegistry.address + ); + + expect(getAddress(await strategy.getAssetAddress())).to.equal(getAddress(supplyToken.address)); + + const strategyBalanceBeforeCommit = await strategy.callStatic.syncBalance(); + console.log( + `Strategy ${supplyTokenSymbol} balance before commit:`, + formatUnits(strategyBalanceBeforeCommit, supplyTokenDecimals) + ); + + const displayCommitAmount = '100'; + const commitAmount = parseUnits(displayCommitAmount, supplyTokenDecimals); + await ensureBalanceAndApproval( + supplyToken, + supplyTokenSymbol, + commitAmount, + deployerSigner, + strategy.address, + supplyTokenFunder + ); + const controllerBalanceBeforeCommit = await supplyToken.balanceOf(deployerSigner.address); + console.log( + `Controller ${supplyTokenSymbol} balance before commit:`, + formatUnits(controllerBalanceBeforeCommit, supplyTokenDecimals) + ); + + console.log(`===== Commit ${displayCommitAmount} ${supplyTokenSymbol} =====`); + const commitGas = await strategy.estimateGas.aggregateCommit(commitAmount); + expect(commitGas.lte(1500000)); + const commitTx = await strategy.aggregateCommit(commitAmount, {gasLimit: 1500000}); + await commitTx.wait(); + + const strategyBalanceAfterCommit = await strategy.callStatic.syncBalance(); + const errAmount = parseUnits('0.000001', supplyTokenDecimals); + expect(strategyBalanceAfterCommit.sub(strategyBalanceBeforeCommit).add(errAmount).gte(commitAmount)).to.be.true; + console.log( + `Strategy ${supplyTokenSymbol} balance after commit:`, + formatUnits(strategyBalanceAfterCommit, supplyTokenDecimals) + ); + + const controllerBalanceAfterCommit = await supplyToken.balanceOf(deployerSigner.address); + expect(controllerBalanceBeforeCommit.sub(controllerBalanceAfterCommit).eq(commitAmount)).to.be.true; + console.log( + `Controller ${supplyTokenSymbol} balance after commit:`, + formatUnits(controllerBalanceAfterCommit, supplyTokenDecimals) + ); + + const displayUncommitAmount = '90'; + console.log(`===== Uncommit ${displayUncommitAmount} ${supplyTokenSymbol} =====`); + const uncommitAmount = parseUnits(displayUncommitAmount, supplyTokenDecimals); + const uncommitGas = await strategy.estimateGas.aggregateUncommit(uncommitAmount); + expect(uncommitGas.lte(1500000)); + const uncommitTx = await strategy.aggregateUncommit(uncommitAmount, {gasLimit: 1500000}); + await uncommitTx.wait(); + const strategyBalanceAfterUncommit = await strategy.callStatic.syncBalance(); + expect(strategyBalanceAfterUncommit.add(uncommitAmount).gte(strategyBalanceAfterCommit)).to.be.true; + console.log( + `Strategy ${supplyTokenSymbol} balance after uncommit:`, + formatUnits(strategyBalanceAfterUncommit, supplyTokenDecimals) + ); + + const controllerBalanceAfterUncommit = await supplyToken.balanceOf(deployerSigner.address); + expect(controllerBalanceAfterUncommit.sub(controllerBalanceAfterCommit).add(errAmount).gte(displayUncommitAmount)).to.be.true; + console.log( + `Controller ${supplyTokenSymbol} balance after uncommit:`, + formatUnits(controllerBalanceAfterUncommit, supplyTokenDecimals) + ); + + console.log('===== Optional harvest ====='); + try { + // Send some COMP to the strategy + const comp = ERC20__factory.connect(process.env.COMPOUND_COMP as string, deployerSigner); + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [process.env.COMPOUND_COMP_FUNDER] + }); + await ( + await comp + .connect(await ethers.getSigner(process.env.COMPOUND_COMP_FUNDER as string)) + .transfer(strategy.address, parseEther('0.01')) + ).wait(); + + // Send some IDLE to the strategy + const idle = ERC20__factory.connect(process.env.IDLE_IDLE as string, deployerSigner); + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [process.env.IDLE_IDLE_FUNDER] + }); + await ( + await idle + .connect(await ethers.getSigner(process.env.IDLE_IDLE_FUNDER as string)) + .transfer(strategy.address, parseEther('0.01')) + ).wait(); + + // Send some AAVE to the strategy + const aave = ERC20__factory.connect(process.env.AAVE_AAVE as string, deployerSigner); + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [process.env.AAVE_AAVE_FUNDER] + }); + await ( + await aave + .connect(await ethers.getSigner(process.env.AAVE_AAVE_FUNDER as string)) + .transfer(strategy.address, parseEther('0.01')) + ).wait(); + + console.log('===== Sent COMP, AAVE and IDLE to the strategy, harvesting ====='); + const harvestGas = await strategy.estimateGas.harvest(); + if (harvestGas.lte(1200000)) { + const harvestTx = await strategy.harvest({ gasLimit: 1200000 }); + const receipt = await harvestTx.wait(); + console.log('Harvest gas used:', receipt.gasUsed.toString()); + const strategyBalanceAfterHarvest = await strategy.callStatic.syncBalance(); + expect(strategyBalanceAfterHarvest.gte(strategyBalanceAfterUncommit)).to.be.true; + console.log( + `Strategy ${supplyTokenSymbol} balance after harvest:`, + formatUnits(strategyBalanceAfterHarvest, supplyTokenDecimals) + ); + } + } catch (e) { + console.log('Cannot harvest: ', e); + } +} \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleSUSDBestYield.spec.ts b/test-strategy/idle/StrategyIdleSUSDBestYield.spec.ts new file mode 100644 index 0000000..9217f5a --- /dev/null +++ b/test-strategy/idle/StrategyIdleSUSDBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleSUSDBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_SUSD_BEST_YIELD, + 'SUSD', + process.env.SUSD as string, + 18, + process.env.IDLE_SUSD_BEST_YIELD as string, + process.env.SUSD_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleTUSDBestYield.spec.ts b/test-strategy/idle/StrategyIdleTUSDBestYield.spec.ts new file mode 100644 index 0000000..c552c1d --- /dev/null +++ b/test-strategy/idle/StrategyIdleTUSDBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleTUSDBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_TUSD_BEST_YIELD, + 'TUSD', + process.env.TUSD as string, + 18, + process.env.IDLE_TUSD_BEST_YIELD as string, + process.env.TUSD_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleUSDCBestYield.spec.ts b/test-strategy/idle/StrategyIdleUSDCBestYield.spec.ts new file mode 100644 index 0000000..bf3360a --- /dev/null +++ b/test-strategy/idle/StrategyIdleUSDCBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleUSDCBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_USDC_BEST_YIELD, + 'USDC', + process.env.USDC as string, + 6, + process.env.IDLE_USDC_BEST_YIELD as string, + process.env.USDC_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleUSDCRiskAdjusted.spec.ts b/test-strategy/idle/StrategyIdleUSDCRiskAdjusted.spec.ts new file mode 100644 index 0000000..9170550 --- /dev/null +++ b/test-strategy/idle/StrategyIdleUSDCRiskAdjusted.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleUSDCRiskAdjusted', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_USDC_RISK_ADJUSTED, + 'USDC', + process.env.USDC as string, + 6, + process.env.IDLE_USDC_RISK_ADJUSTED as string, + process.env.USDC_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleUSDTBestYield.spec.ts b/test-strategy/idle/StrategyIdleUSDTBestYield.spec.ts new file mode 100644 index 0000000..c93d734 --- /dev/null +++ b/test-strategy/idle/StrategyIdleUSDTBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleUSDTBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_USDT_BEST_YIELD, + 'USDT', + process.env.USDT as string, + 6, + process.env.IDLE_USDT_BEST_YIELD as string, + process.env.USDT_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleUSDTRiskAdjusted.spec.ts b/test-strategy/idle/StrategyIdleUSDTRiskAdjusted.spec.ts new file mode 100644 index 0000000..aa140d9 --- /dev/null +++ b/test-strategy/idle/StrategyIdleUSDTRiskAdjusted.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleUSDTRiskAdjusted', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_USDT_RISK_ADJUSTED, + 'USDT', + process.env.USDT as string, + 6, + process.env.IDLE_USDT_RISK_ADJUSTED as string, + process.env.USDT_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleWBTCBestYield.spec.ts b/test-strategy/idle/StrategyIdleWBTCBestYield.spec.ts new file mode 100644 index 0000000..562000b --- /dev/null +++ b/test-strategy/idle/StrategyIdleWBTCBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleWBTCBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_WBTC_BEST_YIELD, + 'WBTC', + process.env.WBTC as string, + 8, + process.env.IDLE_WBTC_BEST_YIELD as string, + process.env.WBTC_FUNDER as string + ); + }); +}); \ No newline at end of file diff --git a/test-strategy/idle/StrategyIdleWETHBestYield.spec.ts b/test-strategy/idle/StrategyIdleWETHBestYield.spec.ts new file mode 100644 index 0000000..cd7643f --- /dev/null +++ b/test-strategy/idle/StrategyIdleWETHBestYield.spec.ts @@ -0,0 +1,20 @@ +import * as dotenv from 'dotenv'; + +import { DESCRIPTION } from '../common'; +import { testStrategyIdleLendingPool } from './StrategyIdleLendingPool.spec'; + +dotenv.config(); + +describe('StrategyIdleWETHBestYield', function () { + it(DESCRIPTION, async function () { + await testStrategyIdleLendingPool( + this, + process.env.STRATEGY_IDLE_WETH_BEST_YIELD, + 'WETH', + process.env.WETH as string, + 18, + process.env.IDLE_WETH_BEST_YIELD as string, + process.env.WETH_FUNDER as string + ); + }); +}); \ No newline at end of file