ERC20TokenizedVault.sol 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.0;
  3. import "../ERC20.sol";
  4. import "../utils/SafeERC20.sol";
  5. import "../../../interfaces/IERC4626.sol";
  6. import "../../../utils/math/Math.sol";
  7. /**
  8. * @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
  9. * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
  10. *
  11. * This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
  12. * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
  13. * the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this
  14. * contract and not the "assets" token which is an independent contract.
  15. *
  16. * _Available since v4.7._
  17. */
  18. abstract contract ERC20TokenizedVault is ERC20, IERC4626 {
  19. using Math for uint256;
  20. IERC20Metadata private immutable _asset;
  21. /**
  22. * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
  23. */
  24. constructor(IERC20Metadata asset_) {
  25. _asset = asset_;
  26. }
  27. /** @dev See {IERC4262-asset} */
  28. function asset() public view virtual override returns (address) {
  29. return address(_asset);
  30. }
  31. /** @dev See {IERC4262-totalAssets} */
  32. function totalAssets() public view virtual override returns (uint256) {
  33. return _asset.balanceOf(address(this));
  34. }
  35. /** @dev See {IERC4262-convertToShares} */
  36. function convertToShares(uint256 assets) public view virtual override returns (uint256 shares) {
  37. return _convertToShares(assets, Math.Rounding.Down);
  38. }
  39. /** @dev See {IERC4262-convertToAssets} */
  40. function convertToAssets(uint256 shares) public view virtual override returns (uint256 assets) {
  41. return _convertToAssets(shares, Math.Rounding.Down);
  42. }
  43. /** @dev See {IERC4262-maxDeposit} */
  44. function maxDeposit(address) public view virtual override returns (uint256) {
  45. return _isVaultCollateralized() ? type(uint256).max : 0;
  46. }
  47. /** @dev See {IERC4262-maxMint} */
  48. function maxMint(address) public view virtual override returns (uint256) {
  49. return type(uint256).max;
  50. }
  51. /** @dev See {IERC4262-maxWithdraw} */
  52. function maxWithdraw(address owner) public view virtual override returns (uint256) {
  53. return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
  54. }
  55. /** @dev See {IERC4262-maxRedeem} */
  56. function maxRedeem(address owner) public view virtual override returns (uint256) {
  57. return balanceOf(owner);
  58. }
  59. /** @dev See {IERC4262-previewDeposit} */
  60. function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
  61. return _convertToShares(assets, Math.Rounding.Down);
  62. }
  63. /** @dev See {IERC4262-previewMint} */
  64. function previewMint(uint256 shares) public view virtual override returns (uint256) {
  65. return _convertToAssets(shares, Math.Rounding.Up);
  66. }
  67. /** @dev See {IERC4262-previewWithdraw} */
  68. function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
  69. return _convertToShares(assets, Math.Rounding.Up);
  70. }
  71. /** @dev See {IERC4262-previewRedeem} */
  72. function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
  73. return _convertToAssets(shares, Math.Rounding.Down);
  74. }
  75. /** @dev See {IERC4262-deposit} */
  76. function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
  77. require(assets <= maxDeposit(receiver), "ERC20TokenizedVault: deposit more than max");
  78. uint256 shares = previewDeposit(assets);
  79. _deposit(_msgSender(), receiver, assets, shares);
  80. return shares;
  81. }
  82. /** @dev See {IERC4262-mint} */
  83. function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
  84. require(shares <= maxMint(receiver), "ERC20TokenizedVault: mint more than max");
  85. uint256 assets = previewMint(shares);
  86. _deposit(_msgSender(), receiver, assets, shares);
  87. return assets;
  88. }
  89. /** @dev See {IERC4262-withdraw} */
  90. function withdraw(
  91. uint256 assets,
  92. address receiver,
  93. address owner
  94. ) public virtual override returns (uint256) {
  95. require(assets <= maxWithdraw(owner), "ERC20TokenizedVault: withdraw more than max");
  96. uint256 shares = previewWithdraw(assets);
  97. _withdraw(_msgSender(), receiver, owner, assets, shares);
  98. return shares;
  99. }
  100. /** @dev See {IERC4262-redeem} */
  101. function redeem(
  102. uint256 shares,
  103. address receiver,
  104. address owner
  105. ) public virtual override returns (uint256) {
  106. require(shares <= maxRedeem(owner), "ERC20TokenizedVault: redeem more than max");
  107. uint256 assets = previewRedeem(shares);
  108. _withdraw(_msgSender(), receiver, owner, assets, shares);
  109. return assets;
  110. }
  111. /**
  112. * @dev Internal convertion function (from assets to shares) with support for rounding direction
  113. *
  114. * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
  115. * would represent an infinite amout of shares.
  116. */
  117. function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256 shares) {
  118. uint256 supply = totalSupply();
  119. return
  120. (assets == 0 || supply == 0)
  121. ? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding)
  122. : assets.mulDiv(supply, totalAssets(), rounding);
  123. }
  124. /**
  125. * @dev Internal convertion function (from shares to assets) with support for rounding direction
  126. */
  127. function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256 assets) {
  128. uint256 supply = totalSupply();
  129. return
  130. (supply == 0)
  131. ? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding)
  132. : shares.mulDiv(totalAssets(), supply, rounding);
  133. }
  134. /**
  135. * @dev Deposit/mint common workflow
  136. */
  137. function _deposit(
  138. address caller,
  139. address receiver,
  140. uint256 assets,
  141. uint256 shares
  142. ) private {
  143. // If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the
  144. // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
  145. // calls the vault, which is assumed not malicious.
  146. //
  147. // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
  148. // assets are transfered and before the shares are minted, which is a valid state.
  149. // slither-disable-next-line reentrancy-no-eth
  150. SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
  151. _mint(receiver, shares);
  152. emit Deposit(caller, receiver, assets, shares);
  153. }
  154. /**
  155. * @dev Withdraw/redeem common workflow
  156. */
  157. function _withdraw(
  158. address caller,
  159. address receiver,
  160. address owner,
  161. uint256 assets,
  162. uint256 shares
  163. ) private {
  164. if (caller != owner) {
  165. _spendAllowance(owner, caller, shares);
  166. }
  167. // If _asset is ERC777, `transfer` can trigger trigger a reentrancy AFTER the transfer happens through the
  168. // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
  169. // calls the vault, which is assumed not malicious.
  170. //
  171. // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
  172. // shares are burned and after the assets are transfered, which is a valid state.
  173. _burn(owner, shares);
  174. SafeERC20.safeTransfer(_asset, receiver, assets);
  175. emit Withdraw(caller, receiver, owner, assets, shares);
  176. }
  177. function _isVaultCollateralized() private view returns (bool) {
  178. return totalAssets() > 0 || totalSupply() == 0;
  179. }
  180. }