Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version #28

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Initial version #28

wants to merge 3 commits into from

Conversation

ashablovskiy
Copy link
Contributor

@ashablovskiy ashablovskiy commented May 4, 2023

Core design ideas:

  • WETH (primary asset) is deposited to the contract and fortETH shares minted proportionally;
  • When primary asset is deposited it is then distributed according to predefined weights* to different asset(LSDs) vaults (separate contract, which could be added to/removed from fortETH assets list. Currently I implemented stETH vault, next step is frxETH vault).

There are two ways we may proceed after deposit (I implemented both, but we need to choice one we want to keep):
(a) deposited assets are immediately distribute between LSDs vaults (gas expensive);
(b) deposited assets are buffered in the contract until distributeBufferedAssets() is called for some bounty (less expensive);

  • Deposit weights might be changed and then two options available:
    (a) previously deposited assets remain untouched and new deposits are distributed according to updated weights
    (b) all asset in LSDs vaults converted to WETH and returned to fortETH contract, after they are redistributed back according to new weights (via calling redistributeAssets()).

Problems to consider: withdrawal (stETH -> ETH) is not currently available via Lido contract (https://www.coindesk.com/business/2023/04/06/lido-stakers-can-expect-ether-withdrawals-no-sooner-than-early-may/), therefore we could either wait when it become available or swap it through stETH/ETH pool (less effective due to slippage).

@xh3b4sd
Copy link

xh3b4sd commented May 4, 2023

Thanks for putting this together. Got some thoughts we may want to discuss separately.

  • We should maybe just wait for stETH to be withdrawable. Should happen the next couple of days/weeks. I saw Lido putting bounties out for their new contracts.
  • I was wondering how the optimal allocation between LSDs may be achieved. One idea would be to have a gauge like governance mechanism. If we want to go with a governance minimized approach, which in my mind is always better, we could allocate based on LSD market share or similar synthetic metrics.

uint256 _totalSupply = totalSupply;

// Calculate a fee - zero if user is the last to withdraw
uint256 _fee = (_totalSupply == 0 || _totalSupply - _shares == 0) ? 0 : assets.mulDivDown(fees.withdrawFeePercentage, DENOMINATOR);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can _totalSupply - _shares underflow? I supposed because it is uint it cannot be negative though.
  2. Why should the fee be zero for the last user to withdraw? Why does the fee charging change sometimes? Can this be exploited somehow?

if (!(_harvestBounty >= _minBounty)) revert InsufficientAmountOut();

IERC20(WETH).safeTransfer(_receiver, _harvestBounty);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, do we have some formatter or linter for Solidity? That indention looks off here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do proper formatting.

Comment on lines +243 to +257
/// @dev Adds emitting of YbTokenTransfer event to the original function
function transfer(address to, uint256 amount) public override returns (bool) {
balanceOf[msg.sender] -= amount;

// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}

emit Transfer(msg.sender, to, amount);
emit YbTokenTransfer(msg.sender, to, amount, convertToAssets(amount));

return true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the best practice here? Shouldn't there be some method that we inject and the rest of transfer remains the standard implementation? The current code looks like everything is copied and one line added for our use case. Not sure this is how we should be doing it.

for(uint256 i = 0; i < length;) {
address assetVault = assetVaultList[i];
totalAUM += IAssetVault(_assetVault).getEthBalance();
unchecked{ ++i; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imma asking all the noob questions here. Why should i be incremented within the unchecked block? Why don't we do it without?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It disables the built-in overflow check (which is not required here because the length would not exceed 2^256) and so saves gas.

Comment on lines 285 to 286
address assetVault = assetVaultList[i];
totalAUM += IAssetVault(_assetVault).getEthBalance();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks wrong somehow. We have assetVault and _assetVault like one comes from a different scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That a mistake. Will fix.

Comment on lines 40 to 43
/// @notice The address of WSTETH token.
address internal constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
/// @notice The address of STETH token.
address internal constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we add or remove LSDs when these are hard coded here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LSD vaults are added/removed on fortETH contract. The commented one is stETH LSD vault implementation.

@johnnyonline
Copy link
Contributor

I have a problem with the design. As I see it, the flow should be something like that;

  • user deposits ETH --> ETH is locked forever (maybe transferred to a treasury contract)
  • user receives fortETH in return for deposited ETH. 1:1 ratio (can use ERC4626 convertToShares calc)
  • deposited ETH is invested in current chosen strategy (strategy chosen by another contract, initially can be controlled by multisig. we can upgrade later)

user has 3 choices now:

  1. LP in fortETH/ETH Curve pool. we should have a Compounder for that
  2. stake fortETH for the staked version of fortETH (sfortETH?)
  3. exit position via fortETH/ETH Curve pool
  • staked version of fortETH should auto-compound rewards into more ETH (invested in chosen strategy), or, if fortETH/ETH price is less than x, purchase fortETH off the market and stake it

@ashablovskiy
Copy link
Contributor Author

Design reworked.

  1. The user deposits WETH, which is permanently locked in fortETH (withdraw, redeem functions are disabled);
  2. The user receives 1:1 fortETH shares;
  3. WETH deposited into the active strategy;
  4. The current strategy I built (Lido-Frax) converts WETH into stETH and frxETH based on predetermined weights. Rewarded stETH and frxETH in excess of those deposited might be distributed to fortETH holders or compounded back (wip).

@johnnyonline
Copy link
Contributor

Sounds good. Will take a look at the code tmr hopefully. In the meantime, can you elaborate on the strategy design you have in mind? How should that look like?

@ashablovskiy
Copy link
Contributor Author

@johnnyonline Strategy design:

  1. Transfer WETH from the fortETH contract to the strategy.
  2. Convert WETH to ETH.
  3. Calculate the share of each LSD asset (stETH, frxETH) based on their weight, which was set during strategy initiation and can be configured at any time.
  4. Deposit ETH into the Lido contract and mint 1:1 stETH (the internal accounting variable stEthDeposited is updated).
  5. Deposit ETH into the Frax contract and mint 1:1 frxETH (the internal accounting variable frxEthDeposited is updated).
  6. If the weights are updated, convert all stETH and frxETH back to ETH (the mechanism for this will be discussed) and then convert them back to stETH and frxETH based on the new weights (as specified in steps 3-5).
  7. The getRewards() function (work in progress) calculates the difference between the actual balance of stETH, frxETH and stEthDeposited, frxEthDeposited.
  8. The Harvest() function (work in progress) converts the difference between the actual balance of stETH, frxETH and stEthDeposited, frxEthDeposited into ETH, and either distributes it to fortETH holders or compounds it back into fortETH.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants