ERC4626.sol 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // SPDX-License-Identifier: MIT
  2. // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC4626.sol)
  3. pragma solidity ^0.8.19;
  4. import "../ERC20.sol";
  5. import "../utils/SafeERC20.sol";
  6. import "../../../interfaces/IERC4626.sol";
  7. import "../../../utils/math/Math.sol";
  8. /**
  9. * @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
  10. * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
  11. *
  12. * This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
  13. * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
  14. * the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this
  15. * contract and not the "assets" token which is an independent contract.
  16. *
  17. * [CAUTION]
  18. * ====
  19. * In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning
  20. * with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation
  21. * attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial
  22. * deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may
  23. * similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by
  24. * verifying the amount received is as expected, using a wrapper that performs these checks such as
  25. * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].
  26. *
  27. * Since v4.9, this implementation uses virtual assets and shares to mitigate that risk. The `_decimalsOffset()`
  28. * corresponds to an offset in the decimal representation between the underlying asset's decimals and the vault
  29. * decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself
  30. * determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset
  31. * (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker's
  32. * donation) matching the attacker's expected gains. With a larger offset, the attack becomes orders of magnitude more
  33. * expensive than it is profitable. More details about the underlying math can be found
  34. * xref:erc4626.adoc#inflation-attack[here].
  35. *
  36. * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued
  37. * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets
  38. * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience
  39. * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the
  40. * `_convertToShares` and `_convertToAssets` functions.
  41. *
  42. * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].
  43. * ====
  44. *
  45. * _Available since v4.7._
  46. */
  47. abstract contract ERC4626 is ERC20, IERC4626 {
  48. using Math for uint256;
  49. IERC20 private immutable _asset;
  50. uint8 private immutable _underlyingDecimals;
  51. /**
  52. * @dev Attempted to deposit more assets than the max amount for `receiver`.
  53. */
  54. error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max);
  55. /**
  56. * @dev Attempted to mint more shares than the max amount for `receiver`.
  57. */
  58. error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max);
  59. /**
  60. * @dev Attempted to withdraw more assets than the max amount for `receiver`.
  61. */
  62. error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);
  63. /**
  64. * @dev Attempted to redeem more shares than the max amount for `receiver`.
  65. */
  66. error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);
  67. /**
  68. * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
  69. */
  70. constructor(IERC20 asset_) {
  71. (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
  72. _underlyingDecimals = success ? assetDecimals : 18;
  73. _asset = asset_;
  74. }
  75. /**
  76. * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
  77. */
  78. function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) {
  79. (bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
  80. abi.encodeWithSelector(IERC20Metadata.decimals.selector)
  81. );
  82. if (success && encodedDecimals.length >= 32) {
  83. uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
  84. if (returnedDecimals <= type(uint8).max) {
  85. return (true, uint8(returnedDecimals));
  86. }
  87. }
  88. return (false, 0);
  89. }
  90. /**
  91. * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
  92. * "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
  93. * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
  94. *
  95. * See {IERC20Metadata-decimals}.
  96. */
  97. function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
  98. return _underlyingDecimals + _decimalsOffset();
  99. }
  100. /** @dev See {IERC4626-asset}. */
  101. function asset() public view virtual override returns (address) {
  102. return address(_asset);
  103. }
  104. /** @dev See {IERC4626-totalAssets}. */
  105. function totalAssets() public view virtual override returns (uint256) {
  106. return _asset.balanceOf(address(this));
  107. }
  108. /** @dev See {IERC4626-convertToShares}. */
  109. function convertToShares(uint256 assets) public view virtual override returns (uint256) {
  110. return _convertToShares(assets, Math.Rounding.Down);
  111. }
  112. /** @dev See {IERC4626-convertToAssets}. */
  113. function convertToAssets(uint256 shares) public view virtual override returns (uint256) {
  114. return _convertToAssets(shares, Math.Rounding.Down);
  115. }
  116. /** @dev See {IERC4626-maxDeposit}. */
  117. function maxDeposit(address) public view virtual override returns (uint256) {
  118. return type(uint256).max;
  119. }
  120. /** @dev See {IERC4626-maxMint}. */
  121. function maxMint(address) public view virtual override returns (uint256) {
  122. return type(uint256).max;
  123. }
  124. /** @dev See {IERC4626-maxWithdraw}. */
  125. function maxWithdraw(address owner) public view virtual override returns (uint256) {
  126. return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
  127. }
  128. /** @dev See {IERC4626-maxRedeem}. */
  129. function maxRedeem(address owner) public view virtual override returns (uint256) {
  130. return balanceOf(owner);
  131. }
  132. /** @dev See {IERC4626-previewDeposit}. */
  133. function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
  134. return _convertToShares(assets, Math.Rounding.Down);
  135. }
  136. /** @dev See {IERC4626-previewMint}. */
  137. function previewMint(uint256 shares) public view virtual override returns (uint256) {
  138. return _convertToAssets(shares, Math.Rounding.Up);
  139. }
  140. /** @dev See {IERC4626-previewWithdraw}. */
  141. function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
  142. return _convertToShares(assets, Math.Rounding.Up);
  143. }
  144. /** @dev See {IERC4626-previewRedeem}. */
  145. function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
  146. return _convertToAssets(shares, Math.Rounding.Down);
  147. }
  148. /** @dev See {IERC4626-deposit}. */
  149. function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
  150. uint256 maxAssets = maxDeposit(receiver);
  151. if (assets > maxAssets) {
  152. revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
  153. }
  154. uint256 shares = previewDeposit(assets);
  155. _deposit(_msgSender(), receiver, assets, shares);
  156. return shares;
  157. }
  158. /** @dev See {IERC4626-mint}.
  159. *
  160. * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero.
  161. * In this case, the shares will be minted without requiring any assets to be deposited.
  162. */
  163. function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
  164. uint256 maxShares = maxMint(receiver);
  165. if (shares > maxShares) {
  166. revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
  167. }
  168. uint256 assets = previewMint(shares);
  169. _deposit(_msgSender(), receiver, assets, shares);
  170. return assets;
  171. }
  172. /** @dev See {IERC4626-withdraw}. */
  173. function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256) {
  174. uint256 maxAssets = maxWithdraw(owner);
  175. if (assets > maxAssets) {
  176. revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
  177. }
  178. uint256 shares = previewWithdraw(assets);
  179. _withdraw(_msgSender(), receiver, owner, assets, shares);
  180. return shares;
  181. }
  182. /** @dev See {IERC4626-redeem}. */
  183. function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256) {
  184. uint256 maxShares = maxRedeem(owner);
  185. if (shares > maxShares) {
  186. revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
  187. }
  188. uint256 assets = previewRedeem(shares);
  189. _withdraw(_msgSender(), receiver, owner, assets, shares);
  190. return assets;
  191. }
  192. /**
  193. * @dev Internal conversion function (from assets to shares) with support for rounding direction.
  194. */
  195. function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
  196. return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
  197. }
  198. /**
  199. * @dev Internal conversion function (from shares to assets) with support for rounding direction.
  200. */
  201. function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
  202. return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);
  203. }
  204. /**
  205. * @dev Deposit/mint common workflow.
  206. */
  207. function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
  208. // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
  209. // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
  210. // calls the vault, which is assumed not malicious.
  211. //
  212. // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
  213. // assets are transferred and before the shares are minted, which is a valid state.
  214. // slither-disable-next-line reentrancy-no-eth
  215. SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
  216. _mint(receiver, shares);
  217. emit Deposit(caller, receiver, assets, shares);
  218. }
  219. /**
  220. * @dev Withdraw/redeem common workflow.
  221. */
  222. function _withdraw(
  223. address caller,
  224. address receiver,
  225. address owner,
  226. uint256 assets,
  227. uint256 shares
  228. ) internal virtual {
  229. if (caller != owner) {
  230. _spendAllowance(owner, caller, shares);
  231. }
  232. // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
  233. // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
  234. // calls the vault, which is assumed not malicious.
  235. //
  236. // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
  237. // shares are burned and after the assets are transferred, which is a valid state.
  238. _burn(owner, shares);
  239. SafeERC20.safeTransfer(_asset, receiver, assets);
  240. emit Withdraw(caller, receiver, owner, assets, shares);
  241. }
  242. function _decimalsOffset() internal view virtual returns (uint8) {
  243. return 0;
  244. }
  245. }