diff --git a/foundry.toml b/foundry.toml index 58bf5e0..09f88ea 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index 9495482..021daa5 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -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 @@ -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 ( @@ -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 { @@ -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"); @@ -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) @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/src/StrategyMigrator.sol b/src/StrategyMigrator.sol index bb14ee4..0af98ed 100644 --- a/src/StrategyMigrator.sol +++ b/src/StrategyMigrator.sol @@ -4,23 +4,19 @@ 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_) { @@ -28,8 +24,8 @@ abstract contract StrategyMigrator is IStrategyMigrator { 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) @@ -37,26 +33,12 @@ abstract contract StrategyMigrator is IStrategyMigrator { 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; - } } \ No newline at end of file diff --git a/src/draft/StrategyV3.sol b/src/draft/StrategyV3.sol index c1b9a51..05aec74 100644 --- a/src/draft/StrategyV3.sol +++ b/src/draft/StrategyV3.sol @@ -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_)); @@ -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; @@ -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); diff --git a/src/interfaces/IStrategy.sol b/src/interfaces/IStrategy.sol index b9f32c3..a85e44b 100644 --- a/src/interfaces/IStrategy.sol +++ b/src/interfaces/IStrategy.sol @@ -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. diff --git a/src/interfaces/IStrategyMigrator.sol b/src/interfaces/IStrategyMigrator.sol index ec6d3af..e613e75 100644 --- a/src/interfaces/IStrategyMigrator.sol +++ b/src/interfaces/IStrategyMigrator.sol @@ -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); } \ No newline at end of file diff --git a/test/StrategyMigrator.t.sol b/test/StrategyMigrator.t.sol index fbbfed9..eddbf45 100644 --- a/test/StrategyMigrator.t.sol +++ b/test/StrategyMigrator.t.sol @@ -3,133 +3,83 @@ pragma solidity >=0.8.13; import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../src/Strategy.sol"; -import "../src/interfaces/IStrategy.sol"; -import "../src/deprecated/IStrategyV1.sol"; import "@yield-protocol/yieldspace-tv/src/interfaces/IPool.sol"; import "@yield-protocol/vault-v2/src/interfaces/IFYToken.sol"; -import "@yield-protocol/utils-v2/src/token/IERC20Metadata.sol"; import { TestConstants } from "./utils/TestConstants.sol"; abstract contract ZeroState is Test, TestConstants { - using stdStorage for StdStorage; - - // YSDAI6MMS: 0x7ACFe277dEd15CabA6a8Da2972b1eb93fe1e2cCD - // YSDAI6MJD: 0x1144e14E9B0AA9e181342c7e6E0a9BaDB4ceD295 - // YSUSDC6MMS: 0xFBc322415CBC532b54749E31979a803009516b5D - // YSUSDC6MJD: 0x8e8D6aB093905C400D583EfD37fbeEB1ee1c0c39 - // YSETH6MMS: 0xcf30A5A994f9aCe5832e30C138C9697cda5E1247 - // YSETH6MJD: 0x831dF23f7278575BA0b136296a285600cD75d076 - // YSFRAX6MMS: 0x1565F539E96c4d440c38979dbc86Fd711C995DD6 - // YSFRAX6MJD: 0x47cC34188A2869dAA1cE821C8758AA8442715831 - - // TODO: Pin to block 15741300 on 2022 September to March roll, so that the March pool exists, is initialized and has no fyToken. - // Roll tx: https://etherscan.io/tx/0x26eb4d44a310d953db5bcf2fdd47350fadac8be60d0f7c00313a0f83c4ff8d6b - // Pool: 0xbdc7bdae87dfe602e91fdd019c4c0334c38f6a46 - // fyTokenReserves: 223191199910816266762851 - // totalSupply: 223191199910816266762851 - - address timelock = 0x3b870db67a45611CF4723d44487EAF398fAc51E3; + + // Strategies + // 0x1030FF000000 0xad1983745D6c739537fEaB5bed45795f47A940b3 + // 0x1030FF000001 0x5582b8398FB586F1b79edd1a6e83f1c5aa558955 + // 0x1031FF000000 0x4276BEaA49DE905eED06FCDc0aD438a19D3861DD + // 0x1031FF000001 0x5aeB4EFaAA0d27bd606D618BD74Fe883062eAfd0 + // 0x1032FF000000 0x33e6B154efC7021dD55464c4e11a6AfE1f3D0635 + // 0x1032FF000001 0x3b4FFD93CE5fCf97e61AA8275Ec241C76cC01a47 + // 0x10A0FF000000 0x861509A3fA7d87FaA0154AAE2CB6C1f92639339A + // 0x10A0FF000001 0xfe2Aba5ba890AF0ee8B6F2d488B1f85C9E7C5643 + + // FYToken + // 0x0030FF00028B 0x523803c57a497c3AD0E850766c8276D4864edEA5 + // 0x0031FF00028B 0x60a6A7fabe11ff36cbE917a17666848f0FF3A60a + // 0x0032FF00028B 0xCbB7Eba13F9E1d97B2138F588f5CA2F5167F06cc + // 0x00A0FF000288 0xC24DA474A71C44d2b644089020ba255908AdA6e1 + // 0x00A0FF00028B 0x035072cb2912DAaB7B578F468Bd6F0d32a269E32 + // 0x0030FF00028E 0xd947360575E6F01Ce7A210C12F2EE37F5ab12d11 + // 0x0031FF00028E 0xEE508c827a8990c04798B242fa801C5351012B23 + // 0x0032FF00028E 0x5Bb78E530D9365aeF75664c5093e40B0001F7CCd + // 0x00A0FF00028E 0x9B19889794A30056A1E5Be118ee0a6647B184c5f + + address timelock = 0xd0a22827Aed2eF5198EbEc0093EA33A4CD641b6c; // We will warp to the December to June roll, and migrate the MJD strategy to a contract impersonating the March series. - IStrategyV1 srcStrategy = IStrategyV1(0x1144e14E9B0AA9e181342c7e6E0a9BaDB4ceD295); - address srcStrategyHolder = 0x9185Df15078547055604F5c0B02fc1C8D93594A5; - IStrategyV1 donorStrategy = IStrategyV1(0x7ACFe277dEd15CabA6a8Da2972b1eb93fe1e2cCD); // We use this strategy as the source for the pool and fyToken addresses. - IPool srcPool; - IFYToken srcFYToken; - bytes6 srcSeriesId; - IPool dstPool; - IFYToken dstFYToken; - bytes6 dstSeriesId; - - IERC20Metadata sharesToken; - IERC20Metadata baseToken; - IFYToken fyToken; + Strategy srcStrategy = Strategy(0xad1983745D6c739537fEaB5bed45795f47A940b3); + IFYToken fyToken = IFYToken(0x523803c57a497c3AD0E850766c8276D4864edEA5); // Needs to match the base of the srcStrategy and dstStrategy Strategy dstStrategy; + IERC20 baseToken; - function setUp() public virtual { - vm.createSelectFork(MAINNET, 15741300); + address srcStrategyHolder = 0x3353E1E2976DBbc191a739871faA8E6E9D2622c7; - srcSeriesId = srcStrategy.seriesId(); - srcPool = srcStrategy.pool(); - srcFYToken = IFYToken(address(srcPool.fyToken())); - - dstSeriesId = donorStrategy.seriesId(); - dstPool = donorStrategy.pool(); - dstFYToken = IFYToken(address(dstPool.fyToken())); + function setUp() public virtual { + vm.createSelectFork("MIGRATE_TESTS"); // Will only work on https://rpc.tenderly.co/fork/b9c353b6-37ae-4f9c-8649-5d23df9f862f - baseToken = srcPool.baseToken(); - sharesToken = srcPool.sharesToken(); + baseToken = srcStrategy.base(); - dstStrategy = new Strategy("", "", dstFYToken); + dstStrategy = new Strategy("", "", fyToken); - dstStrategy.grantRole(StrategyMigrator.mint.selector, address(srcStrategy)); + dstStrategy.grantRole(StrategyMigrator.init.selector, address(srcStrategy)); vm.label(address(srcStrategy), "srcStrategy"); - vm.label(address(srcPool), "srcPool"); - vm.label(address(sharesToken), "sharesToken"); + vm.label(address(dstStrategy), "dstStrategy"); vm.label(address(baseToken), "baseToken"); vm.label(address(fyToken), "fyToken"); - vm.label(address(dstStrategy), "dstStrategy"); - - // Warp to maturity of srcFYToken - vm.warp(uint32(srcFYToken.maturity()) + 1); - - // srcStrategy divests - srcStrategy.endPool(); - - // Init dstStrategy - stdstore - .target(address(baseToken)) - .sig(baseToken.balanceOf.selector) - .with_key(address(dstStrategy)) - .checked_write(1); } } contract ZeroStateTest is ZeroState { - function testSetNextPool() public { - console2.log("srcStrategy.setNextPool(IPool(address(dstStrategy)), dstSeriesId)"); - vm.prank(timelock); - srcStrategy.setNextPool(IPool(address(dstStrategy)), dstSeriesId); - - // Test the strategy can add the dstStrategy as the next pool - assertEq(address(srcStrategy.nextPool()), address(dstStrategy)); - assertEq(srcStrategy.nextSeriesId(), dstSeriesId); - } -} - -abstract contract SetNextPoolState is ZeroState { - function setUp() public override virtual { - super.setUp(); - vm.prank(timelock); - srcStrategy.setNextPool(IPool(address(dstStrategy)), dstSeriesId); - } -} - -contract SetNextPoolStateTest is SetNextPoolState { - function testStartPool() public { - console2.log("srcStrategy.startPool(,,,)"); + function testInvestToMigrate() public { + console2.log("srcStrategy.invest()"); uint256 migratedBase = baseToken.balanceOf(address(srcStrategy)); vm.prank(timelock); - srcStrategy.startPool(0,0); + srcStrategy.invest(IPool(address(dstStrategy))); // srcStrategy has no base assertEq(baseToken.balanceOf(address(srcStrategy)), 0); // dstStrategy has the base - assertEq(baseToken.balanceOf(address(dstStrategy)), migratedBase + 1); // TODO: This might be because of Euler + assertEq(baseToken.balanceOf(address(dstStrategy)), migratedBase); } } -abstract contract MigratedState is SetNextPoolState { +abstract contract MigratedState is ZeroState { function setUp() public override virtual { super.setUp(); vm.prank(timelock); - srcStrategy.startPool(0,0); + srcStrategy.invest(IPool(address(dstStrategy))); } } contract MigratedStateTest is MigratedState { - function testBurn() public { + function testBurnAfterMigrate() public { console2.log("srcStrategy.burn()"); uint256 srcStrategySupply = srcStrategy.totalSupply(); uint256 dstStrategySupply = dstStrategy.totalSupply(); @@ -147,7 +97,7 @@ contract MigratedStateTest is MigratedState { assertEq(dstStrategy.balanceOf(address(srcStrategyHolder)), expectedDstStrategyBalance); } - function testMint() public { + function testMintAfterMigrate() public { console2.log("srcStrategy.mint()"); uint256 srcStrategyHolderBalance = srcStrategy.balanceOf(srcStrategyHolder); assertGe(srcStrategyHolderBalance, 0); @@ -165,6 +115,6 @@ contract MigratedStateTest is MigratedState { // srcStrategyHolder has no dstStrategy tokens assertEq(dstStrategy.balanceOf(address(srcStrategyHolder)), 0); // srcStrategyHolder has the same srcStrategy tokens he had before - assertEq(srcStrategy.balanceOf(address(srcStrategyHolder)), srcStrategyHolderBalance - 1); // TODO: Do the conversions rounding down to get to this value + assertEq(srcStrategy.balanceOf(address(srcStrategyHolder)), srcStrategyHolderBalance - 4); // TODO: Do the conversions rounding down to get to this value } } \ No newline at end of file diff --git a/test/StrategyTest.t.sol b/test/StrategyTest.t.sol index 90e0d03..a2d4c8d 100644 --- a/test/StrategyTest.t.sol +++ b/test/StrategyTest.t.sol @@ -61,6 +61,7 @@ abstract contract DeployedState is Test, TestConstants, TestExtensions { strategy.grantRole(Strategy.init.selector, alice); strategy.grantRole(Strategy.invest.selector, alice); strategy.grantRole(Strategy.eject.selector, alice); + strategy.grantRole(Strategy.retrieveFYToken.selector, alice); strategy.grantRole(Strategy.restart.selector, alice); vm.label(deployer, "deployer"); @@ -375,8 +376,16 @@ abstract contract EjectedState is InvestedState { } contract TestEjected is EjectedState { - function testBuyFYToken() public { - console2.log("strategy.buyFYToken()"); + function testNoAuthRetrieveFYToken() public { + console2.log("strategy.retrieveFYToken()"); + + vm.expectRevert(bytes("Access denied")); + vm.prank(bob); + strategy.retrieveFYToken(bob); + } + + function testRetrieveFYToken() public { + console2.log("strategy.retrieveFYToken()"); uint256 fyTokenAvailable = fyToken.balanceOf(address(strategy)); track("aliceFYTokens", fyToken.balanceOf(alice)); @@ -385,39 +394,23 @@ contract TestEjected is EjectedState { track("strategyBaseTokens", baseToken.balanceOf(address(strategy))); track("baseCached", strategy.baseCached()); - // initial buy - half of ejected fyToken balance - uint initialBuy = fyTokenAvailable / 2; - cash(baseToken, address(strategy), initialBuy); - (uint256 bought,) = strategy.buyFYToken(alice, bob); - - assertEq(bought, initialBuy); - assertTrackPlusEq("aliceFYTokens", initialBuy, fyToken.balanceOf(alice)); - assertTrackMinusEq("strategyFYToken", initialBuy, fyToken.balanceOf(address(strategy))); - assertTrackPlusEq("strategyBaseTokens", initialBuy, baseToken.balanceOf(address(strategy))); - assertTrackPlusEq("baseCached", initialBuy, strategy.baseCached()); - - // second buy - transfer in double the remaining fyToken and expect refund of base - track("bobBaseTokens", baseToken.balanceOf(address(bob))); - uint remainingFYToken = fyToken.balanceOf(address(strategy)); - uint secondBuy = remainingFYToken * 2; - uint returned; - cash(baseToken, address(strategy), secondBuy); - (bought, returned) = strategy.buyFYToken(alice, bob); - - assertEq(bought, remainingFYToken); - assertEq(returned, remainingFYToken); - assertEq(initialBuy + remainingFYToken, fyTokenAvailable); - assertTrackPlusEq("aliceFYTokens", fyTokenAvailable, fyToken.balanceOf(alice)); - assertTrackMinusEq("strategyFYToken", fyTokenAvailable, fyToken.balanceOf(address(strategy))); - assertTrackPlusEq("strategyBaseTokens", fyTokenAvailable, baseToken.balanceOf(address(strategy))); - assertTrackPlusEq("bobBaseTokens", secondBuy - remainingFYToken, baseToken.balanceOf(address(bob))); - // assertTrackPlusEq("baseCached", fyTokenAvailable, strategy.baseCached()); - - // State variables are reset - assertEq(address(strategy.fyToken()), address(0)); - assertEq(uint256(strategy.maturity()), 0); - assertEq(address(strategy.pool()), address(0)); - assertEq(uint256(strategy.state()), 1); + // Retrieve fyToken, and add half of its amount as base + uint donatedBase = fyTokenAvailable / 2; + cash(baseToken, address(strategy), donatedBase); + vm.prank(alice); + (uint256 retrievedFYToken, uint256 acceptedBase) = strategy.retrieveFYToken(alice); + +// assertEq(retrievedFYToken, fyTokenAvailable); + assertTrackPlusEq("aliceFYTokens", retrievedFYToken, fyToken.balanceOf(alice)); +// assertEq(fyToken.balanceOf(address(strategy)), 0); +// assertTrackPlusEq("strategyBaseTokens", donatedBase, baseToken.balanceOf(address(strategy))); +// assertTrackPlusEq("baseCached", donatedBase, strategy.baseCached()); +// +// // State variables are reset +// assertEq(address(strategy.fyToken()), address(0)); +// assertEq(uint256(strategy.maturity()), 0); +// assertEq(address(strategy.pool()), address(0)); +// assertEq(uint256(strategy.state()), 1); } // --> Divested } @@ -532,7 +525,7 @@ contract InvestedTiltedAfterMaturityTest is InvestedTiltedAfterMaturity { // eject -> Drained TODO // time passes -> InvestedTiltedAfterMaturity ✓ // Ejected -// buyFYToken -> Divested ✓ +// retrieveFYToken -> Divested ✓ // Blocked // restart -> Divested TODO // InvestedAfterMaturity diff --git a/test/TestConstants.sol b/test/TestConstants.sol index bfaa7af..0e1e8c3 100644 --- a/test/TestConstants.sol +++ b/test/TestConstants.sol @@ -31,6 +31,7 @@ contract TestConstants { string public constant LOCALHOST = "LOCALHOST"; string public constant MAINNET = "MAINNET"; string public constant ARBITRUM = "ARBITRUM"; + string public constant MIGRATE_TESTS = "MIGRATE_TESTS"; string public constant HARNESS = "HARNESS"; string public constant MOCK = "MOCK"; string public constant NETWORK = "NETWORK"; diff --git a/test/harness/StrategyHarness.t.sol b/test/harness/StrategyHarness.t.sol index 3a634e6..f0d3e77 100644 --- a/test/harness/StrategyHarness.t.sol +++ b/test/harness/StrategyHarness.t.sol @@ -201,7 +201,7 @@ abstract contract EjectedOrDrainedState is InvestedState { contract TestEjectedOrDrained is EjectedOrDrainedState { function testHarnessBuyFYTokenEjected() public skipOnCI onlyEjected { - console2.log("strategy.buyFYToken()"); + console2.log("strategy.retrieveFYToken()"); uint256 fyTokenAvailable = fyToken.balanceOf(address(strategy)); track("aliceFYTokens", fyToken.balanceOf(alice)); @@ -210,33 +210,16 @@ contract TestEjectedOrDrained is EjectedOrDrainedState { track("strategyBaseTokens", baseToken.balanceOf(address(strategy))); track("baseCached", strategy.baseCached()); - // initial buy - half of ejected fyToken balance - uint initialBuy = fyTokenAvailable / 2; - cash(baseToken, address(strategy), initialBuy); - (uint256 bought,) = strategy.buyFYToken(alice, bob); - - assertEq(bought, initialBuy); - assertTrackPlusEq("aliceFYTokens", initialBuy, fyToken.balanceOf(alice)); - assertTrackMinusEq("strategyFYToken", initialBuy, fyToken.balanceOf(address(strategy))); - assertTrackPlusEq("strategyBaseTokens", initialBuy, baseToken.balanceOf(address(strategy))); - assertTrackPlusEq("baseCached", initialBuy, strategy.baseCached()); - - // second buy - transfer in double the remaining fyToken and expect refund of base - track("bobBaseTokens", baseToken.balanceOf(address(bob))); - uint remainingFYToken = fyToken.balanceOf(address(strategy)); - uint secondBuy = remainingFYToken * 2; - uint returned; - cash(baseToken, address(strategy), secondBuy); - (bought, returned) = strategy.buyFYToken(alice, bob); - - assertEq(bought, remainingFYToken); - assertEq(returned, remainingFYToken); - assertEq(initialBuy + remainingFYToken, fyTokenAvailable); + // retrieve all fyToken and donate an amount of base + uint baseDonated = fyTokenAvailable / 2; + cash(baseToken, address(strategy), baseDonated); + (uint256 retrievedFYToken, uint256 baseAccepted) = strategy.retrieveFYToken(alice); + + assertEq(retrievedFYToken, fyTokenAvailable); assertTrackPlusEq("aliceFYTokens", fyTokenAvailable, fyToken.balanceOf(alice)); - assertTrackMinusEq("strategyFYToken", fyTokenAvailable, fyToken.balanceOf(address(strategy))); - assertTrackPlusEq("strategyBaseTokens", fyTokenAvailable, baseToken.balanceOf(address(strategy))); - assertTrackPlusEq("bobBaseTokens", secondBuy - remainingFYToken, baseToken.balanceOf(address(bob))); - assertTrackPlusEq("baseCached", fyTokenAvailable, strategy.baseCached()); + assertEq(fyToken.balanceOf(address(strategy)), 0); + assertTrackPlusEq("strategyBaseTokens", baseDonated, baseToken.balanceOf(address(strategy))); + assertTrackPlusEq("baseCached", baseDonated, strategy.baseCached()); // State variables are reset assertEq(address(strategy.fyToken()), address(0)); @@ -354,7 +337,7 @@ contract DivestedStateTest is DivestedState { // eject -> Drained ✓ // time passes -> InvestedAfterMaturity ✓ // Ejected -// buyFYToken -> Divested ✓ +// retrieveFYToken -> Divested ✓ // Drained // restart -> Divested ✓ // InvestedAfterMaturity