Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 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
13 changes: 9 additions & 4 deletions src/Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {StvStETHPoolFactory} from "./factories/StvStETHPoolFactory.sol";
import {TimelockFactory} from "./factories/TimelockFactory.sol";
import {WithdrawalQueueFactory} from "./factories/WithdrawalQueueFactory.sol";
import {ILidoLocator} from "./interfaces/ILidoLocator.sol";
import {IStrategy} from "./interfaces/IStrategy.sol";
import {IStrategyFactory} from "./interfaces/IStrategyFactory.sol";
import {IVaultHub} from "./interfaces/IVaultHub.sol";
import {DummyImplementation} from "./proxy/DummyImplementation.sol";
Expand Down Expand Up @@ -311,10 +312,14 @@ contract Factory {
dashboard.grantRole(dashboard.BURN_ROLE(), address(pool));
}

address strategy = address(0);
address strategyProxy = address(0);
if (intermediate.strategyFactory != address(0)) {
strategy = IStrategyFactory(intermediate.strategyFactory).deploy(address(pool), STETH, WSTETH);
pool.addToAllowList(strategy);
address strategyImpl = IStrategyFactory(intermediate.strategyFactory).deploy(address(pool));

strategyProxy =
address(new OssifiableProxy(strategyImpl, timelock, abi.encodeCall(IStrategy.initialize, (timelock))));

pool.addToAllowList(strategyProxy);
}

pool.grantRole(DEFAULT_ADMIN_ROLE, timelock);
Expand All @@ -331,7 +336,7 @@ contract Factory {
withdrawalQueue: address(withdrawalQueue),
distributor: address(pool.DISTRIBUTOR()),
timelock: intermediate.timelock,
strategy: strategy
strategy: strategyProxy
});

