diff --git a/src/AngleStrategyVoterProxy.sol b/src/AngleStrategyVoterProxy.sol new file mode 100644 index 0000000..8a1f487 --- /dev/null +++ b/src/AngleStrategyVoterProxy.sol @@ -0,0 +1,181 @@ +// 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, bytes memory result) = voter.execute(to, value, data); + require(success, string(result)); + } +} + +contract AngleStrategyVoterProxy { + using SafeVoter for YearnAngleVoter; + using SafeERC20 for IERC20; + using Address for address; + + 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; + address public governance; + + constructor(address _voter) public { + governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); + 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(uint256 amount) external { + if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { + yearnAngleVoter.createLock(amount, block.timestamp + UNLOCK_TIME); + } + } + + function increaseAmount(uint256 amount) external { + if (amount > 0 && amount <= IERC20(angleToken).balanceOf(address(yearnAngleVoter))) { + 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, address gauge) external { + require(strategies[gauge] == msg.sender, "!strategy"); + + IERC20(token).safeTransfer(address(yearnAngleVoter), amount); + + yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( + "withdraw(uint256,address,address,address)", + amount, + address(yearnAngleVoter), + msg.sender, + poolManager + )); + } + + 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, balanceOfStakedSanToken(_gauge)); + } + + function stake(address gauge, uint256 amount, address token) external { + require(strategies[gauge] == msg.sender, "!strategy"); + + _checkAllowance(token, gauge, amount); + + yearnAngleVoter.safeExecute(gauge, 0, abi.encodeWithSignature( + "deposit(uint256)", + amount + )); + } + + function depositToStableMaster(address stableMaster, uint256 amount, + address poolManager, address token, address gauge) external { + require(strategies[gauge] == msg.sender, "!strategy"); + + IERC20(token).safeTransfer(address(yearnAngleVoter), amount); + + _checkAllowance(token, stableMaster, amount); + + yearnAngleVoter.safeExecute(stableMaster, 0, abi.encodeWithSignature( + "deposit(uint256,address,address)", + amount, + address(yearnAngleVoter), + poolManager + )); + } + + function claimRewards(address _gauge) external { + require(strategies[_gauge] == msg.sender, "!strategy"); + yearnAngleVoter.safeExecute( + _gauge, + 0, + abi.encodeWithSelector( + IAngleGauge.claim_rewards.selector + ) + ); + address _token = address(angleToken); + 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)); + } + + 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 diff --git a/src/Strategy.sol b/src/Strategy.sol index b559b35..cc7b741 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; @@ -85,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( @@ -97,13 +103,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 +122,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 +150,8 @@ contract Strategy is BaseStrategy { _keeper, _sanToken, _sanTokenGauge, - _poolManager + _poolManager, + _strategyProxy ); emit Cloned(newStrategy); @@ -199,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 @@ -208,14 +221,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)); uint256 _tokensAvailable = balanceOfAngleToken(); if (_tokensAvailable > 0) { - uint256 _tokensToGov = + uint256 _tokensToKeep = (_tokensAvailable * percentKeep) / MAX_BPS; - if (_tokensToGov > 0) { - angleToken.transfer(treasury, _tokensToGov); + if (_tokensToKeep > 0) { + IERC20(angleToken).transfer(address(strategyProxy.yearnAngleVoter()), _tokensToKeep); } } @@ -230,13 +243,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.stake(address(sanTokenGauge), _sanTokenBalance, address(sanToken)); } } @@ -278,26 +292,28 @@ contract Strategy is BaseStrategy { uint256 _sanTokenBalance = balanceOfSanToken(); if (_amountInSanToken > _sanTokenBalance) { - sanTokenGauge.withdraw( - Math.min(_amountInSanToken - _sanTokenBalance, balanceOfStakedSanToken()) + _amountInSanToken = Math.min(_amountInSanToken - _sanTokenBalance, balanceOfStakedSanToken()); + strategyProxy.withdraw( + address(sanTokenGauge), + address(sanToken), + _amountInSanToken ); + IERC20(sanToken).safeTransfer(address(strategyProxy), _amountInSanToken); } - withdrawFromStableMaster(Math.min(_amountInSanToken, balanceOfSanToken())); + withdrawFromStableMaster(_amountInSanToken); } // 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 function prepareMigration(address _newStrategy) internal override { - // want is transferred by the base contract's migrate function - sanTokenGauge.withdraw(balanceOfStakedSanToken()); - - IERC20(sanToken).safeTransfer(_newStrategy, balanceOfSanToken()); - 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() @@ -424,7 +440,6 @@ contract Strategy is BaseStrategy { percentKeep = _percentKeep; } - // ----------------- SUPPORT & UTILITY FUNCTIONS ---------- function balanceOfWant() public view returns (uint256) { @@ -432,11 +447,11 @@ contract Strategy is BaseStrategy { } function balanceOfStakedSanToken() public view returns (uint256) { - return IERC20(address(sanTokenGauge)).balanceOf(address(this)); + return strategyProxy.balanceOfStakedSanToken(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 +487,22 @@ contract Strategy is BaseStrategy { } function depositToStableMaster(uint256 _amount) internal { - IStableMaster(angleStableMaster).deposit( + strategyProxy.depositToStableMaster( + address(angleStableMaster), _amount, - address(this), - poolManager + poolManager, + address(want), + address(sanTokenGauge) ); } function withdrawFromStableMaster(uint256 _amountInSanToken) internal { - IStableMaster(angleStableMaster).withdraw( + strategyProxy.withdrawFromStableMaster( + address(angleStableMaster), _amountInSanToken, - address(this), - address(this), - poolManager + poolManager, + address(sanToken), + address(sanTokenGauge) ); } diff --git a/src/YearnAngleVoter.sol b/src/YearnAngleVoter.sol new file mode 100644 index 0000000..3c1b014 --- /dev/null +++ b/src/YearnAngleVoter.sol @@ -0,0 +1,68 @@ +// 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 {IVoteEscrow} from "./interfaces/Angle/IVoteEscrow.sol"; + +contract YearnAngleVoter { + using SafeERC20 for IERC20; + + address constant public angle = address(0x31429d1856aD1377A8A0079410B297e1a9e214c2); + + address constant public veAngle = address(0x0C462Dbb9EC8cD1630f1728B2CFD2769d09f0dd5); + + address public governance; + address public pendingGovernance; + address public proxy; + + constructor() public { + governance = address(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); + } + + function getName() external pure returns (string memory) { + return "YearnAngleVoter"; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function createLock(uint256 _value, uint256 _unlockTime) external { + require(msg.sender == proxy || msg.sender == governance, "!authorized"); + 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).approve(veAngle, _value); + IVoteEscrow(veAngle).increase_amount(_value); + } + + function release() external { + require(msg.sender == proxy || msg.sender == governance, "!authorized"); + IVoteEscrow(veAngle).withdraw(); + } + + function setGovernance(address _governance) external { + require(msg.sender == 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) { + require(msg.sender == proxy || 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/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 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/AngleVoterLock.t.sol b/src/test/AngleVoterLock.t.sol new file mode 100644 index 0000000..93a01c2 --- /dev/null +++ b/src/test/AngleVoterLock.t.sol @@ -0,0 +1,87 @@ +// 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 _voterAngleBalanceBefore = angleToken.balanceOf(address(voter)); + vm.prank(strategist); + strategy.tend(); + uint256 _angleTokenBalance = strategy.balanceOfAngleToken(); + assertGt(_angleTokenBalance, 0); + + 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(_voterAngleBalanceAfter / 2); + uint256 _balance = veAngleToken.balanceOf(address(voter)); + assertGt(_balance, 0); + + // increase amount + uint256 toLock = angleToken.balanceOf(address(voter)); + vm.prank(gov); + voterProxy.increaseAmount(toLock); + 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)) == _voterAngleBalanceAfter); + } + } +} \ No newline at end of file diff --git a/src/test/HandleAngleHack.t.sol b/src/test/HandleAngleHack.t.sol index 2f04339..734dad1 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(); @@ -48,8 +50,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 c4b640e..9da1796 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,32 +31,34 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); - string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); + uint256 _balanceBefore = want.balanceOf(address(user)); + + 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); - 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(); @@ -83,6 +82,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(); @@ -93,21 +93,23 @@ contract StrategyCloneTest is StrategyFixture { } deal(address(want), user, _amount); - - string memory _tokenSymbol = IERC20Metadata(address(want)).symbol(); + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); 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); @@ -117,9 +119,10 @@ contract StrategyCloneTest is StrategyFixture { strategist, rewards, keeper, - sanTokenAddrs[_tokenSymbol], - gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + sanTokenAddrs[tokenSymbol], + gaugeAddrs[tokenSymbol], + poolManagerAddrs[tokenSymbol], + address(voterProxy) ); } } @@ -131,6 +134,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(); @@ -151,7 +155,8 @@ contract StrategyCloneTest is StrategyFixture { keeper, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + address(voterProxy) ); vm.prank(gov); @@ -167,7 +172,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..a1e8b2a 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,7 @@ contract StrategyMigrationTest is StrategyFixture { deal(address(want), user, _amount); // Deposit to the vault and harvest + string memory tokenSymbol = IERC20Metadata(address(want)).symbol(); vm.prank(user); want.approve(address(vault), _amount); vm.prank(user); @@ -43,11 +46,15 @@ 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(), false)); 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)); assertRelApproxEq(newStrategy.estimatedTotalAssets(), _amount, DELTA); } } diff --git a/src/test/StrategyOperation.t.sol b/src/test/StrategyOperation.t.sol index 4cff547..b9e5da8 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 _voterAngleBalanceBefore = angleToken.balanceOf(address(voter)); 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(voter)), _voterAngleBalanceBefore); + assertRelApproxEq(angleToken.balanceOf(address(voter)) - _voterAngleBalanceBefore, _angleTokenBalance / 9, DELTA); address _tokenIn = address(strategy.angleToken()); address _tokenOut = address(want); diff --git a/src/test/utils/StrategyFixture.sol b/src/test/utils/StrategyFixture.sol index 393c68f..f8dba77 100644 --- a/src/test/utils/StrategyFixture.sol +++ b/src/test/utils/StrategyFixture.sol @@ -8,10 +8,12 @@ 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"; -// 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 +30,8 @@ contract StrategyFixture is ExtendedTest { } IERC20 public weth; + AngleStrategyVoterProxy public voterProxy; + YearnAngleVoter public voter; AssetFixture[] public assetFixtures; @@ -50,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); @@ -71,7 +78,14 @@ contract StrategyFixture is ExtendedTest { weth = IERC20(tokenAddrs["WETH"]); - string[2] memory _tokensToTest = ["USDC", "DAI"]; + address _voter = deployAngleVoter(); + address _voterProxy = deployStrategyVoterProxy(_voter); + voter = YearnAngleVoter(_voter); + voterProxy = AngleStrategyVoterProxy(_voterProxy); + vm.prank(gov); + voter.setProxy(_voterProxy); + + string[1] memory _tokensToTest = ["USDC"]; for (uint8 i = 0; i < _tokensToTest.length; ++i) { string memory _tokenToTest = _tokensToTest[i]; @@ -87,7 +101,8 @@ contract StrategyFixture is ExtendedTest { guardian, management, keeper, - strategist + strategist, + address(voterProxy) ); assetFixtures.push(AssetFixture(IVault(_vault), Strategy(_strategy), _want)); @@ -139,16 +154,35 @@ 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); + vm.prank(angleWhitelisterAdmin); + angleWhiteLister.approveWallet(address(_voter)); + + return address(_voter); + } + // Deploys a strategy function deployStrategy( address _vault, - string memory _tokenSymbol + address _voterProxy, + string memory _tokenSymbol, + bool _addStrat ) public returns (address) { Strategy _strategy = new Strategy( _vault, sanTokenAddrs[_tokenSymbol], gaugeAddrs[_tokenSymbol], - poolManagerAddrs[_tokenSymbol] + poolManagerAddrs[_tokenSymbol], + _voterProxy ); vm.startPrank(yMech); @@ -161,6 +195,11 @@ contract StrategyFixture is ExtendedTest { vm.prank(gov); _strategy.setTradeFactory(address(tradeFactory)); + if(_addStrat == true) { + vm.prank(gov); + voterProxy.approveStrategy(gaugeAddrs[_tokenSymbol], address(_strategy)); + } + return address(_strategy); } @@ -175,7 +214,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,7 +231,9 @@ contract StrategyFixture is ExtendedTest { vm.prank(_strategist); _strategyAddr = deployStrategy( _vaultAddr, - _tokenSymbol + _voterProxy, + _tokenSymbol, + true ); Strategy _strategy = Strategy(_strategyAddr);