-
Notifications
You must be signed in to change notification settings - Fork 144
Base strategy #93
base: master
Are you sure you want to change the base?
Base strategy #93
Changes from all commits
aed9bef
62e1e7f
685be30
28e76fc
748153a
240c91e
7b9bbc3
844774d
8efb534
827e166
43c3cfb
efb4ee2
bd97ca9
c5e0b8c
04d1148
eb1238c
a611592
5e05c92
10544cb
5789062
51305bb
3177028
6611eed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
{ | ||
"files.eol": "\n" | ||
"files.eol": "\n", | ||
"solidity.defaultCompiler": "localFile" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
|
||
pragma solidity 0.6.12; | ||
pragma experimental ABIEncoderV2; | ||
|
||
import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; | ||
|
||
/// @notice Minimal interface for BentoBox token vault interactions - `token` is aliased as `address` from `IERC20` for code simplicity. | ||
interface IBentoBoxMinimal { | ||
/// @notice Balance per ERC-20 token per account in shares. | ||
function balanceOf(address, address) external view returns (uint256); | ||
|
||
/// @notice Deposit an amount of `token` represented in either `amount` or `share`. | ||
/// @param token_ The ERC-20 token to deposit. | ||
/// @param from which account to pull the tokens. | ||
/// @param to which account to push the tokens. | ||
/// @param amount Token amount in native representation to deposit. | ||
/// @param share Token amount represented in shares to deposit. Takes precedence over `amount`. | ||
/// @return amountOut The amount deposited. | ||
/// @return shareOut The deposited amount repesented in shares. | ||
function deposit( | ||
address token_, | ||
address from, | ||
address to, | ||
uint256 amount, | ||
uint256 share | ||
) external payable returns (uint256 amountOut, uint256 shareOut); | ||
|
||
/// @notice Withdraws an amount of `token` from a user account. | ||
/// @param token_ The ERC-20 token to withdraw. | ||
/// @param from which user to pull the tokens. | ||
/// @param to which user to push the tokens. | ||
/// @param amount of tokens. Either one of `amount` or `share` needs to be supplied. | ||
/// @param share Like above, but `share` takes precedence over `amount`. | ||
function withdraw( | ||
address token_, | ||
address from, | ||
address to, | ||
uint256 amount, | ||
uint256 share | ||
) external returns (uint256 amountOut, uint256 shareOut); | ||
|
||
/// @notice Transfer shares from a user account to another one. | ||
/// @param token The ERC-20 token to transfer. | ||
/// @param from which user to pull the tokens. | ||
/// @param to which user to push the tokens. | ||
/// @param share The amount of `token` in shares. | ||
function transfer( | ||
address token, | ||
address from, | ||
address to, | ||
uint256 share | ||
) external; | ||
|
||
/// @dev Helper function to represent an `amount` of `token` in shares. | ||
/// @param token The ERC-20 token. | ||
/// @param amount The `token` amount. | ||
/// @param roundUp If the result `share` should be rounded up. | ||
/// @return share The token amount represented in shares. | ||
function toShare( | ||
address token, | ||
uint256 amount, | ||
bool roundUp | ||
) external view returns (uint256 share); | ||
|
||
/// @dev Helper function to represent shares back into the `token` amount. | ||
/// @param token The ERC-20 token. | ||
/// @param share The amount of shares. | ||
/// @param roundUp If the result should be rounded up. | ||
/// @return amount The share amount back into native representation. | ||
function toAmount( | ||
address token, | ||
uint256 share, | ||
bool roundUp | ||
) external view returns (uint256 amount); | ||
|
||
/// @notice Registers this contract so that users can approve it for the BentoBox. | ||
function registerProtocol() external; | ||
|
||
function totals(address token) external view returns (Rebase memory); | ||
|
||
function harvest( | ||
address token, | ||
bool balance, | ||
uint256 maxChangeAmount | ||
) external; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; | |
contract SushiBarMock is ERC20 { | ||
using BoringMath for uint256; | ||
ERC20 public sushi; | ||
uint256 public totalSupply; | ||
uint256 public override totalSupply; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is currently no override, I guess this is supposed to be an inheritance of the missing ERC20WithSupply @gasperbr |
||
string public constant name = "SushiBar"; | ||
string public constant symbol = "xSushi"; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
// SPDX-License-Identifier: MIT | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did a quick review and left some comments. Nice contract and this will make it easier to create robust strategies. If I understand this correctly, the goal is to make implementing strategies simpler by providing a simplified virtual interface. The idea seems to be to allow 'normal' harvesting to happen anytime, but to keep rewards harvesting to only executors. Swapping is already provided, including whitelisting paths. After exit recovery is built in. You might want to put all the virtual methods at the end and document them thoroughly, so anyone using this known what to implement and how. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Appreciate the opinionated review! I have updated the code and added samples accordingly. |
||
|
||
pragma solidity 0.6.12; | ||
pragma experimental ABIEncoderV2; | ||
|
||
import "../interfaces/IStrategy.sol"; | ||
import "../interfaces/IBentoBoxMinimal.sol"; | ||
import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; | ||
import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; | ||
import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; | ||
import "@sushiswap/core/contracts/uniswapv2/libraries/UniswapV2Library.sol"; | ||
|
||
//// @title Base contract which custom BentoBox strategies should extend. | ||
//// @notice Abstract BentoBox interactions away from the strategy implementation. Minimizes risk of sandwich attacks. | ||
//// @dev Implement _skim, _harvest, _withdraw and _exit functions in your strategy. | ||
abstract contract BaseStrategy is IStrategy, BoringOwnable { | ||
using BoringMath for uint256; | ||
using BoringERC20 for IERC20; | ||
|
||
struct BaseStrategyParams { | ||
address token; | ||
address bentoBox; | ||
address strategyExecutor; | ||
address factory; | ||
address bridgeToken; | ||
} | ||
|
||
IERC20 public immutable strategyToken; | ||
IBentoBoxMinimal public immutable bentoBox; | ||
address public immutable factory; | ||
address public immutable bridgeToken; | ||
|
||
bool public exited; | ||
uint256 public maxBentoBoxBalance; | ||
mapping(address => bool) public strategyExecutors; | ||
|
||
event LogConvert(address indexed server, address indexed token0, address indexed token1, uint256 amount0, uint256 amount1); | ||
event LogSetStrategyExecutor(address indexed executor, bool allowed); | ||
|
||
/** @param baseStrategyParams.token Address of the underlying token the strategy invests. | ||
@param baseStrategyParams.bentoBox BentoBox address. | ||
@param baseStrategyParams.strategyExecutor EOA that will execute the safeHarvest function. | ||
@param baseStrategyParams.factory SushiSwap factory. | ||
@param baseStrategyParams.bridgeToken An intermedieary token for swapping any rewards into the underlying token.*/ | ||
constructor(BaseStrategyParams memory baseStrategyParams) public { | ||
strategyToken = IERC20(baseStrategyParams.token); | ||
bentoBox = IBentoBoxMinimal(baseStrategyParams.bentoBox); | ||
strategyExecutors[baseStrategyParams.strategyExecutor] = true; | ||
factory = baseStrategyParams.factory; | ||
bridgeToken = baseStrategyParams.bridgeToken; | ||
} | ||
|
||
//** Strategy implementation: override the following functions: */ | ||
|
||
/// @notice Invests the underlying asset. | ||
/// @param amount The amount of tokens to invest. | ||
/// @dev Assume the contract's balance is greater than the amount | ||
function _skim(uint256 amount) internal virtual; | ||
|
||
/// @notice Harvest any profits made and transfer them to address(this) or report a loss | ||
/// @param balance The amount of tokens that have been invested. | ||
/// @return amountAdded The delta (+profit or -loss) that occured in contrast to `balance`. | ||
/// @dev amountAdded can be left at 0 when reporting profits (gas savings). | ||
/// amountAdded should not reflect any rewards or tokens the strategy received. | ||
/// Calcualte the amount added based on what the current deposit is worth. | ||
/// (The Base Strategy harvest function accounts for rewards). | ||
function _harvest(uint256 balance) internal virtual returns (int256 amountAdded); | ||
|
||
/// @dev Withdraw the requested amount of the underlying tokens to address(this). | ||
/// @param amount The requested amount we want to withdraw. | ||
function _withdraw(uint256 amount) internal virtual; | ||
|
||
/// @notice Withdraw the maximum amount of the invested assets to address(this). | ||
/// @dev This shouldn't revert (use try catch). | ||
function _exit() internal virtual; | ||
|
||
/// @notice Claim any rewards reward tokens and optionally sell them for the underlying token. | ||
/// @dev Doesn't need to be implemented if we don't expect any rewards. | ||
function _harvestRewards() internal virtual {} | ||
|
||
//** End strategy implementation */ | ||
|
||
modifier onlyBentobox() { | ||
require(msg.sender == address(bentoBox), "BentoBox Strategy: only bento"); | ||
require(!exited, "BentoBox Strategy: exited"); | ||
_; | ||
} | ||
|
||
modifier onlyExecutor() { | ||
require(strategyExecutors[msg.sender], "BentoBox Strategy: only executor"); | ||
_; | ||
} | ||
|
||
function setStrategyExecutor(address executor, bool value) external onlyOwner { | ||
strategyExecutors[executor] = value; | ||
emit LogSetStrategyExecutor(executor, strategyExecutors[executor]); | ||
} | ||
|
||
/// @inheritdoc IStrategy | ||
function skim(uint256 amount) external override { | ||
_skim(amount); | ||
} | ||
|
||
/// @notice Harvest profits while preventing a sandwich attack exploit. | ||
/// @param maxBalance The maximum balance of the underlying token that is allowed to be in BentoBox. | ||
/// @param rebalance Whether BentoBox should rebalance the strategy assets to acheive it's target allocation. | ||
/// @param maxChangeAmount When rebalancing - the maximum amount that will be deposited to or withdrawn from a strategy. | ||
/// @param harvestRewards If we want to claim any accrued reward tokens | ||
/// @dev maxBalance can be set to 0 to keep the previous value. | ||
/// @dev maxChangeAmount can be set to 0 to allow for full rebalancing. | ||
function safeHarvest( | ||
uint256 maxBalance, | ||
bool rebalance, | ||
uint256 maxChangeAmount, | ||
bool harvestRewards | ||
) external onlyExecutor { | ||
if (harvestRewards) { | ||
_harvestRewards(); | ||
} | ||
|
||
if (maxBalance > 0) { | ||
maxBentoBoxBalance = maxBalance; | ||
} | ||
|
||
bentoBox.harvest(address(strategyToken), rebalance, maxChangeAmount); | ||
} | ||
|
||
/** @inheritdoc IStrategy | ||
@dev Only BentoBox can call harvest on this strategy. | ||
@dev Ensures that (1) the caller was this contract (called through the safeHarvest function) | ||
and (2) that we are not being frontrun by a large BentoBox deposit when harvesting profits. */ | ||
function harvest(uint256 balance, address sender) external override onlyBentobox returns (int256) { | ||
/** @dev Don't revert if conditions aren't met in order to allow | ||
BentoBox to continiue execution as it might need to do a rebalance. */ | ||
|
||
if (sender == address(this) && IBentoBoxMinimal(bentoBox).totals(address(strategyToken)).elastic <= maxBentoBoxBalance) { | ||
int256 amount = _harvest(balance); | ||
|
||
/** @dev Since harvesting of rewards is accounted for seperately we might also have | ||
some underlying tokens in the contract that the _harvest call doesn't report. | ||
E.g. reward tokens that have been sold into the underlying tokens which are now sitting in the contract. | ||
Meaning the amount returned by the internal _harvest function isn't necessary the final profit/loss amount */ | ||
|
||
uint256 contractBalance = strategyToken.safeBalanceOf(address(this)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. safeBalanceOf does not exist in the library |
||
|
||
if (amount >= 0) { | ||
// _harvest reported a profit | ||
|
||
if (contractBalance > 0) { | ||
strategyToken.safeTransfer(address(bentoBox), contractBalance); | ||
} | ||
return int256(contractBalance); | ||
} else if (contractBalance > 0) { | ||
// _harvest reported a loss but we have some tokens sitting in the contract | ||
|
||
int256 diff = amount + int256(contractBalance); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would we sum the amount and contractBalance if a loss is already reported? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The thought process here is that _harvest shouldn't concern itself about external rewards. |
||
|
||
if (diff > 0) { | ||
// we still made some profit | ||
// send the profit to BentoBox and reinvest the rest | ||
strategyToken.safeTransfer(address(bentoBox), uint256(diff)); | ||
_skim(uint256(-amount)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took me a while to figure out what's going on here... important contracts like this can benefit from commenting every line ;) |
||
return diff; | ||
} else { | ||
// we made a loss but we have some tokens we can reinvest | ||
_skim(contractBalance); | ||
return diff; | ||
} | ||
} else { | ||
// we made a loss | ||
return amount; | ||
} | ||
} | ||
return int256(0); | ||
} | ||
|
||
/// @inheritdoc IStrategy | ||
function withdraw(uint256 amount) external override onlyBentobox returns (uint256 actualAmount) { | ||
_withdraw(amount); | ||
/// @dev Make sure we send and report the exact same amount of tokens by using balanceOf. | ||
actualAmount = strategyToken.safeBalanceOf(address(this)); | ||
strategyToken.safeTransfer(address(bentoBox), actualAmount); | ||
} | ||
|
||
/// @inheritdoc IStrategy | ||
function exit(uint256 balance) external override returns (int256 amountAdded) { | ||
require(msg.sender == address(bentoBox), "BaseStrategy: only BentoBox"); | ||
_exit(); | ||
/// @dev Check balance of token on the contract. | ||
uint256 actualBalance = strategyToken.safeBalanceOf(address(this)); | ||
/// @dev Calculate tokens added (or lost). | ||
amountAdded = int256(actualBalance) - int256(balance); | ||
/// @dev Transfer all tokens to bentoBox. | ||
strategyToken.safeTransfer(address(bentoBox), actualBalance); | ||
/// @dev Flag as exited, allowing the owner to manually deal with any amounts available later. | ||
exited = true; | ||
} | ||
|
||
/** @dev After exited, the owner can perform ANY call. This is to rescue any funds that didn't | ||
get released during exit or got earned afterwards due to vesting or airdrops, etc. */ | ||
function afterExit( | ||
address to, | ||
uint256 value, | ||
bytes memory data | ||
) public onlyOwner returns (bool success) { | ||
require(exited, "BentoBox Strategy: not exited"); | ||
(success, ) = to.call{value: value}(data); | ||
} | ||
|
||
/// @notice Swap some tokens in the contract for the underlying and deposits them to address(this) | ||
function swapExactTokensForUnderlying(uint256 amountOutMin, address inputToken) public onlyExecutor returns (uint256 amountOut) { | ||
require(factory != address(0), "BentoBox Strategy: cannot swap"); | ||
require(inputToken != address(strategyToken), "BentoBox Strategy: invalid swap"); | ||
|
||
///@dev Construct a path array consisting of the input (reward token), | ||
/// underlying token and a potential bridge token | ||
bool useBridge = bridgeToken != address(0); | ||
|
||
address[] memory path = new address[](useBridge ? 3 : 2); | ||
|
||
path[0] = inputToken; | ||
|
||
if (useBridge) { | ||
path[1] = bridgeToken; | ||
} | ||
|
||
path[path.length - 1] = address(strategyToken); | ||
|
||
uint256 amountIn = IERC20(path[0]).safeBalanceOf(address(this)); | ||
|
||
uint256[] memory amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); | ||
|
||
amountOut = amounts[amounts.length - 1]; | ||
|
||
require(amountOut >= amountOutMin, "BentoBox Strategy: insufficient output"); | ||
|
||
IERC20(path[0]).safeTransfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); | ||
|
||
_swap(amounts, path, address(this)); | ||
|
||
emit LogConvert(msg.sender, inputToken, address(strategyToken), amountIn, amountOut); | ||
} | ||
|
||
/// @dev requires the initial amount to have already been sent to the first pair | ||
function _swap( | ||
uint256[] memory amounts, | ||
address[] memory path, | ||
address _to | ||
) internal { | ||
for (uint256 i; i < path.length - 1; i++) { | ||
(address input, address output) = (path[i], path[i + 1]); | ||
(address token0, ) = UniswapV2Library.sortTokens(input, output); | ||
uint256 amountOut = amounts[i + 1]; | ||
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0)); | ||
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; | ||
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0)); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ERC20WithSupply is undeclared