Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
fe6844d
feat: initial version of central veAngle voter
16slim Jun 6, 2022
7a07c62
fix: changes necessary in tests to make them pass with central veAngle
16slim Jun 6, 2022
0be3adf
feat: lock and increase amount locked
16slim Jun 6, 2022
e35f803
feat: add 2nd harvest
16slim Jun 6, 2022
c863f46
fix: remove percentLock
16slim Jun 8, 2022
3676450
fix: remove percentLock setter
16slim Jun 8, 2022
64c75d7
fix: set governance to ychad in constructors
16slim Jun 8, 2022
649c309
fix: remove .address and rename voter fixtures
16slim Jun 8, 2022
f189c10
fix: remove treasury check for strategy proxy
16slim Jun 8, 2022
f389c7e
feat: add two-step process to set governance
16slim Jun 8, 2022
46571e0
fix: naming convention for proxy, setProxy
16slim Jun 8, 2022
d0140f0
feat: changed prepareMigration logic to convert back to want
16slim Jun 8, 2022
a9ff880
chore: rebase onto most recent security-approved master
charlesndalton Jun 24, 2022
4096176
fix: fix multiple strategies using stablemaster
16slim Jun 28, 2022
31ff372
chore: fix translation errors
16slim Jul 5, 2022
9db840c
fix: remove debugging event
16slim Jul 5, 2022
88f18fd
feat: add whitelister interface to test lock
16slim Jul 7, 2022
618637b
feat: test lock & release veAngle
16slim Jul 7, 2022
15ce260
chore: remove debugging make
16slim Jul 7, 2022
de4174d
fix: remove approveStrategy as it's included in fixture
16slim Jul 7, 2022
5072669
chore: removed unused deposit and approve
16slim Jul 7, 2022
06b0dae
chore: remove unnecessary approve
16slim Jul 7, 2022
aa72c39
fix: change test to check voter proxy instead of treasury
16slim Jul 7, 2022
2435759
feat: add boolean to approve and whitelist voter
16slim Jul 7, 2022
a6aa5d6
fix: fix _withdrawSome()
16slim Jul 7, 2022
cdbe6c4
chore: remove .py tests
16slim Jul 7, 2022
915b8a4
fix: remove unused cursor variable
16slim Jul 11, 2022
dfc900d
chore: rename angle to angleToken
16slim Jul 11, 2022
16f7e42
feat: check allowance function
16slim Jul 15, 2022
432873a
fix: remove token parameter from claimRewards
16slim Jul 15, 2022
8c607ed
chore: rename deposit to stake
16slim Jul 15, 2022
ada3a34
fix: typo in _withdrawSome
16slim Jul 15, 2022
8fb4d14
chore: remove comment and add space
16slim Jul 15, 2022
d7571d9
feat: save gas approving angle from voter
16slim Jul 15, 2022
f4e0c73
chore: rename balanceOf to balanceOfSTakedSanToken
16slim Jul 18, 2022
8c63ff1
feat: add access control to lock() function
16slim Jul 22, 2022
528a4c4
fix: replace fro withdrawAll in strategy
16slim Jul 22, 2022
8ba167e
fix: necessary changes for access control lock to pass
16slim Jul 22, 2022
c848384
feat: Angle rewards stay in voter and not in proxy
16slim Jul 22, 2022
542f347
fix: hardcode unlock time and remove access control to locl
16slim Jul 22, 2022
748466f
fix: include force harvest trigger fix
16slim Aug 2, 2022
5c3038e
chore: remove unused imports
16slim Aug 15, 2022
ea48325
feat: add revert message to safeExecute
16slim Aug 15, 2022
cf81296
feat: clean migration process
16slim Aug 15, 2022
6937fad
feat: include message error in safeExecute
16slim Aug 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/AngleStrategyVoterProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// 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);
Comment thread
charlesndalton marked this conversation as resolved.
Outdated

// gauge => strategies
mapping(address => address) public strategies;
mapping(address => bool) public voters;
address public governance;

uint256 lastTimeCursor;
Comment thread
charlesndalton marked this conversation as resolved.
Outdated

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;
}
Comment thread
charlesndalton marked this conversation as resolved.

function revokeVoter(address _voter) external {
require(msg.sender == governance, "!governance");
voters[_voter] = false;
}

function lock(uint256 amount, uint256 unlockTime) external {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see any harm adjusting this function to have zero arguments? My thought is that we always max lock the full balance. There is no access control on this function, what if someone locks for less than max? Do we care?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I included a msg.sender check to it too to check for governance, I normally prefer to leave things as flexible as possible just in case. Wdyt?

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 {
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));
Comment thread
charlesndalton marked this conversation as resolved.
return _balance;
}

