From fe6844dfe4e03b3898d089ec601c09500f86c442 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 6 Jun 2022 18:33:32 +0200 Subject: [PATCH 01/45] feat: initial version of central veAngle voter --- src/AngleStrategyVoterProxy.sol | 166 +++++++++++++++++++++++++++ src/Strategy.sol | 63 ++++++---- src/YearnAngleVoter.sol | 66 +++++++++++ src/interfaces/Angle/IVoteEscrow.sol | 11 ++ src/test/StrategyClone.t.sol | 15 ++- src/test/StrategyMigration.t.sol | 2 +- src/test/utils/StrategyFixture.sol | 42 ++++++- 7 files changed, 332 insertions(+), 33 deletions(-) create mode 100644 src/AngleStrategyVoterProxy.sol create mode 100644 src/YearnAngleVoter.sol create mode 100644 src/interfaces/Angle/IVoteEscrow.sol diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol new file mode 100644 index 0000000..40427ed --- /dev/null +++ b/src/AngleStrategyVoterProxy.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.12; +pragma experimental ABIEncoderV2; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {YearnAngleVoter} from "./YearnAngleVoter.sol"; + +import "./interfaces/curve/ICurve.sol"; +import "./interfaces/Angle/IStableMaster.sol"; +import "./interfaces/Angle/IAngleGauge.sol"; +import "./interfaces/Uniswap/IUniV2.sol"; + +library SafeVoter { + function safeExecute( + YearnAngleVoter voter, + address to, + uint256 value, + bytes memory data + ) internal { + (bool success, ) = voter.execute(to, value, data); + require(success); + } +} + +contract AngleStrategyVoterProxy { + using SafeVoter for YearnAngleVoter; + using SafeERC20 for IERC20; + using Address for address; + + YearnAngleVoter public yearnAngleVoter; + address public constant angle = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + + // gauge => strategies + mapping(address => address) public strategies; + mapping(address => bool) public voters; + address public governance; + + uint256 lastTimeCursor; + + constructor(address _voter) public { + governance = msg.sender; + yearnAngleVoter = YearnAngleVoter(_voter); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function approveStrategy(address _gauge, address _strategy) external { + require(msg.sender == governance, "!governance"); + strategies[_gauge] = _strategy; + } + + function revokeStrategy(address _gauge) external { + require(msg.sender == governance, "!governance"); + strategies[_gauge] = address(0); + } + + function approveVoter(address _voter) external { + require(msg.sender == governance, "!governance"); + voters[_voter] = true; + } + + function revokeVoter(address _voter) external { + require(msg.sender == governance, "!governance"); + voters[_voter] = false; + } + + function lock() external { + uint256 amount = IERC20(angle).balanceOf(address(yearnAngleVoter)); + if (amount > 0) yearnAngleVoter.increaseAmount(amount); + } + + function vote(address _gauge, uint256 _amount) public { + require(voters[msg.sender], "!voter"); + yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("vote_for_gauge_weights(address,uint256)", _gauge, _amount)); + } + + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) public returns (uint256) { + require(strategies[_gauge] == msg.sender, "!strategy"); + uint256 _balance = IERC20(_token).balanceOf(address(yearnAngleVoter)); + yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("withdraw(uint256)", _amount)); + _balance = IERC20(_token).balanceOf(address(yearnAngleVoter)) - _balance; + yearnAngleVoter.safeExecute(_token, 0, abi.encodeWithSignature("transfer(address,uint256)", msg.sender, _balance)); + return _balance; + } + + function withdrawFromStableMaster(address stableMaster, uint256 amount, + address poolManager, address token) external { + require(strategies[stableMaster] == msg.sender, "!strategy"); + + IERC20(token).safeTransfer(address(yearnAngleVoter), amount); + + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, 0)); + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, amount)); + + yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( + "withdraw(uint256,address,address,address)", + amount, + address(yearnAngleVoter), + msg.sender, + poolManager + )); + } + + function balanceOf(address _gauge) public view returns (uint256) { + return IERC20(_gauge).balanceOf(address(yearnAngleVoter)); + } + + function withdrawAll(address _gauge, address _token) external returns (uint256) { + require(strategies[_gauge] == msg.sender, "!strategy"); + return withdraw(_gauge, _token, balanceOf(_gauge)); + } + + function deposit(address gauge, uint256 amount, address token) external { + require(strategies[gauge] == msg.sender, "!strategy"); + + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", gauge, 0)); + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", gauge, amount)); + + yearnAngleVoter.safeExecute(gauge, 0, abi.encodeWithSignature( + "deposit(uint256)", + amount + )); + } + + function depositToStableMaster(address stableMaster, uint256 amount, + address poolManager, address token) external { + require(strategies[stableMaster] == msg.sender, "!strategy"); + + IERC20(token).safeTransfer(address(yearnAngleVoter), amount); + + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, 0)); + yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, amount)); + yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( + "deposit(uint256,address,address)", + amount, + address(yearnAngleVoter), + poolManager + )); + } + + function claimRewards(address _gauge, address _token) external { + require(strategies[_gauge] == msg.sender, "!strategy"); + yearnAngleVoter.safeExecute( + _gauge, + 0, + abi.encodeWithSelector( + IAngleGauge.claim_rewards.selector + ) + ); + yearnAngleVoter.safeExecute(_token, 0, abi.encodeWithSignature("transfer(address,uint256)", msg.sender, IERC20(_token).balanceOf(address(yearnAngleVoter)))); + } + + function balanceOfSanToken(address sanToken) public view returns (uint256) { + return IERC20(sanToken).balanceOf(address(yearnAngleVoter)); + } +} \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index b559b35..233d978 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -18,6 +18,8 @@ import "./interfaces/Angle/IStableMaster.sol"; import "./interfaces/Angle/IAngleGauge.sol"; import "./interfaces/Yearn/ITradeFactory.sol"; import "./interfaces/Uniswap/IUniV2.sol"; +import {AngleStrategyVoterProxy} from "./AngleStrategyVoterProxy.sol"; + interface IBaseFee { function isCurrentBaseFeeAcceptable() external view returns (bool); @@ -33,6 +35,8 @@ contract Strategy is BaseStrategy { IERC20 public constant angleToken = IERC20(0x31429d1856aD1377A8A0079410B297e1a9e214c2); IStableMaster public constant angleStableMaster = IStableMaster(0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87); + AngleStrategyVoterProxy public strategyProxy; + uint256 public constant MAX_BPS = 10000; // variable for determining how much governance token to hold for voting rights @@ -56,13 +60,15 @@ contract Strategy is BaseStrategy { address _vault, address _sanToken, address _sanTokenGauge, - address _poolManager + address _poolManager, + address _strategyProxy ) public BaseStrategy(_vault) { // Constructor should initialize local variables _initializeStrategy( _sanToken, _sanTokenGauge, - _poolManager + _poolManager, + _strategyProxy ); } @@ -71,11 +77,13 @@ contract Strategy is BaseStrategy { function _initializeStrategy( address _sanToken, address _sanTokenGauge, - address _poolManager + address _poolManager, + address _strategyProxy ) internal { sanToken = IERC20(_sanToken); sanTokenGauge = IAngleGauge(_sanTokenGauge); poolManager = _poolManager; + strategyProxy = AngleStrategyVoterProxy(_strategyProxy); percentKeep = 1000; healthCheck = 0xDDCea799fF1699e98EDF118e0629A974Df7DF012; @@ -97,13 +105,15 @@ contract Strategy is BaseStrategy { address _keeper, address _sanToken, address _sanTokenGauge, - address _poolManager + address _poolManager, + address _strategyProxy ) external { _initialize(_vault, _strategist, _rewards, _keeper); _initializeStrategy( _sanToken, _sanTokenGauge, - _poolManager + _poolManager, + _strategyProxy ); } @@ -114,7 +124,8 @@ contract Strategy is BaseStrategy { address _keeper, address _sanToken, address _sanTokenGauge, - address _poolManager + address _poolManager, + address _strategyProxy ) external returns (address newStrategy) { require(isOriginal, "!clone"); bytes20 addressBytes = bytes20(address(this)); @@ -141,7 +152,8 @@ contract Strategy is BaseStrategy { _keeper, _sanToken, _sanTokenGauge, - _poolManager + _poolManager, + _strategyProxy ); emit Cloned(newStrategy); @@ -208,14 +220,14 @@ contract Strategy is BaseStrategy { } // Claim rewards here so that we can chain tend() -> yswap sell -> harvest() in a single transaction - sanTokenGauge.claim_rewards(); + strategyProxy.claimRewards(address(sanTokenGauge), address(angleToken)); uint256 _tokensAvailable = balanceOfAngleToken(); if (_tokensAvailable > 0) { - uint256 _tokensToGov = + uint256 _tokensToLock = (_tokensAvailable * percentKeep) / MAX_BPS; - if (_tokensToGov > 0) { - angleToken.transfer(treasury, _tokensToGov); + if (_tokensToLock > 0) { + IERC20(angleToken).transfer(address(strategyProxy), _tokensToLock); } } @@ -230,13 +242,14 @@ contract Strategy is BaseStrategy { uint256 _wantAvailable = _balanceOfWant - _debtOutstanding; if (_wantAvailable > 0) { // deposit for sanToken + want.safeTransfer(address(strategyProxy), _wantAvailable); depositToStableMaster(_wantAvailable); } // Stake any san tokens, whether they originated through the above deposit or some other means (e.g. migration) uint256 _sanTokenBalance = balanceOfSanToken(); if (_sanTokenBalance > 0) { - sanTokenGauge.deposit(_sanTokenBalance); + strategyProxy.deposit(address(sanTokenGauge), _sanTokenBalance, address(sanToken)); } } @@ -278,11 +291,14 @@ contract Strategy is BaseStrategy { uint256 _sanTokenBalance = balanceOfSanToken(); if (_amountInSanToken > _sanTokenBalance) { - sanTokenGauge.withdraw( + strategyProxy.withdraw( + address(sanTokenGauge), + address(sanToken), Math.min(_amountInSanToken - _sanTokenBalance, balanceOfStakedSanToken()) ); } + IERC20(sanToken).safeTransfer(address(strategyProxy), _amountInSanToken); // Q: what does this do? Didn't want to remove it withdrawFromStableMaster(Math.min(_amountInSanToken, balanceOfSanToken())); } @@ -296,7 +312,7 @@ contract Strategy is BaseStrategy { // want is transferred by the base contract's migrate function sanTokenGauge.withdraw(balanceOfStakedSanToken()); - IERC20(sanToken).safeTransfer(_newStrategy, balanceOfSanToken()); + IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this))); IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken()); } @@ -432,11 +448,11 @@ contract Strategy is BaseStrategy { } function balanceOfStakedSanToken() public view returns (uint256) { - return IERC20(address(sanTokenGauge)).balanceOf(address(this)); + strategyProxy.balanceOf(address(sanTokenGauge)); } function balanceOfSanToken() public view returns (uint256) { - return sanToken.balanceOf(address(this)); + return strategyProxy.balanceOfSanToken(address(sanToken)); } function balanceOfAngleToken() public view returns (uint256) { @@ -472,19 +488,20 @@ contract Strategy is BaseStrategy { } function depositToStableMaster(uint256 _amount) internal { - IStableMaster(angleStableMaster).deposit( + strategyProxy.depositToStableMaster( + address(angleStableMaster), _amount, - address(this), - poolManager + poolManager, + address(want) ); } function withdrawFromStableMaster(uint256 _amountInSanToken) internal { - IStableMaster(angleStableMaster).withdraw( + strategyProxy.withdrawFromStableMaster( + address(angleStableMaster), _amountInSanToken, - address(this), - address(this), - poolManager + poolManager, + address(sanToken) ); } diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol new file mode 100644 index 0000000..0723a37 --- /dev/null +++ b/src/YearnAngleVoter.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.12; +pragma experimental ABIEncoderV2; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {IVoteEscrow} from "./interfaces/Angle/IVoteEscrow.sol"; + +contract YearnAngleVoter { + using SafeERC20 for IERC20; + using Address for address; + + address constant public angle = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + + address constant public veAngle = address(0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5); + + address public governance; + address public strategy; + + constructor() public { + governance = msg.sender; + } + + function getName() external pure returns (string memory) { + return "YearnAngleVoter"; + } + + function setStrategy(address _strategy) external { + require(msg.sender == governance, "!governance"); + strategy = _strategy; + } + + function createLock(uint256 _value, uint256 _unlockTime) external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + IERC20(angle).safeApprove(veAngle, 0); + IERC20(angle).safeApprove(veAngle, _value); + IVoteEscrow(veAngle).create_lock(_value, _unlockTime); + } + + function increaseAmount(uint _value) external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + IERC20(angle).safeApprove(veAngle, 0); + IERC20(angle).safeApprove(veAngle, _value); + IVoteEscrow(veAngle).increase_amount(_value); + } + + function release() external { + require(msg.sender == strategy || msg.sender == governance, "!authorized"); + IVoteEscrow(veAngle).withdraw(); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function execute(address to, uint value, bytes calldata data) external returns (bool, bytes memory) { + require(msg.sender == strategy || msg.sender == governance, "!governance"); + (bool success, bytes memory result) = to.call{value: value}(data); + + return (success, result); + } +} \ No newline at end of file diff --git a/src/interfaces/Angle/IVoteEscrow.sol b/src/interfaces/Angle/IVoteEscrow.sol new file mode 100644 index 0000000..f1b4b60 --- /dev/null +++ b/src/interfaces/Angle/IVoteEscrow.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.12; +pragma experimental ABIEncoderV2; + +interface IVoteEscrow { + function create_lock(uint256, uint256) external; + + function increase_amount(uint256) external; + + function withdraw() external; +} \ No newline at end of file diff --git a/src/test/StrategyClone.t.sol b/src/test/StrategyClone.t.sol index c4b640e..8cc12ad 100644 --- a/src/test/StrategyClone.t.sol +++ b/src/test/StrategyClone.t.sol @@ -44,7 +44,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); vm.prank(gov); @@ -103,7 +104,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); vm.prank(gov); @@ -119,7 +121,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); } } @@ -151,7 +154,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); vm.prank(gov); @@ -167,7 +171,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); } } diff --git a/src/test/StrategyMigration.t.sol b/src/test/StrategyMigration.t.sol index 980d8c6..36f46a9 100644 --- a/src/test/StrategyMigration.t.sol +++ b/src/test/StrategyMigration.t.sol @@ -43,7 +43,7 @@ contract StrategyMigrationTest is StrategyFixture { // Migrate to a new strategy vm.prank(strategist); - Strategy newStrategy = Strategy(deployStrategy(address(vault), IERC20Metadata(address(want)).symbol())); + Strategy newStrategy = Strategy(deployStrategy(address(vault), address(voterProxy), IERC20Metadata(address(want)).symbol())); vm.prank(gov); strategy.claimRewards(); // manual claim rewards vm.prank(gov); diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index 393c68f..9983ea4 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -10,8 +10,9 @@ import {IVault} from "../../interfaces/Yearn/Vault.sol"; import "../../interfaces/Angle/IStableMaster.sol"; import "../../interfaces/Yearn/ITradeFactory.sol"; -// NOTE: if the name of the strat or file changes this needs to be updated import {Strategy} from "../../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../../AngleStrategyVoterProxy.sol"; +import {YearnAngleVoter} from "../../YearnAngleVoter.sol"; // Artifact paths for deploying from the deps folder, assumes that the command is run from // the project root. @@ -28,6 +29,8 @@ contract StrategyFixture is ExtendedTest { } IERC20 public weth; + AngleStrategyVoterProxy public voterProxy; + YearnAngleVoter public voter; AssetFixture[] public assetFixtures; @@ -71,6 +74,13 @@ contract StrategyFixture is ExtendedTest { weth = IERC20(tokenAddrs["WETH"]); + address _voter = deployAngleVoter(); + address _voterProxy = deployStrategyVoterProxy(_voter); + voter = YearnAngleVoter(_voter); + voterProxy = AngleStrategyVoterProxy(_voterProxy); + vm.prank(gov); + voter.setStrategy(_voterProxy); + string[2] memory _tokensToTest = ["USDC", "DAI"]; for (uint8 i = 0; i < _tokensToTest.length; ++i) { @@ -87,7 +97,8 @@ contract StrategyFixture is ExtendedTest { guardian, management, keeper, - strategist + strategist, + address(voterProxy) ); assetFixtures.push(AssetFixture(IVault(_vault), Strategy(_strategy), _want)); @@ -139,16 +150,32 @@ contract StrategyFixture is ExtendedTest { return address(_vault); } + function deployStrategyVoterProxy(address _voter) public returns (address) { + AngleStrategyVoterProxy _voterProxy = new AngleStrategyVoterProxy(_voter); + _voterProxy.setGovernance(gov); + + return address(_voterProxy); + } + + function deployAngleVoter() public returns (address) { + YearnAngleVoter _voter = new YearnAngleVoter(); + _voter.setGovernance(gov); + + return address(_voter); + } + // Deploys a strategy function deployStrategy( address _vault, + address _voterProxy, string memory _tokenSymbol ) public returns (address) { Strategy _strategy = new Strategy( _vault, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + _voterProxy ); vm.startPrank(yMech); @@ -161,6 +188,11 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); _strategy.setTradeFactory(address(tradeFactory)); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); + vm.prank(gov); + voterProxy.approveStrategy(address(stableMaster), address(_strategy)); + return address(_strategy); } @@ -175,7 +207,8 @@ contract StrategyFixture is ExtendedTest { address _guardian, address _management, address _keeper, - address _strategist + address _strategist, + address _voterProxy ) public returns (address _vaultAddr, address _strategyAddr) { _vaultAddr = deployVault( _token, @@ -191,6 +224,7 @@ contract StrategyFixture is ExtendedTest { vm.prank(_strategist); _strategyAddr = deployStrategy( _vaultAddr, + _voterProxy, _tokenSymbol ); Strategy _strategy = Strategy(_strategyAddr); From 7a07c62986b4cc37c314984393bf25e82c69d0dc Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 6 Jun 2022 18:35:44 +0200 Subject: [PATCH 02/45] fix: changes necessary in tests to make them pass with central veAngle --- tests/conftest.py | 349 +++++++++++++++++++++++++++++++++++++++ tests/test_clone.py | 221 +++++++++++++++++++++++++ tests/test_edge_case.py | 67 ++++++++ tests/test_migration.py | 74 +++++++++ tests/test_operation.py | 262 +++++++++++++++++++++++++++++ tests/test_sequential.py | 75 +++++++++ tests/test_shutdown.py | 63 +++++++ 7 files changed, 1111 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/test_clone.py create mode 100644 tests/test_edge_case.py create mode 100644 tests/test_migration.py create mode 100644 tests/test_operation.py create mode 100644 tests/test_sequential.py create mode 100644 tests/test_shutdown.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d0d5329 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,349 @@ +import pytest +from brownie import config, Contract, AngleStrategyProxy, YearnAngleVoter + +# # Snapshots the chain before each test and reverts after test completion. +@pytest.fixture(autouse=True) +def shared_setup(fn_isolation): + pass + + +@pytest.fixture(scope="module", autouse=True) +def gov(accounts): + yield accounts[0] + + +@pytest.fixture(scope="module", autouse=True) +def rewards(accounts): + yield accounts[1] + + +@pytest.fixture(scope="module", autouse=True) +def guardian(accounts): + yield accounts[2] + + +@pytest.fixture(scope="module", autouse=True) +def management(accounts): + yield accounts[3] + + +@pytest.fixture(scope="module", autouse=True) +def strategist(accounts): + yield accounts[4] + + +@pytest.fixture(scope="module", autouse=True) +def keeper(accounts): + yield accounts[5] + + +@pytest.fixture(scope="module", autouse=True) +def alice(accounts): + yield accounts[6] + + +@pytest.fixture(scope="module", autouse=True) +def bob(accounts): + yield accounts[7] + + +@pytest.fixture(scope="module", autouse=True) +def tinytim(accounts): + yield accounts[8] + + +@pytest.fixture(scope="module", autouse=True) +def token_whale(accounts): + whale_address = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" # Curve pool + yield accounts.at(whale_address, force=True) + + +@pytest.fixture(scope="module", autouse=True) +def token(): + token_address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + yield Contract(token_address) + + +@pytest.fixture(scope="module", autouse=True) +def alice_amount(alice, token, token_whale): + amount = 4_000_000_000 + token.transfer(alice, amount, {"from": token_whale}) + yield amount + + +@pytest.fixture(scope="module", autouse=True) +def bob_amount(bob, token, token_whale): + amount = 1_000_000_000 + token.transfer(bob, amount, {"from": token_whale}) + yield amount + + +@pytest.fixture(scope="module", autouse=True) +def tinytim_amount(tinytim, token, token_whale): + amount = 10_000_000 + token.transfer(tinytim, amount, {"from": token_whale}) + yield amount + + +@pytest.fixture(scope="module", autouse=True) +def vault(pm, gov, rewards, guardian, management, token): + Vault = pm(config["dependencies"][0]).Vault + vault = guardian.deploy(Vault) + vault.initialize(token, gov, rewards, "", "", guardian, {"from": gov}) + vault.setDepositLimit(2 ** 256 - 1, {"from": gov}) + vault.setManagement(management, {"from": gov}) + yield vault + +@pytest.fixture(scope ="module") +def deploy_strategy_proxy( + deploy_angle_voter, + gov, + AngleStrategyProxy +): + strategy_proxy = gov.deploy(AngleStrategyProxy, deploy_angle_voter.address) + yield strategy_proxy + + +@pytest.fixture(scope ="module") +def deploy_angle_voter( + gov, + YearnAngleVoter + ): + voter = gov.deploy(YearnAngleVoter) + yield voter + + +@pytest.fixture(scope="module", autouse=True) +def strategy( + strategist, + guardian, + keeper, + vault, + StrategyAngleUSDC, + gov, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy, + deploy_angle_voter +): + strategy = strategist.deploy( + StrategyAngleUSDC, + vault, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address + ) + strategy.setKeeper(keeper, {"from": gov}) + vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) + deploy_strategy_proxy.approveStrategy(san_token_gauge, strategy.address, {"from": gov}) + deploy_strategy_proxy.approveStrategy(angle_stable_master, strategy.address, {"from": gov}) + deploy_angle_voter.setStrategy(deploy_strategy_proxy.address, {"from": gov}) + yield strategy + + +# sushiswap router +@pytest.fixture(scope="module", autouse=True) +def uni(): + yield Contract("0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F") + + +# USDC san_token +@pytest.fixture(scope="module", autouse=True) +def san_token(): + yield Contract.from_explorer("0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad") + + +# USDC san_token whale +@pytest.fixture(scope="module", autouse=True) +def san_token_whale(accounts): + address = "0x51fE22abAF4a26631b2913E417c0560D547797a7" # USDC san_token guage + yield accounts.at(address, force=True) + + +@pytest.fixture(scope="module", autouse=True) +def angle_token(): + yield Contract("0x31429d1856aD1377A8A0079410B297e1a9e214c2") + + +@pytest.fixture(scope="module", autouse=True) +def angle_token_whale(accounts): + yield accounts.at("0xe02F8E39b8cFA7d3b62307E46077669010883459", force=True) + + +@pytest.fixture(scope="module", autouse=True) +def veangle_token(): + yield Contract("0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5") + + +# stable manager front +@pytest.fixture(scope="module", autouse=True) +def angle_stable_master(): + yield Contract("0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87") + + +# usdc stake +@pytest.fixture(scope="module", autouse=True) +def san_token_gauge(): + yield Contract("0x51fE22abAF4a26631b2913E417c0560D547797a7") + + +@pytest.fixture(scope="module", autouse=True) +def pool_manager(): + yield Contract("0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD") + + +@pytest.fixture(scope="module", autouse=True) +def live_yearn_treasury(): + address = "0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde" + yield Contract(address) + + +@pytest.fixture(scope="module", autouse=True) +def newstrategy( + strategist, + guardian, + keeper, + vault, + StrategyAngleUSDC, + gov, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy, + deploy_angle_voter +): + newstrategy = guardian.deploy( + StrategyAngleUSDC, + vault, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + ) + newstrategy.setKeeper(keeper) + deploy_strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) + deploy_strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) + deploy_angle_voter.setStrategy(deploy_strategy_proxy.address, {"from": gov}) + yield newstrategy + + +@pytest.fixture(scope="module", autouse=True) +def fxs_liquidity(accounts): + yield accounts.at("0xf977814e90da44bfa03b6295a0616a897441acec", force=True) + + +@pytest.fixture(scope="module", autouse=True) +def token_owner(accounts): + yield accounts.at("0x8412ebf45bac1b340bbe8f318b928c466c4e39ca", force=True) + + +@pytest.fixture(scope="module", autouse=True) +def angle_gov(accounts): + address = "0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8" + yield accounts.at(address, force=True) + + +@pytest.fixture(scope="module", autouse=True) +def angle_fee_manager(accounts): + address = "0x97B6897AAd7aBa3861c04C0e6388Fc02AF1F227f" + yield accounts.at(address, force=True) + + +@pytest.fixture(scope="module", autouse=True) +def pool_manager_account(accounts): + address = "0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD" + yield accounts.at(address, force=True) + + +@pytest.fixture(scope="session") +def BASE_PARAMS(): + yield 1000000000 + + +@pytest.fixture(scope="module", autouse=True) +def utils( + chain, + pool_manager_account, + pool_manager, + angle_stable_master, + strategy, + vault, + gov, + token, + san_token_gauge, + san_token, +): + return Utils( + chain, + pool_manager_account, + pool_manager, + angle_stable_master, + strategy, + vault, + gov, + token, + san_token_gauge, + san_token, + ) + + +class Utils: + def __init__( + self, + chain, + pool_manager_account, + pool_manager, + angle_stable_master, + strategy, + vault, + gov, + token, + san_token_gauge, + san_token, + ): + self.chain = chain + self.pool_manager_account = pool_manager_account + self.pool_manager = pool_manager + self.angle_stable_master = angle_stable_master + self.strategy = strategy + self.vault = vault + self.gov = gov + self.token = token + self.san_token_gauge = san_token_gauge + self.san_token = san_token + + def mock_angle_slp_profits(self): + max_profits_per_block = self.angle_stable_master.collateralMap( + self.pool_manager + )[7][2] + i = 0 + while i < 100: + self.angle_stable_master.accumulateInterest( + max_profits_per_block, {"from": self.pool_manager_account} + ) + self.chain.mine(1) + self.chain.sleep(1) + i += 1 + + def set_0_vault_fees(self): + self.vault.setManagementFee(0, {"from": self.gov}) + self.vault.setPerformanceFee(0, {"from": self.gov}) + + def assert_strategy_contains_no_tokens(self): + assert self.token.balanceOf(self.strategy) == 0 + assert self.san_token_gauge.balanceOf(self.strategy) == 0 + assert self.san_token.balanceOf(self.strategy) == 0 diff --git a/tests/test_clone.py b/tests/test_clone.py new file mode 100644 index 0000000..e38419d --- /dev/null +++ b/tests/test_clone.py @@ -0,0 +1,221 @@ +from brownie import Contract, reverts +import pytest + + +def test_clone( + strategy, + vault, + strategist, + gov, + token, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + chain, + san_token_gauge, + san_token, + angle_token, + uni, + angle_stable_master, + pool_manager, + deploy_strategy_proxy, + deploy_angle_voter +): + clone_tx = strategy.cloneAngle( + vault, + strategist, + strategist, + strategist, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + {"from": strategist}, + ) + cloned_strategy = Contract.from_abi( + "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi + ) + deploy_strategy_proxy.approveStrategy(san_token_gauge, cloned_strategy.address, {"from": gov}) + deploy_strategy_proxy.approveStrategy(angle_stable_master, cloned_strategy.address, {"from": gov}) + vault.migrateStrategy(strategy.address, cloned_strategy.address, {"from": gov}) + strategy = cloned_strategy + + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + # users deposit to vault + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + vault.setManagementFee(0, {"from": gov}) + vault.setPerformanceFee(0, {"from": gov}) + + assert san_token.balanceOf(strategy) == 0 + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + chain.sleep(3600 * 24 * 2) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + pps_after_first_harvest = vault.pricePerShare() + + # 6 hours for pricepershare to go up, there should be profit + strategy.harvest({"from": gov}) + chain.sleep(3600 * 24 * 2) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + pps_after_second_harvest = vault.pricePerShare() + assert pps_after_second_harvest > pps_after_first_harvest + + # 6 hours for pricepershare to go up + strategy.harvest({"from": gov}) + chain.sleep(3600 * 24 * 1) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + alice_vault_balance = vault.balanceOf(alice) + vault.withdraw(alice_vault_balance, alice, 75, {"from": alice}) + assert token.balanceOf(alice) > 0 + assert token.balanceOf(bob) == 0 + # assert frax.balanceOf(strategy) > 0 + + # 6 hours for pricepershare to go up + strategy.harvest({"from": gov}) + chain.sleep(3600 * 24 * 1) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + bob_vault_balance = vault.balanceOf(bob) + vault.withdraw(bob_vault_balance, bob, 75, {"from": bob}) + assert token.balanceOf(bob) > 0 + # assert usdc.balanceOf(strategy) == 0 + + # 6 hours for pricepershare to go up + strategy.harvest({"from": gov}) + chain.sleep(3600 * 24 * 1) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + tt_vault_balance = vault.balanceOf(tinytim) + vault.withdraw(tt_vault_balance, tinytim, 75, {"from": tinytim}) + assert token.balanceOf(tinytim) > 0 + # assert usdc.balanceOf(strategy) == 0 + + # We should have made profit + assert vault.pricePerShare() > 1e6 + + +def test_clone_of_clone( + strategy, + vault, + strategist, + gov, + san_token_gauge, + san_token, + angle_token, + uni, + angle_stable_master, + pool_manager, + deploy_strategy_proxy +): + clone_tx = strategy.cloneAngle( + vault, + strategist, + strategist, + strategist, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + {"from": strategist}, + ) + cloned_strategy = Contract.from_abi( + "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi + ) + + vault.migrateStrategy(strategy.address, cloned_strategy.address, {"from": gov}) + + # should not clone a clone + with reverts(): + cloned_strategy.cloneAngle( + vault, + strategist, + strategist, + strategist, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + {"from": strategist}, + ) + + +def test_double_initialize( + strategy, + vault, + strategist, + gov, + san_token_gauge, + san_token, + angle_token, + uni, + angle_stable_master, + pool_manager, + deploy_strategy_proxy +): + clone_tx = strategy.cloneAngle( + vault, + strategist, + strategist, + strategist, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + {"from": strategist}, + ) + cloned_strategy = Contract.from_abi( + "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi + ) + + # should not be able to call initialize twice + with reverts("Strategy already initialized"): + cloned_strategy.initialize( + vault, + strategist, + strategist, + strategist, + san_token, + angle_token, + uni, + angle_stable_master, + san_token_gauge, + pool_manager, + deploy_strategy_proxy.address, + {"from": strategist}, + ) diff --git a/tests/test_edge_case.py b/tests/test_edge_case.py new file mode 100644 index 0000000..6298615 --- /dev/null +++ b/tests/test_edge_case.py @@ -0,0 +1,67 @@ +import pytest +from brownie import ZERO_ADDRESS + + +@pytest.mark.require_network("mainnet-fork") +def test_angle_hack( + chain, + token, + vault, + alice, + alice_amount, + strategy, + san_token, + strategist, + san_token_gauge, + angle_stable_master, + gov, + BASE_PARAMS, + angle_fee_manager, + utils, + angle_token, + live_yearn_treasury, + accounts, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + + vault.deposit(alice_amount, {"from": alice}) + + assert san_token.balanceOf(strategy) == 0 + + utils.set_0_vault_fees() + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + chain.sleep(10 ** 10) + chain.mine(100) + + before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + strategy.harvest({"from": strategist}) + after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + + chain.sleep(3600 * 24 * 2) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + # Here's the hack part, where we fake a hack by sending away all of the strat's gauge tokens + san_token_gauge.transfer( + ZERO_ADDRESS, san_token_gauge.balanceOf(deploy_angle_voter), {"from": deploy_angle_voter} + ) + + strategy.setDoHealthCheck(False, {"from": gov}) + strategy.harvest({"from": strategist}) + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one < assets_at_t + + vault.withdraw({"from": alice}) + + assert token.balanceOf(alice) < alice_amount diff --git a/tests/test_migration.py b/tests/test_migration.py new file mode 100644 index 0000000..b9638f1 --- /dev/null +++ b/tests/test_migration.py @@ -0,0 +1,74 @@ +# TODO: Add tests here that show the normal operation of this strategy +# Suggestions to include: +# - strategy loading and unloading (via Vault addStrategy/revokeStrategy) +# - change in loading (from low to high and high to low) +# - strategy operation at different loading levels (anticipated and "extreme") + +import pytest + +from brownie import Wei, accounts, Contract, config +from brownie import StrategyAngleUSDC + + +@pytest.mark.require_network("mainnet-fork") +def test_migration( + chain, + vault, + strategy, + token, + gov, + strategist, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + angle_token, + angle_stable_master, + san_token, + san_token_gauge, + pool_manager, + newstrategy, + utils, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + # users deposit to vault + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + utils.set_0_vault_fees() + + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + assert san_token_gauge.balanceOf(strategy) == 0 + assert san_token_gauge.balanceOf(newstrategy) == 0 + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + + newstrategy.setStrategist(strategist) + vault.migrateStrategy(strategy, newstrategy, {"from": gov}) + deploy_strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) + deploy_strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) + + assert san_token.balanceOf(strategy) == 0 + assert san_token.balanceOf(newstrategy) == 0 + + newstrategy.harvest({"from": strategist}) + assert san_token.balanceOf(newstrategy) == 0 + assert san_token_gauge.balanceOf(newstrategy) == 0 + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 diff --git a/tests/test_operation.py b/tests/test_operation.py new file mode 100644 index 0000000..880dd65 --- /dev/null +++ b/tests/test_operation.py @@ -0,0 +1,262 @@ +from datetime import timedelta +import pytest + +from brownie import Wei, accounts, Contract, config, ZERO_ADDRESS +from brownie import StrategyAngleUSDC + + +@pytest.mark.require_network("mainnet-fork") +def test_operation( + chain, + vault, + strategy, + token, + gov, + strategist, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + san_token, + san_token_gauge, + utils, + angle_stable_master, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + # users deposit to vault + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + utils.set_0_vault_fees() + + assert san_token.balanceOf(strategy) == 0 + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(strategy) == 0 + assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + strategy.harvest({"from": strategist}) + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + chain.mine(1, timedelta=100) + strategy.harvest({"from": strategist}) + chain.mine(1) + + vault.withdraw({"from": alice}) + assert token.balanceOf(alice) > alice_amount + assert token.balanceOf(bob) == 0 + + vault.withdraw({"from": bob}) + assert token.balanceOf(bob) > bob_amount + + vault.withdraw({"from": tinytim}) + assert token.balanceOf(tinytim) > tinytim_amount + + +@pytest.mark.require_network("mainnet-fork") +def test_lossy_strat( + token, + vault, + alice, + alice_amount, + strategy, + san_token, + strategist, + san_token_gauge, + angle_stable_master, + gov, + BASE_PARAMS, + angle_fee_manager, + utils, + chain, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + + vault.deposit(alice_amount, {"from": alice}) + + assert san_token.balanceOf(strategy) == 0 + + utils.set_0_vault_fees() + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(strategy) == 0 + assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + # As a technique to simulate losses, we increase slippage + angle_stable_master.setFeeKeeper( + BASE_PARAMS, BASE_PARAMS, BASE_PARAMS / 100, 0, {"from": angle_fee_manager} + ) # set SLP slippage to 1% + + strategy.setEmergencyExit( + {"from": gov} + ) # this will pull the assets out of angle, so we'll feel the slippage + strategy.setDoHealthCheck(False, {"from": gov}) + strategy.harvest({"from": gov}) + + vault.withdraw({"from": alice}) + + assert token.balanceOf(alice) < assets_at_t_plus_one + assert token.balanceOf(alice) < alice_amount + + +# In this situation, we incur slippage on the exit but the ANGLE rewards should compensate for this +@pytest.mark.require_network("mainnet-fork") +def test_almost_lossy_strat( + chain, + token, + vault, + alice, + alice_amount, + strategy, + san_token, + strategist, + san_token_gauge, + angle_stable_master, + gov, + BASE_PARAMS, + angle_fee_manager, + utils, + angle_token, + angle_token_whale, + live_yearn_treasury, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + + vault.deposit(alice_amount, {"from": alice}) + + assert san_token.balanceOf(strategy) == 0 + + utils.set_0_vault_fees() + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + chain.sleep(10 ** 10) + chain.mine(100) + + before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + strategy.harvest({"from": strategist}) + + for _ in range(5): + angle_token.transfer( + strategy.address, 100 * 1e18, {"from": angle_token_whale} + ) # $100 top up + strategy.harvest({"from": strategist}) + chain.mine(1) + chain.sleep(1) + + after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + + chain.mine(1) + chain.sleep(1) + strategy.harvest({"from": strategist}) + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + # As a technique to simulate losses, we increase slippage + angle_stable_master.setFeeKeeper( + BASE_PARAMS, BASE_PARAMS, BASE_PARAMS / 1000, 0, {"from": angle_fee_manager} + ) # set SLP slippage to 0.01% (a bip) + + chain.sleep(3600 * 24 * 2) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + vault.withdraw(vault.balanceOf(alice), alice.address, 100, {"from": alice}) + + assert token.balanceOf(alice) > alice_amount + + +# We don't recieve any profit here other than angle rewards, and expect everything to work +@pytest.mark.require_network("mainnet-fork") +def test_harvest_angle_rewards( + chain, + token, + vault, + alice, + alice_amount, + strategy, + san_token, + strategist, + san_token_gauge, + angle_stable_master, + gov, + BASE_PARAMS, + angle_fee_manager, + utils, + angle_token, + live_yearn_treasury, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + + vault.deposit(alice_amount, {"from": alice}) + + assert san_token.balanceOf(strategy) == 0 + + utils.set_0_vault_fees() + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + chain.sleep(10 ** 10) + chain.mine(100) + + before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + strategy.harvest({"from": strategist}) + after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + + chain.sleep(3600 * 24 * 2) + chain.mine(1) + chain.sleep(3600 * 1) + chain.mine(1) + + strategy.harvest({"from": strategist}) + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + vault.withdraw({"from": alice}) + + assert token.balanceOf(alice) > alice_amount diff --git a/tests/test_sequential.py b/tests/test_sequential.py new file mode 100644 index 0000000..2467bcd --- /dev/null +++ b/tests/test_sequential.py @@ -0,0 +1,75 @@ +# TODO: Add tests here that show the normal operation of this strategy +# Suggestions to include: +# - strategy loading and unloading (via Vault addStrategy/revokeStrategy) +# - change in loading (from low to high and high to low) +# - strategy operation at different loading levels (anticipated and "extreme") + +import pytest + +from brownie import Wei, accounts, Contract, config +from brownie import StrategyAngleUSDC + + +@pytest.mark.require_network("mainnet-fork") +def test_sequential( + chain, + vault, + strategy, + token, + gov, + strategist, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + san_token_gauge, + utils, + angle_stable_master, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + # users deposit to vault + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + vault.setManagementFee(0, {"from": gov}) + vault.setPerformanceFee(0, {"from": gov}) + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + chain.sleep(1) + strategy.harvest({"from": strategist}) + chain.mine(1) + + alice_vault_balance = vault.balanceOf(alice) + vault.withdraw(alice_vault_balance, alice, 75, {"from": alice}) + assert token.balanceOf(alice) > 0 + assert token.balanceOf(bob) == 0 + # assert frax.balanceOf(strategy) > 0 + + bob_vault_balance = vault.balanceOf(bob) + vault.withdraw(bob_vault_balance, bob, 75, {"from": bob}) + assert token.balanceOf(bob) > 0 + # assert usdc.balanceOf(strategy) == 0 + + tt_vault_balance = vault.balanceOf(tinytim) + vault.withdraw(tt_vault_balance, tinytim, 75, {"from": tinytim}) + assert token.balanceOf(tinytim) > 0 + # assert usdc.balanceOf(strategy) == 0 diff --git a/tests/test_shutdown.py b/tests/test_shutdown.py new file mode 100644 index 0000000..0b9b924 --- /dev/null +++ b/tests/test_shutdown.py @@ -0,0 +1,63 @@ +import pytest + + +@pytest.mark.require_network("mainnet-fork") +def test_shutdown( + chain, + vault, + strategy, + token, + gov, + strategist, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + san_token_gauge, + san_token, + utils, + angle_stable_master, + BASE_PARAMS, + angle_fee_manager, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + utils.set_0_vault_fees() + + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + strategy.harvest({"from": strategist}) + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + strategy.setEmergencyExit({"from": gov}) + chain.sleep(1) + strategy.harvest({"from": strategist}) + chain.mine(1) + + vault.withdraw({"from": alice}) + assert token.balanceOf(alice) > alice_amount + assert token.balanceOf(bob) == 0 + + vault.withdraw({"from": bob}) + assert token.balanceOf(bob) > bob_amount + + vault.withdraw({"from": tinytim}) + assert token.balanceOf(tinytim) > tinytim_amount + + utils.assert_strategy_contains_no_tokens() From 0be3adf2538e6f2f395e21871e39136fde462f43 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 6 Jun 2022 19:25:15 +0200 Subject: [PATCH 03/45] feat: lock and increase amount locked --- src/AngleStrategyVoterProxy.sol | 15 ++++-- tests/test_central_voter.py | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/test_central_voter.py diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 40427ed..0ed0523 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -70,9 +70,18 @@ contract AngleStrategyVoterProxy { voters[_voter] = false; } - function lock() external { - uint256 amount = IERC20(angle).balanceOf(address(yearnAngleVoter)); - if (amount > 0) yearnAngleVoter.increaseAmount(amount); + function lock(uint256 amount, uint256 unlockTime) external { + if (amount > 0) { + IERC20(angle).transfer(address(yearnAngleVoter), amount); + yearnAngleVoter.createLock(amount, unlockTime); + } + } + + function increaseAmount(uint256 amount) external { + if (amount > 0) { + IERC20(angle).transfer(address(yearnAngleVoter), amount); + yearnAngleVoter.increaseAmount(amount); + } } function vote(address _gauge, uint256 _amount) public { diff --git a/tests/test_central_voter.py b/tests/test_central_voter.py new file mode 100644 index 0000000..a313a2a --- /dev/null +++ b/tests/test_central_voter.py @@ -0,0 +1,93 @@ +import pytest +from brownie import Contract + +@pytest.mark.require_network("mainnet-fork") +def test_central_voter( + chain, + vault, + strategy, + token, + gov, + strategist, + live_yearn_treasury, + alice, + alice_amount, + bob, + bob_amount, + tinytim, + tinytim_amount, + angle_token, + veangle_token, + san_token, + san_token_gauge, + utils, + angle_stable_master, + deploy_strategy_proxy, + deploy_angle_voter +): + token.approve(vault, 1_000_000_000_000, {"from": alice}) + token.approve(vault, 1_000_000_000_000, {"from": bob}) + token.approve(vault, 1_000_000_000_000, {"from": tinytim}) + + # users deposit to vault + vault.deposit(alice_amount, {"from": alice}) + vault.deposit(bob_amount, {"from": bob}) + vault.deposit(tinytim_amount, {"from": tinytim}) + + utils.set_0_vault_fees() + + assert san_token.balanceOf(strategy) == 0 + + # First harvest + chain.sleep(1) + strategy.harvest({"from": strategist}) + + assert san_token_gauge.balanceOf(strategy) == 0 + assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 + assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assets_at_t = strategy.estimatedTotalAssets() + + utils.mock_angle_slp_profits() + previous_voter_angle = angle_token.balanceOf(deploy_angle_voter) + previous_proxy_angle = angle_token.balanceOf(deploy_strategy_proxy) + previous_treasury_angle = angle_token.balanceOf(live_yearn_treasury) + + strategy.harvest({"from": strategist}) + + assert angle_token.balanceOf(deploy_angle_voter) == previous_voter_angle + assert angle_token.balanceOf(deploy_strategy_proxy) - previous_proxy_angle > 0 + assert angle_token.balanceOf(live_yearn_treasury) - previous_treasury_angle > 0 + + assets_at_t_plus_one = strategy.estimatedTotalAssets() + assert assets_at_t_plus_one > assets_at_t + + chain.mine(1, timedelta=100) + strategy.harvest({"from": strategist}) + chain.mine(1) + + vault.withdraw({"from": alice}) + assert token.balanceOf(alice) > alice_amount + assert token.balanceOf(bob) == 0 + + vault.withdraw({"from": bob}) + assert token.balanceOf(bob) > bob_amount + + vault.withdraw({"from": tinytim}) + assert token.balanceOf(tinytim) > tinytim_amount + + whitelister = Contract(veangle_token.smart_wallet_checker()) + whitelister.approveWallet(deploy_angle_voter, {"from": whitelister.admin()}) + + angle_balance = angle_token.balanceOf(deploy_strategy_proxy) + lock_now = int(angle_balance / 2) + + unlock_time = chain.time() + 4*86400*365 + deploy_strategy_proxy.lock(lock_now, unlock_time, {"from": gov}) + + assert veangle_token.balanceOf(deploy_angle_voter) > 0 + assert veangle_token.locked(deploy_angle_voter)[0] == lock_now + assert veangle_token.locked(deploy_angle_voter)[1] - chain.time() > 4*86400*364 + + lock_rest = angle_balance - lock_now + deploy_strategy_proxy.increaseAmount(lock_rest, {"from": gov}) + assert veangle_token.locked(deploy_angle_voter)[0] == angle_balance \ No newline at end of file From e35f803b80980f4e66f7bf580164256d46162e05 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 6 Jun 2022 19:37:57 +0200 Subject: [PATCH 04/45] feat: add 2nd harvest --- tests/test_central_voter.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/test_central_voter.py b/tests/test_central_voter.py index a313a2a..0e8f13a 100644 --- a/tests/test_central_voter.py +++ b/tests/test_central_voter.py @@ -61,20 +61,6 @@ def test_central_voter( assets_at_t_plus_one = strategy.estimatedTotalAssets() assert assets_at_t_plus_one > assets_at_t - chain.mine(1, timedelta=100) - strategy.harvest({"from": strategist}) - chain.mine(1) - - vault.withdraw({"from": alice}) - assert token.balanceOf(alice) > alice_amount - assert token.balanceOf(bob) == 0 - - vault.withdraw({"from": bob}) - assert token.balanceOf(bob) > bob_amount - - vault.withdraw({"from": tinytim}) - assert token.balanceOf(tinytim) > tinytim_amount - whitelister = Contract(veangle_token.smart_wallet_checker()) whitelister.approveWallet(deploy_angle_voter, {"from": whitelister.admin()}) @@ -90,4 +76,9 @@ def test_central_voter( lock_rest = angle_balance - lock_now deploy_strategy_proxy.increaseAmount(lock_rest, {"from": gov}) - assert veangle_token.locked(deploy_angle_voter)[0] == angle_balance \ No newline at end of file + assert veangle_token.locked(deploy_angle_voter)[0] == angle_balance + + utils.mock_angle_slp_profits() + + strategy.harvest({"from": strategist}) + \ No newline at end of file From c863f46258986d78d26b6833eb2267fbcdaa6b3c Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 10:41:36 +0200 Subject: [PATCH 05/45] fix: remove percentLock --- src/Strategy.sol | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Strategy.sol b/src/Strategy.sol index 233d978..6f994b6 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -41,12 +41,21 @@ contract Strategy is BaseStrategy { // variable for determining how much governance token to hold for voting rights uint256 public percentKeep; +<<<<<<< HEAD:src/Strategy.sol IERC20 public sanToken; IAngleGauge public sanTokenGauge; address public constant treasury = 0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde; // To change this, migrate address public constant unirouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; // SushiSwap address public constant usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +======= + address public sanToken; + address public angleToken; + address public unirouter; + address public angleStableMaster; + address public sanTokenGauge; + address public treasury; +>>>>>>> f5f31cf (fix: remove percentLock):contracts/StrategyAngleUSDC.sol address public poolManager; address public tradeFactory = address(0); @@ -183,7 +192,26 @@ contract Strategy is BaseStrategy { uint256 _debtPayment ) { +<<<<<<< HEAD:src/Strategy.sol // Run initial profit + loss calculations. +======= + // First, claim & sell any rewards. + + // IAngleGauge(sanTokenGauge).claim_rewards(); + strategyProxy.claimRewards(sanTokenGauge, angleToken); + + uint256 _tokensAvailable = balanceOfAngleToken(); + if (_tokensAvailable > 0) { + uint256 _tokensToKeep = + _tokensAvailable.mul(percentKeep).div(MAX_BPS); + if (_tokensToKeep > 0) { + IERC20(angleToken).transfer(address(strategyProxy), _tokensToKeep); + _swap(_tokensAvailable.sub(_tokensToKeep), address(angleToken)); + } + } + + // Second, run initial profit + loss calculations. +>>>>>>> f5f31cf (fix: remove percentLock):contracts/StrategyAngleUSDC.sol uint256 _totalAssets = estimatedTotalAssets(); uint256 _totalDebt = vault.strategies(address(this)).totalDebt; From 367645005d70b7243712f388fba69892a7dde42b Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 10:42:27 +0200 Subject: [PATCH 06/45] fix: remove percentLock setter --- src/Strategy.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Strategy.sol b/src/Strategy.sol index 6f994b6..70ad6b3 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -95,6 +95,10 @@ contract Strategy is BaseStrategy { strategyProxy = AngleStrategyVoterProxy(_strategyProxy); percentKeep = 1000; +<<<<<<< HEAD:src/Strategy.sol +======= + treasury = address(0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde); +>>>>>>> b1e4049 (fix: remove percentLock setter):contracts/StrategyAngleUSDC.sol healthCheck = 0xDDCea799fF1699e98EDF118e0629A974Df7DF012; doHealthCheck = true; @@ -468,6 +472,14 @@ contract Strategy is BaseStrategy { percentKeep = _percentKeep; } +<<<<<<< HEAD:src/Strategy.sol +======= + // where angleToken goes + function setTreasury(address _treasury) external onlyVaultManagers { + require(_treasury != address(0), "!zero_address"); + treasury = _treasury; + } +>>>>>>> b1e4049 (fix: remove percentLock setter):contracts/StrategyAngleUSDC.sol // ----------------- SUPPORT & UTILITY FUNCTIONS ---------- From 64c75d77eb3fb6ccb620ebaf1875777fc319f224 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 10:46:17 +0200 Subject: [PATCH 07/45] fix: set governance to ychad in constructors --- src/AngleStrategyVoterProxy.sol | 2 +- src/YearnAngleVoter.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 0ed0523..cbc7a63 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -41,7 +41,7 @@ contract AngleStrategyVoterProxy { uint256 lastTimeCursor; constructor(address _voter) public { - governance = msg.sender; + governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); yearnAngleVoter = YearnAngleVoter(_voter); } diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index 0723a37..8759fa8 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -21,7 +21,7 @@ contract YearnAngleVoter { address public strategy; constructor() public { - governance = msg.sender; + governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); } function getName() external pure returns (string memory) { From 649c3098720f4c475d584ca4cd0da63d10c40fa2 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 11:06:10 +0200 Subject: [PATCH 08/45] fix: remove .address and rename voter fixtures --- tests/conftest.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d0d5329..e3d685e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ def shared_setup(fn_isolation): @pytest.fixture(scope="module", autouse=True) def gov(accounts): - yield accounts[0] + yield accounts.at("0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", force=True) @pytest.fixture(scope="module", autouse=True) @@ -95,17 +95,17 @@ def vault(pm, gov, rewards, guardian, management, token): yield vault @pytest.fixture(scope ="module") -def deploy_strategy_proxy( - deploy_angle_voter, +def strategy_proxy( + angle_voter, gov, AngleStrategyProxy ): - strategy_proxy = gov.deploy(AngleStrategyProxy, deploy_angle_voter.address) + strategy_proxy = gov.deploy(AngleStrategyProxy, angle_voter.address) yield strategy_proxy @pytest.fixture(scope ="module") -def deploy_angle_voter( +def angle_voter( gov, YearnAngleVoter ): @@ -127,8 +127,8 @@ def strategy( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): strategy = strategist.deploy( StrategyAngleUSDC, @@ -139,13 +139,13 @@ def strategy( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address + strategy_proxy.address ) strategy.setKeeper(keeper, {"from": gov}) vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) - deploy_strategy_proxy.approveStrategy(san_token_gauge, strategy.address, {"from": gov}) - deploy_strategy_proxy.approveStrategy(angle_stable_master, strategy.address, {"from": gov}) - deploy_angle_voter.setStrategy(deploy_strategy_proxy.address, {"from": gov}) + strategy_proxy.approveStrategy(san_token_gauge, strategy, {"from": gov}) + strategy_proxy.approveStrategy(angle_stable_master, strategy, {"from": gov}) + angle_voter.setStrategy(strategy_proxy, {"from": gov}) yield strategy @@ -220,8 +220,8 @@ def newstrategy( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): newstrategy = guardian.deploy( StrategyAngleUSDC, @@ -232,12 +232,12 @@ def newstrategy( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, ) newstrategy.setKeeper(keeper) - deploy_strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) - deploy_strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) - deploy_angle_voter.setStrategy(deploy_strategy_proxy.address, {"from": gov}) + strategy_proxy.approveStrategy(san_token_gauge, newstrategy, {"from": gov}) + strategy_proxy.approveStrategy(angle_stable_master, newstrategy, {"from": gov}) + angle_voter.setStrategy(strategy_proxy, {"from": gov}) yield newstrategy From f189c10deb6f2ff700464b67c589421429d8bbe5 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 11:06:40 +0200 Subject: [PATCH 09/45] fix: remove treasury check for strategy proxy --- tests/test_central_voter.py | 34 +++++++++++++++---------------- tests/test_clone.py | 24 +++++++++++----------- tests/test_edge_case.py | 14 ++++++------- tests/test_migration.py | 14 ++++++------- tests/test_operation.py | 40 ++++++++++++++++++------------------- tests/test_sequential.py | 6 +++--- tests/test_shutdown.py | 4 ++-- 7 files changed, 67 insertions(+), 69 deletions(-) diff --git a/tests/test_central_voter.py b/tests/test_central_voter.py index 0e8f13a..acf057f 100644 --- a/tests/test_central_voter.py +++ b/tests/test_central_voter.py @@ -22,8 +22,8 @@ def test_central_voter( san_token_gauge, utils, angle_stable_master, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) token.approve(vault, 1_000_000_000_000, {"from": bob}) @@ -43,40 +43,38 @@ def test_central_voter( strategy.harvest({"from": strategist}) assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(strategy_proxy) == 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() - previous_voter_angle = angle_token.balanceOf(deploy_angle_voter) - previous_proxy_angle = angle_token.balanceOf(deploy_strategy_proxy) - previous_treasury_angle = angle_token.balanceOf(live_yearn_treasury) + previous_voter_angle = angle_token.balanceOf(angle_voter) + previous_proxy_angle = angle_token.balanceOf(strategy_proxy) strategy.harvest({"from": strategist}) - assert angle_token.balanceOf(deploy_angle_voter) == previous_voter_angle - assert angle_token.balanceOf(deploy_strategy_proxy) - previous_proxy_angle > 0 - assert angle_token.balanceOf(live_yearn_treasury) - previous_treasury_angle > 0 + assert angle_token.balanceOf(angle_voter) == previous_voter_angle + assert angle_token.balanceOf(strategy_proxy) - previous_proxy_angle > 0 assets_at_t_plus_one = strategy.estimatedTotalAssets() assert assets_at_t_plus_one > assets_at_t whitelister = Contract(veangle_token.smart_wallet_checker()) - whitelister.approveWallet(deploy_angle_voter, {"from": whitelister.admin()}) + whitelister.approveWallet(angle_voter, {"from": whitelister.admin()}) - angle_balance = angle_token.balanceOf(deploy_strategy_proxy) + angle_balance = angle_token.balanceOf(strategy_proxy) lock_now = int(angle_balance / 2) unlock_time = chain.time() + 4*86400*365 - deploy_strategy_proxy.lock(lock_now, unlock_time, {"from": gov}) + strategy_proxy.lock(lock_now, unlock_time, {"from": gov}) - assert veangle_token.balanceOf(deploy_angle_voter) > 0 - assert veangle_token.locked(deploy_angle_voter)[0] == lock_now - assert veangle_token.locked(deploy_angle_voter)[1] - chain.time() > 4*86400*364 + assert veangle_token.balanceOf(angle_voter) > 0 + assert veangle_token.locked(angle_voter)[0] == lock_now + assert veangle_token.locked(angle_voter)[1] - chain.time() > 4*86400*364 lock_rest = angle_balance - lock_now - deploy_strategy_proxy.increaseAmount(lock_rest, {"from": gov}) - assert veangle_token.locked(deploy_angle_voter)[0] == angle_balance + strategy_proxy.increaseAmount(lock_rest, {"from": gov}) + assert veangle_token.locked(angle_voter)[0] == angle_balance utils.mock_angle_slp_profits() diff --git a/tests/test_clone.py b/tests/test_clone.py index e38419d..9f30db1 100644 --- a/tests/test_clone.py +++ b/tests/test_clone.py @@ -21,8 +21,8 @@ def test_clone( uni, angle_stable_master, pool_manager, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): clone_tx = strategy.cloneAngle( vault, @@ -35,14 +35,14 @@ def test_clone( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, {"from": strategist}, ) cloned_strategy = Contract.from_abi( "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi ) - deploy_strategy_proxy.approveStrategy(san_token_gauge, cloned_strategy.address, {"from": gov}) - deploy_strategy_proxy.approveStrategy(angle_stable_master, cloned_strategy.address, {"from": gov}) + strategy_proxy.approveStrategy(san_token_gauge, cloned_strategy.address, {"from": gov}) + strategy_proxy.approveStrategy(angle_stable_master, cloned_strategy.address, {"from": gov}) vault.migrateStrategy(strategy.address, cloned_strategy.address, {"from": gov}) strategy = cloned_strategy @@ -64,7 +64,7 @@ def test_clone( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 chain.sleep(3600 * 24 * 2) chain.mine(1) chain.sleep(3600 * 1) @@ -132,7 +132,7 @@ def test_clone_of_clone( uni, angle_stable_master, pool_manager, - deploy_strategy_proxy + strategy_proxy ): clone_tx = strategy.cloneAngle( vault, @@ -145,7 +145,7 @@ def test_clone_of_clone( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, {"from": strategist}, ) cloned_strategy = Contract.from_abi( @@ -167,7 +167,7 @@ def test_clone_of_clone( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, {"from": strategist}, ) @@ -183,7 +183,7 @@ def test_double_initialize( uni, angle_stable_master, pool_manager, - deploy_strategy_proxy + strategy_proxy ): clone_tx = strategy.cloneAngle( vault, @@ -196,7 +196,7 @@ def test_double_initialize( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, {"from": strategist}, ) cloned_strategy = Contract.from_abi( @@ -216,6 +216,6 @@ def test_double_initialize( angle_stable_master, san_token_gauge, pool_manager, - deploy_strategy_proxy.address, + strategy_proxy.address, {"from": strategist}, ) diff --git a/tests/test_edge_case.py b/tests/test_edge_case.py index 6298615..fe566e8 100644 --- a/tests/test_edge_case.py +++ b/tests/test_edge_case.py @@ -21,8 +21,8 @@ def test_angle_hack( angle_token, live_yearn_treasury, accounts, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) @@ -36,16 +36,16 @@ def test_angle_hack( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() chain.sleep(10 ** 10) chain.mine(100) - before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) strategy.harvest({"from": strategist}) - after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) - assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + after_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) + assert after_harvest_proxy_rewards_bal > before_harvest_proxy_rewards_bal chain.sleep(3600 * 24 * 2) chain.mine(1) @@ -54,7 +54,7 @@ def test_angle_hack( # Here's the hack part, where we fake a hack by sending away all of the strat's gauge tokens san_token_gauge.transfer( - ZERO_ADDRESS, san_token_gauge.balanceOf(deploy_angle_voter), {"from": deploy_angle_voter} + ZERO_ADDRESS, san_token_gauge.balanceOf(angle_voter), {"from": angle_voter} ) strategy.setDoHealthCheck(False, {"from": gov}) diff --git a/tests/test_migration.py b/tests/test_migration.py index b9638f1..9248cb4 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -31,8 +31,8 @@ def test_migration( pool_manager, newstrategy, utils, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) token.approve(vault, 1_000_000_000_000, {"from": bob}) @@ -48,7 +48,7 @@ def test_migration( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() @@ -58,12 +58,12 @@ def test_migration( assert san_token_gauge.balanceOf(strategy) == 0 assert san_token_gauge.balanceOf(newstrategy) == 0 - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 newstrategy.setStrategist(strategist) vault.migrateStrategy(strategy, newstrategy, {"from": gov}) - deploy_strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) - deploy_strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) + strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) + strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) assert san_token.balanceOf(strategy) == 0 assert san_token.balanceOf(newstrategy) == 0 @@ -71,4 +71,4 @@ def test_migration( newstrategy.harvest({"from": strategist}) assert san_token.balanceOf(newstrategy) == 0 assert san_token_gauge.balanceOf(newstrategy) == 0 - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 diff --git a/tests/test_operation.py b/tests/test_operation.py index 880dd65..bf752ff 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -23,8 +23,8 @@ def test_operation( san_token_gauge, utils, angle_stable_master, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) token.approve(vault, 1_000_000_000_000, {"from": bob}) @@ -44,8 +44,8 @@ def test_operation( strategy.harvest({"from": strategist}) assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(strategy_proxy) == 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() @@ -85,8 +85,8 @@ def test_lossy_strat( angle_fee_manager, utils, chain, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) @@ -101,8 +101,8 @@ def test_lossy_strat( strategy.harvest({"from": strategist}) assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(deploy_strategy_proxy) == 0 - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(strategy_proxy) == 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() @@ -147,8 +147,8 @@ def test_almost_lossy_strat( angle_token, angle_token_whale, live_yearn_treasury, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) @@ -162,13 +162,13 @@ def test_almost_lossy_strat( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() chain.sleep(10 ** 10) chain.mine(100) - before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) strategy.harvest({"from": strategist}) for _ in range(5): @@ -179,8 +179,8 @@ def test_almost_lossy_strat( chain.mine(1) chain.sleep(1) - after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) - assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + after_harvest_proxyy_rewards_bal = angle_token.balanceOf(strategy_proxy) + assert after_harvest_proxyy_rewards_bal > before_harvest_proxy_rewards_bal chain.mine(1) chain.sleep(1) @@ -222,8 +222,8 @@ def test_harvest_angle_rewards( utils, angle_token, live_yearn_treasury, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) @@ -237,16 +237,16 @@ def test_harvest_angle_rewards( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() chain.sleep(10 ** 10) chain.mine(100) - before_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) + before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) strategy.harvest({"from": strategist}) - after_harvest_treasury_rewards_bal = angle_token.balanceOf(live_yearn_treasury) - assert after_harvest_treasury_rewards_bal > before_harvest_treasury_rewards_bal + after_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) + assert after_harvest_proxy_rewards_bal > before_harvest_proxy_rewards_bal chain.sleep(3600 * 24 * 2) chain.mine(1) diff --git a/tests/test_sequential.py b/tests/test_sequential.py index 2467bcd..25010fa 100644 --- a/tests/test_sequential.py +++ b/tests/test_sequential.py @@ -27,8 +27,8 @@ def test_sequential( san_token_gauge, utils, angle_stable_master, - deploy_strategy_proxy, - deploy_angle_voter + strategy_proxy, + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": bob}) token.approve(vault, 1_000_000_000_000, {"from": alice}) @@ -46,7 +46,7 @@ def test_sequential( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() diff --git a/tests/test_shutdown.py b/tests/test_shutdown.py index 0b9b924..56e4aeb 100644 --- a/tests/test_shutdown.py +++ b/tests/test_shutdown.py @@ -21,7 +21,7 @@ def test_shutdown( angle_stable_master, BASE_PARAMS, angle_fee_manager, - deploy_angle_voter + angle_voter ): token.approve(vault, 1_000_000_000_000, {"from": alice}) token.approve(vault, 1_000_000_000_000, {"from": bob}) @@ -36,7 +36,7 @@ def test_shutdown( chain.sleep(1) strategy.harvest({"from": strategist}) - assert san_token_gauge.balanceOf(deploy_angle_voter) > 0 + assert san_token_gauge.balanceOf(angle_voter) > 0 assets_at_t = strategy.estimatedTotalAssets() utils.mock_angle_slp_profits() From f389c7e73f296e82ee57def70151290a08dc81cd Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 11:10:52 +0200 Subject: [PATCH 10/45] feat: add two-step process to set governance --- src/YearnAngleVoter.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index 8759fa8..78069c7 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -18,6 +18,7 @@ contract YearnAngleVoter { address constant public veAngle = address(0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5); address public governance; + address public pendingGovernance; address public strategy; constructor() public { @@ -54,7 +55,13 @@ contract YearnAngleVoter { function setGovernance(address _governance) external { require(msg.sender == governance, "!governance"); - governance = _governance; + pendingGovernance = _governance; + } + + function acceptGovernance() external { + require(msg.sender == pendingGovernance, "!pending_governance"); + governance = msg.sender; + pendingGovernance = address(0); } function execute(address to, uint value, bytes calldata data) external returns (bool, bytes memory) { From 46571e07f119441a39a6ab6c8e33ab87676195ca Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 11:16:48 +0200 Subject: [PATCH 11/45] fix: naming convention for proxy, setProxy --- src/YearnAngleVoter.sol | 17 +++++++++++------ tests/conftest.py | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index 78069c7..4d5b5c4 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -19,7 +19,7 @@ contract YearnAngleVoter { address public governance; address public pendingGovernance; - address public strategy; + address public proxy; constructor() public { governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); @@ -29,27 +29,27 @@ contract YearnAngleVoter { return "YearnAngleVoter"; } - function setStrategy(address _strategy) external { + function setProxy(address _proxy) external { require(msg.sender == governance, "!governance"); - strategy = _strategy; + proxy = _proxy; } function createLock(uint256 _value, uint256 _unlockTime) external { - require(msg.sender == strategy || msg.sender == governance, "!authorized"); + require(msg.sender == proxy || msg.sender == governance, "!authorized"); IERC20(angle).safeApprove(veAngle, 0); IERC20(angle).safeApprove(veAngle, _value); IVoteEscrow(veAngle).create_lock(_value, _unlockTime); } function increaseAmount(uint _value) external { - require(msg.sender == strategy || msg.sender == governance, "!authorized"); + require(msg.sender == proxy || msg.sender == governance, "!authorized"); IERC20(angle).safeApprove(veAngle, 0); IERC20(angle).safeApprove(veAngle, _value); IVoteEscrow(veAngle).increase_amount(_value); } function release() external { - require(msg.sender == strategy || msg.sender == governance, "!authorized"); + require(msg.sender == proxy || msg.sender == governance, "!authorized"); IVoteEscrow(veAngle).withdraw(); } @@ -65,8 +65,13 @@ contract YearnAngleVoter { } function execute(address to, uint value, bytes calldata data) external returns (bool, bytes memory) { +<<<<<<< HEAD:src/YearnAngleVoter.sol require(msg.sender == strategy || msg.sender == governance, "!governance"); (bool success, bytes memory result) = to.call{value: value}(data); +======= + require(msg.sender == proxy || msg.sender == governance, "!governance"); + (bool success, bytes memory result) = to.call.value(value)(data); +>>>>>>> 23a283f (fix: naming convention for proxy, setProxy):contracts/YearnAngleVoter.sol return (success, result); } diff --git a/tests/conftest.py b/tests/conftest.py index e3d685e..85c6981 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -145,7 +145,7 @@ def strategy( vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) strategy_proxy.approveStrategy(san_token_gauge, strategy, {"from": gov}) strategy_proxy.approveStrategy(angle_stable_master, strategy, {"from": gov}) - angle_voter.setStrategy(strategy_proxy, {"from": gov}) + angle_voter.setProxy(strategy_proxy, {"from": gov}) yield strategy @@ -237,7 +237,7 @@ def newstrategy( newstrategy.setKeeper(keeper) strategy_proxy.approveStrategy(san_token_gauge, newstrategy, {"from": gov}) strategy_proxy.approveStrategy(angle_stable_master, newstrategy, {"from": gov}) - angle_voter.setStrategy(strategy_proxy, {"from": gov}) + angle_voter.setProxy(strategy_proxy, {"from": gov}) yield newstrategy From d0140f0af7c18027815cf4e2506011a3aa8a7bf8 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Wed, 8 Jun 2022 11:54:41 +0200 Subject: [PATCH 12/45] feat: changed prepareMigration logic to convert back to want --- src/Strategy.sol | 13 +++++++++++++ tests/test_migration.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 70ad6b3..1153d24 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -341,11 +341,24 @@ contract Strategy is BaseStrategy { // transfers all tokens to new strategy function prepareMigration(address _newStrategy) internal override { +<<<<<<< HEAD:src/Strategy.sol // want is transferred by the base contract's migrate function sanTokenGauge.withdraw(balanceOfStakedSanToken()); IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this))); IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken()); +======= + uint256 _angleBalance = balanceOfAngleToken(); + if(_angleBalance > 0) { + IERC20(angleToken).safeTransfer(_newStrategy, _angleBalance); + } + uint256 _stakedBalance = balanceOfStake(); + if (_stakedBalance > 0) { + strategyProxy.withdraw(sanTokenGauge, sanToken, _stakedBalance); + IERC20(sanToken).safeTransfer(address(strategyProxy), _stakedBalance); + withdrawFromStableMaster(_stakedBalance); + } +>>>>>>> 8372725 (feat: changed prepareMigration logic to convert back to want):contracts/StrategyAngleUSDC.sol } function protectedTokens() diff --git a/tests/test_migration.py b/tests/test_migration.py index 9248cb4..ef8642c 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -61,14 +61,24 @@ def test_migration( assert san_token_gauge.balanceOf(angle_voter) > 0 newstrategy.setStrategist(strategist) - vault.migrateStrategy(strategy, newstrategy, {"from": gov}) - strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) - strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) + san_token_balance = strategy.balanceOfStake() + assets = strategy.estimatedTotalAssets() + assert san_token_balance > 0 + assert san_token.balanceOf(strategy) == 0 + assert san_token.balanceOf(newstrategy) == 0 + vault.migrateStrategy(strategy, newstrategy, {"from": gov}) assert san_token.balanceOf(strategy) == 0 assert san_token.balanceOf(newstrategy) == 0 + assert san_token.balanceOf(strategy_proxy) == 0 + assert strategy.balanceOfStake() == 0 + assert newstrategy.balanceOfStake() == 0 + strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) + strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) + assert pytest.approx(newstrategy.estimatedTotalAssets(),rel=1e-3) == assets newstrategy.harvest({"from": strategist}) assert san_token.balanceOf(newstrategy) == 0 assert san_token_gauge.balanceOf(newstrategy) == 0 assert san_token_gauge.balanceOf(angle_voter) > 0 + assert token.balanceOf(newstrategy) == 0 From a9ff880b05a40c4e957445c546d0c3d9b1213173 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 24 Jun 2022 13:51:13 -0700 Subject: [PATCH 13/45] chore: rebase onto most recent security-approved master --- src/Strategy.sol | 60 +++++------------------------- src/YearnAngleVoter.sol | 7 +--- src/test/utils/StrategyFixture.sol | 6 +-- 3 files changed, 13 insertions(+), 60 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 1153d24..c7d5620 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -41,21 +41,12 @@ contract Strategy is BaseStrategy { // variable for determining how much governance token to hold for voting rights uint256 public percentKeep; -<<<<<<< HEAD:src/Strategy.sol IERC20 public sanToken; IAngleGauge public sanTokenGauge; address public constant treasury = 0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde; // To change this, migrate address public constant unirouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; // SushiSwap address public constant usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; -======= - address public sanToken; - address public angleToken; - address public unirouter; - address public angleStableMaster; - address public sanTokenGauge; - address public treasury; ->>>>>>> f5f31cf (fix: remove percentLock):contracts/StrategyAngleUSDC.sol address public poolManager; address public tradeFactory = address(0); @@ -95,10 +86,6 @@ contract Strategy is BaseStrategy { strategyProxy = AngleStrategyVoterProxy(_strategyProxy); percentKeep = 1000; -<<<<<<< HEAD:src/Strategy.sol -======= - treasury = address(0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde); ->>>>>>> b1e4049 (fix: remove percentLock setter):contracts/StrategyAngleUSDC.sol healthCheck = 0xDDCea799fF1699e98EDF118e0629A974Df7DF012; doHealthCheck = true; @@ -196,26 +183,7 @@ contract Strategy is BaseStrategy { uint256 _debtPayment ) { -<<<<<<< HEAD:src/Strategy.sol // Run initial profit + loss calculations. -======= - // First, claim & sell any rewards. - - // IAngleGauge(sanTokenGauge).claim_rewards(); - strategyProxy.claimRewards(sanTokenGauge, angleToken); - - uint256 _tokensAvailable = balanceOfAngleToken(); - if (_tokensAvailable > 0) { - uint256 _tokensToKeep = - _tokensAvailable.mul(percentKeep).div(MAX_BPS); - if (_tokensToKeep > 0) { - IERC20(angleToken).transfer(address(strategyProxy), _tokensToKeep); - _swap(_tokensAvailable.sub(_tokensToKeep), address(angleToken)); - } - } - - // Second, run initial profit + loss calculations. ->>>>>>> f5f31cf (fix: remove percentLock):contracts/StrategyAngleUSDC.sol uint256 _totalAssets = estimatedTotalAssets(); uint256 _totalDebt = vault.strategies(address(this)).totalDebt; @@ -256,10 +224,10 @@ contract Strategy is BaseStrategy { uint256 _tokensAvailable = balanceOfAngleToken(); if (_tokensAvailable > 0) { - uint256 _tokensToLock = + uint256 _tokensToKeep = (_tokensAvailable * percentKeep) / MAX_BPS; - if (_tokensToLock > 0) { - IERC20(angleToken).transfer(address(strategyProxy), _tokensToLock); + if (_tokensToKeep > 0) { + IERC20(angleToken).transfer(address(strategyProxy), _tokensToKeep); } } @@ -341,24 +309,23 @@ contract Strategy is BaseStrategy { // transfers all tokens to new strategy function prepareMigration(address _newStrategy) internal override { -<<<<<<< HEAD:src/Strategy.sol // want is transferred by the base contract's migrate function sanTokenGauge.withdraw(balanceOfStakedSanToken()); - IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this))); - IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken()); -======= uint256 _angleBalance = balanceOfAngleToken(); if(_angleBalance > 0) { IERC20(angleToken).safeTransfer(_newStrategy, _angleBalance); } - uint256 _stakedBalance = balanceOfStake(); + + uint256 _stakedBalance = balanceOfStakedSanToken(); if (_stakedBalance > 0) { - strategyProxy.withdraw(sanTokenGauge, sanToken, _stakedBalance); + strategyProxy.withdraw(address(sanTokenGauge), address(sanToken), _stakedBalance); IERC20(sanToken).safeTransfer(address(strategyProxy), _stakedBalance); withdrawFromStableMaster(_stakedBalance); } ->>>>>>> 8372725 (feat: changed prepareMigration logic to convert back to want):contracts/StrategyAngleUSDC.sol + + IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this))); + IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken()); } function protectedTokens() @@ -485,15 +452,6 @@ contract Strategy is BaseStrategy { percentKeep = _percentKeep; } -<<<<<<< HEAD:src/Strategy.sol -======= - // where angleToken goes - function setTreasury(address _treasury) external onlyVaultManagers { - require(_treasury != address(0), "!zero_address"); - treasury = _treasury; - } ->>>>>>> b1e4049 (fix: remove percentLock setter):contracts/StrategyAngleUSDC.sol - // ----------------- SUPPORT & UTILITY FUNCTIONS ---------- function balanceOfWant() public view returns (uint256) { diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index 4d5b5c4..c80863c 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -65,13 +65,8 @@ contract YearnAngleVoter { } function execute(address to, uint value, bytes calldata data) external returns (bool, bytes memory) { -<<<<<<< HEAD:src/YearnAngleVoter.sol - require(msg.sender == strategy || msg.sender == governance, "!governance"); - (bool success, bytes memory result) = to.call{value: value}(data); -======= require(msg.sender == proxy || msg.sender == governance, "!governance"); - (bool success, bytes memory result) = to.call.value(value)(data); ->>>>>>> 23a283f (fix: naming convention for proxy, setProxy):contracts/YearnAngleVoter.sol + (bool success, bytes memory result) = to.call{value: value}(data); return (success, result); } diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index 9983ea4..7152437 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -79,7 +79,7 @@ contract StrategyFixture is ExtendedTest { voter = YearnAngleVoter(_voter); voterProxy = AngleStrategyVoterProxy(_voterProxy); vm.prank(gov); - voter.setStrategy(_voterProxy); + voter.setProxy(_voterProxy); string[2] memory _tokensToTest = ["USDC", "DAI"]; @@ -152,14 +152,14 @@ contract StrategyFixture is ExtendedTest { function deployStrategyVoterProxy(address _voter) public returns (address) { AngleStrategyVoterProxy _voterProxy = new AngleStrategyVoterProxy(_voter); - _voterProxy.setGovernance(gov); + //_voterProxy.setGovernance(gov); return address(_voterProxy); } function deployAngleVoter() public returns (address) { YearnAngleVoter _voter = new YearnAngleVoter(); - _voter.setGovernance(gov); + //_voter.setGovernance(gov); return address(_voter); } From 409617698c7cdf352fcaa0191aac2e5c98aef388 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Tue, 28 Jun 2022 11:02:40 +0200 Subject: [PATCH 14/45] fix: fix multiple strategies using stablemaster --- Makefile | 1 + src/AngleStrategyVoterProxy.sol | 8 ++++---- src/Strategy.sol | 6 ++++-- src/test/utils/StrategyFixture.sol | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 71f099c..b495037 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ constructor-args := build :; forge build test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-operation :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testProfitableHarvest_AngleFarmingProfit" +test-debug :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testMigration" test-all-operation :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-contract "StrategyOperation" trace :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-contract :; forge test -vv --fork-url ${FORK_URL} --match-contract $(contract) --etherscan-api-key ${ETHERSCAN_API_KEY} diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index cbc7a63..1613602 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -103,8 +103,8 @@ contract AngleStrategyVoterProxy { } function withdrawFromStableMaster(address stableMaster, uint256 amount, - address poolManager, address token) external { - require(strategies[stableMaster] == msg.sender, "!strategy"); + address poolManager, address token, address gauge) external { + require(strategies[gauge] == msg.sender, "!strategy"); IERC20(token).safeTransfer(address(yearnAngleVoter), amount); @@ -142,8 +142,8 @@ contract AngleStrategyVoterProxy { } function depositToStableMaster(address stableMaster, uint256 amount, - address poolManager, address token) external { - require(strategies[stableMaster] == msg.sender, "!strategy"); + address poolManager, address token, address gauge) external { + require(strategies[gauge] == msg.sender, "!strategy"); IERC20(token).safeTransfer(address(yearnAngleVoter), amount); diff --git a/src/Strategy.sol b/src/Strategy.sol index c7d5620..9ff61a4 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -503,7 +503,8 @@ contract Strategy is BaseStrategy { address(angleStableMaster), _amount, poolManager, - address(want) + address(want), + address(sanTokenGauge) ); } @@ -512,7 +513,8 @@ contract Strategy is BaseStrategy { address(angleStableMaster), _amountInSanToken, poolManager, - address(sanToken) + address(sanToken), + address(sanTokenGauge) ); } diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index 7152437..6b30ed4 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -81,7 +81,7 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); voter.setProxy(_voterProxy); - string[2] memory _tokensToTest = ["USDC", "DAI"]; + string[1] memory _tokensToTest = ["USDC"]; for (uint8 i = 0; i < _tokensToTest.length; ++i) { string memory _tokenToTest = _tokensToTest[i]; @@ -190,8 +190,8 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); - vm.prank(gov); - voterProxy.approveStrategy(address(stableMaster), address(_strategy)); + // vm.prank(gov); + // voterProxy.approveStrategy(address(stableMaster), address(_strategy)); return address(_strategy); } From 31ff372b49ebe6c4d08b3883527688b30dcf2c07 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Tue, 5 Jul 2022 16:57:15 +0200 Subject: [PATCH 15/45] chore: fix translation errors --- Makefile | 2 +- src/AngleStrategyVoterProxy.sol | 4 ++- src/Strategy.sol | 4 +-- src/test/HandleAngleHack.t.sol | 10 +++++-- src/test/StrategyClone.t.sol | 48 +++++++++++++++++++----------- src/test/StrategyMigration.t.sol | 7 +++++ src/test/utils/StrategyFixture.sol | 4 +-- 7 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index b495037..8800433 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ constructor-args := build :; forge build test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-operation :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testProfitableHarvest_AngleFarmingProfit" -test-debug :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testMigration" +test-debug :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testStrategyClone" test-all-operation :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-contract "StrategyOperation" trace :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-contract :; forge test -vv --fork-url ${FORK_URL} --match-contract $(contract) --etherscan-api-key ${ETHERSCAN_API_KEY} diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 1613602..fc1922f 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -88,12 +88,14 @@ contract AngleStrategyVoterProxy { require(voters[msg.sender], "!voter"); yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("vote_for_gauge_weights(address,uint256)", _gauge, _amount)); } - + event fire(address _ad); function withdraw( address _gauge, address _token, uint256 _amount ) public returns (uint256) { + emit fire(msg.sender); + emit fire(strategies[_gauge]); require(strategies[_gauge] == msg.sender, "!strategy"); uint256 _balance = IERC20(_token).balanceOf(address(yearnAngleVoter)); yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("withdraw(uint256)", _amount)); diff --git a/src/Strategy.sol b/src/Strategy.sol index 9ff61a4..68c2f5a 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -310,7 +310,7 @@ contract Strategy is BaseStrategy { // transfers all tokens to new strategy function prepareMigration(address _newStrategy) internal override { // want is transferred by the base contract's migrate function - sanTokenGauge.withdraw(balanceOfStakedSanToken()); + // sanTokenGauge.withdraw(balanceOfStakedSanToken()); uint256 _angleBalance = balanceOfAngleToken(); if(_angleBalance > 0) { @@ -459,7 +459,7 @@ contract Strategy is BaseStrategy { } function balanceOfStakedSanToken() public view returns (uint256) { - strategyProxy.balanceOf(address(sanTokenGauge)); + return strategyProxy.balanceOf(address(sanTokenGauge)); } function balanceOfSanToken() public view returns (uint256) { diff --git a/src/test/HandleAngleHack.t.sol b/src/test/HandleAngleHack.t.sol index 2f04339..cdb1e26 100644 --- a/src/test/HandleAngleHack.t.sol +++ b/src/test/HandleAngleHack.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.12; import {StrategyFixture} from "./utils/StrategyFixture.sol"; import {IVault} from "../interfaces/Yearn/Vault.sol"; import {Strategy} from "../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../AngleStrategyVoterProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@yearnvaults/contracts/yToken.sol"; @@ -19,6 +20,7 @@ contract HandleAngleHackTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -29,6 +31,9 @@ contract HandleAngleHackTest is StrategyFixture { } deal(address(want), user, _amount); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); uint256 _balanceBefore = want.balanceOf(address(user)); @@ -48,8 +53,9 @@ contract HandleAngleHackTest is StrategyFixture { // We simulate a hack by sending away all of the strat's gauge tokens address sanTokenGauge = address(strategy.sanTokenGauge()); - vm.startPrank(address(strategy)); - IERC20(sanTokenGauge).transfer(address(0), IERC20(sanTokenGauge).balanceOf(address(strategy))); + address yearnVoter = address(voterProxy.yearnAngleVoter()); + vm.startPrank(address(yearnVoter)); + IERC20(sanTokenGauge).transfer(address(0), IERC20(sanTokenGauge).balanceOf(yearnVoter)); vm.stopPrank(); // skip(1); diff --git a/src/test/StrategyClone.t.sol b/src/test/StrategyClone.t.sol index 8cc12ad..a2e2e93 100644 --- a/src/test/StrategyClone.t.sol +++ b/src/test/StrategyClone.t.sol @@ -1,19 +1,15 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.12; -import "forge-std/console.sol"; import {StrategyFixture} from "./utils/StrategyFixture.sol"; import {IVault} from "../interfaces/Yearn/Vault.sol"; import {Strategy} from "../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../AngleStrategyVoterProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@yearnvaults/contracts/yToken.sol"; -import "../interfaces/Angle/IStableMaster.sol"; -import "../interfaces/Yearn/ITradeFactory.sol"; contract StrategyCloneTest is StrategyFixture { - // setup is run on before each test function setUp() public override { - // setup vault super.setUp(); } @@ -24,6 +20,7 @@ contract StrategyCloneTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -34,22 +31,31 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); - string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(user); + want.approve(address(vault), _amount); + + vm.prank(user); + vault.deposit(_amount); address _newStrategy = strategy.cloneAngle( address(vault), strategist, rewards, keeper, - sanTokenAddrs[_tokenSymbol], - gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol], + sanTokenAddrs[tokenSymbol], + gaugeAddrs[tokenSymbol], + poolManagerAddrs[tokenSymbol], address(voterProxy) ); vm.prank(gov); vault.migrateStrategy(address(strategy), _newStrategy); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(_newStrategy)); strategy = Strategy(_newStrategy); @@ -84,6 +90,7 @@ contract StrategyCloneTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -94,22 +101,25 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); - - string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); address _newStrategy = strategy.cloneAngle( address(vault), strategist, rewards, keeper, - sanTokenAddrs[_tokenSymbol], - gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol], + sanTokenAddrs[tokenSymbol], + gaugeAddrs[tokenSymbol], + poolManagerAddrs[tokenSymbol], address(voterProxy) ); vm.prank(gov); vault.migrateStrategy(address(strategy), _newStrategy); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(_newStrategy)); strategy = Strategy(_newStrategy); @@ -119,9 +129,9 @@ contract StrategyCloneTest is StrategyFixture { strategist, rewards, keeper, - sanTokenAddrs[_tokenSymbol], - gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol], + sanTokenAddrs[tokenSymbol], + gaugeAddrs[tokenSymbol], + poolManagerAddrs[tokenSymbol], address(voterProxy) ); } @@ -134,6 +144,7 @@ contract StrategyCloneTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -144,6 +155,9 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); diff --git a/src/test/StrategyMigration.t.sol b/src/test/StrategyMigration.t.sol index 36f46a9..49323f7 100644 --- a/src/test/StrategyMigration.t.sol +++ b/src/test/StrategyMigration.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.12; import {StrategyFixture} from "./utils/StrategyFixture.sol"; import {IVault} from "../interfaces/Yearn/Vault.sol"; import {Strategy} from "../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../AngleStrategyVoterProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@yearnvaults/contracts/yToken.sol"; // NOTE: if the name of the strat or file changes this needs to be updated @@ -20,6 +21,7 @@ contract StrategyMigrationTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -32,6 +34,9 @@ contract StrategyMigrationTest is StrategyFixture { deal(address(want), user, _amount); // Deposit to the vault and harvest + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); vm.prank(user); want.approve(address(vault), _amount); vm.prank(user); @@ -48,6 +53,8 @@ contract StrategyMigrationTest is StrategyFixture { strategy.claimRewards(); // manual claim rewards vm.prank(gov); vault.migrateStrategy(address(strategy), address(newStrategy)); + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(newStrategy)); assertRelApproxEq(newStrategy.estimatedTotalAssets(), _amount, DELTA); } } diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index 6b30ed4..d5a7138 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -188,8 +188,8 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); _strategy.setTradeFactory(address(tradeFactory)); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); + // vm.prank(gov); + // voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); // vm.prank(gov); // voterProxy.approveStrategy(address(stableMaster), address(_strategy)); From 9db840c17c0613153ef1143820dcfdc8431539be Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Tue, 5 Jul 2022 16:59:13 +0200 Subject: [PATCH 16/45] fix: remove debugging event --- src/AngleStrategyVoterProxy.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index fc1922f..1613602 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -88,14 +88,12 @@ contract AngleStrategyVoterProxy { require(voters[msg.sender], "!voter"); yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("vote_for_gauge_weights(address,uint256)", _gauge, _amount)); } - event fire(address _ad); + function withdraw( address _gauge, address _token, uint256 _amount ) public returns (uint256) { - emit fire(msg.sender); - emit fire(strategies[_gauge]); require(strategies[_gauge] == msg.sender, "!strategy"); uint256 _balance = IERC20(_token).balanceOf(address(yearnAngleVoter)); yearnAngleVoter.safeExecute(_gauge, 0, abi.encodeWithSignature("withdraw(uint256)", _amount)); From 88f18fdc4f3e7d45ac3f2c0ece17997ee53e1361 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:37:48 +0200 Subject: [PATCH 17/45] feat: add whitelister interface to test lock --- src/interfaces/Angle/IAngleWhitelister.sol | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/interfaces/Angle/IAngleWhitelister.sol diff --git a/src/interfaces/Angle/IAngleWhitelister.sol b/src/interfaces/Angle/IAngleWhitelister.sol new file mode 100644 index 0000000..23dca8e --- /dev/null +++ b/src/interfaces/Angle/IAngleWhitelister.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.12; + +interface IAngleWhitelister { + + function approveWallet(address _wallet) external; + +} \ No newline at end of file From 618637b2b364d11460d1e13aaf35d67e1efb6e19 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:38:11 +0200 Subject: [PATCH 18/45] feat: test lock & release veAngle --- src/test/AngleVoterLock.t.sol | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/test/AngleVoterLock.t.sol diff --git a/src/test/AngleVoterLock.t.sol b/src/test/AngleVoterLock.t.sol new file mode 100644 index 0000000..8516b7b --- /dev/null +++ b/src/test/AngleVoterLock.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.12; +import "forge-std/console.sol"; + +import {StrategyFixture} from "./utils/StrategyFixture.sol"; +import {IVault} from "../interfaces/Yearn/Vault.sol"; +import {Strategy} from "../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../AngleStrategyVoterProxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@yearnvaults/contracts/yToken.sol"; +import "../interfaces/Angle/IStableMaster.sol"; +import "../interfaces/Yearn/ITradeFactory.sol"; + +contract AngleVoterLock is StrategyFixture { + // setup is run on before each test + function setUp() public override { + // setup vault + super.setUp(); + } + + function testLockAngle(uint256 _fuzzAmount) public { + vm.assume(_fuzzAmount > minFuzzAmt && _fuzzAmount < maxFuzzAmt); + for(uint8 i = 0; i < assetFixtures.length; ++i) { + AssetFixture memory _assetFixture = assetFixtures[i]; + IVault vault = _assetFixture.vault; + Strategy strategy = _assetFixture.strategy; + IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); + + uint256 _amount = _fuzzAmount; + uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); + if (_wantDecimals != 18) { + console.log("Less than 18 decimals"); + uint256 _decimalDifference = 18 - _wantDecimals; + + _amount = _amount / (10 ** _decimalDifference); + } + deal(address(want), user, _amount); + + // Deposit to the vault + vm.prank(user); + want.approve(address(vault), _amount); + vm.prank(user); + vault.deposit(_amount); + assertRelApproxEq(want.balanceOf(address(vault)), _amount, DELTA); + + skip(1); + vm.prank(strategist); + strategy.harvest(); + assertRelApproxEq(strategy.estimatedTotalAssets(), _amount, DELTA); + + _mockSLPProfits(strategy); + + // Airdrop 1 angle for every $1000 + deal(address(angleToken), address(strategy), _fuzzAmount / 1000); + + uint256 _proxyAngleBalanceBefore = angleToken.balanceOf(address(voterProxy)); + vm.prank(strategist); + strategy.tend(); + uint256 _angleTokenBalance = strategy.balanceOfAngleToken(); + assertGt(_angleTokenBalance, 0); + + uint256 _proxyAngleBalanceAfter = angleToken.balanceOf(address(voterProxy)); + assertGt(_proxyAngleBalanceAfter, _proxyAngleBalanceBefore); + assertRelApproxEq(_proxyAngleBalanceAfter - _proxyAngleBalanceBefore, _angleTokenBalance / 9, DELTA); + + // 4 years + uint256 _unlockTime = block.timestamp + 4 * 365 * 86_400; + voterProxy.lock(_proxyAngleBalanceAfter / 2, _unlockTime); + uint256 _balance = veAngleToken.balanceOf(address(voter)); + assertGt(_balance, 0); + + // increase amount + voterProxy.increaseAmount(angleToken.balanceOf(address(voterProxy))); + assertGt(veAngleToken.balanceOf(address(voter)), _balance); + + skip(4 * 365 * 86_400 + 1); + vm.prank(gov); + voter.release(); + assert(veAngleToken.balanceOf(address(voter)) == 0); + assert(angleToken.balanceOf(address(voter)) == _proxyAngleBalanceAfter); + } + } +} \ No newline at end of file From 15ce260b71ea687f0d981838c0c4a127ac305a4e Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:38:49 +0200 Subject: [PATCH 19/45] chore: remove debugging make --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 8800433..71f099c 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,6 @@ constructor-args := build :; forge build test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-operation :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testProfitableHarvest_AngleFarmingProfit" -test-debug :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testStrategyClone" test-all-operation :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-contract "StrategyOperation" trace :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} test-contract :; forge test -vv --fork-url ${FORK_URL} --match-contract $(contract) --etherscan-api-key ${ETHERSCAN_API_KEY} From de4174db77c083efdcd9da2e6778048ab594d805 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:40:01 +0200 Subject: [PATCH 20/45] fix: remove approveStrategy as it's included in fixture --- src/test/HandleAngleHack.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/HandleAngleHack.t.sol b/src/test/HandleAngleHack.t.sol index cdb1e26..734dad1 100644 --- a/src/test/HandleAngleHack.t.sol +++ b/src/test/HandleAngleHack.t.sol @@ -31,9 +31,6 @@ contract HandleAngleHackTest is StrategyFixture { } deal(address(want), user, _amount); - string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); uint256 _balanceBefore = want.balanceOf(address(user)); From 507266937373ec59e99eb5a7efb26cf886a85e02 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:41:02 +0200 Subject: [PATCH 21/45] chore: removed unused deposit and approve --- src/test/StrategyClone.t.sol | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/test/StrategyClone.t.sol b/src/test/StrategyClone.t.sol index a2e2e93..9da1796 100644 --- a/src/test/StrategyClone.t.sol +++ b/src/test/StrategyClone.t.sol @@ -32,8 +32,8 @@ contract StrategyCloneTest is StrategyFixture { deal(address(want), user, _amount); string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); + + uint256 _balanceBefore = want.balanceOf(address(user)); vm.prank(user); want.approve(address(vault), _amount); @@ -59,14 +59,6 @@ contract StrategyCloneTest is StrategyFixture { strategy = Strategy(_newStrategy); - uint256 _balanceBefore = want.balanceOf(address(user)); - - vm.prank(user); - want.approve(address(vault), _amount); - - vm.prank(user); - vault.deposit(_amount); - skip(3 minutes); vm.prank(strategist); strategy.harvest(); @@ -102,8 +94,6 @@ contract StrategyCloneTest is StrategyFixture { deal(address(want), user, _amount); string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); address _newStrategy = strategy.cloneAngle( address(vault), @@ -155,9 +145,6 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); - string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); From 06b0dae0cb445456c8286be5b817ee33eab1af10 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:41:51 +0200 Subject: [PATCH 22/45] chore: remove unnecessary approve --- src/test/StrategyMigration.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/StrategyMigration.t.sol b/src/test/StrategyMigration.t.sol index 49323f7..7d2c7c4 100644 --- a/src/test/StrategyMigration.t.sol +++ b/src/test/StrategyMigration.t.sol @@ -35,8 +35,6 @@ contract StrategyMigrationTest is StrategyFixture { // Deposit to the vault and harvest string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - vm.prank(gov); - voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(strategy)); vm.prank(user); want.approve(address(vault), _amount); vm.prank(user); @@ -48,7 +46,7 @@ contract StrategyMigrationTest is StrategyFixture { // Migrate to a new strategy vm.prank(strategist); - Strategy newStrategy = Strategy(deployStrategy(address(vault), address(voterProxy), IERC20Metadata(address(want)).symbol())); + Strategy newStrategy = Strategy(deployStrategy(address(vault), address(voterProxy), IERC20Metadata(address(want)).symbol(), false)); vm.prank(gov); strategy.claimRewards(); // manual claim rewards vm.prank(gov); From aa72c3949a8aeb40687f5995cd4aeaf73132d832 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:42:28 +0200 Subject: [PATCH 23/45] fix: change test to check voter proxy instead of treasury --- src/test/StrategyOperation.t.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/StrategyOperation.t.sol b/src/test/StrategyOperation.t.sol index 4cff547..20c48de 100644 --- a/src/test/StrategyOperation.t.sol +++ b/src/test/StrategyOperation.t.sol @@ -5,6 +5,7 @@ import "forge-std/console.sol"; import {StrategyFixture} from "./utils/StrategyFixture.sol"; import {IVault} from "../interfaces/Yearn/Vault.sol"; import {Strategy} from "../Strategy.sol"; +import {AngleStrategyVoterProxy} from "../AngleStrategyVoterProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@yearnvaults/contracts/yToken.sol"; import "../interfaces/Angle/IStableMaster.sol"; @@ -151,6 +152,7 @@ contract StrategyOperationsTest is StrategyFixture { IVault vault = _assetFixture.vault; Strategy strategy = _assetFixture.strategy; IERC20 want = _assetFixture.want; + AngleStrategyVoterProxy voterProxy = strategy.strategyProxy(); uint256 _amount = _fuzzAmount; uint8 _wantDecimals = IERC20Metadata(address(want)).decimals(); @@ -181,14 +183,14 @@ contract StrategyOperationsTest is StrategyFixture { // Airdrop 1 angle for every $1000 deal(address(angleToken), address(strategy), _fuzzAmount / 1000); - uint256 _treasuryVaultAngleBalanceBefore = angleToken.balanceOf(yearnTreasuryVault); + uint256 _proxyAngleBalanceBefore = angleToken.balanceOf(address(voterProxy)); vm.prank(strategist); strategy.tend(); uint256 _angleTokenBalance = strategy.balanceOfAngleToken(); assertGt(_angleTokenBalance, 0); - uint256 _treasuryVaultAngleBalanceAfter = angleToken.balanceOf(yearnTreasuryVault); - assertGt(_treasuryVaultAngleBalanceAfter, _treasuryVaultAngleBalanceBefore); - assertRelApproxEq(_treasuryVaultAngleBalanceAfter - _treasuryVaultAngleBalanceBefore, _angleTokenBalance / 9, DELTA); + + assertGt(angleToken.balanceOf(address(voterProxy)), _proxyAngleBalanceBefore); + assertRelApproxEq(angleToken.balanceOf(address(voterProxy)) - _proxyAngleBalanceBefore, _angleTokenBalance / 9, DELTA); address _tokenIn = address(strategy.angleToken()); address _tokenOut = address(want); From 243575928b1e457b35374508d8a226216f034d65 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:43:01 +0200 Subject: [PATCH 24/45] feat: add boolean to approve and whitelist voter --- src/test/utils/StrategyFixture.sol | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index d5a7138..f8dba77 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -8,6 +8,7 @@ import {ExtendedTest} from "./ExtendedTest.sol"; import {Vm} from "forge-std/Vm.sol"; import {IVault} from "../../interfaces/Yearn/Vault.sol"; import "../../interfaces/Angle/IStableMaster.sol"; +import "../../interfaces/Angle/IAngleWhitelister.sol"; import "../../interfaces/Yearn/ITradeFactory.sol"; import {Strategy} from "../../Strategy.sol"; @@ -53,9 +54,12 @@ contract StrategyFixture is ExtendedTest { address public constant sushiswapSwapper = 0x408Ec47533aEF482DC8fA568c36EC0De00593f44; address public constant angleFeeManager = 0x97B6897AAd7aBa3861c04C0e6388Fc02AF1F227f; address public constant yearnTreasuryVault = 0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde; + address public constant angleWhitelisterAdmin = 0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8; + IAngleWhitelister angleWhiteLister = IAngleWhitelister(0xAa241Ccd398feC742f463c534a610529dCC5888E); ITradeFactory public constant tradeFactory = ITradeFactory(0x7BAF843e06095f68F4990Ca50161C2C4E4e01ec6); IERC20 public constant angleToken = IERC20(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + IERC20 public constant veAngleToken = IERC20(0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5); IStableMaster public constant stableMaster = IStableMaster(0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87); @@ -160,6 +164,8 @@ contract StrategyFixture is ExtendedTest { function deployAngleVoter() public returns (address) { YearnAngleVoter _voter = new YearnAngleVoter(); //_voter.setGovernance(gov); + vm.prank(angleWhitelisterAdmin); + angleWhiteLister.approveWallet(address(_voter)); return address(_voter); } @@ -168,7 +174,8 @@ contract StrategyFixture is ExtendedTest { function deployStrategy( address _vault, address _voterProxy, - string memory _tokenSymbol + string memory _tokenSymbol, + bool _addStrat ) public returns (address) { Strategy _strategy = new Strategy( _vault, @@ -188,10 +195,10 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); _strategy.setTradeFactory(address(tradeFactory)); - // vm.prank(gov); - // voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); - // vm.prank(gov); - // voterProxy.approveStrategy(address(stableMaster), address(_strategy)); + if(_addStrat == true) { + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); + } return address(_strategy); } @@ -225,7 +232,8 @@ contract StrategyFixture is ExtendedTest { _strategyAddr = deployStrategy( _vaultAddr, _voterProxy, - _tokenSymbol + _tokenSymbol, + true ); Strategy _strategy = Strategy(_strategyAddr); From a6aa5d6e35412a25507dbeca2fc829201ba789ee Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:51:46 +0200 Subject: [PATCH 25/45] fix: fix _withdrawSome() --- src/Strategy.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 68c2f5a..e99aaa6 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -291,15 +291,16 @@ contract Strategy is BaseStrategy { uint256 _sanTokenBalance = balanceOfSanToken(); if (_amountInSanToken > _sanTokenBalance) { + _amountInSanToken = Math.min(_amountInSanToken - _sanTokenBalance, balanceOfStakedSanToken()); strategyProxy.withdraw( address(sanTokenGauge), address(sanToken), - Math.min(_amountInSanToken - _sanTokenBalance, balanceOfStakedSanToken()) + _amountInSanToken ); + IERC20(sanToken).safeTransfer(address(strategyProxy), _amountInSanToken); } - IERC20(sanToken).safeTransfer(address(strategyProxy), _amountInSanToken); // Q: what does this do? Didn't want to remove it - withdrawFromStableMaster(Math.min(_amountInSanToken, balanceOfSanToken())); + withdrawFromStableMaster(Math.min(_amountInSanToken, _amountInSanToken)); } // can be used in conjunction with migration if this function is still working From cdbe6c4d2a65ddbebfc7da393070882a5020753e Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 7 Jul 2022 11:54:39 +0200 Subject: [PATCH 26/45] chore: remove .py tests --- tests/conftest.py | 349 ------------------------------------ tests/test_central_voter.py | 82 --------- tests/test_clone.py | 221 ----------------------- tests/test_edge_case.py | 67 ------- tests/test_migration.py | 84 --------- tests/test_operation.py | 262 --------------------------- tests/test_sequential.py | 75 -------- tests/test_shutdown.py | 63 ------- 8 files changed, 1203 deletions(-) delete mode 100644 tests/conftest.py delete mode 100644 tests/test_central_voter.py delete mode 100644 tests/test_clone.py delete mode 100644 tests/test_edge_case.py delete mode 100644 tests/test_migration.py delete mode 100644 tests/test_operation.py delete mode 100644 tests/test_sequential.py delete mode 100644 tests/test_shutdown.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 85c6981..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,349 +0,0 @@ -import pytest -from brownie import config, Contract, AngleStrategyProxy, YearnAngleVoter - -# # Snapshots the chain before each test and reverts after test completion. -@pytest.fixture(autouse=True) -def shared_setup(fn_isolation): - pass - - -@pytest.fixture(scope="module", autouse=True) -def gov(accounts): - yield accounts.at("0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", force=True) - - -@pytest.fixture(scope="module", autouse=True) -def rewards(accounts): - yield accounts[1] - - -@pytest.fixture(scope="module", autouse=True) -def guardian(accounts): - yield accounts[2] - - -@pytest.fixture(scope="module", autouse=True) -def management(accounts): - yield accounts[3] - - -@pytest.fixture(scope="module", autouse=True) -def strategist(accounts): - yield accounts[4] - - -@pytest.fixture(scope="module", autouse=True) -def keeper(accounts): - yield accounts[5] - - -@pytest.fixture(scope="module", autouse=True) -def alice(accounts): - yield accounts[6] - - -@pytest.fixture(scope="module", autouse=True) -def bob(accounts): - yield accounts[7] - - -@pytest.fixture(scope="module", autouse=True) -def tinytim(accounts): - yield accounts[8] - - -@pytest.fixture(scope="module", autouse=True) -def token_whale(accounts): - whale_address = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" # Curve pool - yield accounts.at(whale_address, force=True) - - -@pytest.fixture(scope="module", autouse=True) -def token(): - token_address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - yield Contract(token_address) - - -@pytest.fixture(scope="module", autouse=True) -def alice_amount(alice, token, token_whale): - amount = 4_000_000_000 - token.transfer(alice, amount, {"from": token_whale}) - yield amount - - -@pytest.fixture(scope="module", autouse=True) -def bob_amount(bob, token, token_whale): - amount = 1_000_000_000 - token.transfer(bob, amount, {"from": token_whale}) - yield amount - - -@pytest.fixture(scope="module", autouse=True) -def tinytim_amount(tinytim, token, token_whale): - amount = 10_000_000 - token.transfer(tinytim, amount, {"from": token_whale}) - yield amount - - -@pytest.fixture(scope="module", autouse=True) -def vault(pm, gov, rewards, guardian, management, token): - Vault = pm(config["dependencies"][0]).Vault - vault = guardian.deploy(Vault) - vault.initialize(token, gov, rewards, "", "", guardian, {"from": gov}) - vault.setDepositLimit(2 ** 256 - 1, {"from": gov}) - vault.setManagement(management, {"from": gov}) - yield vault - -@pytest.fixture(scope ="module") -def strategy_proxy( - angle_voter, - gov, - AngleStrategyProxy -): - strategy_proxy = gov.deploy(AngleStrategyProxy, angle_voter.address) - yield strategy_proxy - - -@pytest.fixture(scope ="module") -def angle_voter( - gov, - YearnAngleVoter - ): - voter = gov.deploy(YearnAngleVoter) - yield voter - - -@pytest.fixture(scope="module", autouse=True) -def strategy( - strategist, - guardian, - keeper, - vault, - StrategyAngleUSDC, - gov, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy, - angle_voter -): - strategy = strategist.deploy( - StrategyAngleUSDC, - vault, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address - ) - strategy.setKeeper(keeper, {"from": gov}) - vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) - strategy_proxy.approveStrategy(san_token_gauge, strategy, {"from": gov}) - strategy_proxy.approveStrategy(angle_stable_master, strategy, {"from": gov}) - angle_voter.setProxy(strategy_proxy, {"from": gov}) - yield strategy - - -# sushiswap router -@pytest.fixture(scope="module", autouse=True) -def uni(): - yield Contract("0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F") - - -# USDC san_token -@pytest.fixture(scope="module", autouse=True) -def san_token(): - yield Contract.from_explorer("0x9C215206Da4bf108aE5aEEf9dA7caD3352A36Dad") - - -# USDC san_token whale -@pytest.fixture(scope="module", autouse=True) -def san_token_whale(accounts): - address = "0x51fE22abAF4a26631b2913E417c0560D547797a7" # USDC san_token guage - yield accounts.at(address, force=True) - - -@pytest.fixture(scope="module", autouse=True) -def angle_token(): - yield Contract("0x31429d1856aD1377A8A0079410B297e1a9e214c2") - - -@pytest.fixture(scope="module", autouse=True) -def angle_token_whale(accounts): - yield accounts.at("0xe02F8E39b8cFA7d3b62307E46077669010883459", force=True) - - -@pytest.fixture(scope="module", autouse=True) -def veangle_token(): - yield Contract("0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5") - - -# stable manager front -@pytest.fixture(scope="module", autouse=True) -def angle_stable_master(): - yield Contract("0x5adDc89785D75C86aB939E9e15bfBBb7Fc086A87") - - -# usdc stake -@pytest.fixture(scope="module", autouse=True) -def san_token_gauge(): - yield Contract("0x51fE22abAF4a26631b2913E417c0560D547797a7") - - -@pytest.fixture(scope="module", autouse=True) -def pool_manager(): - yield Contract("0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD") - - -@pytest.fixture(scope="module", autouse=True) -def live_yearn_treasury(): - address = "0x93A62dA5a14C80f265DAbC077fCEE437B1a0Efde" - yield Contract(address) - - -@pytest.fixture(scope="module", autouse=True) -def newstrategy( - strategist, - guardian, - keeper, - vault, - StrategyAngleUSDC, - gov, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy, - angle_voter -): - newstrategy = guardian.deploy( - StrategyAngleUSDC, - vault, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - ) - newstrategy.setKeeper(keeper) - strategy_proxy.approveStrategy(san_token_gauge, newstrategy, {"from": gov}) - strategy_proxy.approveStrategy(angle_stable_master, newstrategy, {"from": gov}) - angle_voter.setProxy(strategy_proxy, {"from": gov}) - yield newstrategy - - -@pytest.fixture(scope="module", autouse=True) -def fxs_liquidity(accounts): - yield accounts.at("0xf977814e90da44bfa03b6295a0616a897441acec", force=True) - - -@pytest.fixture(scope="module", autouse=True) -def token_owner(accounts): - yield accounts.at("0x8412ebf45bac1b340bbe8f318b928c466c4e39ca", force=True) - - -@pytest.fixture(scope="module", autouse=True) -def angle_gov(accounts): - address = "0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8" - yield accounts.at(address, force=True) - - -@pytest.fixture(scope="module", autouse=True) -def angle_fee_manager(accounts): - address = "0x97B6897AAd7aBa3861c04C0e6388Fc02AF1F227f" - yield accounts.at(address, force=True) - - -@pytest.fixture(scope="module", autouse=True) -def pool_manager_account(accounts): - address = "0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD" - yield accounts.at(address, force=True) - - -@pytest.fixture(scope="session") -def BASE_PARAMS(): - yield 1000000000 - - -@pytest.fixture(scope="module", autouse=True) -def utils( - chain, - pool_manager_account, - pool_manager, - angle_stable_master, - strategy, - vault, - gov, - token, - san_token_gauge, - san_token, -): - return Utils( - chain, - pool_manager_account, - pool_manager, - angle_stable_master, - strategy, - vault, - gov, - token, - san_token_gauge, - san_token, - ) - - -class Utils: - def __init__( - self, - chain, - pool_manager_account, - pool_manager, - angle_stable_master, - strategy, - vault, - gov, - token, - san_token_gauge, - san_token, - ): - self.chain = chain - self.pool_manager_account = pool_manager_account - self.pool_manager = pool_manager - self.angle_stable_master = angle_stable_master - self.strategy = strategy - self.vault = vault - self.gov = gov - self.token = token - self.san_token_gauge = san_token_gauge - self.san_token = san_token - - def mock_angle_slp_profits(self): - max_profits_per_block = self.angle_stable_master.collateralMap( - self.pool_manager - )[7][2] - i = 0 - while i < 100: - self.angle_stable_master.accumulateInterest( - max_profits_per_block, {"from": self.pool_manager_account} - ) - self.chain.mine(1) - self.chain.sleep(1) - i += 1 - - def set_0_vault_fees(self): - self.vault.setManagementFee(0, {"from": self.gov}) - self.vault.setPerformanceFee(0, {"from": self.gov}) - - def assert_strategy_contains_no_tokens(self): - assert self.token.balanceOf(self.strategy) == 0 - assert self.san_token_gauge.balanceOf(self.strategy) == 0 - assert self.san_token.balanceOf(self.strategy) == 0 diff --git a/tests/test_central_voter.py b/tests/test_central_voter.py deleted file mode 100644 index acf057f..0000000 --- a/tests/test_central_voter.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest -from brownie import Contract - -@pytest.mark.require_network("mainnet-fork") -def test_central_voter( - chain, - vault, - strategy, - token, - gov, - strategist, - live_yearn_treasury, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - angle_token, - veangle_token, - san_token, - san_token_gauge, - utils, - angle_stable_master, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - # users deposit to vault - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - utils.set_0_vault_fees() - - assert san_token.balanceOf(strategy) == 0 - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(strategy_proxy) == 0 - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - previous_voter_angle = angle_token.balanceOf(angle_voter) - previous_proxy_angle = angle_token.balanceOf(strategy_proxy) - - strategy.harvest({"from": strategist}) - - assert angle_token.balanceOf(angle_voter) == previous_voter_angle - assert angle_token.balanceOf(strategy_proxy) - previous_proxy_angle > 0 - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - whitelister = Contract(veangle_token.smart_wallet_checker()) - whitelister.approveWallet(angle_voter, {"from": whitelister.admin()}) - - angle_balance = angle_token.balanceOf(strategy_proxy) - lock_now = int(angle_balance / 2) - - unlock_time = chain.time() + 4*86400*365 - strategy_proxy.lock(lock_now, unlock_time, {"from": gov}) - - assert veangle_token.balanceOf(angle_voter) > 0 - assert veangle_token.locked(angle_voter)[0] == lock_now - assert veangle_token.locked(angle_voter)[1] - chain.time() > 4*86400*364 - - lock_rest = angle_balance - lock_now - strategy_proxy.increaseAmount(lock_rest, {"from": gov}) - assert veangle_token.locked(angle_voter)[0] == angle_balance - - utils.mock_angle_slp_profits() - - strategy.harvest({"from": strategist}) - \ No newline at end of file diff --git a/tests/test_clone.py b/tests/test_clone.py deleted file mode 100644 index 9f30db1..0000000 --- a/tests/test_clone.py +++ /dev/null @@ -1,221 +0,0 @@ -from brownie import Contract, reverts -import pytest - - -def test_clone( - strategy, - vault, - strategist, - gov, - token, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - chain, - san_token_gauge, - san_token, - angle_token, - uni, - angle_stable_master, - pool_manager, - strategy_proxy, - angle_voter -): - clone_tx = strategy.cloneAngle( - vault, - strategist, - strategist, - strategist, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - {"from": strategist}, - ) - cloned_strategy = Contract.from_abi( - "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi - ) - strategy_proxy.approveStrategy(san_token_gauge, cloned_strategy.address, {"from": gov}) - strategy_proxy.approveStrategy(angle_stable_master, cloned_strategy.address, {"from": gov}) - vault.migrateStrategy(strategy.address, cloned_strategy.address, {"from": gov}) - strategy = cloned_strategy - - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - # users deposit to vault - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - vault.setManagementFee(0, {"from": gov}) - vault.setPerformanceFee(0, {"from": gov}) - - assert san_token.balanceOf(strategy) == 0 - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - chain.sleep(3600 * 24 * 2) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - pps_after_first_harvest = vault.pricePerShare() - - # 6 hours for pricepershare to go up, there should be profit - strategy.harvest({"from": gov}) - chain.sleep(3600 * 24 * 2) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - pps_after_second_harvest = vault.pricePerShare() - assert pps_after_second_harvest > pps_after_first_harvest - - # 6 hours for pricepershare to go up - strategy.harvest({"from": gov}) - chain.sleep(3600 * 24 * 1) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - alice_vault_balance = vault.balanceOf(alice) - vault.withdraw(alice_vault_balance, alice, 75, {"from": alice}) - assert token.balanceOf(alice) > 0 - assert token.balanceOf(bob) == 0 - # assert frax.balanceOf(strategy) > 0 - - # 6 hours for pricepershare to go up - strategy.harvest({"from": gov}) - chain.sleep(3600 * 24 * 1) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - bob_vault_balance = vault.balanceOf(bob) - vault.withdraw(bob_vault_balance, bob, 75, {"from": bob}) - assert token.balanceOf(bob) > 0 - # assert usdc.balanceOf(strategy) == 0 - - # 6 hours for pricepershare to go up - strategy.harvest({"from": gov}) - chain.sleep(3600 * 24 * 1) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - tt_vault_balance = vault.balanceOf(tinytim) - vault.withdraw(tt_vault_balance, tinytim, 75, {"from": tinytim}) - assert token.balanceOf(tinytim) > 0 - # assert usdc.balanceOf(strategy) == 0 - - # We should have made profit - assert vault.pricePerShare() > 1e6 - - -def test_clone_of_clone( - strategy, - vault, - strategist, - gov, - san_token_gauge, - san_token, - angle_token, - uni, - angle_stable_master, - pool_manager, - strategy_proxy -): - clone_tx = strategy.cloneAngle( - vault, - strategist, - strategist, - strategist, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - {"from": strategist}, - ) - cloned_strategy = Contract.from_abi( - "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi - ) - - vault.migrateStrategy(strategy.address, cloned_strategy.address, {"from": gov}) - - # should not clone a clone - with reverts(): - cloned_strategy.cloneAngle( - vault, - strategist, - strategist, - strategist, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - {"from": strategist}, - ) - - -def test_double_initialize( - strategy, - vault, - strategist, - gov, - san_token_gauge, - san_token, - angle_token, - uni, - angle_stable_master, - pool_manager, - strategy_proxy -): - clone_tx = strategy.cloneAngle( - vault, - strategist, - strategist, - strategist, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - {"from": strategist}, - ) - cloned_strategy = Contract.from_abi( - "StrategyAngleUSDC", clone_tx.events["Cloned"]["clone"], strategy.abi - ) - - # should not be able to call initialize twice - with reverts("Strategy already initialized"): - cloned_strategy.initialize( - vault, - strategist, - strategist, - strategist, - san_token, - angle_token, - uni, - angle_stable_master, - san_token_gauge, - pool_manager, - strategy_proxy.address, - {"from": strategist}, - ) diff --git a/tests/test_edge_case.py b/tests/test_edge_case.py deleted file mode 100644 index fe566e8..0000000 --- a/tests/test_edge_case.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest -from brownie import ZERO_ADDRESS - - -@pytest.mark.require_network("mainnet-fork") -def test_angle_hack( - chain, - token, - vault, - alice, - alice_amount, - strategy, - san_token, - strategist, - san_token_gauge, - angle_stable_master, - gov, - BASE_PARAMS, - angle_fee_manager, - utils, - angle_token, - live_yearn_treasury, - accounts, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - - vault.deposit(alice_amount, {"from": alice}) - - assert san_token.balanceOf(strategy) == 0 - - utils.set_0_vault_fees() - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - chain.sleep(10 ** 10) - chain.mine(100) - - before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) - strategy.harvest({"from": strategist}) - after_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) - assert after_harvest_proxy_rewards_bal > before_harvest_proxy_rewards_bal - - chain.sleep(3600 * 24 * 2) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - # Here's the hack part, where we fake a hack by sending away all of the strat's gauge tokens - san_token_gauge.transfer( - ZERO_ADDRESS, san_token_gauge.balanceOf(angle_voter), {"from": angle_voter} - ) - - strategy.setDoHealthCheck(False, {"from": gov}) - strategy.harvest({"from": strategist}) - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one < assets_at_t - - vault.withdraw({"from": alice}) - - assert token.balanceOf(alice) < alice_amount diff --git a/tests/test_migration.py b/tests/test_migration.py deleted file mode 100644 index ef8642c..0000000 --- a/tests/test_migration.py +++ /dev/null @@ -1,84 +0,0 @@ -# TODO: Add tests here that show the normal operation of this strategy -# Suggestions to include: -# - strategy loading and unloading (via Vault addStrategy/revokeStrategy) -# - change in loading (from low to high and high to low) -# - strategy operation at different loading levels (anticipated and "extreme") - -import pytest - -from brownie import Wei, accounts, Contract, config -from brownie import StrategyAngleUSDC - - -@pytest.mark.require_network("mainnet-fork") -def test_migration( - chain, - vault, - strategy, - token, - gov, - strategist, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - angle_token, - angle_stable_master, - san_token, - san_token_gauge, - pool_manager, - newstrategy, - utils, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - # users deposit to vault - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - utils.set_0_vault_fees() - - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(newstrategy) == 0 - assert san_token_gauge.balanceOf(angle_voter) > 0 - - newstrategy.setStrategist(strategist) - - san_token_balance = strategy.balanceOfStake() - assets = strategy.estimatedTotalAssets() - assert san_token_balance > 0 - assert san_token.balanceOf(strategy) == 0 - assert san_token.balanceOf(newstrategy) == 0 - vault.migrateStrategy(strategy, newstrategy, {"from": gov}) - assert san_token.balanceOf(strategy) == 0 - assert san_token.balanceOf(newstrategy) == 0 - assert san_token.balanceOf(strategy_proxy) == 0 - assert strategy.balanceOfStake() == 0 - assert newstrategy.balanceOfStake() == 0 - strategy_proxy.approveStrategy(san_token_gauge, newstrategy.address, {"from": gov}) - strategy_proxy.approveStrategy(angle_stable_master, newstrategy.address, {"from": gov}) - assert pytest.approx(newstrategy.estimatedTotalAssets(),rel=1e-3) == assets - - newstrategy.harvest({"from": strategist}) - assert san_token.balanceOf(newstrategy) == 0 - assert san_token_gauge.balanceOf(newstrategy) == 0 - assert san_token_gauge.balanceOf(angle_voter) > 0 - assert token.balanceOf(newstrategy) == 0 diff --git a/tests/test_operation.py b/tests/test_operation.py deleted file mode 100644 index bf752ff..0000000 --- a/tests/test_operation.py +++ /dev/null @@ -1,262 +0,0 @@ -from datetime import timedelta -import pytest - -from brownie import Wei, accounts, Contract, config, ZERO_ADDRESS -from brownie import StrategyAngleUSDC - - -@pytest.mark.require_network("mainnet-fork") -def test_operation( - chain, - vault, - strategy, - token, - gov, - strategist, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - san_token, - san_token_gauge, - utils, - angle_stable_master, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - # users deposit to vault - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - utils.set_0_vault_fees() - - assert san_token.balanceOf(strategy) == 0 - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(strategy_proxy) == 0 - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - strategy.harvest({"from": strategist}) - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - chain.mine(1, timedelta=100) - strategy.harvest({"from": strategist}) - chain.mine(1) - - vault.withdraw({"from": alice}) - assert token.balanceOf(alice) > alice_amount - assert token.balanceOf(bob) == 0 - - vault.withdraw({"from": bob}) - assert token.balanceOf(bob) > bob_amount - - vault.withdraw({"from": tinytim}) - assert token.balanceOf(tinytim) > tinytim_amount - - -@pytest.mark.require_network("mainnet-fork") -def test_lossy_strat( - token, - vault, - alice, - alice_amount, - strategy, - san_token, - strategist, - san_token_gauge, - angle_stable_master, - gov, - BASE_PARAMS, - angle_fee_manager, - utils, - chain, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - - vault.deposit(alice_amount, {"from": alice}) - - assert san_token.balanceOf(strategy) == 0 - - utils.set_0_vault_fees() - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(strategy) == 0 - assert san_token_gauge.balanceOf(strategy_proxy) == 0 - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - # As a technique to simulate losses, we increase slippage - angle_stable_master.setFeeKeeper( - BASE_PARAMS, BASE_PARAMS, BASE_PARAMS / 100, 0, {"from": angle_fee_manager} - ) # set SLP slippage to 1% - - strategy.setEmergencyExit( - {"from": gov} - ) # this will pull the assets out of angle, so we'll feel the slippage - strategy.setDoHealthCheck(False, {"from": gov}) - strategy.harvest({"from": gov}) - - vault.withdraw({"from": alice}) - - assert token.balanceOf(alice) < assets_at_t_plus_one - assert token.balanceOf(alice) < alice_amount - - -# In this situation, we incur slippage on the exit but the ANGLE rewards should compensate for this -@pytest.mark.require_network("mainnet-fork") -def test_almost_lossy_strat( - chain, - token, - vault, - alice, - alice_amount, - strategy, - san_token, - strategist, - san_token_gauge, - angle_stable_master, - gov, - BASE_PARAMS, - angle_fee_manager, - utils, - angle_token, - angle_token_whale, - live_yearn_treasury, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - - vault.deposit(alice_amount, {"from": alice}) - - assert san_token.balanceOf(strategy) == 0 - - utils.set_0_vault_fees() - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - chain.sleep(10 ** 10) - chain.mine(100) - - before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) - strategy.harvest({"from": strategist}) - - for _ in range(5): - angle_token.transfer( - strategy.address, 100 * 1e18, {"from": angle_token_whale} - ) # $100 top up - strategy.harvest({"from": strategist}) - chain.mine(1) - chain.sleep(1) - - after_harvest_proxyy_rewards_bal = angle_token.balanceOf(strategy_proxy) - assert after_harvest_proxyy_rewards_bal > before_harvest_proxy_rewards_bal - - chain.mine(1) - chain.sleep(1) - strategy.harvest({"from": strategist}) - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - # As a technique to simulate losses, we increase slippage - angle_stable_master.setFeeKeeper( - BASE_PARAMS, BASE_PARAMS, BASE_PARAMS / 1000, 0, {"from": angle_fee_manager} - ) # set SLP slippage to 0.01% (a bip) - - chain.sleep(3600 * 24 * 2) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - vault.withdraw(vault.balanceOf(alice), alice.address, 100, {"from": alice}) - - assert token.balanceOf(alice) > alice_amount - - -# We don't recieve any profit here other than angle rewards, and expect everything to work -@pytest.mark.require_network("mainnet-fork") -def test_harvest_angle_rewards( - chain, - token, - vault, - alice, - alice_amount, - strategy, - san_token, - strategist, - san_token_gauge, - angle_stable_master, - gov, - BASE_PARAMS, - angle_fee_manager, - utils, - angle_token, - live_yearn_treasury, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - - vault.deposit(alice_amount, {"from": alice}) - - assert san_token.balanceOf(strategy) == 0 - - utils.set_0_vault_fees() - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - chain.sleep(10 ** 10) - chain.mine(100) - - before_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) - strategy.harvest({"from": strategist}) - after_harvest_proxy_rewards_bal = angle_token.balanceOf(strategy_proxy) - assert after_harvest_proxy_rewards_bal > before_harvest_proxy_rewards_bal - - chain.sleep(3600 * 24 * 2) - chain.mine(1) - chain.sleep(3600 * 1) - chain.mine(1) - - strategy.harvest({"from": strategist}) - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - vault.withdraw({"from": alice}) - - assert token.balanceOf(alice) > alice_amount diff --git a/tests/test_sequential.py b/tests/test_sequential.py deleted file mode 100644 index 25010fa..0000000 --- a/tests/test_sequential.py +++ /dev/null @@ -1,75 +0,0 @@ -# TODO: Add tests here that show the normal operation of this strategy -# Suggestions to include: -# - strategy loading and unloading (via Vault addStrategy/revokeStrategy) -# - change in loading (from low to high and high to low) -# - strategy operation at different loading levels (anticipated and "extreme") - -import pytest - -from brownie import Wei, accounts, Contract, config -from brownie import StrategyAngleUSDC - - -@pytest.mark.require_network("mainnet-fork") -def test_sequential( - chain, - vault, - strategy, - token, - gov, - strategist, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - san_token_gauge, - utils, - angle_stable_master, - strategy_proxy, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - # users deposit to vault - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - vault.setManagementFee(0, {"from": gov}) - vault.setPerformanceFee(0, {"from": gov}) - - # First harvest - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - chain.sleep(1) - strategy.harvest({"from": strategist}) - chain.mine(1) - - alice_vault_balance = vault.balanceOf(alice) - vault.withdraw(alice_vault_balance, alice, 75, {"from": alice}) - assert token.balanceOf(alice) > 0 - assert token.balanceOf(bob) == 0 - # assert frax.balanceOf(strategy) > 0 - - bob_vault_balance = vault.balanceOf(bob) - vault.withdraw(bob_vault_balance, bob, 75, {"from": bob}) - assert token.balanceOf(bob) > 0 - # assert usdc.balanceOf(strategy) == 0 - - tt_vault_balance = vault.balanceOf(tinytim) - vault.withdraw(tt_vault_balance, tinytim, 75, {"from": tinytim}) - assert token.balanceOf(tinytim) > 0 - # assert usdc.balanceOf(strategy) == 0 diff --git a/tests/test_shutdown.py b/tests/test_shutdown.py deleted file mode 100644 index 56e4aeb..0000000 --- a/tests/test_shutdown.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest - - -@pytest.mark.require_network("mainnet-fork") -def test_shutdown( - chain, - vault, - strategy, - token, - gov, - strategist, - alice, - alice_amount, - bob, - bob_amount, - tinytim, - tinytim_amount, - san_token_gauge, - san_token, - utils, - angle_stable_master, - BASE_PARAMS, - angle_fee_manager, - angle_voter -): - token.approve(vault, 1_000_000_000_000, {"from": alice}) - token.approve(vault, 1_000_000_000_000, {"from": bob}) - token.approve(vault, 1_000_000_000_000, {"from": tinytim}) - - vault.deposit(alice_amount, {"from": alice}) - vault.deposit(bob_amount, {"from": bob}) - vault.deposit(tinytim_amount, {"from": tinytim}) - - utils.set_0_vault_fees() - - chain.sleep(1) - strategy.harvest({"from": strategist}) - - assert san_token_gauge.balanceOf(angle_voter) > 0 - assets_at_t = strategy.estimatedTotalAssets() - - utils.mock_angle_slp_profits() - strategy.harvest({"from": strategist}) - - assets_at_t_plus_one = strategy.estimatedTotalAssets() - assert assets_at_t_plus_one > assets_at_t - - strategy.setEmergencyExit({"from": gov}) - chain.sleep(1) - strategy.harvest({"from": strategist}) - chain.mine(1) - - vault.withdraw({"from": alice}) - assert token.balanceOf(alice) > alice_amount - assert token.balanceOf(bob) == 0 - - vault.withdraw({"from": bob}) - assert token.balanceOf(bob) > bob_amount - - vault.withdraw({"from": tinytim}) - assert token.balanceOf(tinytim) > tinytim_amount - - utils.assert_strategy_contains_no_tokens() From 915b8a4111321ce5235549efc596014d7a354495 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 11 Jul 2022 16:34:47 +0200 Subject: [PATCH 27/45] fix: remove unused cursor variable --- src/AngleStrategyVoterProxy.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 1613602..a7b0be9 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -38,8 +38,6 @@ contract AngleStrategyVoterProxy { mapping(address => bool) public voters; address public governance; - uint256 lastTimeCursor; - constructor(address _voter) public { governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); yearnAngleVoter = YearnAngleVoter(_voter); From dfc900d4ba4d50e45be3c1a05285fb79b7b9fea4 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 11 Jul 2022 16:36:32 +0200 Subject: [PATCH 28/45] chore: rename angle to angleToken --- src/AngleStrategyVoterProxy.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index a7b0be9..36473ed 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -31,7 +31,7 @@ contract AngleStrategyVoterProxy { using Address for address; YearnAngleVoter public yearnAngleVoter; - address public constant angle = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + address public constant angleToken = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); // gauge => strategies mapping(address => address) public strategies; @@ -70,14 +70,14 @@ contract AngleStrategyVoterProxy { function lock(uint256 amount, uint256 unlockTime) external { if (amount > 0) { - IERC20(angle).transfer(address(yearnAngleVoter), amount); + IERC20(angleToken).transfer(address(yearnAngleVoter), amount); yearnAngleVoter.createLock(amount, unlockTime); } } function increaseAmount(uint256 amount) external { if (amount > 0) { - IERC20(angle).transfer(address(yearnAngleVoter), amount); + IERC20(angleToken).transfer(address(yearnAngleVoter), amount); yearnAngleVoter.increaseAmount(amount); } } From 16f7e420f39790db6b3bbb7cc7e55c96ebde5c2c Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 16:32:38 +0200 Subject: [PATCH 29/45] feat: check allowance function --- src/AngleStrategyVoterProxy.sol | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 36473ed..e0f4588 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -106,9 +106,6 @@ contract AngleStrategyVoterProxy { IERC20(token).safeTransfer(address(yearnAngleVoter), amount); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, 0)); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, amount)); - yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( "withdraw(uint256,address,address,address)", amount, @@ -130,8 +127,7 @@ contract AngleStrategyVoterProxy { function deposit(address gauge, uint256 amount, address token) external { require(strategies[gauge] == msg.sender, "!strategy"); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", gauge, 0)); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", gauge, amount)); + _checkAllowance(token, gauge, amount); yearnAngleVoter.safeExecute(gauge, 0, abi.encodeWithSignature( "deposit(uint256)", @@ -145,8 +141,8 @@ contract AngleStrategyVoterProxy { IERC20(token).safeTransfer(address(yearnAngleVoter), amount); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, 0)); - yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, amount)); + _checkAllowance(token, stableMaster, amount); + yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( "deposit(uint256,address,address)", amount, @@ -170,4 +166,15 @@ contract AngleStrategyVoterProxy { function balanceOfSanToken(address sanToken) public view returns (uint256) { return IERC20(sanToken).balanceOf(address(yearnAngleVoter)); } + + function _checkAllowance( + address _token, + address _contract, + uint256 _amount + ) internal { + if (IERC20(_token).allowance(address(yearnAngleVoter), _contract) < _amount) { + yearnAngleVoter.safeExecute(_token, 0, abi.encodeWithSignature("approve(address,uint256)", _contract, 0)); + yearnAngleVoter.safeExecute(_token, 0, abi.encodeWithSignature("approve(address,uint256)", _contract, _amount)); + } + } } \ No newline at end of file From 432873a055607f54454d3904584b38518a1369b9 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 16:40:34 +0200 Subject: [PATCH 30/45] fix: remove token parameter from claimRewards --- src/AngleStrategyVoterProxy.sol | 3 ++- src/Strategy.sol | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index e0f4588..3536399 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -151,7 +151,7 @@ contract AngleStrategyVoterProxy { )); } - function claimRewards(address _gauge, address _token) external { + function claimRewards(address _gauge) external { require(strategies[_gauge] == msg.sender, "!strategy"); yearnAngleVoter.safeExecute( _gauge, @@ -160,6 +160,7 @@ contract AngleStrategyVoterProxy { IAngleGauge.claim_rewards.selector ) ); + address _token = address(angleToken); yearnAngleVoter.safeExecute(_token, 0, abi.encodeWithSignature("transfer(address,uint256)", msg.sender, IERC20(_token).balanceOf(address(yearnAngleVoter)))); } diff --git a/src/Strategy.sol b/src/Strategy.sol index e99aaa6..d411c73 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -220,7 +220,7 @@ contract Strategy is BaseStrategy { } // Claim rewards here so that we can chain tend() -> yswap sell -> harvest() in a single transaction - strategyProxy.claimRewards(address(sanTokenGauge), address(angleToken)); + strategyProxy.claimRewards(address(sanTokenGauge)); uint256 _tokensAvailable = balanceOfAngleToken(); if (_tokensAvailable > 0) { From 8c607ed7322b725f8c26e11429e6097220f31e78 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 16:44:32 +0200 Subject: [PATCH 31/45] chore: rename deposit to stake --- src/AngleStrategyVoterProxy.sol | 2 +- src/Strategy.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 3536399..afb6af7 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -124,7 +124,7 @@ contract AngleStrategyVoterProxy { return withdraw(_gauge, _token, balanceOf(_gauge)); } - function deposit(address gauge, uint256 amount, address token) external { + function stake(address gauge, uint256 amount, address token) external { require(strategies[gauge] == msg.sender, "!strategy"); _checkAllowance(token, gauge, amount); diff --git a/src/Strategy.sol b/src/Strategy.sol index d411c73..b814e82 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -249,7 +249,7 @@ contract Strategy is BaseStrategy { // Stake any san tokens, whether they originated through the above deposit or some other means (e.g. migration) uint256 _sanTokenBalance = balanceOfSanToken(); if (_sanTokenBalance > 0) { - strategyProxy.deposit(address(sanTokenGauge), _sanTokenBalance, address(sanToken)); + strategyProxy.stake(address(sanTokenGauge), _sanTokenBalance, address(sanToken)); } } From ada3a34446ee0a82b42a24102e485e8d8b2892bc Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 16:49:30 +0200 Subject: [PATCH 32/45] fix: typo in _withdrawSome --- src/Strategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index b814e82..03904d5 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -300,7 +300,7 @@ contract Strategy is BaseStrategy { IERC20(sanToken).safeTransfer(address(strategyProxy), _amountInSanToken); } - withdrawFromStableMaster(Math.min(_amountInSanToken, _amountInSanToken)); + withdrawFromStableMaster(_amountInSanToken); } // can be used in conjunction with migration if this function is still working From 8fb4d148cd8cdca5f587acc63ea3c9ec83cddc44 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 16:49:58 +0200 Subject: [PATCH 33/45] chore: remove comment and add space --- src/Strategy.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 03904d5..3f62722 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -311,10 +311,9 @@ contract Strategy is BaseStrategy { // transfers all tokens to new strategy function prepareMigration(address _newStrategy) internal override { // want is transferred by the base contract's migrate function - // sanTokenGauge.withdraw(balanceOfStakedSanToken()); uint256 _angleBalance = balanceOfAngleToken(); - if(_angleBalance > 0) { + if (_angleBalance > 0) { IERC20(angleToken).safeTransfer(_newStrategy, _angleBalance); } From d7571d97f113960cdd0fbb66293acb764d764be6 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 15 Jul 2022 17:03:08 +0200 Subject: [PATCH 34/45] feat: save gas approving angle from voter --- src/YearnAngleVoter.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index c80863c..10d3678 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -36,15 +36,13 @@ contract YearnAngleVoter { function createLock(uint256 _value, uint256 _unlockTime) external { require(msg.sender == proxy || msg.sender == governance, "!authorized"); - IERC20(angle).safeApprove(veAngle, 0); - IERC20(angle).safeApprove(veAngle, _value); + IERC20(angle).approve(veAngle, _value); IVoteEscrow(veAngle).create_lock(_value, _unlockTime); } function increaseAmount(uint _value) external { require(msg.sender == proxy || msg.sender == governance, "!authorized"); - IERC20(angle).safeApprove(veAngle, 0); - IERC20(angle).safeApprove(veAngle, _value); + IERC20(angle).approve(veAngle, _value); IVoteEscrow(veAngle).increase_amount(_value); } From f4e0c738c4fd34e36a20d5831371085f1660b101 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 18 Jul 2022 10:21:08 +0200 Subject: [PATCH 35/45] chore: rename balanceOf to balanceOfSTakedSanToken --- src/AngleStrategyVoterProxy.sol | 4 ++-- src/Strategy.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index afb6af7..dd34956 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -115,13 +115,13 @@ contract AngleStrategyVoterProxy { )); } - function balanceOf(address _gauge) public view returns (uint256) { + function balanceOfStakedSanToken(address _gauge) public view returns (uint256) { return IERC20(_gauge).balanceOf(address(yearnAngleVoter)); } function withdrawAll(address _gauge, address _token) external returns (uint256) { require(strategies[_gauge] == msg.sender, "!strategy"); - return withdraw(_gauge, _token, balanceOf(_gauge)); + return withdraw(_gauge, _token, balanceOfStakedSanToken(_gauge)); } function stake(address gauge, uint256 amount, address token) external { diff --git a/src/Strategy.sol b/src/Strategy.sol index 3f62722..45d62f1 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -459,7 +459,7 @@ contract Strategy is BaseStrategy { } function balanceOfStakedSanToken() public view returns (uint256) { - return strategyProxy.balanceOf(address(sanTokenGauge)); + return strategyProxy.balanceOfStakedSanToken(address(sanTokenGauge)); } function balanceOfSanToken() public view returns (uint256) { From 8c63ff11b8a7ea3d733e59fd8c909264340f6e3e Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 22 Jul 2022 18:40:06 +0200 Subject: [PATCH 36/45] feat: add access control to lock() function --- src/AngleStrategyVoterProxy.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index dd34956..76018d4 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -69,6 +69,7 @@ contract AngleStrategyVoterProxy { } function lock(uint256 amount, uint256 unlockTime) external { + require(msg.sender == governance, "!governance"); if (amount > 0) { IERC20(angleToken).transfer(address(yearnAngleVoter), amount); yearnAngleVoter.createLock(amount, unlockTime); @@ -76,6 +77,7 @@ contract AngleStrategyVoterProxy { } function increaseAmount(uint256 amount) external { + require(msg.sender == governance, "!governance"); if (amount > 0) { IERC20(angleToken).transfer(address(yearnAngleVoter), amount); yearnAngleVoter.increaseAmount(amount); From 528a4c41084ef8e80e5038ad99487e4949104578 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 22 Jul 2022 18:41:25 +0200 Subject: [PATCH 37/45] fix: replace fro withdrawAll in strategy --- src/Strategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 45d62f1..a7e3936 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -319,7 +319,7 @@ contract Strategy is BaseStrategy { uint256 _stakedBalance = balanceOfStakedSanToken(); if (_stakedBalance > 0) { - strategyProxy.withdraw(address(sanTokenGauge), address(sanToken), _stakedBalance); + strategyProxy.withdrawAll(address(sanTokenGauge), address(sanToken)); IERC20(sanToken).safeTransfer(address(strategyProxy), _stakedBalance); withdrawFromStableMaster(_stakedBalance); } From 8ba167e66bee955907102d39cc46b300f25d7736 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 22 Jul 2022 19:08:04 +0200 Subject: [PATCH 38/45] fix: necessary changes for access control lock to pass --- src/test/AngleVoterLock.t.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/AngleVoterLock.t.sol b/src/test/AngleVoterLock.t.sol index 8516b7b..4c088e1 100644 --- a/src/test/AngleVoterLock.t.sol +++ b/src/test/AngleVoterLock.t.sol @@ -66,12 +66,15 @@ contract AngleVoterLock is StrategyFixture { // 4 years uint256 _unlockTime = block.timestamp + 4 * 365 * 86_400; + vm.prank(gov); voterProxy.lock(_proxyAngleBalanceAfter / 2, _unlockTime); uint256 _balance = veAngleToken.balanceOf(address(voter)); assertGt(_balance, 0); // increase amount - voterProxy.increaseAmount(angleToken.balanceOf(address(voterProxy))); + uint256 toLock = angleToken.balanceOf(address(voterProxy)); + vm.prank(gov); + voterProxy.increaseAmount(toLock); assertGt(veAngleToken.balanceOf(address(voter)), _balance); skip(4 * 365 * 86_400 + 1); From c848384dd6cdfbd3006188a52f25adf1510fc7de Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 22 Jul 2022 19:29:11 +0200 Subject: [PATCH 39/45] feat: Angle rewards stay in voter and not in proxy --- src/AngleStrategyVoterProxy.sol | 6 ++---- src/Strategy.sol | 4 ++-- src/test/AngleVoterLock.t.sol | 14 +++++++------- src/test/StrategyOperation.t.sol | 6 +++--- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 76018d4..4e0b235 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -70,16 +70,14 @@ contract AngleStrategyVoterProxy { function lock(uint256 amount, uint256 unlockTime) external { require(msg.sender == governance, "!governance"); - if (amount > 0) { - IERC20(angleToken).transfer(address(yearnAngleVoter), amount); + if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { yearnAngleVoter.createLock(amount, unlockTime); } } function increaseAmount(uint256 amount) external { require(msg.sender == governance, "!governance"); - if (amount > 0) { - IERC20(angleToken).transfer(address(yearnAngleVoter), amount); + if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { yearnAngleVoter.increaseAmount(amount); } } diff --git a/src/Strategy.sol b/src/Strategy.sol index a7e3936..fe20140 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -227,7 +227,7 @@ contract Strategy is BaseStrategy { uint256 _tokensToKeep = (_tokensAvailable * percentKeep) / MAX_BPS; if (_tokensToKeep > 0) { - IERC20(angleToken).transfer(address(strategyProxy), _tokensToKeep); + IERC20(angleToken).transfer(address(strategyProxy.yearnAngleVoter()), _tokensToKeep); } } @@ -305,7 +305,7 @@ contract Strategy is BaseStrategy { // can be used in conjunction with migration if this function is still working function claimRewards() external onlyVaultManagers { - sanTokenGauge.claim_rewards(); + strategyProxy.claimRewards(address(sanTokenGauge)); } // transfers all tokens to new strategy diff --git a/src/test/AngleVoterLock.t.sol b/src/test/AngleVoterLock.t.sol index 4c088e1..fba0e8a 100644 --- a/src/test/AngleVoterLock.t.sol +++ b/src/test/AngleVoterLock.t.sol @@ -54,25 +54,25 @@ contract AngleVoterLock is StrategyFixture { // Airdrop 1 angle for every $1000 deal(address(angleToken), address(strategy), _fuzzAmount / 1000); - uint256 _proxyAngleBalanceBefore = angleToken.balanceOf(address(voterProxy)); + uint256 _voterAngleBalanceBefore = angleToken.balanceOf(address(voter)); vm.prank(strategist); strategy.tend(); uint256 _angleTokenBalance = strategy.balanceOfAngleToken(); assertGt(_angleTokenBalance, 0); - uint256 _proxyAngleBalanceAfter = angleToken.balanceOf(address(voterProxy)); - assertGt(_proxyAngleBalanceAfter, _proxyAngleBalanceBefore); - assertRelApproxEq(_proxyAngleBalanceAfter - _proxyAngleBalanceBefore, _angleTokenBalance / 9, DELTA); + uint256 _voterAngleBalanceAfter = angleToken.balanceOf(address(voter)); + assertGt(_voterAngleBalanceAfter, _voterAngleBalanceBefore); + assertRelApproxEq(_voterAngleBalanceAfter - _voterAngleBalanceBefore, _angleTokenBalance / 9, DELTA); // 4 years uint256 _unlockTime = block.timestamp + 4 * 365 * 86_400; vm.prank(gov); - voterProxy.lock(_proxyAngleBalanceAfter / 2, _unlockTime); + voterProxy.lock(_voterAngleBalanceAfter / 2, _unlockTime); uint256 _balance = veAngleToken.balanceOf(address(voter)); assertGt(_balance, 0); // increase amount - uint256 toLock = angleToken.balanceOf(address(voterProxy)); + uint256 toLock = angleToken.balanceOf(address(voter)); vm.prank(gov); voterProxy.increaseAmount(toLock); assertGt(veAngleToken.balanceOf(address(voter)), _balance); @@ -81,7 +81,7 @@ contract AngleVoterLock is StrategyFixture { vm.prank(gov); voter.release(); assert(veAngleToken.balanceOf(address(voter)) == 0); - assert(angleToken.balanceOf(address(voter)) == _proxyAngleBalanceAfter); + assert(angleToken.balanceOf(address(voter)) == _voterAngleBalanceAfter); } } } \ No newline at end of file diff --git a/src/test/StrategyOperation.t.sol b/src/test/StrategyOperation.t.sol index 20c48de..b9e5da8 100644 --- a/src/test/StrategyOperation.t.sol +++ b/src/test/StrategyOperation.t.sol @@ -183,14 +183,14 @@ contract StrategyOperationsTest is StrategyFixture { // Airdrop 1 angle for every $1000 deal(address(angleToken), address(strategy), _fuzzAmount / 1000); - uint256 _proxyAngleBalanceBefore = angleToken.balanceOf(address(voterProxy)); + uint256 _voterAngleBalanceBefore = angleToken.balanceOf(address(voter)); vm.prank(strategist); strategy.tend(); uint256 _angleTokenBalance = strategy.balanceOfAngleToken(); assertGt(_angleTokenBalance, 0); - assertGt(angleToken.balanceOf(address(voterProxy)), _proxyAngleBalanceBefore); - assertRelApproxEq(angleToken.balanceOf(address(voterProxy)) - _proxyAngleBalanceBefore, _angleTokenBalance / 9, DELTA); + assertGt(angleToken.balanceOf(address(voter)), _voterAngleBalanceBefore); + assertRelApproxEq(angleToken.balanceOf(address(voter)) - _voterAngleBalanceBefore, _angleTokenBalance / 9, DELTA); address _tokenIn = address(strategy.angleToken()); address _tokenOut = address(want); From 542f347a621794432f461d79b9e62ebe42de5918 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Fri, 22 Jul 2022 21:14:40 +0200 Subject: [PATCH 40/45] fix: hardcode unlock time and remove access control to locl --- src/AngleStrategyVoterProxy.sol | 8 ++++---- src/test/AngleVoterLock.t.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 4e0b235..822aff9 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -33,6 +33,8 @@ contract AngleStrategyVoterProxy { YearnAngleVoter public yearnAngleVoter; address public constant angleToken = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + uint256 public constant UNLOCK_TIME = 4 * 365 * 24 * 60 * 60; + // gauge => strategies mapping(address => address) public strategies; mapping(address => bool) public voters; @@ -68,15 +70,13 @@ contract AngleStrategyVoterProxy { voters[_voter] = false; } - function lock(uint256 amount, uint256 unlockTime) external { - require(msg.sender == governance, "!governance"); + function lock(uint256 amount) external { if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { - yearnAngleVoter.createLock(amount, unlockTime); + yearnAngleVoter.createLock(amount, block.timestamp + UNLOCK_TIME); } } function increaseAmount(uint256 amount) external { - require(msg.sender == governance, "!governance"); if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { yearnAngleVoter.increaseAmount(amount); } diff --git a/src/test/AngleVoterLock.t.sol b/src/test/AngleVoterLock.t.sol index fba0e8a..93a01c2 100644 --- a/src/test/AngleVoterLock.t.sol +++ b/src/test/AngleVoterLock.t.sol @@ -67,7 +67,7 @@ contract AngleVoterLock is StrategyFixture { // 4 years uint256 _unlockTime = block.timestamp + 4 * 365 * 86_400; vm.prank(gov); - voterProxy.lock(_voterAngleBalanceAfter / 2, _unlockTime); + voterProxy.lock(_voterAngleBalanceAfter / 2); uint256 _balance = veAngleToken.balanceOf(address(voter)); assertGt(_balance, 0); From 748466f27be8b5c7a9c161c8476454ae636bb05d Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Tue, 2 Aug 2022 13:57:10 +0100 Subject: [PATCH 41/45] fix: include force harvest trigger fix --- src/Strategy.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index fe20140..e342eb3 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -93,9 +93,7 @@ contract Strategy is BaseStrategy { harvestProfitMin = 2_000e6; harvestProfitMax = 10_000e6; creditThreshold = 1e6 * 1e18; - - IERC20(want).safeApprove(address(angleStableMaster), type(uint256).max); - IERC20(sanToken).safeApprove(_sanTokenGauge, type(uint256).max); + } function initialize( @@ -211,6 +209,9 @@ contract Strategy is BaseStrategy { _profit = _profit - _loss; _loss = 0; } + + // we're done harvesting, so reset our trigger if we used it + forceHarvestTriggerOnce = false; } // Deposit value & stake From 5c3038ea2276af44d6a8e5fdac88efb482d9a952 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 15 Aug 2022 16:29:56 +0200 Subject: [PATCH 42/45] chore: remove unused imports --- src/YearnAngleVoter.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol index 10d3678..3c1b014 100644 --- a/src/YearnAngleVoter.sol +++ b/src/YearnAngleVoter.sol @@ -2,16 +2,13 @@ pragma solidity ^0.8.12; pragma experimental ABIEncoderV2; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IVoteEscrow} from "./interfaces/Angle/IVoteEscrow.sol"; contract YearnAngleVoter { using SafeERC20 for IERC20; - using Address for address; address constant public angle = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); From ea483254048d5b194599ebf1a9b9d1cf7285327e Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 15 Aug 2022 18:46:00 +0200 Subject: [PATCH 43/45] feat: add revert message to safeExecute --- src/AngleStrategyVoterProxy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 822aff9..9278871 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -21,7 +21,7 @@ library SafeVoter { bytes memory data ) internal { (bool success, ) = voter.execute(to, value, data); - require(success); + require(success, "execute call returned False"); } } From cf81296fc599d91cfff8e2b6b0036ee042c81faa Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Mon, 15 Aug 2022 19:12:57 +0200 Subject: [PATCH 44/45] feat: clean migration process --- src/Strategy.sol | 19 +++---------------- src/test/StrategyMigration.t.sol | 2 ++ 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index e342eb3..cc7b741 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -311,22 +311,9 @@ contract Strategy is BaseStrategy { // transfers all tokens to new strategy function prepareMigration(address _newStrategy) internal override { - // want is transferred by the base contract's migrate function - - uint256 _angleBalance = balanceOfAngleToken(); - if (_angleBalance > 0) { - IERC20(angleToken).safeTransfer(_newStrategy, _angleBalance); - } - - uint256 _stakedBalance = balanceOfStakedSanToken(); - if (_stakedBalance > 0) { - strategyProxy.withdrawAll(address(sanTokenGauge), address(sanToken)); - IERC20(sanToken).safeTransfer(address(strategyProxy), _stakedBalance); - withdrawFromStableMaster(_stakedBalance); - } - - IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this))); - IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken()); + // Claim rewards is called externally + sweep by governance + // Governance can then revoke this strategy and approve the new one so the + // funds assigned to this gauge in the proxy are available } function protectedTokens() diff --git a/src/test/StrategyMigration.t.sol b/src/test/StrategyMigration.t.sol index 7d2c7c4..a1e8b2a 100644 --- a/src/test/StrategyMigration.t.sol +++ b/src/test/StrategyMigration.t.sol @@ -50,6 +50,8 @@ contract StrategyMigrationTest is StrategyFixture { vm.prank(gov); strategy.claimRewards(); // manual claim rewards vm.prank(gov); + voterProxy.revokeStrategy(gaugeAddrs[tokenSymbol]); + vm.prank(gov); vault.migrateStrategy(address(strategy), address(newStrategy)); vm.prank(gov); voterProxy.approveStrategy(gaugeAddrs[tokenSymbol], address(newStrategy)); From 6937fad89feb03a42deb893e635b5eafcdd5a5f9 Mon Sep 17 00:00:00 2001 From: 16slim <16slimchance16@gmail.com> Date: Thu, 25 Aug 2022 17:23:00 +0200 Subject: [PATCH 45/45] feat: include message error in safeExecute --- src/AngleStrategyVoterProxy.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol index 9278871..8a1f487 100644 --- a/src/AngleStrategyVoterProxy.sol +++ b/src/AngleStrategyVoterProxy.sol @@ -20,8 +20,8 @@ library SafeVoter { uint256 value, bytes memory data ) internal { - (bool success, ) = voter.execute(to, value, data); - require(success, "execute call returned False"); + (bool success, bytes memory result) = voter.execute(to, value, data); + require(success, string(result)); } }