Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ out = 'artifacts'
libs = ['lib']
solc_version = '0.8.15'

rpc_endpoints = { MAINNET = "${MAINNET_RPC}", ARBITRUM = "${ARBITRUM_RPC}", UNIT_TESTS = "https://rpc.tenderly.co/fork/eb4cc00e-c12c-45f5-905a-04f0cfdefa2f", HARNESS = "https://rpc.tenderly.co/fork/78da602e-78a8-4705-b73c-3c62991231aa" }
rpc_endpoints = { MAINNET = "${MAINNET_RPC}", ARBITRUM = "${ARBITRUM_RPC}", UNIT_TESTS = "https://rpc.tenderly.co/fork/eb4cc00e-c12c-45f5-905a-04f0cfdefa2f", MIGRATE_TESTS = "https://rpc.tenderly.co/fork/b9c353b6-37ae-4f9c-8649-5d23df9f862f", HARNESS = "https://rpc.tenderly.co/fork/78da602e-78a8-4705-b73c-3c62991231aa" }

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
98 changes: 36 additions & 62 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
event Divested(address indexed pool, uint256 lpTokenDivested, uint256 baseObtained);
event Ejected(address indexed pool, uint256 lpTokenDivested, uint256 baseObtained, uint256 fyTokenObtained);
event Drained(address indexed pool, uint256 lpTokenDivested);
event SoldFYToken(uint256 soldFYToken, uint256 returnedBase);
event RetrievedFYToken(uint256 retrievedFYToken, uint256 acceptedBase);

State public state; // The state determines which functions are available

Expand All @@ -48,15 +48,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
StrategyMigrator(
IERC20(fyToken_.underlying()),
fyToken_)
{
// Deploy with a seriesId_ matching the migrating strategy if using the migration feature
// Deploy with any series matching the desired base in any other case
fyToken = fyToken_;

base = IERC20(fyToken_.underlying());

_grantRole(Strategy.init.selector, address(this)); // Enable the `mint` -> `init` hook.
}
{}

modifier isState(State target) {
require (
Expand All @@ -66,7 +58,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
_;
}

/// @dev State and state variable management
/// @notice State and state variable management
/// @param target State to transition to
/// @param pool_ If transitioning to invested, update pool state variable with this parameter
function _transition(State target, IPool pool_) internal {
Expand All @@ -88,7 +80,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
state = target;
}

/// @dev State and state variable management
/// @notice State and state variable management
/// @param target State to transition to
function _transition(State target) internal {
require (target != State.INVESTED, "Must provide a pool");
Expand All @@ -97,34 +89,23 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'

// ----------------------- INVEST & DIVEST --------------------------- //

/// @notice Mock pool mint called by a strategy when trying to migrate.
/// @dev Will initialize the strategy and return strategy tokens.
/// @notice Mint the first strategy tokens, without investing
/// @dev Returns additional values to match the pool init function and allow for strategy migrations.
/// It is expected that base has been transferred in, but no fyTokens
/// @return baseIn Amount of base tokens found in contract
/// @return fyTokenIn This is always returned as 0 since they aren't used
/// @return minted Amount of strategy tokens minted from base tokens which is the same as baseIn
function mint(address, address, uint256, uint256)
function init(address to)
external
override
auth
returns (uint256 baseIn, uint256 fyTokenIn, uint256 minted)
{
fyTokenIn = 0; // Silence compiler warning
baseIn = minted = _init(msg.sender);
}

/// @dev Mint the first strategy tokens, without investing
/// @param to Recipient for the strategy tokens
/// @return minted Amount of strategy tokens minted from base tokens
function init(address to)
external
auth
returns (uint256 minted)
{
minted = _init(to);
baseIn = minted = _init(to);
}

/// @dev Mint the first strategy tokens, without investing
/// @notice Mint the first strategy tokens, without investing
/// @param to Recipient for the strategy tokens
/// @return minted Amount of strategy tokens minted from base tokens
function _init(address to)
Expand All @@ -143,7 +124,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
_transition(State.DIVESTED);
}

/// @dev Start the strategy investments in the next pool
/// @notice Start the strategy investments in the next pool
/// @param pool_ Pool to invest into
/// @return poolTokensObtained Amount of pool tokens minted from base tokens
/// @notice When calling this function for the first pool, some underlying needs to be transferred to the strategy first, using a batchable router.
Expand Down Expand Up @@ -174,7 +155,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
emit Invested(address(pool_), baseCached_, poolTokensObtained);
}

