diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol new file mode 100644 index 00000000000..2e0ec698d7e --- /dev/null +++ b/contracts/interfaces/IERC7540.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC7540.sol) + +pragma solidity ^0.8.20; + +interface IERC7540Operator { + /** + * @dev The event emitted when an operator is set. + * + * @param controller The address of the controller. + * @param operator The address of the operator. + * @param approved The approval status. + */ + event OperatorSet(address indexed controller, address indexed operator, bool approved); + + /** + * @dev Sets or removes an operator for the caller. + * + * @param operator The address of the operator. + * @param approved The approval status. + * @return Whether the call was executed successfully or not + */ + function setOperator(address operator, bool approved) external returns (bool); + + /** + * @dev Returns `true` if the `operator` is approved as an operator for an `controller`. + * + * @param controller The address of the controller. + * @param operator The address of the operator. + * @return status The approval status + */ + function isOperator(address controller, address operator) external view returns (bool status); +} + +interface IERC7540Deposit { + event DepositRequest( + address indexed controller, address indexed owner, uint256 indexed requestId, address sender, uint256 assets + ); + /** + * @dev Transfers assets from sender into the Vault and submits a Request for asynchronous deposit. + * + * - MUST support ERC-20 approve / transferFrom on asset as a deposit Request flow. + * - MUST revert if all of assets cannot be requested for deposit. + * - owner MUST be msg.sender unless some unspecified explicit approval is given by the caller, + * approval of ERC-20 tokens from owner to sender is NOT enough. + * + * @param assets the amount of deposit assets to transfer from owner + * @param controller the controller of the request who will be able to operate the request + * @param owner the source of the deposit assets + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault's underlying asset token. + */ + + function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId); + + /** + * @dev Returns the amount of requested assets in Pending state. + * + * - MUST NOT include any assets in Claimable state for deposit or mint. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function pendingDepositRequest(uint256 requestId, address controller) + external + view + returns (uint256 pendingAssets); + + /** + * @dev Returns the amount of requested assets in Claimable state for the controller to deposit or mint. + * + * - MUST NOT include any assets in Pending state. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function claimableDepositRequest(uint256 requestId, address controller) + external + view + returns (uint256 claimableAssets); + + /** + * @dev Mints shares Vault shares to receiver by claiming the Request of the controller. + * + * - MUST emit the Deposit event. + * - controller MUST equal msg.sender unless the controller has approved the msg.sender as an operator. + */ + function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares); + + /** + * @dev Mints exactly shares Vault shares to receiver by claiming the Request of the controller. + * + * - MUST emit the Deposit event. + * - controller MUST equal msg.sender unless the controller has approved the msg.sender as an operator. + */ + function mint(uint256 shares, address receiver, address controller) external returns (uint256 assets); +} + +interface IERC7540Redeem { + event RedeemRequest( + address indexed controller, address indexed owner, uint256 indexed requestId, address sender, uint256 assets + ); + + /** + * @dev Assumes control of shares from sender into the Vault and submits a Request for asynchronous redeem. + * + * - MUST support a redeem Request flow where the control of shares is taken from sender directly + * where msg.sender has ERC-20 approval over the shares of owner. + * - MUST revert if all of shares cannot be requested for redeem. + * + * @param shares the amount of shares to be redeemed to transfer from owner + * @param controller the controller of the request who will be able to operate the request + * @param owner the source of the shares to be redeemed + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault's share token. + */ + function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId); + + /** + * @dev Returns the amount of requested shares in Pending state. + * + * - MUST NOT include any shares in Claimable state for redeem or withdraw. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function pendingRedeemRequest(uint256 requestId, address controller) + external + view + returns (uint256 pendingShares); + + /** + * @dev Returns the amount of requested shares in Claimable state for the controller to redeem or withdraw. + * + * - MUST NOT include any shares in Pending state for redeem or withdraw. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function claimableRedeemRequest(uint256 requestId, address controller) + external + view + returns (uint256 claimableShares); +} diff --git a/contracts/interfaces/IERC7575.sol b/contracts/interfaces/IERC7575.sol new file mode 100644 index 00000000000..c450e762818 --- /dev/null +++ b/contracts/interfaces/IERC7575.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC7575.sol) + +pragma solidity ^0.8.20; + +import {IERC165} from "../utils/introspection/IERC165.sol"; + +interface IERC7575 is IERC165 { + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + event Withdraw( + address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares + ); + + /** + * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + function asset() external view returns (address assetTokenAddress); + + /** + * @dev Returns the address of the share token + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + function share() external view returns (address shareTokenAddress); + + /** + * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Returns the total amount of the underlying asset that is “managed” by Vault. + * + * - SHOULD include any compounding that occurs from yield. + * - MUST be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT revert. + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, + * through a deposit call. + * + * - MUST return a limited value if receiver is subject to some deposit limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * - MUST NOT revert. + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit + * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called + * in the same transaction. + * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the + * deposit would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * deposit execution, and are accounted for during deposit. + * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. + * - MUST return a limited value if receiver is subject to some mint limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. + * - MUST NOT revert. + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call + * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the + * same transaction. + * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint + * would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by minting. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint + * execution, and are accounted for during mint. + * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the + * Vault, through a withdraw call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw + * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if + * called + * in the same transaction. + * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though + * the withdrawal would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * withdraw execution, and are accounted for during withdraw. + * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, + * through a redeem call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call + * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the + * same transaction. + * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the + * redemption would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by redeeming. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * redeem execution, and are accounted for during redeem. + * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); +} diff --git a/contracts/token/ERC20/extensions/BaseERC7540.sol b/contracts/token/ERC20/extensions/BaseERC7540.sol new file mode 100644 index 00000000000..06e84577d41 --- /dev/null +++ b/contracts/token/ERC20/extensions/BaseERC7540.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol) + +pragma solidity ^0.8.20; + +import {ERC20, IERC20} from "../ERC20.sol"; +import {ERC4626} from "./ERC4626.sol"; +import {IERC165} from "../../../interfaces/IERC165.sol"; +import {IERC7540Operator} from "../../../interfaces/IERC7540.sol"; +import {IERC7575} from "../../../interfaces/IERC7575.sol"; + +/** + * @dev Implementation of the ERC-7540 "Asynchronous ERC-4626 Tokenized Vaults" as defined in + * https://eips.ethereum.org/EIPS/eip-7540[ERC-7540]. + */ +abstract contract BaseERC7540 is ERC4626, IERC7540Operator { + /** + * @dev Assume requests are non-fungible and all have ID = 0 + */ + uint256 internal constant REQUEST_ID = 0; + + /** + * @dev See {IERC7540-isOperator}. + */ + mapping(address => mapping(address => bool)) public isOperator; + + /** + * @dev See {IERC7540-authorizations}. + */ + mapping(address controller => mapping(bytes32 nonce => bool used)) public authorizations; + + /** + * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777). + */ + constructor(IERC20 asset_) ERC4626(asset_) {} + + /** + * @dev See {IERC7575-totalAssets}. + */ + function totalAssets() public view virtual override returns (uint256) { + return _asset.balanceOf(address(this)); + } + + /** + * @dev See {IERC7540-setOperator}. + */ + function setOperator(address operator, bool approved) public virtual returns (bool success) { + require(msg.sender != operator, "ERC7540Vault/cannot-set-self-as-operator"); + isOperator[msg.sender][operator] = approved; + emit OperatorSet(msg.sender, operator, approved); + success = true; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return interfaceId == type(IERC7575).interfaceId || interfaceId == type(IERC7540Operator).interfaceId + || interfaceId == type(IERC165).interfaceId; + } +} diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index c71b14ad48c..ee9e14fa6ad 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -48,9 +48,14 @@ import {Math} from "../../../utils/math/Math.sol"; abstract contract ERC4626 is ERC20, IERC4626 { using Math for uint256; - IERC20 private immutable _asset; + IERC20 internal immutable _asset; uint8 private immutable _underlyingDecimals; + /** + * @dev See {IERC7575-share}. + */ + address public share = address(this); + /** * @dev Attempted to deposit more assets than the max amount for `receiver`. */ @@ -84,9 +89,8 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way. */ function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) { - (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( - abi.encodeCall(IERC20Metadata.decimals, ()) - ); + (bool success, bytes memory encodedDecimals) = + address(asset_).staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); if (success && encodedDecimals.length >= 32) { uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256)); if (returnedDecimals <= type(uint8).max) { @@ -107,67 +111,93 @@ abstract contract ERC4626 is ERC20, IERC4626 { return _underlyingDecimals + _decimalsOffset(); } - /** @dev See {IERC4626-asset}. */ + /** + * @dev See {IERC4626-asset}. + */ function asset() public view virtual returns (address) { return address(_asset); } - /** @dev See {IERC4626-totalAssets}. */ + /** + * @dev See {IERC4626-totalAssets}. + */ function totalAssets() public view virtual returns (uint256) { return _asset.balanceOf(address(this)); } - /** @dev See {IERC4626-convertToShares}. */ + /** + * @dev See {IERC4626-convertToShares}. + */ function convertToShares(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Floor); } - /** @dev See {IERC4626-convertToAssets}. */ + /** + * @dev See {IERC4626-convertToAssets}. + */ function convertToAssets(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Floor); } - /** @dev See {IERC4626-maxDeposit}. */ + /** + * @dev See {IERC4626-maxDeposit}. + */ function maxDeposit(address) public view virtual returns (uint256) { return type(uint256).max; } - /** @dev See {IERC4626-maxMint}. */ + /** + * @dev See {IERC4626-maxMint}. + */ function maxMint(address) public view virtual returns (uint256) { return type(uint256).max; } - /** @dev See {IERC4626-maxWithdraw}. */ + /** + * @dev See {IERC4626-maxWithdraw}. + */ function maxWithdraw(address owner) public view virtual returns (uint256) { return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); } - /** @dev See {IERC4626-maxRedeem}. */ + /** + * @dev See {IERC4626-maxRedeem}. + */ function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); } - /** @dev See {IERC4626-previewDeposit}. */ + /** + * @dev See {IERC4626-previewDeposit}. + */ function previewDeposit(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Floor); } - /** @dev See {IERC4626-previewMint}. */ + /** + * @dev See {IERC4626-previewMint}. + */ function previewMint(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Ceil); } - /** @dev See {IERC4626-previewWithdraw}. */ + /** + * @dev See {IERC4626-previewWithdraw}. + */ function previewWithdraw(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Ceil); } - /** @dev See {IERC4626-previewRedeem}. */ + /** + * @dev See {IERC4626-previewRedeem}. + */ function previewRedeem(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Floor); } - /** @dev See {IERC4626-deposit}. */ + /** + * @dev See {IERC4626-deposit}. + */ function deposit(uint256 assets, address receiver) public virtual returns (uint256) { uint256 maxAssets = maxDeposit(receiver); if (assets > maxAssets) { @@ -180,7 +210,9 @@ abstract contract ERC4626 is ERC20, IERC4626 { return shares; } - /** @dev See {IERC4626-mint}. */ + /** + * @dev See {IERC4626-mint}. + */ function mint(uint256 shares, address receiver) public virtual returns (uint256) { uint256 maxShares = maxMint(receiver); if (shares > maxShares) { @@ -193,7 +225,9 @@ abstract contract ERC4626 is ERC20, IERC4626 { return assets; } - /** @dev See {IERC4626-withdraw}. */ + /** + * @dev See {IERC4626-withdraw}. + */ function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { uint256 maxAssets = maxWithdraw(owner); if (assets > maxAssets) { @@ -206,7 +240,9 @@ abstract contract ERC4626 is ERC20, IERC4626 { return shares; } - /** @dev See {IERC4626-redeem}. */ + /** + * @dev See {IERC4626-redeem}. + */ function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { uint256 maxShares = maxRedeem(owner); if (shares > maxShares) { @@ -253,13 +289,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** * @dev Withdraw/redeem common workflow. */ - function _withdraw( - address caller, - address receiver, - address owner, - uint256 assets, - uint256 shares - ) internal virtual { + function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) + internal + virtual + { if (caller != owner) { _spendAllowance(owner, caller, shares); } diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol new file mode 100644 index 00000000000..54c6c0f85f0 --- /dev/null +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol) + +pragma solidity ^0.8.20; + +import {BaseERC7540} from "./BaseERC7540.sol"; +import {BaseERC7540Deposit} from "./ERC7540Deposit.sol"; +import {BaseERC7540Redeem} from "./ERC7540Redeem.sol"; +import {ERC4626} from "./ERC4626.sol"; +import {ERC20, IERC20} from "../ERC20.sol"; +import {IERC7540Deposit, IERC7540Redeem} from "../../../interfaces/IERC7540.sol"; +import {SafeERC20} from "../utils/SafeERC20.sol"; +import {Math} from "../../../utils/math/Math.sol"; + +/** + * @dev Implementation of the {IERC7540Deposit} and {IERC7540Redeem} interfaces. + * + * This implementation is agnostic to the way deposit and redeem request fulfillments are integrated. + * This means that a derived contract must implement {_fulfillDeposit} and {_fulfillRedeem}. + */ +contract ERC7540 is BaseERC7540Deposit, BaseERC7540Redeem { + constructor(ERC20 _asset, string memory _name, string memory _symbol) + BaseERC7540Deposit() + BaseERC7540Redeem() + BaseERC7540(_asset) + ERC20(_name, _symbol) + {} + + /** + * @dev See {IERC4626-totalAssets}. + */ + function totalAssets() public view virtual override(BaseERC7540Deposit, BaseERC7540) returns (uint256) { + return totalAssets() - _totalPendingDepositAssets; + } + + /** + * @dev See {IERC4626-maxDeposit}. + */ + function maxDeposit(address controller) + public + view + virtual + override(BaseERC7540Deposit, ERC4626) + returns (uint256) + { + return BaseERC7540Deposit.maxDeposit(controller); + } + + /** + * @dev See {IERC4626-previewDeposit}. + */ + function previewDeposit(uint256) public pure virtual override(BaseERC7540Deposit, ERC4626) returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-deposit}. + */ + function deposit(uint256 assets, address receiver) + public + virtual + override(BaseERC7540Deposit, ERC4626) + returns (uint256 shares) + { + shares = BaseERC7540Deposit.deposit(assets, receiver, receiver); + } + + /** + * @dev See {IERC4626-maxMint}. + */ + function maxMint(address controller) public view virtual override(BaseERC7540Deposit, ERC4626) returns (uint256) { + return BaseERC7540Deposit.maxMint(controller); + } + + /** + * @dev See {IERC4626-previewMint}. + */ + function previewMint(uint256) public pure virtual override(BaseERC7540Deposit, ERC4626) returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-mint}. + */ + function mint(uint256 shares, address receiver) + public + virtual + override(BaseERC7540Deposit, ERC4626) + returns (uint256 assets) + { + assets = BaseERC7540Deposit.mint(shares, receiver, receiver); + } + + /** + * @dev See {IERC4626-maxWithdraw}. + */ + function maxWithdraw(address controller) + public + view + virtual + override(BaseERC7540Redeem, ERC4626) + returns (uint256) + { + return BaseERC7540Redeem.maxWithdraw(controller); + } + + /** + * @dev See {IERC4626-previewWithdraw}. + */ + function previewWithdraw(uint256) public pure virtual override(BaseERC7540Redeem, ERC4626) returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-withdraw}. + */ + function withdraw(uint256 assets, address receiver, address controller) + public + virtual + override(BaseERC7540Redeem, ERC4626) + returns (uint256 shares) + { + shares = BaseERC7540Redeem.withdraw(assets, receiver, controller); + } + + /** + * @dev See {IERC4626-maxRedeem}. + */ + function maxRedeem(address controller) public view virtual override(BaseERC7540Redeem, ERC4626) returns (uint256) { + return BaseERC7540Redeem.maxRedeem(controller); + } + + /** + * @dev See {IERC4626-previewRedeem}. + */ + function previewRedeem(uint256) public pure virtual override(BaseERC7540Redeem, ERC4626) returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-redeem}. + */ + function redeem(uint256 shares, address receiver, address controller) + public + virtual + override(BaseERC7540Redeem, ERC4626) + returns (uint256 assets) + { + assets = BaseERC7540Redeem.redeem(shares, receiver, controller); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) + public + pure + virtual + override(BaseERC7540Deposit, BaseERC7540Redeem) + returns (bool) + { + return interfaceId == type(IERC7540Deposit).interfaceId || interfaceId == type(IERC7540Redeem).interfaceId + || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/token/ERC20/extensions/ERC7540Deposit.sol b/contracts/token/ERC20/extensions/ERC7540Deposit.sol new file mode 100644 index 00000000000..7f06a61cb05 --- /dev/null +++ b/contracts/token/ERC20/extensions/ERC7540Deposit.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol) + +pragma solidity ^0.8.20; + +import {BaseERC7540} from "./BaseERC7540.sol"; +import {ERC20, IERC20} from "../ERC20.sol"; +import {IERC7540Deposit} from "../../../interfaces/IERC7540.sol"; +import {SafeERC20} from "../utils/SafeERC20.sol"; +import {Math} from "../../../utils/math/Math.sol"; + +/** + * @dev Implementation of the {IERC7540Deposit} interface. + * + * This implementation is agnostic to the way deposit request fulfillments are integrated. + * This means that a derived contract must implement {_fulfillDeposit}. + */ +abstract contract BaseERC7540Deposit is BaseERC7540, IERC7540Deposit { + using Math for uint256; + + uint256 internal _totalPendingDepositAssets; + mapping(address => PendingDeposit) internal _pendingDeposit; + mapping(address => ClaimableDeposit) internal _claimableDeposit; + + struct PendingDeposit { + uint256 assets; + } + + struct ClaimableDeposit { + uint256 assets; + uint256 shares; + } + + /** + * @dev See {IERC4626-totalAssets}. + */ + function totalAssets() public view virtual override returns (uint256) { + // Total assets pending redemption must be removed from the reported total assets + // otherwise pending assets would be treated as yield for outstanding shares + return totalAssets() - _totalPendingDepositAssets; + } + + /** + * @dev See {IERC7540Deposit-requestDeposit}. + */ + function requestDeposit(uint256 assets, address controller, address owner) + external + virtual + returns (uint256 requestId) + { + require(owner == msg.sender || isOperator[owner][msg.sender], "ERC7540Vault/invalid-owner"); + require(_asset.balanceOf(owner) >= assets, "ERC7540Vault/insufficient-balance"); + require(assets != 0, "ZERO_ASSETS"); + + SafeERC20.safeTransferFrom(_asset, owner, address(this), assets); + + uint256 currentPendingAssets = _pendingDeposit[controller].assets; + _pendingDeposit[controller] = PendingDeposit(assets + currentPendingAssets); + + _totalPendingDepositAssets += assets; + + emit DepositRequest(controller, owner, REQUEST_ID, msg.sender, assets); + return REQUEST_ID; + } + + /** + * @dev See {IERC7540Deposit-pendingDepositRequest}. + */ + function pendingDepositRequest(uint256, address controller) public view virtual returns (uint256 pendingAssets) { + pendingAssets = _pendingDeposit[controller].assets; + } + + /** + * @dev See {IERC7540Deposit-claimableDepositRequest}. + */ + function claimableDepositRequest(uint256, address controller) + public + view + virtual + returns (uint256 claimableAssets) + { + claimableAssets = _claimableDeposit[controller].assets; + } + + /** + * @dev TODO + */ + function _fulfillDeposit(address controller, uint256 assets) internal returns (uint256 shares) { + PendingDeposit storage request = _pendingDeposit[controller]; + require(request.assets != 0 && assets <= request.assets, "ZERO_ASSETS"); + + shares = convertToShares(assets); + _mint(address(this), shares); + + _claimableDeposit[controller] = ClaimableDeposit( + _claimableDeposit[controller].assets + assets, _claimableDeposit[controller].shares + shares + ); + + request.assets -= assets; + _totalPendingDepositAssets -= assets; + } + + /** + * @dev See {IERC7540Deposit-deposit}. + */ + function deposit(uint256 assets, address receiver, address controller) public virtual returns (uint256 shares) { + require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller"); + require(assets != 0, "Must claim nonzero amount"); + + // Claiming partially introduces precision loss. The user therefore receives a rounded down amount, + // while the claimable balance is reduced by a rounded up amount. + ClaimableDeposit storage claimable = _claimableDeposit[controller]; + shares = assets.mulDiv(claimable.shares, claimable.assets, Math.Rounding.Floor); + uint256 sharesUp = assets.mulDiv(claimable.shares, claimable.assets, Math.Rounding.Ceil); + + claimable.assets -= assets; + claimable.shares = claimable.shares > sharesUp ? claimable.shares - sharesUp : 0; + + ERC20(address(this)).transfer(receiver, shares); + + emit Deposit(receiver, controller, assets, shares); + } + + /** + * @dev See {IERC7540Deposit-mint}. + */ + function mint(uint256 shares, address receiver, address controller) + public + virtual + override + returns (uint256 assets) + { + require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller"); + require(shares != 0, "Must claim nonzero amount"); + + // Claiming partially introduces precision loss. The user therefore receives a rounded down amount, + // while the claimable balance is reduced by a rounded up amount. + ClaimableDeposit storage claimable = _claimableDeposit[controller]; + assets = shares.mulDiv(claimable.assets, claimable.shares, Math.Rounding.Floor); + uint256 assetsUp = shares.mulDiv(claimable.assets, claimable.shares, Math.Rounding.Ceil); + + claimable.assets = claimable.assets > assetsUp ? claimable.assets - assetsUp : 0; + claimable.shares -= shares; + + ERC20(address(this)).transfer(receiver, shares); + + emit Deposit(receiver, controller, assets, shares); + } + + /** + * @dev See {IERC4626-deposit}. + */ + function deposit(uint256 assets, address receiver) public virtual override returns (uint256 shares) { + shares = deposit(assets, receiver, receiver); + } + + /** + * @dev See {IERC4626-mint}. + */ + function mint(uint256 shares, address receiver) public virtual override returns (uint256 assets) { + assets = mint(shares, receiver, receiver); + } + + /** + * @dev See {IERC4626-maxDeposit}. + */ + function maxDeposit(address controller) public view virtual override returns (uint256) { + return _claimableDeposit[controller].assets; + } + + /** + * @dev See {IERC4626-maxMint}. + */ + function maxMint(address controller) public view virtual override returns (uint256) { + return _claimableDeposit[controller].shares; + } + + /** + * @dev See {IERC4626-previewDeposit}. + */ + function previewDeposit(uint256) public pure virtual override returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-previewMint}. + */ + function previewMint(uint256) public pure virtual override returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IERC7540Deposit).interfaceId || super.supportsInterface(interfaceId); + } +} + +contract ERC7540Deposit is BaseERC7540Deposit { + constructor(ERC20 _asset, string memory _name, string memory _symbol) BaseERC7540(_asset) ERC20(_name, _symbol) {} +} diff --git a/contracts/token/ERC20/extensions/ERC7540Redeem.sol b/contracts/token/ERC20/extensions/ERC7540Redeem.sol new file mode 100644 index 00000000000..b430e248c63 --- /dev/null +++ b/contracts/token/ERC20/extensions/ERC7540Redeem.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol) + +pragma solidity ^0.8.20; + +import {BaseERC7540} from "./BaseERC7540.sol"; +import {ERC20, IERC20} from "../ERC20.sol"; +import {IERC7540Redeem} from "../../../interfaces/IERC7540.sol"; +import {SafeERC20} from "../utils/SafeERC20.sol"; +import {Math} from "../../../utils/math/Math.sol"; + +/** + * @dev Implementation of the {IERC7540Redeem} interface. + * + * This implementation is agnostic to the way redeem request fulfillments are integrated. + * This means that a derived contract must implement {_fulfillRedeem}. + */ +abstract contract BaseERC7540Redeem is BaseERC7540, IERC7540Redeem { + using Math for uint256; + + mapping(address => PendingRedeem) internal _pendingRedeem; + mapping(address => ClaimableRedeem) internal _claimableRedeem; + + struct PendingRedeem { + uint256 shares; + } + + struct ClaimableRedeem { + uint256 assets; + uint256 shares; + } + + /** + * @dev See {IERC7540Redeem-requestRedeem}. + */ + function requestRedeem(uint256 shares, address controller, address owner) + external + virtual + returns (uint256 requestId) + { + require(owner == msg.sender || isOperator[owner][msg.sender], "ERC7540Vault/invalid-owner"); + require(ERC20(address(this)).balanceOf(owner) >= shares, "ERC7540Vault/insufficient-balance"); + require(shares != 0, "ZERO_SHARES"); + + SafeERC20.safeTransferFrom(this, owner, address(this), shares); + + uint256 currentPendingShares = _pendingRedeem[controller].shares; + _pendingRedeem[controller] = PendingRedeem(shares + currentPendingShares); + + emit RedeemRequest(controller, owner, REQUEST_ID, msg.sender, shares); + return REQUEST_ID; + } + + /** + * @dev See {IERC7540Redeem-pendingRedeemRequest}. + */ + function pendingRedeemRequest(uint256, address controller) public view virtual returns (uint256 pendingAssets) { + pendingAssets = _pendingRedeem[controller].shares; + } + + /** + * @dev See {IERC7540Redeem-claimableRedeemRequest}. + */ + function claimableRedeemRequest(uint256, address controller) + public + view + virtual + returns (uint256 claimableAssets) + { + claimableAssets = _claimableRedeem[controller].shares; + } + + /** + * @dev TODO + */ + function _fulfillRedeem(address controller, uint256 shares) internal returns (uint256 assets) { + PendingRedeem storage request = _pendingRedeem[controller]; + require(request.shares != 0 && shares <= request.shares, "ZERO_SHARES"); + + assets = convertToAssets(shares); + + _claimableRedeem[controller] = + ClaimableRedeem(_claimableRedeem[controller].assets + assets, _claimableRedeem[controller].shares + shares); + + request.shares -= shares; + } + + /** + * @dev See {IERC4626-withdraw}. + */ + function withdraw(uint256 assets, address receiver, address controller) + public + virtual + override + returns (uint256 shares) + { + require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller"); + require(assets != 0, "Must claim nonzero amount"); + + // Claiming partially introduces precision loss. The user therefore receives a rounded down amount, + // while the claimable balance is reduced by a rounded up amount. + ClaimableRedeem storage claimable = _claimableRedeem[controller]; + shares = assets.mulDiv(claimable.shares, claimable.assets, Math.Rounding.Floor); + uint256 sharesUp = assets.mulDiv(claimable.shares, claimable.assets, Math.Rounding.Ceil); + + claimable.assets -= assets; + claimable.shares = claimable.shares > sharesUp ? claimable.shares - sharesUp : 0; + + SafeERC20.safeTransfer(_asset, receiver, assets); + + emit Withdraw(msg.sender, receiver, controller, assets, shares); + } + + /** + * @dev See {IERC4626-redeem}. + */ + function redeem(uint256 shares, address receiver, address controller) + public + virtual + override + returns (uint256 assets) + { + require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-caller"); + require(shares != 0, "Must claim nonzero amount"); + + // Claiming partially introduces precision loss. The user therefore receives a rounded down amount, + // while the claimable balance is reduced by a rounded up amount. + ClaimableRedeem storage claimable = _claimableRedeem[controller]; + assets = shares.mulDiv(claimable.assets, claimable.shares, Math.Rounding.Floor); + uint256 assetsUp = shares.mulDiv(claimable.assets, claimable.shares, Math.Rounding.Ceil); + + claimable.assets = claimable.assets > assetsUp ? claimable.assets - assetsUp : 0; + claimable.shares -= shares; + + SafeERC20.safeTransfer(_asset, receiver, assets); + + emit Withdraw(msg.sender, receiver, controller, assets, shares); + } + + /** + * @dev See {IERC4626-maxWithdraw}. + */ + function maxWithdraw(address controller) public view virtual override returns (uint256) { + return _claimableRedeem[controller].assets; + } + + /** + * @dev See {IERC4626-maxRedeem}. + */ + function maxRedeem(address controller) public view virtual override returns (uint256) { + return _claimableRedeem[controller].shares; + } + + /** + * @dev See {IERC4626-previewWithdraw}. + */ + function previewWithdraw(uint256) public pure virtual override returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC4626-previewRedeem}. + */ + function previewRedeem(uint256) public pure virtual override returns (uint256) { + revert("async-flow"); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IERC7540Redeem).interfaceId || super.supportsInterface(interfaceId); + } +} + +contract ERC7540Redeem is BaseERC7540Redeem { + constructor(ERC20 _asset, string memory _name, string memory _symbol) BaseERC7540(_asset) ERC20(_name, _symbol) {} +}