diff --git a/contracts/impl/balancer/StrategyBalancerUniversal.sol b/contracts/impl/balancer/StrategyBalancerUniversal.sol new file mode 100644 index 0000000..9897d23 --- /dev/null +++ b/contracts/impl/balancer/StrategyBalancerUniversal.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: ISC +/** +* By using this software, you understand, acknowledge and accept that Tetu +* and/or the underlying software are provided “as is” and “as available” +* basis and without warranties or representations of any kind either expressed +* or implied. Any use of this open source software released under the ISC +* Internet Systems Consortium license is done at your own risk to the fullest +* extent permissible pursuant to applicable law any and all liability as well +* as all warranties, including any fitness for a particular purpose with respect +* to Tetu and/or the underlying software and the use thereof are disclaimed. +*/ +pragma solidity 0.8.4; + +import "../../strategies/balancer/BalancerUniversalStrategyBase.sol"; + +contract StrategyBalancerUniversal is BalancerUniversalStrategyBase { + + function initialize( + address controller_, + address vault_, + bytes32 poolId_, + address gauge_, + bool isCompound_, + uint _bbRatio, + address depositToken_ + ) external initializer { + initializeStrategy( + controller_, + vault_, + poolId_, + gauge_, + isCompound_, + _bbRatio, + depositToken_ + ); + } + +} diff --git a/contracts/strategies/balancer/BalancerUniversalStrategyBase.sol b/contracts/strategies/balancer/BalancerUniversalStrategyBase.sol new file mode 100644 index 0000000..cf23006 --- /dev/null +++ b/contracts/strategies/balancer/BalancerUniversalStrategyBase.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: ISC +/** +* By using this software, you understand, acknowledge and accept that Tetu +* and/or the underlying software are provided “as is” and “as available” +* basis and without warranties or representations of any kind either expressed +* or implied. Any use of this open source software released under the ISC +* Internet Systems Consortium license is done at your own risk to the fullest +* extent permissible pursuant to applicable law any and all liability as well +* as all warranties, including any fitness for a particular purpose with respect +* to Tetu and/or the underlying software and the use thereof are disclaimed. +*/ + +pragma solidity 0.8.4; + +import "@tetu_io/tetu-contracts/contracts/base/strategies/ProxyStrategyBase.sol"; +import "../../third_party/balancer/IBalancerGauge.sol"; +import "../../third_party/balancer/IBVault.sol"; +import "../../interface/ITetuLiquidator.sol"; + +/// @title Base contract for farming Balancer boosted pools +/// @author belbix +abstract contract BalancerUniversalStrategyBase is ProxyStrategyBase { + using SafeERC20 for IERC20; + + // ******************************************************* + // CONSTANTS + // ******************************************************* + + /// @notice Strategy type for statistical purposes + string public constant override STRATEGY_NAME = "BalancerUniversalStrategyBase"; + /// @notice Version of the contract + /// @dev Should be incremented when contract changed + string public constant VERSION = "1.0.0"; + + uint private constant PRICE_IMPACT_TOLERANCE = 10_000; + IBVault public constant BALANCER_VAULT = IBVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + ITetuLiquidator public constant TETU_LIQUIDATOR = ITetuLiquidator(0xC737eaB847Ae6A92028862fE38b828db41314772); + + + address public constant VAULT_BB_T_USD = 0x4028cba3965e8Aea7320e9eA50914861A14dc724; + /// @dev bb-t-USDC linear pool BPT + address public constant VAULT_BB_T_USD_ENTER_TOKEN = 0xae646817e458C0bE890b81e8d880206710E3c44e; + bytes32 public constant BB_T_USD_POOL_ID = 0xb3d658d5b95bf04e2932370dd1ff976fe18dd66a000000000000000000000ace; + address internal constant DEFAULT_PERF_FEE_RECEIVER = 0x9Cc199D4353b5FB3e6C8EEBC99f5139e0d8eA06b; + + // ******************************************************* + // VARIABLES + // ******************************************************* + + IAsset[] public poolTokens; + uint public lastHw; + IBalancerGauge public gauge; + bool public isCompound; + bytes32 public poolId; + address public depositToken; + + /// @notice Initialize contract after setup it as proxy implementation + function initializeStrategy( + address controller_, + address vault_, + bytes32 poolId_, + address gauge_, + bool isCompound_, + uint _bbRatio, + address depositToken_ + ) public initializer { + poolId = poolId_; + depositToken = depositToken_; + gauge = IBalancerGauge(gauge_); + isCompound = isCompound_; + + (IERC20[] memory tokens,,) = BALANCER_VAULT.getPoolTokens(poolId_); + IAsset[] memory tokenAssets = new IAsset[](tokens.length); + for (uint i = 0; i < tokens.length; i++) { + tokenAssets[i] = IAsset(address(tokens[i])); + } + poolTokens = tokenAssets; + + IERC20(_getPoolAddress(poolId_)).safeApprove(gauge_, type(uint).max); + + + address[] memory rewardTokensTmp = new address[](100); + uint rtsLength; + for (uint i = 0; i < 100; ++i) { + address rt = IBalancerGauge(gauge_).reward_tokens(i); + if (rt == address(0)) { + break; + } + rewardTokensTmp[i] = rt; + rtsLength++; + } + address[] memory rewardTokens_ = new address[](rtsLength); + for (uint i = 0; i < rtsLength; ++i) { + rewardTokens_[i] = rewardTokensTmp[i]; + } + + ProxyStrategyBase.initializeStrategyBase( + controller_, + _getPoolAddress(poolId_), + vault_, + rewardTokens_, + _bbRatio + ); + } + + // ******************************************************* + // GOV ACTIONS + // ******************************************************* + + /// @dev Set new reward tokens + function setRewardTokens(address[] memory rts) external restricted { + delete _rewardTokens; + for (uint i = 0; i < rts.length; i++) { + _rewardTokens.push(rts[i]); + _unsalvageableTokens[rts[i]] = true; + } + } + + // ******************************************************* + // STRATEGY LOGIC + // ******************************************************* + + /// @dev Balance of staked LPs in the gauge + function _rewardPoolBalance() internal override view returns (uint256) { + return gauge.balanceOf(address(this)); + } + + /// @dev Rewards amount ready to claim + function readyToClaim() external view override returns (uint256[] memory toClaim) { + toClaim = new uint256[](_rewardTokens.length); + for (uint i; i < toClaim.length; i++) { + address rt = _rewardTokens[i]; + toClaim[i] = gauge.claimable_reward(address(this), rt); + } + } + + /// @dev Return TVL of the farmable pool + function poolTotalAmount() external view override returns (uint256) { + return IERC20(_underlying()).balanceOf(address(gauge)); + } + + /// @dev Platform name for statistical purposes + /// @return Platform enum index + function platform() external override pure returns (Platform) { + return Platform.BALANCER; + } + + /// @dev assets should reflect underlying tokens need to investing + function assets() external override view returns (address[] memory) { + address[] memory token = new address[](poolTokens.length); + for (uint i = 0; i < poolTokens.length; i++) { + token[i] = address(poolTokens[i]); + } + return token; + } + + /// @dev Deposit LP tokens to gauge + function depositToPool(uint256 amount) internal override { + _doHardWork(true, false); + // doHardWork can deposit all underlyings + amount = IERC20(_underlying()).balanceOf(address(this)); + if (amount != 0) { + gauge.deposit(amount); + } + } + + /// @dev Withdraw LP tokens from gauge + function withdrawAndClaimFromPool(uint256 amount) internal override { + if (amount != 0) { + gauge.withdraw(amount); + } + _doHardWork(true, false); + } + + /// @dev Emergency withdraw all from a gauge + function emergencyWithdrawFromPool() internal override { + gauge.withdraw(gauge.balanceOf(address(this))); + } + + /// @dev Make something useful with rewards + function doHardWork() external onlyNotPausedInvesting override hardWorkers { + _doHardWork(false, true); + } + + function _doHardWork(bool silently, bool push) internal { + uint _lastHw = lastHw; + if (push || _lastHw == 0 || block.timestamp - _lastHw > 12 hours) { + gauge.claim_rewards(); + _liquidateRewards(silently); + lastHw = block.timestamp; + } + } + + /// @dev Deprecated + function liquidateReward() internal override { + // noop + } + + function _liquidateRewards(bool silently) internal { + address _depositToken = depositToken; + bool _isCompound = isCompound; + uint bbRatio = _buyBackRatio(); + address[] memory rts = _rewardTokens; + uint undBalanceBefore = IERC20(_underlying()).balanceOf(address(this)); + for (uint i = 0; i < rts.length; i++) { + address rt = rts[i]; + uint amount = IERC20(rt).balanceOf(address(this)); + if (amount != 0) { + uint toRewards = amount * (_BUY_BACK_DENOMINATOR - bbRatio) / _BUY_BACK_DENOMINATOR; + uint toGov = amount - toRewards; + if (toRewards != 0) { + if (_isCompound) { + _liquidate(rt, _depositToken, toRewards, silently); + } else { + _liquidate(rt, VAULT_BB_T_USD_ENTER_TOKEN, toRewards, silently); + } + + } + + if (toGov != 0) { + IERC20(rt).safeTransfer(DEFAULT_PERF_FEE_RECEIVER, toGov); + } + } + } + + if (_isCompound) { + + uint toPool = IERC20(_depositToken).balanceOf(address(this)); + if (toPool != 0) { + _balancerJoin(poolTokens, poolId, _depositToken, toPool); + } + uint undBalance = IERC20(_underlying()).balanceOf(address(this)) - undBalanceBefore; + if (undBalance != 0) { + gauge.deposit(undBalance); + } + + } else { + + uint toPool = IERC20(VAULT_BB_T_USD_ENTER_TOKEN).balanceOf(address(this)); + if (toPool != 0) { + (IERC20[] memory tokens,,) = BALANCER_VAULT.getPoolTokens(BB_T_USD_POOL_ID); + IAsset[] memory tokenAssets = new IAsset[](tokens.length); + for (uint i = 0; i < tokens.length; i++) { + tokenAssets[i] = IAsset(address(tokens[i])); + } + _balancerJoin(tokenAssets, BB_T_USD_POOL_ID, VAULT_BB_T_USD_ENTER_TOKEN, toPool); + } + uint bbamUSDBalance = IERC20(_getPoolAddress(BB_T_USD_POOL_ID)).balanceOf(address(this)); + if (bbamUSDBalance != 0) { + // deposit to baamVAULT + _approveIfNeeds(_getPoolAddress(BB_T_USD_POOL_ID), bbamUSDBalance, VAULT_BB_T_USD); + ISmartVault(VAULT_BB_T_USD).deposit(bbamUSDBalance); + uint rewardBalance = IERC20(VAULT_BB_T_USD).balanceOf(address(this)); + address __vault = _vault(); + _approveIfNeeds(VAULT_BB_T_USD, rewardBalance, __vault); + ISmartVault(__vault).notifyTargetRewardAmount(VAULT_BB_T_USD, rewardBalance); + } + + } + + IBookkeeper(IController(_controller()).bookkeeper()).registerStrategyEarned(0); + + } + + /// @dev Join to the given pool (exchange tokenIn to underlying BPT) + function _balancerJoin(IAsset[] memory _poolTokens, bytes32 _poolId, address _tokenIn, uint _amountIn) internal { + if (_amountIn != 0) { + if (_isBoostedPool(_poolTokens, _poolId)) { + // just swap for enter + _balancerSwap(_poolId, _tokenIn, _getPoolAddress(_poolId), _amountIn); + } else { + uint[] memory amounts = new uint[](_poolTokens.length); + for (uint i = 0; i < amounts.length; i++) { + amounts[i] = address(_poolTokens[i]) == _tokenIn ? _amountIn : 0; + } + bytes memory userData = abi.encode(1, amounts, 1); + IBVault.JoinPoolRequest memory request = IBVault.JoinPoolRequest({ + assets: _poolTokens, + maxAmountsIn: amounts, + userData: userData, + fromInternalBalance: false + }); + _approveIfNeeds(_tokenIn, _amountIn, address(BALANCER_VAULT)); + BALANCER_VAULT.joinPool(_poolId, address(this), address(this), request); + } + } + } + + function _isBoostedPool(IAsset[] memory _poolTokens, bytes32 _poolId) internal pure returns (bool){ + address poolAdr = _getPoolAddress(_poolId); + for (uint i; i < _poolTokens.length; ++i) { + if (address(_poolTokens[i]) == poolAdr) { + return true; + } + } + return false; + } + + function _liquidate(address tokenIn, address tokenOut, uint amount, bool silently) internal { + address tokenOutRewrite = _rewriteLinearUSDC(tokenOut); + + if (tokenIn != tokenOutRewrite && amount != 0) { + _approveIfNeeds(tokenIn, amount, address(TETU_LIQUIDATOR)); + // don't revert on errors + if (silently) { + try TETU_LIQUIDATOR.liquidate(tokenIn, tokenOutRewrite, amount, PRICE_IMPACT_TOLERANCE) {} catch {} + } else { + TETU_LIQUIDATOR.liquidate(tokenIn, tokenOutRewrite, amount, PRICE_IMPACT_TOLERANCE); + } + } + + // assume need to swap rewritten token manually + if (tokenOut != tokenOutRewrite && amount != 0) { + _swapLinearUSDC(tokenOutRewrite, tokenOut); + } + } + + /// @dev It is a temporally logic until liquidator doesn't have swapper for LinearPool + function _rewriteLinearUSDC(address token) internal pure returns (address){ + if (token == 0xF93579002DBE8046c43FEfE86ec78b1112247BB8 /*bbamUSDC*/) { + // USDC + return 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; + } + if (token == 0xae646817e458C0bE890b81e8d880206710E3c44e /*bb-t-USDC*/) { + // USDC + return 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; + } + return token; + } + + /// @dev It is a temporally logic until liquidator doesn't have swapper for LinearPool + function _swapLinearUSDC(address tokenIn, address tokenOut) internal { + bytes32 linearPoolId; + if (tokenOut == 0xF93579002DBE8046c43FEfE86ec78b1112247BB8 /*bbamUSDC*/) { + linearPoolId = 0xf93579002dbe8046c43fefe86ec78b1112247bb8000000000000000000000759; + } + if (tokenOut == 0xae646817e458C0bE890b81e8d880206710E3c44e /*bb-t-USDC*/) { + linearPoolId = 0xae646817e458c0be890b81e8d880206710e3c44e000000000000000000000acb; + } + _balancerSwap( + linearPoolId, + tokenIn, + tokenOut, + IERC20(tokenIn).balanceOf(address(this)) + ); + } + + /// @dev Swap _tokenIn to _tokenOut using pool identified by _poolId + function _balancerSwap(bytes32 _poolId, address _tokenIn, address _tokenOut, uint _amountIn) internal { + if (_amountIn != 0) { + IBVault.SingleSwap memory singleSwapData = IBVault.SingleSwap({ + poolId: _poolId, + kind: IBVault.SwapKind.GIVEN_IN, + assetIn: IAsset(_tokenIn), + assetOut: IAsset(_tokenOut), + amount: _amountIn, + userData: "" + }); + + IBVault.FundManagement memory fundManagementStruct = IBVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + _approveIfNeeds(_tokenIn, _amountIn, address(BALANCER_VAULT)); + BALANCER_VAULT.swap(singleSwapData, fundManagementStruct, 1, block.timestamp); + } + } + + function _approveIfNeeds(address token, uint amount, address spender) internal { + if (IERC20(token).allowance(address(this), spender) < amount) { + IERC20(token).safeApprove(spender, 0); + IERC20(token).safeApprove(spender, type(uint).max); + } + } + + + /// @dev Returns the address of a Pool's contract. + /// Due to how Pool IDs are created, this is done with no storage accesses and costs little gas. + function _getPoolAddress(bytes32 id) internal pure returns (address) { + // 12 byte logical shift left to remove the nonce and specialization setting. We don't need to mask, + // since the logical shift already sets the upper bits to zero. + return address(uint160(uint(id) >> (12 * 8))); + } + + + //slither-disable-next-line unused-state + uint256[48] private ______gap; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 338b655..75b3dc2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -37,7 +37,7 @@ const argv = require('yargs/yargs')() }, maticForkBlock: { type: "number", - default: 41785211 + default: 42047930 }, }).argv; diff --git a/scripts/addresses/MaticAddresses.ts b/scripts/addresses/MaticAddresses.ts index be998f5..fa60188 100644 --- a/scripts/addresses/MaticAddresses.ts +++ b/scripts/addresses/MaticAddresses.ts @@ -286,11 +286,18 @@ export class MaticAddresses { public static BALANCER_USD_TETU_BOOSTED_ID = "0xb3d658d5b95bf04e2932370dd1ff976fe18dd66a000000000000000000000ace".toLowerCase(); public static BALANCER_USD_TETU_BOOSTED_GAUGE = "0xd95e4DfcF7eb4897918dD3750447aFc5a10F9BC0".toLowerCase(); - public static BALANCER_TNGBL_USDC = "0x9F9F548354B7C66Dc9a9f3373077D86AAACCF8F2".toLowerCase(); public static BALANCER_TNGBL_USDC_ID = "0x9f9f548354b7c66dc9a9f3373077d86aaaccf8f2000200000000000000000a4a".toLowerCase(); public static BALANCER_TNGBL_USDC_GAUGE = "0x07222E30b751c1AB4A730745aFe19810cFd762c0".toLowerCase(); + public static BALANCER_MATIC_BOOSTED_AAVE3 = "0x216690738Aac4aa0C4770253CA26a28f0115c595".toLowerCase(); + public static BALANCER_MATIC_BOOSTED_AAVE3_ID = "0x216690738aac4aa0c4770253ca26a28f0115c595000000000000000000000b2c".toLowerCase(); + public static BALANCER_MATIC_BOOSTED_AAVE3_GAUGE = "0x51BE9bC648714CC07503Ad46F354BC8D1A3B727B".toLowerCase(); + + public static BALANCER_MATICX_BOOSTED_AAVE3 = "0xE78b25c06dB117fdF8F98583CDaaa6c92B79E917".toLowerCase(); + public static BALANCER_MATICX_BOOSTED_AAVE3_ID = "0xe78b25c06db117fdf8f98583cdaaa6c92b79e917000000000000000000000b2b".toLowerCase(); + public static BALANCER_MATICX_BOOSTED_AAVE3_GAUGE = "0xB0B28d7A74e62DF5F6F9E0d9Ae0f4e7982De9585".toLowerCase(); + // KLIMA public static KLIMA_STAKING = '0x25d28a24Ceb6F81015bB0b2007D795ACAc411b4d'.toLowerCase(); diff --git a/scripts/deploy/strategies/balancer/DeployVaultAndBalancerUniversalStrategy.ts b/scripts/deploy/strategies/balancer/DeployVaultAndBalancerUniversalStrategy.ts new file mode 100644 index 0000000..7864264 --- /dev/null +++ b/scripts/deploy/strategies/balancer/DeployVaultAndBalancerUniversalStrategy.ts @@ -0,0 +1,58 @@ +import {ethers} from "hardhat"; +import {DeployerUtilsLocal} from "../../DeployerUtilsLocal"; +import {ISmartVault__factory, StrategyBalancerUniversal__factory,} from "../../../../typechain"; +import {writeFileSync} from "fs"; +import {RunHelper} from "../../../utils/tools/RunHelper"; +import {TokenUtils} from "../../../../test/TokenUtils"; +import {MaticAddresses} from "../../../addresses/MaticAddresses"; + +// tslint:disable-next-line:no-var-requires +const hre = require("hardhat"); + +export async function deployBalancerVaultAndUniversalStrategy( + bpt: string, + poolId: string, + gauge: string, + isCompound: boolean, + depositToken: string, + buyBackRatio: number, +) { + const signer = (await ethers.getSigners())[0]; + const core = await DeployerUtilsLocal.getCoreAddresses(); + const undSymbol = await TokenUtils.tokenSymbol(bpt) + + if (await DeployerUtilsLocal.findVaultUnderlyingInBookkeeper(signer, bpt)) { + console.error("VAULT WITH THIS UNDERLYING EXIST! skip"); + return; + } + + const vaultProxy = await DeployerUtilsLocal.deployContract(signer, "TetuProxyControlled", DeployerUtilsLocal.getVaultLogic(signer).address); + await RunHelper.runAndWait(() => ISmartVault__factory.connect(vaultProxy.address, signer).initializeSmartVault( + "Tetu Vault " + undSymbol, + "x" + undSymbol, + core.controller, + bpt, + 60 * 60 * 24 * 7, + false, + MaticAddresses.TETU_TOKEN, + 0 + )); + + + const strategyLogic = await DeployerUtilsLocal.deployContract(signer, "StrategyBalancerUniversal"); + const strategyProxy = await DeployerUtilsLocal.deployContract(signer, "TetuProxyControlled", strategyLogic.address); + await RunHelper.runAndWait(() => StrategyBalancerUniversal__factory.connect(strategyProxy.address, signer).initialize( + core.controller, + vaultProxy.address, + poolId, + gauge, + isCompound, + buyBackRatio, + depositToken, + )); + + if (hre.network.name !== 'hardhat') { + const txt = `vault: ${vaultProxy.address}\nstrategy: ${strategyProxy.address}`; + writeFileSync(`tmp/deployed/balancer_${undSymbol.replace('/', '-')}.txt`, txt, 'utf8'); + } +} diff --git a/scripts/deploy/strategies/balancer/vaults/MaticX-WMATIC-aave3-boosted.ts b/scripts/deploy/strategies/balancer/vaults/MaticX-WMATIC-aave3-boosted.ts new file mode 100644 index 0000000..f2edb9a --- /dev/null +++ b/scripts/deploy/strategies/balancer/vaults/MaticX-WMATIC-aave3-boosted.ts @@ -0,0 +1,27 @@ +import {MaticAddresses} from "../../../../addresses/MaticAddresses"; +import {deployBalancerVaultAndUniversalStrategy} from "../DeployVaultAndBalancerUniversalStrategy"; + +async function main() { + const underlying = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3; + const poolId = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3_ID; + const gauge = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3_GAUGE; + const isCompound = true; + const depositToken = MaticAddresses.MATIC_X; + const buyBackRatio = 5_00; + + await deployBalancerVaultAndUniversalStrategy( + underlying, + poolId, + gauge, + isCompound, + depositToken, + buyBackRatio, + ); +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deploy/strategies/balancer/vaults/stMATIC-WMATIC-aave3-boosted.ts b/scripts/deploy/strategies/balancer/vaults/stMATIC-WMATIC-aave3-boosted.ts new file mode 100644 index 0000000..699ca6c --- /dev/null +++ b/scripts/deploy/strategies/balancer/vaults/stMATIC-WMATIC-aave3-boosted.ts @@ -0,0 +1,27 @@ +import {MaticAddresses} from "../../../../addresses/MaticAddresses"; +import {deployBalancerVaultAndUniversalStrategy} from "../DeployVaultAndBalancerUniversalStrategy"; + +async function main() { + const underlying = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3; + const poolId = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3_ID; + const gauge = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3_GAUGE; + const isCompound = false; + const depositToken = MaticAddresses.stMATIC_TOKEN; + const buyBackRatio = 8_00; + + await deployBalancerVaultAndUniversalStrategy( + underlying, + poolId, + gauge, + isCompound, + depositToken, + buyBackRatio, + ); +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/utils/tools/voting-utils.ts b/scripts/utils/tools/voting-utils.ts index 99ec44d..21b7114 100644 --- a/scripts/utils/tools/voting-utils.ts +++ b/scripts/utils/tools/voting-utils.ts @@ -34,6 +34,47 @@ export async function getSnapshotData(proposalId: string): Promise { return resp.proposals[0] } +// tslint:disable-next-line:no-any +export async function getSnapshotVoters(proposalId: string, voter: string): Promise { + const resp = await request( + SNAPSHOT_GRAPHQL_ENDPOINT, + gql` + query { + votes ( + first: 1000 + where: { + proposal: "${proposalId}" + voter: "${voter.toLowerCase()}" + } + ) { + id + voter + created + choice + vp + vp_by_strategy + vp_state + ipfs + voter + metadata + reason + app + space { + id + } + + proposal { + title + snapshot + } + + } + } + ` + ) + + return resp.votes[0] +} // tslint:disable-next-line:no-any export async function getAllGaugesFromSubgraph(): Promise { diff --git a/scripts/utils/xtetuBAL-airdrop.ts b/scripts/utils/xtetuBAL-airdrop.ts index 233d5df..40a6f8f 100644 --- a/scripts/utils/xtetuBAL-airdrop.ts +++ b/scripts/utils/xtetuBAL-airdrop.ts @@ -2,34 +2,78 @@ import {DeployerUtilsLocal} from "../deploy/DeployerUtilsLocal"; import {Misc} from "./tools/Misc"; import {ethers} from "hardhat"; import {MaticAddresses} from "../addresses/MaticAddresses"; -import {IERC20__factory, ISmartVault__factory, XtetuBALDistributor__factory} from "../../typechain"; +import { + IERC20__factory, + ISmartVault__factory, + TetuBalVotingPower__factory, + XtetuBALDistributor__factory +} from "../../typechain"; import {formatUnits, parseUnits} from "ethers/lib/utils"; import {Web3Utils} from "./tools/Web3Utils"; import {TransferEvent} from "../../typechain/IERC20"; import {BigNumber} from "ethers"; import {RunHelper} from "./tools/RunHelper"; - -// block of the last snapshot https://snapshot.org/#/tetubal.eth -const BLOCK = 41782584; -// USDC amount received from all bribes - perf fee -const USDC_AMOUNT = 10_000; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; +import {getSnapshotVoters} from "./tools/voting-utils"; +import {expect} from "chai"; + +// After airdrop receiving from all sources you need to liquidate all tokens to USDC +// Then launch this script on hardhat network and make sure that you have enough tokens. +// If you need xtetuBAL for distribute you will keep some USDC for perform buybacks and send xtetuBAL from remaining balance. If not enough - buyback right now, otherwise perform buybacks more wisly. +// After that send USDC and xtetuBAL tokens to EOA who will call this script. +// The POL holder will receive back some USDC - it is fine, we should distribute the whole amount throught distributor for properly calc TVL. +// This received USDC will be used for tetuBAL buybacks. + +// ------------------ CHANGE ME ---------------------------- + +// MAKE SURE YOUR LOCL SNAPSHOT BLOCK IS ACTUAL! +// USDC amount received from all bribes +const USDC_AMOUNT = 4922; +// the last snapshot https://snapshot.org/#/tetubal.eth +const PROPOSAL_ID = '0x5f4cd1fd2edff65587af82b23f54e2bf2c4a79f499ccfb09f89272ff121d8088'; + +// ---------------------------------------------- +const xtetuBALPerfFee = 0.85; +const tetuBALPower = '0x8FFBa974Efa7C262C97b9521449Fd2B3c69bE4E6'.toLowerCase(); +const POL_OWNER = '0x6672a074b98a7585a8549356f97db02f9416849e'.toLowerCase(); +const DISTRIBUTOR = '0x6DdD4dB035FC15F90D74C1E98ABa967D6b3Ce3Dd'; +const X_TETU_BAL_STRATEGY = '0xdade618E95F5E51198c69bA0A9CC3033874Fa643'; async function main() { - let signer + let signer: SignerWithAddress; if (Misc.getChainName() === 'hardhat') { - signer = await DeployerUtilsLocal.impersonate(); + signer = await DeployerUtilsLocal.impersonate(POL_OWNER); + + const distributorGov = XtetuBALDistributor__factory.connect('0x6DdD4dB035FC15F90D74C1E98ABa967D6b3Ce3Dd', await DeployerUtilsLocal.impersonate()); + await distributorGov.changeOperatorStatus(signer.address, true); } else { signer = (await ethers.getSigners())[0]; } + // --------- collect proposal data + + const snapshotData = await getSnapshotVoters(PROPOSAL_ID, POL_OWNER); + console.log('PROPOSAL DATA', snapshotData.proposal.title); + const curDate = Math.floor(new Date().getTime() / 1000); + const sinceProposal = (curDate - +snapshotData.created); + console.log('sinceProposal days', sinceProposal / 60 / 60 / 24); + if (sinceProposal > 7 * 60 * 60 * 24) throw new Error('Wrong proposal'); + const votedPower = snapshotData.vp; + console.log('PROPOSAL votedPower', votedPower); + + const BLOCK = +snapshotData.proposal.snapshot; + console.log('BLOCK', BLOCK); + + // ---------------- + const tools = await DeployerUtilsLocal.getToolsAddressesWrapper(signer); const xtetuBalPrice = await tools.calculator.getPriceWithDefaultOutput(MaticAddresses.xtetuBAL_TOKEN, {blockTag: BLOCK}); const xtetuBalTVL = await ISmartVault__factory.connect(MaticAddresses.xtetuBAL_TOKEN, signer).underlyingBalanceWithInvestment({blockTag: BLOCK}); const xtetuBalTVLUSD = +formatUnits(xtetuBalPrice.mul(xtetuBalTVL), 36); - const distributor = XtetuBALDistributor__factory.connect('0x6DdD4dB035FC15F90D74C1E98ABa967D6b3Ce3Dd', signer); + const distributor = XtetuBALDistributor__factory.connect(DISTRIBUTOR, signer); const usersBalance = await collectUsers(BLOCK); const usersForUSDC: string[] = []; @@ -40,11 +84,23 @@ async function main() { let usdcAmountForDistributing = 0; let xtetuBalAmountForDistributing = 0; + const power = TetuBalVotingPower__factory.connect(tetuBALPower, signer); + const xtetuBALStrategyPower = await power.balanceOf(X_TETU_BAL_STRATEGY, {blockTag: BLOCK}); + console.log('X_TETU_BAL_STRATEGY power', xtetuBALStrategyPower); + + const veTETUCut = +formatUnits(xtetuBALStrategyPower) / +votedPower + console.log('veTETUCut', veTETUCut); + + const usdcFromStrategy = USDC_AMOUNT * veTETUCut; + console.log('Received from votes from strategy: ', usdcFromStrategy) + const usdcForDistribute = usdcFromStrategy * xtetuBALPerfFee; + console.log('Pure USDC to distribute: ', usdcForDistribute); + for (const [user, amount] of usersBalance) { const userRatio = amount / +formatUnits(xtetuBalTVL); const isUseXtetuBal = await distributor.useXtetuBal(user); - const usdcAmountForUser = USDC_AMOUNT * userRatio; + const usdcAmountForUser = usdcForDistribute * userRatio; if (isUseXtetuBal) { usersForXtetuBAL.push(user); @@ -61,17 +117,39 @@ async function main() { } } - console.log('xtetuBalTVLUSD', xtetuBalTVLUSD); - console.log('amountForBuyingTetuBal', amountForBuyingTetuBal); - console.log('usdcAmountForDistributing', usdcAmountForDistributing); - console.log('xtetuBalAmountForDistributing', xtetuBalAmountForDistributing); + console.log('xtetuBal vault TVL at the moment of snapshot:', xtetuBalTVLUSD); + + console.log('>>> USDC to distribute: ', usdcAmountForDistributing); + console.log('>>> xtetuBal to distribute', xtetuBalAmountForDistributing); + + console.log('Need to buy TetuBal', amountForBuyingTetuBal); + const balanceXtetuBal = await IERC20__factory.connect(MaticAddresses.xtetuBAL_TOKEN, signer).balanceOf(signer.address); - const balanceUSDC = await IERC20__factory.connect(MaticAddresses.xtetuBAL_TOKEN, signer).balanceOf(signer.address); + const balanceUSDC = await IERC20__factory.connect(MaticAddresses.USDC_TOKEN, signer).balanceOf(signer.address); console.log('balanceXtetuBal', +formatUnits(balanceXtetuBal)); console.log('balanceUSDC', +formatUnits(balanceUSDC, 6)); + const usdcAllowance = await IERC20__factory.connect(MaticAddresses.USDC_TOKEN, signer).allowance(signer.address, distributor.address); + const xtetuBALAllowance = await IERC20__factory.connect(MaticAddresses.xtetuBAL_TOKEN, signer).allowance(signer.address, distributor.address); + + if (usdcAllowance.lt(parseUnits(usdcAmountForDistributing.toFixed(6), 6))) { + console.log('APPROVE USDC', usdcAmountForDistributing); + await RunHelper.runAndWait(() => + IERC20__factory.connect(MaticAddresses.USDC_TOKEN, signer) + .approve( + distributor.address, + parseUnits((usdcAmountForDistributing + 1).toFixed(6), 6) + ) + ); + } + + if (xtetuBALAllowance.lt(parseUnits(xtetuBalAmountForDistributing.toFixed(18)))) { + console.log('APPROVE xtetuBAL', xtetuBalAmountForDistributing); + await RunHelper.runAndWait(() => IERC20__factory.connect(MaticAddresses.xtetuBAL_TOKEN, signer).approve(distributor.address, parseUnits(xtetuBalAmountForDistributing.toFixed(18)).add(1))); + } + if ( balanceUSDC.gte(parseUnits(usdcAmountForDistributing.toFixed(6), 6)) && balanceXtetuBal.gte(parseUnits((xtetuBalAmountForDistributing).toFixed(18))) @@ -83,6 +161,13 @@ async function main() { usersForXtetuBALAmounts, parseUnits(xtetuBalTVLUSD.toFixed(18)) )); + + + const apr = formatUnits(await distributor.lastAPR(), 6); + console.log('APR', apr); + expect(+apr).is.greaterThan(10); + expect(+apr).is.lessThan(30); + } else { console.error('not enough tokens'); } diff --git a/test/TokenUtils.ts b/test/TokenUtils.ts index 84eb5bd..58268ae 100644 --- a/test/TokenUtils.ts +++ b/test/TokenUtils.ts @@ -88,6 +88,8 @@ export class TokenUtils { [MaticAddresses.BALANCER_USD_TETU_BOOSTED, MaticAddresses.BALANCER_VAULT.toLowerCase()], // [MaticAddresses.BALANCER_stMATIC_WMATIC_TETU_BOOSTED, MaticAddresses.BALANCER_VAULT.toLowerCase()], // [MaticAddresses.BALANCER_TNGBL_USDC, '0xD1758fbABAE91c805BE76D56548A584EF68B81f0'.toLowerCase()], // + [MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3, MaticAddresses.BALANCER_VAULT.toLowerCase()], // + [MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3, MaticAddresses.BALANCER_VAULT.toLowerCase()], // ]); public static async balanceOf(tokenAddress: string, account: string): Promise { diff --git a/test/VaultUtils.ts b/test/VaultUtils.ts index 27bf58c..a3bcf7a 100644 --- a/test/VaultUtils.ts +++ b/test/VaultUtils.ts @@ -30,6 +30,7 @@ export const XTETU_NO_INCREASE = new Set([ 'BalancerBPTSphereWmaticStrategyBase', 'BalancerBPTstMaticTetuBoostedStrategyBase', 'BalancerBPTTngblUsdcStrategyBase', + 'BalancerUniversalStrategyBase', ]) export const VAULT_SHARE_NO_INCREASE = new Set([ 'QiStakingStrategyBase', @@ -41,6 +42,7 @@ export const VAULT_SHARE_NO_INCREASE = new Set([ 'BalancerBPTSphereWmaticStrategyBase', 'BalancerBPTstMaticTetuBoostedStrategyBase', 'BalancerBPTTngblUsdcStrategyBase', + 'BalancerUniversalStrategyBase', ]) export class VaultUtils { diff --git a/test/strategies/matic/balancer/BalancerBPT_MaticX-MATIC-AAVE3_Test.ts b/test/strategies/matic/balancer/BalancerBPT_MaticX-MATIC-AAVE3_Test.ts new file mode 100644 index 0000000..cefd657 --- /dev/null +++ b/test/strategies/matic/balancer/BalancerBPT_MaticX-MATIC-AAVE3_Test.ts @@ -0,0 +1,34 @@ +import {MaticAddresses} from "../../../../scripts/addresses/MaticAddresses"; +import {DeployInfo} from "../../DeployInfo"; +import {StrategyTestUtils} from "../../StrategyTestUtils"; +import {balancerUniversalTest} from "./universal-test"; + +describe('BalancerBPT_MaticX-MATIC_Test', async () => { + const deployInfo: DeployInfo = new DeployInfo(); + before(async function () { + await StrategyTestUtils.deployCoreAndInit(deployInfo, true); + }); + + + // ********************************************** + // ************** CONFIG************************* + // ********************************************** + + const underlying = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3; + const poolId = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3_ID; + const gauge = MaticAddresses.BALANCER_MATICX_BOOSTED_AAVE3_GAUGE; + const isCompound = true; + const depositToken = MaticAddresses.MATIC_X; + const buyBackRatio = 5_00; + + await balancerUniversalTest( + deployInfo, + underlying, + poolId, + gauge, + isCompound, + depositToken, + buyBackRatio, + ) + +}); diff --git a/test/strategies/matic/balancer/BalancerBPT_stMATIC-MATIC-AAVE3_Test.ts b/test/strategies/matic/balancer/BalancerBPT_stMATIC-MATIC-AAVE3_Test.ts new file mode 100644 index 0000000..2555732 --- /dev/null +++ b/test/strategies/matic/balancer/BalancerBPT_stMATIC-MATIC-AAVE3_Test.ts @@ -0,0 +1,34 @@ +import {MaticAddresses} from "../../../../scripts/addresses/MaticAddresses"; +import {DeployInfo} from "../../DeployInfo"; +import {StrategyTestUtils} from "../../StrategyTestUtils"; +import {balancerUniversalTest} from "./universal-test"; + +describe('BalancerBPT_stMATIC-MATIC_Test', async () => { + const deployInfo: DeployInfo = new DeployInfo(); + before(async function () { + await StrategyTestUtils.deployCoreAndInit(deployInfo, true); + }); + + + // ********************************************** + // ************** CONFIG************************* + // ********************************************** + + const underlying = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3; + const poolId = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3_ID; + const gauge = MaticAddresses.BALANCER_MATIC_BOOSTED_AAVE3_GAUGE; + const isCompound = false; + const depositToken = MaticAddresses.stMATIC_TOKEN; + const buyBackRatio = 8_00; + + await balancerUniversalTest( + deployInfo, + underlying, + poolId, + gauge, + isCompound, + depositToken, + buyBackRatio, + ) + +}); diff --git a/test/strategies/matic/balancer/universal-test.ts b/test/strategies/matic/balancer/universal-test.ts new file mode 100644 index 0000000..bbeb168 --- /dev/null +++ b/test/strategies/matic/balancer/universal-test.ts @@ -0,0 +1,124 @@ +import {SpecificStrategyTest} from "../../SpecificStrategyTest"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; +import {CoreContractsWrapper} from "../../../CoreContractsWrapper"; +import {StrategyTestUtils} from "../../StrategyTestUtils"; +import {DeployerUtilsLocal} from "../../../../scripts/deploy/DeployerUtilsLocal"; +import {ISmartVault, IStrategy, StrategyBalancerUniversal__factory} from "../../../../typechain"; +import {ToolsContractsWrapper} from "../../../ToolsContractsWrapper"; +import {BalancerBPTSpecificHardWork} from "./BalancerBPTSpecificHardWork"; +import {universalStrategyTest} from "../../UniversalStrategyTest"; +import {DeployInfo} from "../../DeployInfo"; +import {Misc} from "../../../../scripts/utils/tools/Misc"; + +export async function balancerUniversalTest( + deployInfo: DeployInfo, + underlying: string, + poolId: string, + gauge: string, + isCompound: boolean, + depositToken: string, + buyBackRatio: number, +) { + + // ********************************************** + // ************** CONFIG************************* + // ********************************************** + + const strategyContractName = 'StrategyBalancerUniversal'; + const vaultName = "StrategyBalancerUniversal"; + const VAULT_BB_T_USD = '0x4028cba3965e8Aea7320e9eA50914861A14dc724'.toLowerCase(); + + // const underlying = token; + // add custom liquidation path if necessary + const forwarderConfigurator = null; + // only for strategies where we expect PPFS fluctuations + const ppfsDecreaseAllowed = false; + // only for strategies where we expect PPFS fluctuations + const balanceTolerance = 0; + const finalBalanceTolerance = 0; + const deposit = 100_000; + // at least 3 + const loops = 3; + const loopValue = 300; + const advanceBlocks = false; + const specificTests: SpecificStrategyTest[] = []; + // ********************************************** + const deployer = (signer: SignerWithAddress) => { + const core = deployInfo.core as CoreContractsWrapper; + return StrategyTestUtils.deploy( + signer, + core, + vaultName, + async vaultAddress => { + const strategy = await DeployerUtilsLocal.deployStrategyProxy( + signer, + strategyContractName, + ); + await StrategyBalancerUniversal__factory.connect(strategy.address, signer).initialize( + core.controller.address, + vaultAddress, + poolId, + gauge, + isCompound, + buyBackRatio, + depositToken, + ); + + + if (!isCompound) { + await core.controller.setRewardDistribution([strategy.address], true); + await core.vaultController.addRewardTokens([vaultAddress], VAULT_BB_T_USD); + } + + + return strategy; + }, + underlying, + 0, + false + ); + }; + const hwInitiator = ( + _signer: SignerWithAddress, + _user: SignerWithAddress, + _core: CoreContractsWrapper, + _tools: ToolsContractsWrapper, + _underlying: string, + _vault: ISmartVault, + _strategy: IStrategy, + _balanceTolerance: number + ) => { + const hw = new BalancerBPTSpecificHardWork( + _signer, + _user, + _core, + _tools, + _underlying, + _vault, + _strategy, + _balanceTolerance, + finalBalanceTolerance, + ); + if (!isCompound) { + hw.vaultRt = VAULT_BB_T_USD; + } else { + hw.vaultRt = Misc.ZERO_ADDRESS; + } + return hw; + }; + + await universalStrategyTest( + strategyContractName + vaultName, + deployInfo, + deployer, + hwInitiator, + forwarderConfigurator, + ppfsDecreaseAllowed, + balanceTolerance, + deposit, + loops, + loopValue, + advanceBlocks, + specificTests, + ); +}