/// @dev Divest out of a pool once it has matured
/// @notice Divest out of a pool once it has matured
/// @return baseObtained Amount of base tokens obtained from burning pool tokens
function divest()
external
Expand Down Expand Up @@ -206,7 +187,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'

// ----------------------- EJECT --------------------------- //

/// @dev Divest out of a pool at any time. If possible the pool tokens will be burnt for base and fyToken, the latter of which
/// @notice Divest out of a pool at any time. If possible the pool tokens will be burnt for base and fyToken, the latter of which
/// must be sold to return the strategy to a functional state. If the pool token burn reverts, the pool tokens will be transferred
/// to the caller as a last resort.
/// @return baseReceived Amount of base tokens received from pool tokens
Expand Down Expand Up @@ -242,8 +223,8 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
}
}

/// @dev Burn an amount of pool tokens.
/// @notice Only the Strategy itself can call this function. It is external and exists so that the transfer is reverted if the burn also reverts.
/// @notice Burn an amount of pool tokens.
/// @dev Only the Strategy itself can call this function. It is external and exists so that the transfer is reverted if the burn also reverts.
/// @param pool_ Pool for the pool tokens.
/// @param poolTokens Amount of tokens to burn.
/// @return baseReceived Amount of base tokens received from pool tokens
Expand All @@ -263,45 +244,38 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
require(fyToken.balanceOf(address(this)) - fyTokenBalance == fyTokenReceived, "Burn failed - fyToken");
}

/// @dev Buy ejected fyToken in the strategy at face value
/// @param fyTokenTo Address to send the purchased fyToken to.
/// @param baseTo Address to send any remaining base to.
/// @return soldFYToken Amount of fyToken sold.
/// @return returnedBase Amount of base unused and returned.
function buyFYToken(address fyTokenTo, address baseTo)
/// @notice Retrieve ejected fyToken in the strategy, and accept base donations.
/// @param to Address to send the purchased fyToken to.
/// @return retrievedFYToken Amount of fyToken retrieved.
/// @return acceptedBase Amount of base accepted.
function retrieveFYToken(address to)
external
auth
isState(State.EJECTED)
returns (uint256 soldFYToken, uint256 returnedBase)
returns (uint256 retrievedFYToken, uint256 acceptedBase)
{
// Caching
IFYToken fyToken_ = fyToken;
uint256 baseCached_ = baseCached;
uint256 fyTokenCached_ = fyTokenCached;
uint256 fyTokenCached_ = retrievedFYToken = fyTokenCached;

uint256 baseIn = base.balanceOf(address(this)) - baseCached_;
(soldFYToken, returnedBase) = baseIn > fyTokenCached_ ? (fyTokenCached_, baseIn - fyTokenCached_) : (baseIn, 0);
acceptedBase = base.balanceOf(address(this)) - baseCached_;

// Update base and fyToken cache
baseCached = baseCached_ + soldFYToken; // soldFYToken is base not returned
fyTokenCached = fyTokenCached_ -= soldFYToken;

// Transition to divested if done
if (fyTokenCached_ == 0) {
// Transition to Divested
_transition(State.DIVESTED);
emit Divested(address(0), 0, 0);
}
baseCached = baseCached_ + acceptedBase; // soldFYToken is base not returned
delete fyTokenCached;

// Transition to Divested
_transition(State.DIVESTED);
emit Divested(address(0), 0, 0);

// Transfer fyToken and base (if surplus)
fyToken_.safeTransfer(fyTokenTo, soldFYToken);
if (soldFYToken < baseIn) {
base.safeTransfer(baseTo, baseIn - soldFYToken);
}
fyToken_.safeTransfer(to, fyTokenCached_);

emit SoldFYToken(soldFYToken, returnedBase);
emit RetrievedFYToken(retrievedFYToken, acceptedBase);
}

