Skip to content
Merged
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 script/DeployFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ contract DeployFactory is Script {
f.stvStETHPoolFactory = address(new StvStETHPoolFactory());
f.withdrawalQueueFactory = address(new WithdrawalQueueFactory());
f.distributorFactory = address(new DistributorFactory());
f.ggvStrategyFactory = address(new GGVStrategyFactory(_ggvTeller, _ggvBoringQueue, _steth, _wsteth));
f.ggvStrategyFactory = address(new GGVStrategyFactory(_ggvTeller, _ggvBoringQueue));
f.timelockFactory = address(new TimelockFactory());
}

Expand Down
13 changes: 9 additions & 4 deletions src/Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {StvPoolFactory} from "./factories/StvPoolFactory.sol";
import {StvStETHPoolFactory} from "./factories/StvStETHPoolFactory.sol";
import {TimelockFactory} from "./factories/TimelockFactory.sol";
import {WithdrawalQueueFactory} from "./factories/WithdrawalQueueFactory.sol";
import {IStrategy} from "./interfaces/IStrategy.sol";
import {IStrategyFactory} from "./interfaces/IStrategyFactory.sol";
import {ILidoLocator} from "./interfaces/core/ILidoLocator.sol";
import {IVaultHub} from "./interfaces/core/IVaultHub.sol";
Expand Down Expand Up @@ -491,11 +492,15 @@ 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)
address strategyImpl = IStrategyFactory(_intermediate.strategyFactory)
.deploy(address(pool), _intermediate.strategyDeployBytes);
pool.addToAllowList(strategy);

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

pool.addToAllowList(strategyProxy);
}

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

emit PoolCreated(
Expand Down
11 changes: 3 additions & 8 deletions src/factories/GGVStrategyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@ 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;
address public immutable STETH;
address public immutable WSTETH;

constructor(address _teller, address _boringQueue, address _steth, address _wsteth) {
constructor(address _teller, address _boringQueue) {
require(_teller.code.length > 0, "TELLER: not a contract");
require(_boringQueue.code.length > 0, "BORING_QUEUE: not a contract");
require(_steth.code.length > 0, "STETH: not a contract");
require(_wsteth.code.length > 0, "WSTETH: not a contract");
TELLER = _teller;
BORING_QUEUE = _boringQueue;
STETH = _steth;
WSTETH = _wsteth;
}

function deploy(address _pool, bytes calldata _deployBytes) external returns (address impl) {
// _deployBytes is unused for GGVStrategy, but required by IStrategyFactory interface
_deployBytes;
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));
}
}
103 changes: 84 additions & 19 deletions src/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,96 @@
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);

/**
* @notice Initializes the strategy
* @param _admin The admin address
*/
function initialize(address _admin) external;

/**
* @notice Returns the address of the pool
* @return The address of the pool
*/
function POOL() external view returns (address);

/// @notice Supplies stETH to the strategy
function supply(address _referral, bytes calldata _params) external payable;
/**
* @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 minted amount of stv
*/
function supply(address _referral, uint256 _wstethToMint, bytes calldata _params)
external
payable
returns (uint256 stv);

/**
* @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
view
returns (uint256 stethShares);

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

/// @notice Requests a withdrawal from the strategy
function requestExitByStethShares(uint256 stethSharesToBurn, bytes calldata params)
/**
* @notice Finalizes exit from the strategy
* @param requestId The Strategy request id
*/
function finalizeRequestExit(bytes32 requestId) external;

/**
* @notice Burns wstETH to reduce the user's minted stETH shares 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 (bytes32 requestId);
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 Finalizes a withdrawal from the strategy
function finalizeRequestExit(address receiver, bytes32 requestId) external;
/**
* @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 Recovers ERC20 tokens from the strategy
function recoverERC20(address _token, address _recipient, uint256 _amount) external;
/**
* @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);
}
9 changes: 3 additions & 6 deletions src/interfaces/IStrategyCallForwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ pragma solidity >=0.8.25;

interface IStrategyCallForwarder {
function initialize(address _owner) external;
function call(address _target, bytes calldata _data) external payable returns (bytes memory);
function callWithValue(address _target, bytes calldata _data, uint256 _value)
external
payable
returns (bytes memory);
function sendValue(address payable _recipient, uint256 _amount) external payable;
function doCall(address _target, bytes calldata _data) external returns (bytes memory);
function doCallWithValue(address _target, bytes calldata _data, uint256 _value) external returns (bytes memory);
function sendValue(address payable _recipient, uint256 _amount) external;
}
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/core/IStETH.sol";
import {IWstETH} from "src/interfaces/core/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/core/IStETH.sol";
import {IWstETH} from "src/interfaces/core/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