function withdrawFromStableMaster(address stableMaster, uint256 amount,
address poolManager, address token, address gauge) external {
Comment thread
16slim marked this conversation as resolved.
require(strategies[gauge] == 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));
Comment thread
charlesndalton marked this conversation as resolved.
Outdated

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) {
Comment thread
charlesndalton marked this conversation as resolved.
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));
Comment thread
16slim marked this conversation as resolved.
Outdated

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);

yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, 0));
yearnAngleVoter.safeExecute(token, 0, abi.encodeWithSignature("approve(address,uint256)", stableMaster, amount));
Comment thread
charlesndalton marked this conversation as resolved.
Outdated
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));
}
}
85 changes: 58 additions & 27 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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
);
}

Expand All @@ -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;
Expand All @@ -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
);
}

Expand All @@ -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));
Expand All @@ -141,7 +152,8 @@ contract Strategy is BaseStrategy {
_keeper,
_sanToken,
_sanTokenGauge,
_poolManager
_poolManager,
_strategyProxy
);

emit Cloned(newStrategy);
Expand Down Expand Up @@ -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));
Comment thread
charlesndalton marked this conversation as resolved.
Outdated

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), _tokensToKeep);
Comment thread
16slim marked this conversation as resolved.
Outdated
}
}

Expand All @@ -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));
Comment thread
charlesndalton marked this conversation as resolved.
Outdated
}
}

Expand Down Expand Up @@ -278,12 +291,16 @@ 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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense when you think about it, and it's also kind of unintuitive. Would we still need to do this if stablemaster operations were done in the strategy (e.g., want -> sanToken and back)? Is that even possible?

@16slim 16slim Jul 15, 2022

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's more of a design choice, I wanted the strategy.sol contract to not do anything and let the proxy handle it but happy to change my mind if you think it's optimal

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess there's not a clear better way. My personal thinking is that it makes sense for the voter and proxy to be responsible for gauge staking and voting, and let the strategy be responsible for sanToken <-> want stuff, but I'm okay if you think it makes more sense this way

}

withdrawFromStableMaster(Math.min(_amountInSanToken, balanceOfSanToken()));
withdrawFromStableMaster(Math.min(_amountInSanToken, _amountInSanToken));
Comment thread
charlesndalton marked this conversation as resolved.
Outdated
}

// can be used in conjunction with migration if this function is still working
Expand All @@ -294,9 +311,21 @@ 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());
Comment thread
charlesndalton marked this conversation as resolved.
Outdated

uint256 _angleBalance = balanceOfAngleToken();
if(_angleBalance > 0) {
Comment thread
charlesndalton marked this conversation as resolved.
Outdated
IERC20(angleToken).safeTransfer(_newStrategy, _angleBalance);
}

uint256 _stakedBalance = balanceOfStakedSanToken();
if (_stakedBalance > 0) {
strategyProxy.withdraw(address(sanTokenGauge), address(sanToken), _stakedBalance);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not withdrawAll?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about its existence, should be same result but replacing as it's exactly its purpose!

IERC20(sanToken).safeTransfer(address(strategyProxy), _stakedBalance);
withdrawFromStableMaster(_stakedBalance);
}

IERC20(sanToken).safeTransfer(_newStrategy, balanceOfSanToken());
IERC20(sanToken).safeTransfer(_newStrategy, IERC20(sanToken).balanceOf(address(this)));
IERC20(angleToken).transfer(_newStrategy, balanceOfAngleToken());
}

Expand Down Expand Up @@ -424,19 +453,18 @@ contract Strategy is BaseStrategy {
percentKeep = _percentKeep;
}


// ----------------- SUPPORT & UTILITY FUNCTIONS ----------

function balanceOfWant() public view returns (uint256) {
return want.balanceOf(address(this));
}

function balanceOfStakedSanToken() public view returns (uint256) {
return IERC20(address(sanTokenGauge)).balanceOf(address(this));
return strategyProxy.balanceOf(address(sanTokenGauge));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add a helper function like you did with balanceOfSanToken?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I am not understanding, it's the same case as balanceOfSanToken no? Calling a function in the proxy

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was saying add a balanceOfStakedSanToken function in the proxy, like this:

function balanceOfSanToken(address sanToken) public view returns (uint256) {
return IERC20(sanToken).balanceOf(address(yearnAngleVoter));
}

}

function balanceOfSanToken() public view returns (uint256) {
return sanToken.balanceOf(address(this));
return strategyProxy.balanceOfSanToken(address(sanToken));
}

function balanceOfAngleToken() public view returns (uint256) {
Expand Down Expand Up @@ -472,19 +500,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)
);
}

Expand Down
Loading