/// @dev If we drained the strategy, we can recapitalize it with base to avoid a forced migration
/// @notice If we drained the strategy, we can recapitalize it with base to avoid a forced migration
/// @return baseIn Amount of base tokens used to restart
function restart()
external
Expand All @@ -316,7 +290,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'

// ----------------------- MINT & BURN --------------------------- //

/// @dev Mint strategy tokens with pool tokens. It can be called only when invested.
/// @notice Mint strategy tokens with pool tokens. It can be called only when invested.
/// @param to Recipient for the strategy tokens
/// @return minted Amount of strategy tokens minted
/// @notice The pool tokens that the user contributes need to have been transferred previously, using a batchable router.
Expand All @@ -342,7 +316,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
_mint(to, minted);
}

/// @dev Burn strategy tokens to withdraw pool tokens. It can be called only when invested.
/// @notice Burn strategy tokens to withdraw pool tokens. It can be called only when invested.
/// @param to Recipient for the pool tokens
/// @return poolTokensObtained Amount of pool tokens obtained
/// @notice The strategy tokens that the user burns need to have been transferred previously, using a batchable router.
Expand All @@ -367,7 +341,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
poolCached = poolCached_ - poolTokensObtained;
}

/// @dev Mint strategy tokens with base tokens. It can be called only when not invested and not ejected.
/// @notice Mint strategy tokens with base tokens. It can be called only when not invested and not ejected.
/// @param to Recipient for the strategy tokens
/// @return minted Amount of strategy tokens minted
/// @notice The base tokens that the user invests need to have been transferred previously, using a batchable router.
Expand All @@ -386,7 +360,7 @@ contract Strategy is AccessControl, ERC20Rewards, StrategyMigrator { // TODO: I'
_mint(to, minted);
}

/// @dev Burn strategy tokens to withdraw base tokens. It can be called when not invested and not ejected.
/// @notice Burn strategy tokens to withdraw base tokens. It can be called when not invested and not ejected.
/// @param to Recipient for the base tokens
/// @return baseObtained Amount of base tokens obtained
/// @notice The strategy tokens that the user burns need to have been transferred previously, using a batchable router.
Expand Down
38 changes: 10 additions & 28 deletions src/StrategyMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,41 @@ pragma solidity ^0.8.13;
import {IStrategyMigrator} from "./interfaces/IStrategyMigrator.sol";
import {IFYToken} from "@yield-protocol/vault-v2/src/interfaces/IFYToken.sol";
import {IERC20} from "@yield-protocol/utils-v2/src/token/IERC20.sol";
import {ERC20Permit} from "@yield-protocol/utils-v2/src/token/ERC20Permit.sol";


