diff --git a/contracts/src/instances/erc4626/ERC4626Base.sol b/contracts/src/instances/erc4626/ERC4626Base.sol index 67c3ef7bf..ac39aba56 100644 --- a/contracts/src/instances/erc4626/ERC4626Base.sol +++ b/contracts/src/instances/erc4626/ERC4626Base.sol @@ -34,114 +34,82 @@ abstract contract ERC4626Base is HyperdriveBase { /// Yield Source /// - /// @notice Accepts a trader's deposit in either base or vault shares. If - /// the deposit is settled in base, the base is deposited into the - /// yield source immediately. - /// @param _amount The amount of token to transfer. The units of this - /// quantity are either base or vault shares, depending on the value - /// of `_options.asBase`. - /// @param _options The options that configure the deposit. The only option - /// used in this implementation is `_options.asBase`, which - /// determines if the deposit is settled in base or vault shares. - /// @return sharesMinted The amount deposited measured in vault shares. - /// @return vaultSharePrice The vault share price at the time of deposit. - function _deposit( - uint256 _amount, - IHyperdrive.Options calldata _options - ) - internal - override - returns (uint256 sharesMinted, uint256 vaultSharePrice) - { - if (_options.asBase) { - // Take custody of the deposit in base. - ERC20(address(_baseToken)).safeTransferFrom( - msg.sender, - address(this), - _amount - ); + /// @dev Accepts a deposit from the user in base. + /// @param _baseAmount The base amount to deposit. + /// @return The shares that were minted in the deposit. + /// @return The amount of ETH to refund. Since this yield source isn't + /// payable, this is always zero. + function _depositWithBase( + uint256 _baseAmount, + bytes calldata // unused + ) internal override returns (uint256, uint256) { + // Take custody of the deposit in base. + ERC20(address(_baseToken)).safeTransferFrom( + msg.sender, + address(this), + _baseAmount + ); - // Deposit the base into the yield source. - // - // NOTE: We increase the required approval amount by 1 wei so that - // the vault ends with an approval of 1 wei. This makes future - // approvals cheaper by keeping the storage slot warm. - ERC20(address(_baseToken)).forceApprove( - address(_vault), - _amount + 1 - ); - sharesMinted = _vault.deposit(_amount, address(this)); - } else { - // WARN: This logic doesn't account for slippage in the conversion - // from base to shares. If deposits to the yield source incur - // slippage, this logic will be incorrect. - sharesMinted = _amount; + // Deposit the base into the yield source. + // + // NOTE: We increase the required approval amount by 1 wei so that + // the vault ends with an approval of 1 wei. This makes future + // approvals cheaper by keeping the storage slot warm. + ERC20(address(_baseToken)).forceApprove( + address(_vault), + _baseAmount + 1 + ); + uint256 sharesMinted = _vault.deposit(_baseAmount, address(this)); - // Take custody of the deposit in vault shares. - ERC20(address(_vault)).safeTransferFrom( - msg.sender, - address(this), - sharesMinted - ); - } - vaultSharePrice = _pricePerVaultShare(); + return (sharesMinted, 0); } - /// @notice Processes a trader's withdrawal in either base or vault shares. - /// If the withdrawal is settled in base, the base is withdrawn from - /// the yield source. - /// @param _shares The amount of vault shares to withdraw from Hyperdrive. - /// @param _vaultSharePrice The vault share price. - /// @param _options The options that configure the withdrawal. The options - /// used in this implementation are `_options.destination`, which - /// specifies the recipient of the withdrawal, and `_options.asBase`, - /// which determines if the withdrawal is settled in base or vault - /// shares. - /// @return amountWithdrawn The proceeds of the withdrawal. The units of - /// this quantity are vault shares since this yield source doesn't - /// support withdrawals in base. - function _withdraw( - uint256 _shares, - uint256 _vaultSharePrice, - IHyperdrive.Options calldata _options - ) internal override returns (uint256 amountWithdrawn) { - // NOTE: Round down to underestimate the base proceeds. - // - // Correct for any error that crept into the calculation of the share - // amount by converting the shares to base and then back to shares - // using the vault's share conversion logic. - uint256 baseAmount = _shares.mulDown(_vaultSharePrice); - _shares = _vault.convertToShares(baseAmount); + /// @dev Process a deposit in vault shares. + /// @param _shareAmount The vault shares amount to deposit. + function _depositWithShares( + uint256 _shareAmount, + bytes calldata // unused + ) internal override { + // Take custody of the deposit in vault shares. + ERC20(address(_vault)).safeTransferFrom( + msg.sender, + address(this), + _shareAmount + ); + } - // If we're withdrawing zero shares, short circuit and return 0. - if (_shares == 0) { - return 0; - } + /// @dev Process a withdrawal in base and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + /// @return amountWithdrawn The amount of base withdrawn. + function _withdrawWithBase( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override returns (uint256 amountWithdrawn) { + // Redeem from the yield source and transfer the + // resulting base to the destination address. + amountWithdrawn = _vault.redeem( + _shareAmount, + _destination, + address(this) + ); - // If we're withdrawing in base, we redeem the shares from the yield - // source, and we transfer base to the destination. - if (_options.asBase) { - // Redeem from the yield source and transfer the - // resulting base to the destination address. - amountWithdrawn = _vault.redeem( - _shares, - _options.destination, - address(this) - ); - } - // Otherwise, we're withdrawing in vault shares, and we transfer vault - // shares to the destination. - else { - // Transfer vault shares to the destination. - ERC20(address(_vault)).safeTransfer(_options.destination, _shares); - amountWithdrawn = _shares; - } + return amountWithdrawn; } - /// @notice Loads the vault share price from the yield source. - /// @return The current vault share price. - function _pricePerVaultShare() internal view override returns (uint256) { - return _vault.convertToAssets(ONE); + /// @dev Process a withdrawal in vault shares and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + function _withdrawWithShares( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override { + // Transfer vault shares to the destination. + ERC20(address(_vault)).safeTransfer(_destination, _shareAmount); } /// @dev Ensure that ether wasn't sent because ERC4626 vaults don't support @@ -151,4 +119,22 @@ abstract contract ERC4626Base is HyperdriveBase { revert IHyperdrive.NotPayable(); } } + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _shareAmount The vault shares amount. + /// @return The base amount. + function _convertToBase( + uint256 _shareAmount + ) internal view override returns (uint256) { + return _vault.convertToAssets(_shareAmount); + } + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _baseAmount The base amount. + /// @return The vault shares amount. + function _convertToShares( + uint256 _baseAmount + ) internal view override returns (uint256) { + return _vault.convertToShares(_baseAmount); + } } diff --git a/contracts/src/instances/steth/StETHBase.sol b/contracts/src/instances/steth/StETHBase.sol index e53bafd6a..1535c6ea7 100644 --- a/contracts/src/instances/steth/StETHBase.sol +++ b/contracts/src/instances/steth/StETHBase.sol @@ -30,118 +30,89 @@ abstract contract StETHBase is HyperdriveBase { /// Yield Source /// - /// @dev Accepts a transfer from the user in base or the yield source token. - /// @param _amount The amount of capital to deposit. The units of this - /// quantity are either base or vault shares, depending on the value - /// of `_options.asBase`. - /// @param _options The options that configure how the trade is settled. The - /// only option used in this deposit implementation is - /// `_options.asBase` which determines if the deposit is settled in - /// ETH or stETH shares. - /// @return shares The amount of capital deposited measured in vault shares. - /// @return vaultSharePrice The current vault share price. - function _deposit( - uint256 _amount, - IHyperdrive.Options calldata _options - ) internal override returns (uint256 shares, uint256 vaultSharePrice) { - uint256 refund; - if (_options.asBase) { - // Ensure that sufficient ether was provided. - if (msg.value < _amount) { - revert IHyperdrive.TransferFailed(); - } - - // If the user sent more ether than the amount specified, refund the - // excess ether. - unchecked { - refund = msg.value - _amount; - } - - // Submit the provided ether to Lido to be deposited. The fee - // collector address is passed as the referral address; however, - // users can specify whatever referrer they'd like by depositing - // stETH instead of ETH. - shares = _lido.submit{ value: _amount }(_feeCollector); - } else { - // Refund any ether that was sent to the contract. - refund = msg.value; + /// @dev Accepts a deposit from the user in base. + /// @param _baseAmount The base amount to deposit. + /// @return sharesMinted The shares that were minted in the deposit. + /// @return refund The amount of ETH to refund. This should be zero for + /// yield sources that don't accept ETH. + function _depositWithBase( + uint256 _baseAmount, + bytes calldata // unused + ) internal override returns (uint256 sharesMinted, uint256 refund) { + // Ensure that sufficient ether was provided. + if (msg.value < _baseAmount) { + revert IHyperdrive.TransferFailed(); + } - // Transfer stETH shares into the contract. - shares = _amount; - _lido.transferSharesFrom(msg.sender, address(this), shares); + // If the user sent more ether than the amount specified, refund the + // excess ether. + unchecked { + refund = msg.value - _baseAmount; } - // Calculate the vault share price. - vaultSharePrice = _pricePerVaultShare(); + // Submit the provided ether to Lido to be deposited. The fee + // collector address is passed as the referral address; however, + // users can specify whatever referrer they'd like by depositing + // stETH instead of ETH. + sharesMinted = _lido.submit{ value: _baseAmount }(_feeCollector); - // Return excess ether that was sent to the contract. - if (refund > 0) { - (bool success, ) = payable(msg.sender).call{ value: refund }(""); - if (!success) { - revert IHyperdrive.TransferFailed(); - } - } + return (sharesMinted, refund); + } - return (shares, vaultSharePrice); + /// @dev Process a deposit in vault shares. + /// @param _shareAmount The vault shares amount to deposit. + function _depositWithShares( + uint256 _shareAmount, + bytes calldata // unused + ) internal override { + // Transfer stETH shares into the contract. + _lido.transferSharesFrom(msg.sender, address(this), _shareAmount); } - /// @notice Processes a trader's withdrawal. This yield source only supports - /// withdrawals in stETH shares. - /// @param _shares The amount of vault shares to withdraw from Hyperdrive. - /// @param _vaultSharePrice The vault share price. - /// @param _options The options that configure the withdrawal. The options - /// used in this withdrawal implementation are `_options.destination`, - /// which specifies the recipient of the withdrawal, and - /// `_options.asBase`, which determines if the withdrawal is settled - /// in ETH or stETH. The `_options.asBase` option must be false since - /// stETH withdrawals aren't processed instantaneously. Users that - /// want to withdraw can manage their withdrawal separately. - /// @return The proceeds of the withdrawal. The units of this quantity are - /// vault shares since this yield source doesn't support withdrawals - /// in base. - function _withdraw( - uint256 _shares, - uint256 _vaultSharePrice, - IHyperdrive.Options calldata _options - ) internal override returns (uint256) { + /// @dev Process a withdrawal in base and send the proceeds to the + /// destination. + function _withdrawWithBase( + uint256, // unused + address, // unused + bytes calldata // unused + ) internal pure override returns (uint256) { // stETH withdrawals aren't necessarily instantaneous. Users that want // to withdraw can manage their withdrawal separately. - if (_options.asBase) { - revert IHyperdrive.UnsupportedToken(); - } - - // NOTE: Round down to underestimate the base proceeds. - // - // Correct for any error that crept into the calculation of the share - // amount by converting the shares to base and then back to shares - // using the vault's share conversion logic. - uint256 baseAmount = _shares.mulDown(_vaultSharePrice); - _shares = _lido.getSharesByPooledEth(baseAmount); - - // If we're withdrawing zero shares, short circuit and return 0. - if (_shares == 0) { - return 0; - } - - // Transfer the stETH shares to the destination. - _lido.transferShares(_options.destination, _shares); - - return _shares; + revert IHyperdrive.UnsupportedToken(); } - /// @dev Returns the current vault share price. We simply use Lido's - /// internal share price. - /// @return price The current vault share price. - function _pricePerVaultShare() - internal - view - override - returns (uint256 price) - { - return _lido.getPooledEthByShares(ONE); + /// @dev Process a withdrawal in vault shares and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + function _withdrawWithShares( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override { + // Transfer the stETH shares to the destination. + _lido.transferShares(_destination, _shareAmount); } /// @dev We override the message value check since this integration is /// payable. function _checkMessageValue() internal pure override {} + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _shareAmount The vault shares amount. + /// @return baseAmount The base amount. + function _convertToBase( + uint256 _shareAmount + ) internal view override returns (uint256) { + return _lido.getPooledEthByShares(_shareAmount); + } + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _baseAmount The base amount. + /// @return shareAmount The vault shares amount. + function _convertToShares( + uint256 _baseAmount + ) internal view override returns (uint256) { + return _lido.getSharesByPooledEth(_baseAmount); + } } diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 58bcd020d..bce748a0f 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -26,25 +26,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { /// Yield Source /// - /// @dev A yield source dependent check that prevents ether from being - /// transferred to Hyperdrive instances that don't accept ether. - function _checkMessageValue() internal view virtual; - - /// @dev A yield source dependent check that verifies that the provided - /// options are valid. The default check is that the destination is - /// non-zero to prevent users from accidentally transferring funds - /// to the zero address. Custom integrations can override this to - /// implement additional checks. - /// @param _options The provided options for the transaction. - function _checkOptions( - IHyperdrive.Options calldata _options - ) internal pure virtual { - if (_options.destination == address(0)) { - revert IHyperdrive.RestrictedZeroAddress(); - } - } - - /// @dev Accepts a deposit from the user and commits it to the yield source. + /// @dev Process a deposit in either base or vault shares. /// @param _amount The amount of capital to deposit. The units of this /// quantity are either base or vault shares, depending on the value /// of `_options.asBase`. @@ -57,10 +39,46 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { function _deposit( uint256 _amount, IHyperdrive.Options calldata _options - ) internal virtual returns (uint256 sharesMinted, uint256 vaultSharePrice); + ) internal returns (uint256 sharesMinted, uint256 vaultSharePrice) { + // Deposit with either base or shares depending on the provided options. + uint256 refund; + if (_options.asBase) { + // Process the deposit in base. + (sharesMinted, refund) = _depositWithBase( + _amount, + _options.extraData + ); + } else { + // The refund is equal to the full message value since ETH will + // never be a shares asset. + refund = msg.value; - /// @dev Withdraws shares from the yield source and sends the proceeds to - /// the destination. + // Process the deposit in shares. + _depositWithShares(_amount, _options.extraData); + + // WARN: This logic doesn't account for slippage in the conversion + // from base to shares. If deposits to the yield source incur + // slippage, this logic will be incorrect. + // + // The amount of shares minted is equal to the input amount. + sharesMinted = _amount; + } + + // Calculate the vault share price. + vaultSharePrice = _pricePerVaultShare(); + + // Return excess ether that was sent to the contract. + if (refund > 0) { + (bool success, ) = payable(msg.sender).call{ value: refund }(""); + if (!success) { + revert IHyperdrive.TransferFailed(); + } + } + + return (sharesMinted, vaultSharePrice); + } + + /// @dev Process a withdrawal and send the proceeds to the destination. /// @param _shares The vault shares to withdraw from the yield source. /// @param _vaultSharePrice The vault share price. /// @param _options The options that configure how the withdrawal is @@ -74,15 +92,124 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { uint256 _shares, uint256 _vaultSharePrice, IHyperdrive.Options calldata _options - ) internal virtual returns (uint256 amountWithdrawn); + ) internal returns (uint256 amountWithdrawn) { + // NOTE: Round down to underestimate the base proceeds. + // + // Correct for any error that crept into the calculation of the share + // amount by converting the shares to base and then back to shares + // using the vault's share conversion logic. + uint256 baseAmount = _shares.mulDown(_vaultSharePrice); + _shares = _convertToShares(baseAmount); + + // If we're withdrawing zero shares, short circuit and return 0. + if (_shares == 0) { + return 0; + } + + // Withdraw in either base or shares depending on the provided options. + if (_options.asBase) { + // Process the withdrawal in base. + amountWithdrawn = _withdrawWithBase( + _shares, + _options.destination, + _options.extraData + ); + } else { + // Process the withdrawal in shares. + _withdrawWithShares( + _shares, + _options.destination, + _options.extraData + ); + amountWithdrawn = _shares; + } + + return amountWithdrawn; + } /// @dev Loads the share price from the yield source. /// @return vaultSharePrice The current vault share price. function _pricePerVaultShare() internal view - virtual - returns (uint256 vaultSharePrice); + returns (uint256 vaultSharePrice) + { + return _convertToBase(ONE); + } + + /// @dev Accepts a deposit from the user in base. + /// @param _baseAmount The base amount to deposit. + /// @param _extraData The extra data to use in the deposit. + /// @return sharesMinted The shares that were minted in the deposit. + /// @return refund The amount of ETH to refund. This should be zero for + /// yield sources that don't accept ETH. + function _depositWithBase( + uint256 _baseAmount, + bytes calldata _extraData + ) internal virtual returns (uint256 sharesMinted, uint256 refund); + + /// @dev Process a deposit in vault shares. + /// @param _shareAmount The vault shares amount to deposit. + /// @param _extraData The extra data to use in the deposit. + function _depositWithShares( + uint256 _shareAmount, + bytes calldata _extraData + ) internal virtual; + + /// @dev Process a withdrawal in base and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + /// @param _extraData The extra data used to settle the withdrawal. + /// @return amountWithdrawn The amount of base withdrawn. + function _withdrawWithBase( + uint256 _shareAmount, + address _destination, + bytes calldata _extraData + ) internal virtual returns (uint256 amountWithdrawn); + + /// @dev Process a withdrawal in vault shares and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + /// @param _extraData The extra data used to settle the withdrawal. + function _withdrawWithShares( + uint256 _shareAmount, + address _destination, + bytes calldata _extraData + ) internal virtual; + + /// @dev A yield source dependent check that prevents ether from being + /// transferred to Hyperdrive instances that don't accept ether. + function _checkMessageValue() internal view virtual; + + /// @dev A yield source dependent check that verifies that the provided + /// options are valid. The default check is that the destination is + /// non-zero to prevent users from accidentally transferring funds + /// to the zero address. Custom integrations can override this to + /// implement additional checks. + /// @param _options The provided options for the transaction. + function _checkOptions( + IHyperdrive.Options calldata _options + ) internal pure virtual { + if (_options.destination == address(0)) { + revert IHyperdrive.RestrictedZeroAddress(); + } + } + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _shareAmount The vault shares amount. + /// @return baseAmount The base amount. + function _convertToBase( + uint256 _shareAmount + ) internal view virtual returns (uint256 baseAmount); + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _baseAmount The base amount. + /// @return shareAmount The vault shares amount. + function _convertToShares( + uint256 _baseAmount + ) internal view virtual returns (uint256 shareAmount); /// Pause /// diff --git a/contracts/test/MockHyperdrive.sol b/contracts/test/MockHyperdrive.sol index 5b1089d96..d14709306 100644 --- a/contracts/test/MockHyperdrive.sol +++ b/contracts/test/MockHyperdrive.sol @@ -40,38 +40,37 @@ abstract contract MockHyperdriveBase is HyperdriveBase { uint256 internal totalShares; - function _deposit( - uint256 amount, - IHyperdrive.Options calldata options + /// @dev Accepts a deposit from the user in base. + /// @param _baseAmount The base amount to deposit. + /// @return The shares that were minted in the deposit. + /// @return The amount of ETH to refund. Since this yield source isn't + /// payable, this is always zero. + function _depositWithBase( + uint256 _baseAmount, + bytes calldata // unused ) internal override returns (uint256, uint256) { - // Calculate the base amount of the deposit. + // Calculate the total amount of assets. uint256 assets; if (address(_baseToken) == ETH) { assets = address(this).balance; } else { assets = _baseToken.balanceOf(address(this)); } - uint256 baseAmount = options.asBase - ? amount - : amount.mulDivDown(assets, totalShares); // Transfer the specified amount of funds from the trader. If the trader // overpaid, we return the excess amount. bool success = true; + uint256 refund; if (address(_baseToken) == ETH) { - if (msg.value < baseAmount) { + if (msg.value < _baseAmount) { revert IHyperdrive.TransferFailed(); } - if (msg.value > baseAmount) { - (success, ) = payable(msg.sender).call{ - value: msg.value - baseAmount - }(""); - } + refund = msg.value - _baseAmount; } else { success = _baseToken.transferFrom( msg.sender, address(this), - baseAmount + _baseAmount ); } if (!success) { @@ -81,80 +80,117 @@ abstract contract MockHyperdriveBase is HyperdriveBase { // Increase the total shares and return with the amount of shares minted // and the current share price. if (totalShares == 0) { - totalShares = amount.divDown(_initialVaultSharePrice); - return (totalShares, _initialVaultSharePrice); + totalShares = _baseAmount.divDown(_initialVaultSharePrice); + return (totalShares, refund); } else { - uint256 newShares = amount.mulDivDown(totalShares, assets); + uint256 newShares = _baseAmount.mulDivDown(totalShares, assets); totalShares += newShares; - return (newShares, _pricePerVaultShare()); + return (newShares, refund); } } - function _withdraw( - uint256 shares, - uint256 sharePrice, - IHyperdrive.Options calldata options - ) internal override returns (uint256 withdrawValue) { - // Get the total amount of assets held in the pool. - uint256 assets; - if (address(_baseToken) == ETH) { - assets = address(this).balance; + /// @dev Process a deposit in vault shares. + /// @param _shareAmount The vault shares amount to deposit. + function _depositWithShares( + uint256 _shareAmount, + bytes calldata // unused + ) internal override { + // Calculate the base amount of the deposit. + uint256 baseAmount = _convertToBase(_shareAmount); + + // Increase the total shares and return with the amount of shares minted + // and the current share price. + if (totalShares == 0) { + totalShares = baseAmount.divDown(_initialVaultSharePrice); } else { - assets = _baseToken.balanceOf(address(this)); + uint256 newShares = _convertToShares(baseAmount); + totalShares += newShares; } - // Correct for any error that crept into the calculation of the share - // amount by converting the shares to base and then back to shares - // using the vault's share conversion logic. - uint256 baseAmount = shares.mulDown(sharePrice); - shares = baseAmount.mulDivDown(totalShares, assets); + // Transfer the specified amount of funds from the trader. If the trader + // overpaid, we return the excess amount. + bool success = true; + uint256 refund; + if (address(_baseToken) == ETH) { + if (msg.value < baseAmount) { + revert IHyperdrive.TransferFailed(); + } + refund = msg.value - baseAmount; + } else { + success = _baseToken.transferFrom( + msg.sender, + address(this), + baseAmount + ); + } + if (!success) { + revert IHyperdrive.TransferFailed(); + } + } + /// @dev Process a withdrawal in base and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + /// @return amountWithdrawn The amount of base withdrawn. + function _withdrawWithBase( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override returns (uint256 amountWithdrawn) { // If the shares to withdraw is greater than the total shares, we clamp // to the total shares. - shares = shares > totalShares ? totalShares : shares; + _shareAmount = _shareAmount > totalShares ? totalShares : _shareAmount; // Calculate the base proceeds. - withdrawValue = totalShares != 0 - ? shares.mulDivDown(assets, totalShares) - : 0; + uint256 withdrawValue = _convertToBase(_shareAmount); // Transfer the base proceeds to the destination and burn the shares. - totalShares -= shares; + totalShares -= _shareAmount; bool success; if (address(_baseToken) == ETH) { - (success, ) = payable(options.destination).call{ - value: withdrawValue - }(""); + (success, ) = payable(_destination).call{ value: withdrawValue }( + "" + ); } else { - success = _baseToken.transfer(options.destination, withdrawValue); + success = _baseToken.transfer(_destination, withdrawValue); } if (!success) { revert IHyperdrive.TransferFailed(); } - withdrawValue = options.asBase - ? withdrawValue - : withdrawValue.divDown(_pricePerVaultShare()); return withdrawValue; } - function _pricePerVaultShare() - internal - view - override - returns (uint256 vaultSharePrice) - { - // Get the total amount of base held in Hyperdrive. - uint256 assets; + /// @dev Process a withdrawal in vault shares and send the proceeds to the + /// destination. + /// @param _shareAmount The amount of vault shares to withdraw. + /// @param _destination The destination of the withdrawal. + function _withdrawWithShares( + uint256 _shareAmount, + address _destination, + bytes calldata // unused + ) internal override { + // If the shares to withdraw is greater than the total shares, we clamp + // to the total shares. + _shareAmount = _shareAmount > totalShares ? totalShares : _shareAmount; + + // Calculate the base proceeds. + uint256 withdrawValue = _convertToBase(_shareAmount); + + // Transfer the base proceeds to the destination and burn the shares. + totalShares -= _shareAmount; + bool success; if (address(_baseToken) == ETH) { - assets = address(this).balance; + (success, ) = payable(_destination).call{ value: withdrawValue }( + "" + ); } else { - assets = _baseToken.balanceOf(address(this)); + success = _baseToken.transfer(_destination, withdrawValue); + } + if (!success) { + revert IHyperdrive.TransferFailed(); } - - // The share price is the total amount of base divided by the total - // amount of shares. - vaultSharePrice = totalShares != 0 ? assets.divDown(totalShares) : 0; } // This overrides checkMessageValue to serve the dual purpose of making @@ -165,6 +201,41 @@ abstract contract MockHyperdriveBase is HyperdriveBase { revert IHyperdrive.NotPayable(); } } + + /// @dev Convert an amount of vault shares to an amount of base. + /// @param _shareAmount The vault shares amount. + /// @return The base amount. + function _convertToBase( + uint256 _shareAmount + ) internal view override returns (uint256) { + // Get the total amount of base held in Hyperdrive. + uint256 assets; + if (address(_baseToken) == ETH) { + assets = address(this).balance; + } else { + assets = _baseToken.balanceOf(address(this)); + } + + return + totalShares != 0 ? _shareAmount.mulDivDown(assets, totalShares) : 0; + } + + /// @dev Convert an amount of base to an amount of vault shares. + /// @param _baseAmount The base amount. + /// @return The vault shares amount. + function _convertToShares( + uint256 _baseAmount + ) internal view override returns (uint256) { + // Get the total amount of base held in Hyperdrive. + uint256 assets; + if (address(_baseToken) == ETH) { + assets = address(this).balance; + } else { + assets = _baseToken.balanceOf(address(this)); + } + + return _baseAmount.mulDivDown(totalShares, assets); + } } contract MockHyperdrive is Hyperdrive, MockHyperdriveBase { diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol index 47a0ff0be..3a845649d 100644 --- a/test/instances/steth/StETHHyperdrive.t.sol +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -677,8 +677,11 @@ contract StETHHyperdriveTest is HyperdriveTest { (uint256 maturityTime, uint256 basePaid) = openShort(bob, shortAmount); vm.deal(bob, balanceBefore - basePaid); + // NOTE: The variable rate must be greater than 0 since the unsupported + // check is only triggered if the shares amount is non-zero. + // // The term passes and interest accrues. - variableRate = variableRate.normalizeToRange(0, 2.5e18); + variableRate = variableRate.normalizeToRange(0.01e18, 2.5e18); advanceTime(POSITION_DURATION, variableRate); // Bob attempts to close his short with ETH as the target asset. This