emit VaultPoolCreated(
Expand Down
5 changes: 3 additions & 2 deletions src/factories/GGVStrategyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {GGVStrategy} from "src/strategy/GGVStrategy.sol";
import {StrategyCallForwarder} from "src/strategy/StrategyCallForwarder.sol";

contract GGVStrategyFactory is IStrategyFactory {
bytes32 public immutable STRATEGY_ID = keccak256("strategy.ggv.v1");
address public immutable TELLER;
address public immutable BORING_QUEUE;

Expand All @@ -16,8 +17,8 @@ contract GGVStrategyFactory is IStrategyFactory {
BORING_QUEUE = _boringQueue;
}

function deploy(address _pool, address _steth, address _wsteth) external returns (address impl) {
function deploy(address _pool) external returns (address impl) {
address strategyCallForwarderImpl = address(new StrategyCallForwarder());
impl = address(new GGVStrategy(strategyCallForwarderImpl, _pool, _steth, _wsteth, TELLER, BORING_QUEUE));
impl = address(new GGVStrategy(STRATEGY_ID, strategyCallForwarderImpl, _pool, TELLER, BORING_QUEUE));
}
}
96 changes: 77 additions & 19 deletions src/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,89 @@
pragma solidity >=0.8.25;

interface IStrategy {
event StrategySupplied(address indexed user, uint256 stv, uint256 stethShares, uint256 stethAmount, bytes data);
event StrategyExitRequested(address indexed user, bytes32 requestId, uint256 stethSharesToBurn, bytes data);
event StrategyExitFinalized(address indexed user, bytes32 requestId, uint256 stethShares);
event StrategySupplied(
address indexed user, address indexed referral, uint256 ethAmount, uint256 stv, uint256 wstethToMint, bytes data
);
event StrategyExitRequested(address indexed user, bytes32 requestId, uint256 wsteth, bytes data);
event StrategyExitFinalized(address indexed user, bytes32 requestId, uint256 wsteth);

function POOL() external view returns (address);
function initialize(address _admin) external;

/// @notice Supplies stETH to the strategy
function supply(address _referral, bytes calldata _params) external payable;
function POOL() external view returns (address);

/// @notice Requests a withdrawal from the Withdrawal Queue
function requestWithdrawal(
uint256 _stvToWithdraw,
uint256 _stethSharesToBurn,
uint256 _stethSharesToRebalance,
address _receiver
) external returns (uint256 requestId);
/**
* @notice Supplies wstETH to the strategy
* @param _referral The referral address
* @param _wstethToMint The amount of wstETH to mint
* @param _params The parameters for the supply
* @return stv The amount of stv that would be minted
*/
function supply(address _referral, uint256 _wstethToMint, bytes calldata _params)
external
payable
returns (uint256 stv);

/// @notice Requests a withdrawal from the strategy
function requestExitByStethShares(uint256 stethSharesToBurn, bytes calldata params)
/**
* @notice Returns the remaining minting capacity shares of a user
* @param _user The user to get the remaining minting capacity shares for
* @param _ethToFund The amount of ETH to fund
* @return stethShares The remaining minting capacity shares
*/
function remainingMintingCapacitySharesOf(address _user, uint256 _ethToFund)
external
returns (bytes32 requestId);
view
returns (uint256 stethShares);

/// @notice Finalizes a withdrawal from the strategy
/**
* @notice Requests a withdrawal from the strategy
* @param _wsteth The amount of wstETH to request exit for
* @param _params The parameters for the withdrawal
* @return requestId The Strategy request id
*/
function requestExitByWsteth(uint256 _wsteth, bytes calldata _params) external returns (bytes32 requestId);

/**
* @notice Finalizes a withdrawal from the strategy
* @param receiver The address to receive the withdrawal
* @param requestId The Strategy request id
*/
function finalizeRequestExit(address receiver, bytes32 requestId) external;

/// @notice Recovers ERC20 tokens from the strategy
function recoverERC20(address _token, address _recipient, uint256 _amount) external;
/**
* @notice Burns wstETH to reduce the user's minted stETH obligation
* @param _wstethToBurn The amount of wstETH to burn
*/
function burnWsteth(uint256 _wstethToBurn) external;

/**
* @notice Requests a withdrawal from the Withdrawal Queue
* @param _recipient The address to receive the withdrawal
* @param _stvToWithdraw The amount of stv to withdraw
* @param _stethSharesToRebalance The amount of stETH shares to rebalance
* @return requestId The Withdrawal Queue request ID
*/
function requestWithdrawalFromPool(address _recipient, uint256 _stvToWithdraw, uint256 _stethSharesToRebalance)
external
returns (uint256 requestId);

/**
* @notice Returns the amount of wstETH of a user
* @param _user The user to get the wstETH for
* @return wsteth The amount of wstETH
*/
function wstethOf(address _user) external view returns (uint256);

/**
* @notice Returns the amount of stv of a user
* @param _user The user to get the stv for
* @return stv The amount of stv
*/
function stvOf(address _user) external view returns (uint256);

/**
* @notice Returns the amount of minted stETH shares of a user
* @param _user The user to get the minted stETH shares for
* @return mintedStethShares The amount of minted stETH shares
*/
function mintedStethSharesOf(address _user) external view returns (uint256 mintedStethShares);
}
2 changes: 1 addition & 1 deletion src/interfaces/IStrategyCallForwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ interface IStrategyCallForwarder {
external
payable
returns (bytes memory);
function sendValue(address payable _recipient, uint256 _amount) external payable;
function sendValue(address payable _recipient, uint256 _amount) external;
}
4 changes: 1 addition & 3 deletions src/interfaces/IStrategyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ pragma solidity >=0.8.25;
interface IStrategyFactory {
/// @notice Deploys a new strategy contract instance
/// @param _pool Address of the pool contract
/// @param _steth Address of the stETH token
/// @param _wsteth Address of the wstETH token
/// @return impl The address of the newly deployed strategy contract
function deploy(address _pool, address _steth, address _wsteth) external returns (address impl);
function deploy(address _pool) external returns (address impl);
}
14 changes: 12 additions & 2 deletions src/mock/ggv/GGVMockTeller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import {GGVVaultMock} from "./GGVVaultMock.sol";
import {IStETH} from "src/interfaces/IStETH.sol";
import {IWstETH} from "src/interfaces/IWstETH.sol";
import {ITellerWithMultiAssetSupport} from "src/interfaces/ggv/ITellerWithMultiAssetSupport.sol";

contract GGVMockTeller is ITellerWithMultiAssetSupport {
Expand All @@ -18,6 +19,7 @@ contract GGVMockTeller is ITellerWithMultiAssetSupport {
GGVVaultMock public immutable _vault;
uint256 internal immutable ONE_SHARE;
IStETH public immutable steth;
IWstETH public immutable wsteth;

mapping(ERC20 asset => Asset) public assets;

Expand All @@ -27,12 +29,13 @@ contract GGVMockTeller is ITellerWithMultiAssetSupport {
owner = _owner;
_vault = GGVVaultMock(__vault);
steth = IStETH(_steth);
wsteth = IWstETH(_wsteth);

// eq to 10 ** vault.decimals()
ONE_SHARE = 10 ** 18;

_updateAssetData(ERC20(_steth), true, false, 0);
_updateAssetData(ERC20(_wsteth), false, true, 0);
_updateAssetData(ERC20(_wsteth), true, true, 0);
}

function deposit(ERC20 depositAsset, uint256 depositAmount, uint256 minimumMint, address referralAddress)
Expand All @@ -47,7 +50,14 @@ contract GGVMockTeller is ITellerWithMultiAssetSupport {
revert("Deposit amount must be greater than 0");
}

uint256 stethShares = steth.getSharesByPooledEth(depositAmount);
uint256 stethShares;
if (address(depositAsset) == address(steth)) {
stethShares = steth.getSharesByPooledEth(depositAmount);
} else if (address(depositAsset) == address(wsteth)) {
stethShares = depositAmount;
} else {
revert("Unsupported asset");
}

// hardcode share calculation for only steth
shares = _vault.getSharesByAssets(stethShares);
Expand Down
56 changes: 46 additions & 10 deletions src/mock/ggv/GGVVaultMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {BorrowedMath} from "./BorrowedMath.sol";
import {GGVMockTeller} from "./GGVMockTeller.sol";
import {GGVQueueMock} from "./GGVQueueMock.sol";
import {IStETH} from "src/interfaces/IStETH.sol";
import {IWstETH} from "src/interfaces/IWstETH.sol";
import {IBoringOnChainQueue} from "src/interfaces/ggv/IBoringOnChainQueue.sol";
import {ITellerWithMultiAssetSupport} from "src/interfaces/ggv/ITellerWithMultiAssetSupport.sol";

Expand All @@ -15,34 +16,64 @@ contract GGVVaultMock is ERC20 {
ITellerWithMultiAssetSupport public immutable TELLER;
GGVQueueMock public immutable BORING_QUEUE;
IStETH public immutable steth;
IWstETH public immutable wsteth;

// steth shares as base vault asset
// real ggv uses weth but it should be okay to peg it to steth shares for mock
uint256 public _totalAssets;

error OnlyOwner();
error OnlyTeller();
error OnlyQueue();

constructor(address _owner, address _steth, address _wsteth) ERC20("GGVVaultMock", "tGGV") {
owner = _owner;
TELLER = ITellerWithMultiAssetSupport(address(new GGVMockTeller(_owner, address(this), _steth, _wsteth)));
BORING_QUEUE = new GGVQueueMock(address(this), _steth, _wsteth, _owner);
steth = IStETH(_steth);
wsteth = IWstETH(_wsteth);

// Mint some initial tokens to the dead address to avoid zero totalSupply issues
_mint(address(0xdead), 1e18);
_totalAssets = 1e18;
}

function rebase(uint256 stethSharesToRebaseWith) external {
require(msg.sender == owner, "Only owner can rebase");
steth.transferSharesFrom(msg.sender, address(this), stethSharesToRebaseWith);
_totalAssets += stethSharesToRebaseWith;
function _onlyOwner() internal view {
if (msg.sender != owner) revert OnlyOwner();
}

function _onlyTeller() internal view {
if (msg.sender != address(TELLER)) revert OnlyTeller();
}

function negativeRebase(uint256 stethSharesToRebaseWith) external {
require(msg.sender == owner, "Only owner can rebase");
function _onlyQueue() internal view {
if (msg.sender != address(BORING_QUEUE)) revert OnlyQueue();
}

function rebaseSteth(uint256 _stethShares) external {
_onlyOwner();
steth.transferSharesFrom(msg.sender, address(this), _stethShares);
_totalAssets += _stethShares;
}

function negativeRebaseSteth(uint256 stethSharesToRebaseWith) external {
_onlyOwner();
steth.transferShares(msg.sender, stethSharesToRebaseWith);
_totalAssets -= stethSharesToRebaseWith;
}

function rebaseWsteth(uint256 wstethAmount) external {
_onlyOwner();
wsteth.transferFrom(msg.sender, address(this), wstethAmount);
_totalAssets += wstethAmount;
}

function negativeRebaseWsteth(uint256 wstethAmount) external {
_onlyOwner();
wsteth.transfer(msg.sender, wstethAmount);
_totalAssets -= wstethAmount;
}

function getSharesByAssets(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply();
uint256 totalAssets_ = totalAssets();
Expand All @@ -59,17 +90,22 @@ contract GGVVaultMock is ERC20 {
}

function depositByTeller(address asset, uint256 shares, uint256 assets, address user) external {
require(msg.sender == address(TELLER), "Only teller can call depositByTeller");
require(asset == address(steth), "Only steth asset supported");
_onlyTeller();

steth.transferSharesFrom(user, address(this), assets);
if (asset == address(steth)) {
steth.transferSharesFrom(user, address(this), assets);
} else if (asset == address(wsteth)) {
wsteth.transferFrom(user, address(this), assets);
} else {
revert("Unsupported asset");
}

_mint(user, shares);
_totalAssets += assets;
}

function burnSharesReturnAssets(ERC20 assetOut, uint256 shares, uint256 assets, address user) external {
require(msg.sender == address(BORING_QUEUE), "Only queue can call burnShares");
_onlyQueue();
_burn(address(BORING_QUEUE), shares);
_totalAssets -= assets;
assetOut.transfer(user, assets);
Expand Down
Loading
Loading