/// @dev The Migrator contract poses as a Pool to receive all assets from a Strategy
/// during a roll operation.
/// @notice The Pool and fyToken must exist. The fyToken needs to be not mature, and the pool needs to have no fyToken in it.
/// There will be no state changes on pool or fyToken.
/// TODO: For this to work, the implementing class must inherit from ERC20 and make sure that totalSupply is not zero after the `mint` call.
/// @dev The Migrator contract poses as a Pool to receive all assets from a Strategy during an invest call.
/// TODO: For this to work, the implementing class must inherit from ERC20.
abstract contract StrategyMigrator is IStrategyMigrator {

/// Mock pool base - Must match that of the calling strategy
IERC20 public base;
IERC20 public immutable base;

/// Mock pool fyToken - Must be set to a real fyToken registered to a series in the Cauldron, any will do
/// Mock pool fyToken - Can be any address
IFYToken public fyToken;

/// Mock pool maturity - Its contents don't matter
/// Mock pool maturity - Can be set to a value far in the future to avoid `divest` calls
uint32 public maturity;

constructor(IERC20 base_, IFYToken fyToken_) {
base = base_;
fyToken = fyToken_;
}

/// @dev Mock pool mint. Called within `startPool`. This contract must hold 1 wei of base.
function mint(address, address, uint256, uint256)
/// @dev Mock pool init. Called within `invest`.
function init(address)
external
virtual
returns (uint256, uint256, uint256)
{
return (0, 0, 0);
}

/// @dev Mock pool burn and make it revert so that `endPool`never suceeds, and `burnForBase` can never be called.
/// @dev Mock pool burn that reverts so that `divest` never suceeds, but `eject` does.
function burn(address, address, uint256, uint256)
external
returns (uint256, uint256, uint256)
virtual
returns (uint256, uint256, uint256)
{
revert();
}

/// @dev Mock pool getBaseBalance
function getBaseBalance() external view returns(uint128) {
return 0;
}

/// @dev Mock pool getFYTokenBalance
function getFYTokenBalance() external view returns(uint128) {
return 0;
}

/// @dev Mock pool ts
function ts() external view returns(int128) {
return 0;
}
}
23 changes: 4 additions & 19 deletions src/draft/StrategyV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,6 @@ contract StrategyV3 is AccessControl, ERC20Rewards, StrategyMigrator { // TODO:
ladle = ladle_;
cauldron = ladle_.cauldron();

// Deploy with a seriesId_ matching the migrating strategy if using the migration feature
// Deploy with any series matching the desired base in any other case
fyToken = fyToken_;

base = IERC20(fyToken_.underlying());
bytes6 baseId_;
baseId = baseId_ = fyToken_.underlyingId();
baseJoin = address(ladle_.joins(baseId_));
Expand Down Expand Up @@ -160,25 +155,14 @@ contract StrategyV3 is AccessControl, ERC20Rewards, StrategyMigrator { // TODO:

// ----------------------- STATE CHANGES --------------------------- //

/// @dev Mock pool mint hooked up to initialize the strategy and return strategy tokens.
function mint(address, address, uint256, uint256)
external
override
isState(State.DEPLOYED)
auth
returns (uint256 baseIn, uint256 fyTokenIn, uint256 minted)
{
baseIn = minted = this.init(msg.sender);
fyTokenIn = 0;
}

/// @dev Mint the first strategy tokens, without investing.
/// @param to Receiver of the strategy tokens.
function init(address to)
external
override
isState(State.DEPLOYED)
auth
returns (uint256 minted)
returns (uint256 baseIn, uint256 fyTokenIn, uint256 minted)
{
// Clear state variables from a potential migration
delete seriesId;
Expand All @@ -188,7 +172,8 @@ contract StrategyV3 is AccessControl, ERC20Rewards, StrategyMigrator { // TODO:
delete vaultId;

require (_totalSupply == 0, "Already initialized");
value = minted = base.balanceOf(address(this));
fyTokenIn = 0;
value = baseIn = minted = base.balanceOf(address(this));
require (minted > 0, "Not enough base in");
// Make sure that at the end of the transaction the strategy has enough tokens as to not expose itself to a rounding-down liquidity attack.
_mint(to, minted);
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface IStrategy is IStrategyMigrator {
/// @dev Mint the first strategy tokens, without investing
function init(address to)
external
returns (uint256 minted);
returns (uint256 baseIn, uint256 fyTokenIn, uint256 minted);

/// @dev Start the strategy investments in the next pool
/// @notice When calling this function for the first pool, some underlying needs to be transferred to the strategy first, using a batchable router.
Expand Down
18 changes: 3 additions & 15 deletions src/interfaces/IStrategyMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,12 @@ interface IStrategyMigrator is IERC20 {
/// @dev Mock pool base - Must match that of the calling strategy
function base() external view returns(IERC20);

/// @dev Mock pool fyToken - Must be set to a real fyToken registered to a series in the Cauldron, any will do
/// @dev Mock pool fyToken - Can be any address, including address(0)
function fyToken() external view returns(IFYToken);

/// @dev Mock pool mint. Called within `startPool`. This contract must hold 1 wei of base.
function mint(address, address, uint256, uint256) external returns (uint256, uint256, uint256);
/// @dev Mock pool init. Called within `invest`.
function init(address) external returns (uint256, uint256, uint256);

/// @dev Mock pool burn and make it revert so that `endPool`never suceeds, and `burnForBase` can never be called.
function burn(address, address, uint256, uint256) external returns (uint256, uint256, uint256);

/// @dev Mock pool maturity
function maturity() external view returns(uint32);

/// @dev Mock pool getBaseBalance
function getBaseBalance() external view returns(uint128);

/// @dev Mock pool getFYTokenBalance
function getFYTokenBalance() external view returns(uint128);

/// @dev Mock pool ts
function ts() external view returns(int128);
}
Loading