diff --git a/.gitignore b/.gitignore index 0ff2835..2c71213 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ reports/ # Node/npm node_modules/ +@openzeppelin +@yearnvaults \ No newline at end of file diff --git a/brownie-config.yml b/brownie-config.yml index 906db86..03b4488 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -5,14 +5,14 @@ networks: autofetch_sources: True dependencies: - - iearn-finance/yearn-vaults@0.3.5 + - iearn-finance/yearn-vaults@0.4.3 - OpenZeppelin/openzeppelin-contracts@3.1.0 compiler: solc: version: 0.6.12 remappings: - - "@yearnvaults=iearn-finance/yearn-vaults@0.3.5" + - "@yearnvaults=iearn-finance/yearn-vaults@0.4.3" - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.1.0" reports: diff --git a/contracts/EthStrategy.sol b/contracts/EthStrategy.sol deleted file mode 100644 index 45df655..0000000 --- a/contracts/EthStrategy.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; - -import {BaseStrategy} from "@yearnvaults/contracts/BaseStrategy.sol"; -import { - SafeERC20, - SafeMath, - IERC20, - Address -} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import "@openzeppelin/contracts/math/Math.sol"; - -interface IFusdt { - function Swapout(uint256 amount, address bindaddr) external; -} - -contract Strategy is BaseStrategy { - using SafeERC20 for IERC20; - using Address for address; - using SafeMath for uint256; - - constructor(address _vault) public BaseStrategy(_vault) {} - - function name() external view override returns (string memory) { - return "StrategyEthUsdt"; - } - - function estimatedTotalAssets() public view override returns (uint256) { - return want.balanceOf(address(this)); - } - - function prepareReturn(uint256 _debtOutstanding) - internal - override - returns ( - uint256 _profit, - uint256 _loss, - uint256 _debtPayment - ) - { - uint256 debt = vault.strategies(address(this)).totalDebt; - uint256 wantBalance = balanceOfWant(); - - // Set profit or loss based on the initial debt - if (debt <= wantBalance) { - _profit = wantBalance - debt; - } else { - _loss = debt - wantBalance; - } - - // Repay debt. Amount will depend if we had profit or loss - if (_debtOutstanding > 0) { - if (_profit >= 0) { - _debtPayment = Math.min( - _debtOutstanding, - wantBalance.sub(_profit) - ); - } else { - _debtPayment = Math.min( - _debtOutstanding, - wantBalance.sub(_loss) - ); - } - } - } - - function adjustPosition(uint256 _debtOutstanding) internal override { - if (emergencyExit) { - return; - } - - uint256 balance = balanceOfWant(); - if (_debtOutstanding >= balance) { - return; - } - - IFusdt(address(want)).Swapout(balance, address(this)); - } - - function liquidatePosition(uint256 _amountNeeded) - internal - override - returns (uint256 _liquidatedAmount, uint256 _loss) - { - uint256 totalAssets = want.balanceOf(address(this)); - if (_amountNeeded > totalAssets) { - _liquidatedAmount = totalAssets; - _loss = _amountNeeded.sub(totalAssets); - } else { - _liquidatedAmount = _amountNeeded; - } - } - - function balanceOfWant() public view returns (uint256) { - return IERC20(want).balanceOf(address(this)); - } - - function prepareMigration(address _newStrategy) internal override {} - - function protectedTokens() - internal - view - override - returns (address[] memory) - {} -} diff --git a/contracts/GenericEthStrategy.sol b/contracts/GenericEthStrategy.sol new file mode 100644 index 0000000..910ecec --- /dev/null +++ b/contracts/GenericEthStrategy.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseStrategy, StrategyParams} from "@yearnvaults/contracts/BaseStrategy.sol"; +import { + SafeERC20, + SafeMath, + IERC20, + Address +} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/Math.sol"; + + +contract Strategy is BaseStrategy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public bridge; + uint256 public pendingProfit; + uint256 public minSend; + uint256 public maxSend; + + bool public fourthreeprotection; + + constructor(address _vault, address _bridge, uint256 _minSend, uint256 _maxSend) public BaseStrategy(_vault) { + + bridge = _bridge; + minSend = _minSend; + maxSend = _maxSend; + } + + function name() external view override returns (string memory) { + return "BridgeStrategy"; + } + + function delegatedAssets() external view override returns (uint256) { + return totalDebt(); + } + + function estimatedTotalAssets() public view override returns (uint256) { + return totalDebt().add(pendingProfit); + } + + function totalDebt() public view returns (uint256) { + return vault.strategies(address(this)).totalDebt; + } + + function _wantBalance() internal view returns (uint256){ + return IERC20(want).balanceOf(address(this)); + } + + function setBridge(address _bridge) external onlyGovernance { + bridge = _bridge; + } + + function setMinSend(uint256 _minSend) external onlyEmergencyAuthorized { + minSend = _minSend; + } + + function setMaxSend(uint256 _maxSend) external onlyEmergencyAuthorized { + maxSend = _maxSend; + } + + function setFourThreeProtection(bool _protection) external onlyEmergencyAuthorized { + fourthreeprotection = _protection; + } + + function setPendingProfit(uint256 _pendingProfit) external onlyEmergencyAuthorized { + pendingProfit = _pendingProfit; + } + + function prepareReturn(uint256 _debtOutstanding) + internal + override + returns ( + uint256 _profit, + uint256 _loss, + uint256 _debtPayment + ) + { + uint256 wantBal = _wantBalance(); + + if(wantBal == 0){ + return (0,0,0); + } + + _debtPayment = Math.min(wantBal, _debtOutstanding); + + wantBal = wantBal.sub(_debtPayment); + + if(pendingProfit > 0 && wantBal > 0){ + _profit = Math.min(pendingProfit, wantBal); + pendingProfit = pendingProfit.sub(_profit); + } + + } + + function adjustPosition(uint256 _debtOutstanding) internal override { + if (emergencyExit) { + return; + } + + uint256 balance = _wantBalance(); + if (_debtOutstanding >= balance) { + return; + } + balance = balance.sub(_debtOutstanding); + + if(balance > minSend){ + want.safeTransfer(bridge, Math.min(balance, maxSend)); + } + + + } + + function liquidateAllPositions() + internal + override + returns (uint256 _amountFreed) + { + + liquidatePosition(type(uint256).max); + _amountFreed = _wantBalance(); + require(_amountFreed >= totalDebt(), "Money in bridge"); + } + + //we dont use this as harvest trigger is overriden + function ethToWant(uint256 _amtInWei) + public + view + override + returns (uint256) + { + return(_amtInWei); + } + + //should never really be called as we keep late in queue + function liquidatePosition(uint256 _amountNeeded) + internal + override + returns (uint256 _liquidatedAmount, uint256 _loss) + { + uint256 totalAssets = _wantBalance(); + _liquidatedAmount = Math.min(totalAssets, _amountNeeded); + + //sub 43 protection + if(fourthreeprotection){ + require(_amountNeeded == _liquidatedAmount, "fourthreeprotection"); + } + } + + //simplified harvest function + function harvestTrigger(uint256 gasCost) public override view returns (bool) { + + StrategyParams memory params = vault.strategies(address(this)); + + // Should not trigger if strategy is not activated + if (params.activation == 0) return false; + + // Check for profits and losses + uint256 wantBal = _wantBalance(); + bool harvest; + if(wantBal > debtThreshold){ + harvest = vault.debtOutstanding() >= debtThreshold; + harvest = pendingProfit >= debtThreshold; + } + + return harvest; + } + + + function prepareMigration(address _newStrategy) internal override {} + + function protectedTokens() + internal + view + override + returns (address[] memory) + {} +} diff --git a/contracts/GenericVaultProxy.sol b/contracts/GenericVaultProxy.sol new file mode 100644 index 0000000..6e54f1f --- /dev/null +++ b/contracts/GenericVaultProxy.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import { + SafeERC20, + IERC20, + Address +} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +interface IVault { + function deposit() external; + + function withdraw() external; + function token() external view returns(address); + function decimals() external view returns(uint256); + function balanceOf(address) external view returns(uint256); + function pricePerShare() external view returns(uint256); + function withdraw(uint256 amount) external; + function withdraw( + uint256 amount, + address account, + uint256 maxLoss + ) external returns (uint256); +} + +interface IAny { + function Swapout(uint256 amount, address bindaddr) external; +} + +contract GenericVaultProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IVault public vault; + address public want; + + address public strategist; + address public keeper; + address public governance; + address public pendingGovernance; + uint256 public maxLoss; + uint256 public dustThreshold; + + constructor(address _keeper, address _gov, address _vault, uint256 _dustThreshold) public { + vault = IVault(_vault); + want = vault.token(); + keeper = _keeper; + governance = _gov; + strategist = msg.sender; + dustThreshold = _dustThreshold; + maxLoss = 1; + IERC20(want).safeApprove(address(vault), type(uint256).max); + } + + modifier onlyGov { + require(msg.sender == governance); + _; + } + + modifier onlyManagers { + require(msg.sender == strategist || + msg.sender == governance); + _; + } + + modifier onlyGuardians { + require( + msg.sender == strategist || + msg.sender == keeper || + msg.sender == governance + ); + _; + } + + // Move yvDAI funds to a new yVault + function migrateToNewDaiYVault(IVault newYVault) external onlyGov { + uint256 balanceOfYVault = vault.balanceOf(address(this)); + if (balanceOfYVault > 0) { + vault.withdraw(balanceOfYVault, address(this), maxLoss); + } + IERC20(want).safeApprove(address(vault), 0); + + vault = newYVault; + IERC20(want).safeApprove(address(vault), type(uint256).max); + vault.deposit(); + } + + function name() external pure returns (string memory) { + return "BridgeVaultProxyV2"; + } + + function _wantBalance() internal view returns (uint256){ + return IERC20(want).balanceOf(address(this)); + } + function _vaultBalance() internal view returns (uint256){ + return vault.balanceOf(address(this)).mul(vault.pricePerShare()).div(10 ** vault.decimals()); + } + + function acceptGovernor() external { + require(msg.sender == pendingGovernance); + governance = pendingGovernance; + pendingGovernance = address(0); + } + + function setPendingGovernance(address _pendingGovernance) external onlyGov { + pendingGovernance = _pendingGovernance; + } + function setKeeper(address _keeper) external onlyManagers { + keeper = _keeper; + } + function setStrategist(address _strategist) external onlyManagers { + strategist = _strategist; + } + function setMaxLoss(uint256 _maxLoss) external onlyManagers { + maxLoss = _maxLoss; + } + + function totalAssets() public view returns (uint256) { + return _vaultBalance().add(_wantBalance()); + } + + //to mimic harvest on normal strats + function harvest() external onlyGuardians { + if (_wantBalance() > 0) { + vault.deposit(); + } + } + + function harvestTrigger(uint256 callCost) public view returns (bool) { + return _wantBalance() > dustThreshold; + } + + function sendAllBack() external onlyManagers { + uint256 balanceOfYVault = vault.balanceOf(address(this)); + if (balanceOfYVault > 0) { + vault.withdraw(balanceOfYVault, address(this), maxLoss); + } + IAny(want).Swapout(_wantBalance(), address(this)); + } + + function sendWantBack(uint256 amount) external onlyManagers { + uint256 wantBal = _wantBalance(); + if(wantBal < amount){ + uint256 toWithdraw = amount.sub(wantBal); + vault.withdraw(toWithdraw.mul(10 ** vault.decimals()).div(vault.pricePerShare()), address(this), maxLoss); + } + + IAny(want).Swapout(Math.min(_wantBalance(), amount), address(this)); + } + + //sweep function in case we get extra tokens sent + function sweep(address token, uint256 amount) external onlyGov { + assert(token != want && token != address(vault)); + IERC20(token).safeTransfer(governance, amount); + } + + function migrate(address newStrategy) external onlyGov { + uint256 balanceWant = _wantBalance(); + + if(balanceWant > 0){ + IERC20(want).safeTransfer(newStrategy, balanceWant); + } + uint256 ytokens = vault.balanceOf(address(this)); + if(ytokens >0){ + IERC20(address(vault)).safeTransfer(newStrategy, ytokens); + } + + } + +} \ No newline at end of file diff --git a/contracts/WethToBscStrategy.sol b/contracts/WethToBscStrategy.sol deleted file mode 100644 index 273a1a9..0000000 --- a/contracts/WethToBscStrategy.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; - -import {BaseStrategy} from "@yearnvaults/contracts/BaseStrategy.sol"; -import { - SafeERC20, - SafeMath, - IERC20, - Address -} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import "@openzeppelin/contracts/math/Math.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -interface IWETH is IERC20 { - function deposit() external payable; - - function withdraw(uint256) external; -} - -contract WethToBscStrategy is BaseStrategy { - using SafeERC20 for IERC20; - using Address for address; - using SafeMath for uint256; - - address public ethDepositToBsc = - address(0x13B432914A996b0A48695dF9B2d701edA45FF264); - - event Transfer(address indexed from, address indexed to, uint256 value); - - constructor(address _vault) public BaseStrategy(_vault) {} - - function setEthDepositAddress(address _ethDepositToBsc) - external - onlyGovernance - { - ethDepositToBsc = _ethDepositToBsc; - } - - function name() external view override returns (string memory) { - return "WethToBscStrategy"; - } - - function estimatedTotalAssets() public view override returns (uint256) { - return balanceOfWant(); - } - - function prepareReturn(uint256 _debtOutstanding) - internal - override - returns ( - uint256 _profit, - uint256 _loss, - uint256 _debtPayment - ) - { - // If we got eth back from the proxy, let's convert to weth - uint256 balanceReturnedFromBSC = address(this).balance; - if (balanceReturnedFromBSC > 0) { - IWETH(address(want)).deposit{value: address(this).balance}(); - } - - uint256 debt = vault.strategies(address(this)).totalDebt; - if (debt < balanceOfWant()) { - _profit = balanceOfWant().sub(debt); - } - - if (_debtOutstanding > 0) { - _debtPayment = Math.min(_debtOutstanding, balanceOfWant()); - } - } - - function adjustPosition(uint256 _debtOutstanding) internal override { - if (emergencyExit) { - return; - } - - uint256 balance = balanceOfWant(); - if (_debtOutstanding >= balance) { - return; - } - - IWETH(address(want)).withdraw(balanceOfWant()); - - uint256 balanceToTransfer = address(this).balance; - payable(ethDepositToBsc).transfer(balanceToTransfer); - emit Transfer(address(this), ethDepositToBsc, balanceToTransfer); - } - - function liquidatePosition(uint256 _amountNeeded) - internal - override - returns (uint256 _liquidatedAmount, uint256 _loss) - { - uint256 totalAssets = want.balanceOf(address(this)); - if (_amountNeeded > totalAssets) { - _liquidatedAmount = totalAssets; - _loss = _amountNeeded.sub(totalAssets); - } else { - _liquidatedAmount = _amountNeeded; - } - } - - function balanceOfWant() public view returns (uint256) { - return IERC20(want).balanceOf(address(this)); - } - - function prepareMigration(address _newStrategy) internal override { - uint256 balanceReturnedFromBSC = address(this).balance; - if (balanceReturnedFromBSC > 0) { - IWETH(address(want)).deposit{value: address(this).balance}(); - } - } - - function protectedTokens() - internal - view - override - returns (address[] memory) - {} - - receive() external payable {} -} diff --git a/tests/test_eth_strat.py b/tests/test_eth_strat.py new file mode 100644 index 0000000..2402d84 --- /dev/null +++ b/tests/test_eth_strat.py @@ -0,0 +1,59 @@ +import pytest +import brownie +from brownie import Contract, Wei, accounts, chain + +def test_deploy( + chain, + whale, + interface, + Strategy, + Contract, + accounts, +): + deployer = accounts.at('0xBb4eDcFeC106B378e4b4ec478a985017Bd423523', force=True) + strategist = accounts.at('0xFeC07aca9d4311FE6F114Dbd25BBb8E6f8894AEA', force=True) + + registry = Contract('0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804') + dai = interface.ERC20('0x6B175474E89094C44Da98b954EedeAC495271d0F') + + expected_address = '0xAd02E4C635DA744CC1754d14170dC157df6232aF' + strat_ms = accounts.at('0x16388463d60FFE0661Cf7F1f31a7D658aC790ff7', force=True) + yearn_dev_ms = '0x846e211e8ba920B353FB717631C015cf04061Cc9' + + tx = registry.newExperimentalVault(dai, strat_ms, yearn_dev_ms, strat_ms, "", "", {'from': strategist}) + vault = Contract(tx.return_value) + + bridge = '0xC564EE9f21Ed8A2d8E7e76c085740d5e4c5FaFbE' + min_send = 1_000 * 1e18 + max_send = 950_000 * 1e18 + + strategy = Strategy.deploy(vault, bridge, min_send, max_send, {'from': deployer}) + + assert strategy == expected_address + print(vault.apiVersion()) + + print(strategy.estimatedTotalAssets()/1e18) + + vault.addStrategy(strategy, 10000, 0, 2**256-1, 1000, {'from': strat_ms}) + vault.setDepositLimit(100_000_000 *1e18, {'from': strat_ms}) + + dai.approve(vault, 2 ** 256 - 1, {"from": whale}) + before_balance = dai.balanceOf(bridge) + + deposit = 10_000 *1e18 + + vault.deposit(deposit, {"from": whale}) + + strategy.harvest({'from': deployer}) + + print((dai.balanceOf(bridge) - before_balance)/1e18 ) + + + dai.transfer(strategy, 1000 *1e18, {"from": whale}) + strategy.setPendingProfit(100 *1e18, {'from': strat_ms}) + strategy.harvest({'from': deployer}) + assert dai.balanceOf(vault) == 100 *1e18 + assert dai.balanceOf(strategy) == 900 *1e18 + + +