From 9c8b97d1af17f084037a82c12c5dc019aabc1e28 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 24 Jan 2025 11:27:00 +0100 Subject: [PATCH 01/30] ERC-7540 - Add IERC7540 interface --- contracts/interfaces/IERC7540.sol | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 contracts/interfaces/IERC7540.sol diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol new file mode 100644 index 00000000000..335d5d26c9e --- /dev/null +++ b/contracts/interfaces/IERC7540.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC4626} from "../interfaces/IERC4626.sol"; + +/** + * @dev Interface for the ERC-7540 Asynchronous Tokenized Vaults standard. + * https://eips.ethereum.org/EIPS/eip-7540[ERC-7540] + */ +interface IERC7540 is IERC4626 { + // Events + event DepositRequest( + address indexed controller, + address indexed owner, + uint256 indexed requestId, + address sender, + uint256 assets + ); + + event RedeemRequest( + address indexed controller, + address indexed owner, + uint256 indexed requestId, + address sender, + uint256 shares + ); + + event OperatorSet(address indexed controller, address indexed operator, bool approved); + + // Methods + + /** + * @dev Initiates a deposit request. + * @param assets The amount of assets to deposit. + * @param controller The address of the controller managing the request. + * @param owner The owner of the assets. + * @return requestId The unique identifier for this deposit request. + */ + function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId); + + /** + * @dev Initiates a redeem request. + * @param shares The amount of shares to redeem. + * @param controller The address of the controller managing the request. + * @param owner The owner of the shares. + * @return requestId The unique identifier for this redeem request. + */ + function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId); + + /** + * @dev Gets the pending deposit request amount for a given controller and requestId. + * @param requestId The unique identifier for the request. + * @param controller The address of the controller. + * @return assets The amount of assets in the pending state. + */ + function pendingDepositRequest(uint256 requestId, address controller) external view returns (uint256 assets); + + /** + * @dev Gets the claimable deposit request amount for a given controller and requestId. + * @param requestId The unique identifier for the request. + * @param controller The address of the controller. + * @return assets The amount of assets in the claimable state. + */ + function claimableDepositRequest(uint256 requestId, address controller) external view returns (uint256 assets); + + /** + * @dev Gets the pending redeem request amount for a given controller and requestId. + * @param requestId The unique identifier for the request. + * @param controller The address of the controller. + * @return shares The amount of shares in the pending state. + */ + function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares); + + /** + * @dev Gets the claimable redeem request amount for a given controller and requestId. + * @param requestId The unique identifier for the request. + * @param controller The address of the controller. + * @return shares The amount of shares in the claimable state. + */ + function claimableRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares); + + /** + * @dev Sets or revokes an operator for the given controller. + * @param operator The address of the operator. + * @param approved The approval status of the operator. + * @return success Whether the operation was successful. + */ + function setOperator(address operator, bool approved) external returns (bool success); + + /** + * @dev Checks if an operator is approved for a controller. + * @param controller The address of the controller. + * @param operator The address of the operator. + * @return status Whether the operator is approved. + */ + function isOperator(address controller, address operator) external view returns (bool status); +} From 6e276bd998f0bf505f71ffb64ced34ad497b494f Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 24 Jan 2025 13:26:36 +0100 Subject: [PATCH 02/30] ERC-7540 - Add ERC7540 initial implementation - requestDeposit & pendingDepositRequest/claimableDepositRequest --- contracts/token/ERC20/extensions/ERC7540.sol | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 contracts/token/ERC20/extensions/ERC7540.sol diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol new file mode 100644 index 00000000000..de49ede9fd2 --- /dev/null +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC4626} from "./ERC4626.sol"; +import {IERC7540} from "../../../interfaces/IERC7540.sol"; +import {SafeERC20} from "../utils/SafeERC20.sol"; +import {IERC20} from "../../../interfaces/IERC20.sol"; +import {Math} from "../../../utils/math/Math.sol"; + +/** + * @dev Abstract implementation of the ERC-7540 standard, extending ERC-4626. + */ +abstract contract ERC7540 is ERC4626, IERC7540 { + using SafeERC20 for IERC20; + using Math for uint256; + + struct Request { + uint256 amount; + uint256 claimable; + } + + // Mappings to track pending and claimable requests + mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; + + mapping(address => mapping(address => bool)) private _operators; + + /** + * @dev Creates a new deposit request. + */ + function requestDeposit( + uint256 assets, + address controller, + address owner + ) external override returns (uint256 requestId) { + require(assets > 0, "ERC7540: assets must be greater than zero"); + require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); + + requestId = _generateRequestId(controller, assets); + + IERC20(asset()).safeTransferFrom(owner, address(this), assets); + + _pendingDepositRequests[controller][requestId].amount += assets; + + emit DepositRequest(controller, owner, requestId, msg.sender, assets); + } + + /** + * @dev Gets the pending deposit request amount. + */ + function pendingDepositRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingDepositRequests[controller][requestId].amount; + } + + /** + * @dev Gets the claimable deposit request amount. + */ + function claimableDepositRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingDepositRequests[controller][requestId].claimable; + } + + /** + * @dev Sets or revokes an operator for the given controller. + */ + function setOperator(address operator, bool approved) external override returns (bool) { + _operators[msg.sender][operator] = approved; + emit OperatorSet(msg.sender, operator, approved); + return true; + } + + /** + * @dev Checks if an operator is approved for a controller. + */ + function isOperator(address controller, address operator) public view override returns (bool) { + return _operators[controller][operator]; + } + + /** + * @dev Internal function to generate a unique request ID. + */ + function _generateRequestId(address controller, uint256 input) internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(block.timestamp, controller, input))); + } + + /** + * @dev Abstract function for transitioning requests from Pending to Claimable. + */ + function _processPendingRequests(uint256 requestId, address controller) internal virtual; +} From d11665c2e6cf129f70b950e4682eec632eeefe3d Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 24 Jan 2025 13:52:30 +0100 Subject: [PATCH 03/30] ERC-7540 - Add ERC7540 - requestRedeem & pendingRedeemRequest/claimableRedeemRequest --- contracts/token/ERC20/extensions/ERC7540.sol | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index de49ede9fd2..a0d823dd20b 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -22,6 +22,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { // Mappings to track pending and claimable requests mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; + mapping(address => mapping(uint256 => Request)) private _pendingRedeemRequests; mapping(address => mapping(address => bool)) private _operators; @@ -45,6 +46,26 @@ abstract contract ERC7540 is ERC4626, IERC7540 { emit DepositRequest(controller, owner, requestId, msg.sender, assets); } + /** + * @dev Creates a new redeem request. + */ + function requestRedeem( + uint256 shares, + address controller, + address owner + ) external override returns (uint256 requestId) { + require(shares > 0, "ERC7540: shares must be greater than zero"); + require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); + + requestId = _generateRequestId(controller, shares); + + _burn(owner, shares); + + _pendingRedeemRequests[controller][requestId].amount += shares; + + emit RedeemRequest(controller, owner, requestId, msg.sender, shares); + } + /** * @dev Gets the pending deposit request amount. */ @@ -59,6 +80,20 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _pendingDepositRequests[controller][requestId].claimable; } + /** + * @dev Gets the pending redeem request amount. + */ + function pendingRedeemRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingRedeemRequests[controller][requestId].amount; + } + + /** + * @dev Gets the claimable redeem request amount. + */ + function claimableRedeemRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingRedeemRequests[controller][requestId].claimable; + } + /** * @dev Sets or revokes an operator for the given controller. */ From 7c0d1e6e3e9c29aca63e0698f6df35c8a5f2162e Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Sun, 26 Jan 2025 02:42:50 +0100 Subject: [PATCH 04/30] ERC-7540 - Add ERC7540 - ERC7540Fees.sol and ERC7540FeesMock.sol --- contracts/mocks/docs/ERC7540Fees.sol | 89 +++++++++++++++++++++++ contracts/mocks/token/ERC7540FeesMock.sol | 43 +++++++++++ 2 files changed, 132 insertions(+) create mode 100644 contracts/mocks/docs/ERC7540Fees.sol create mode 100644 contracts/mocks/token/ERC7540FeesMock.sol diff --git a/contracts/mocks/docs/ERC7540Fees.sol b/contracts/mocks/docs/ERC7540Fees.sol new file mode 100644 index 00000000000..b80a47a3fa9 --- /dev/null +++ b/contracts/mocks/docs/ERC7540Fees.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC4626} from "../../interfaces/IERC4626.sol"; +import {IERC20} from "../../token/ERC20/IERC20.sol"; +import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; +import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; +import {SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol"; +import {Math} from "../../utils/math/Math.sol"; + +/** + * @dev ERC-7540 vault with entry/exit fees expressed in basis points. + * + * NOTE: This contract charges fees in terms of assets, not shares. The fees are calculated + * based on the amount of assets being deposited or withdrawn, not the shares being minted or redeemed. + * This is an opinionated design decision that should be taken into account when integrating. + * + * WARNING: This contract is for demonstration purposes and has not been audited. Use it with caution. + */ +abstract contract ERC7540Fees is ERC7540 { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant _BASIS_POINT_SCALE = 1e4; + + function previewDeposit(uint256 assets) public view virtual override(ERC4626, IERC4626) returns (uint256) { + uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); + return super.previewDeposit(assets - fee); + } + + function previewRedeem(uint256 shares) public view virtual override(ERC4626, IERC4626) returns (uint256) { + uint256 assets = super.previewRedeem(shares); + uint256 fee = _feeOnTotal(assets, _exitFeeBasisPoints()); + return assets - fee; + } + + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); + address recipient = _entryFeeRecipient(); + + super._deposit(caller, receiver, assets - fee, shares); + + if (fee > 0 && recipient != address(this)) { + IERC20(asset()).safeTransfer(recipient, fee); + } + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + uint256 fee = _feeOnRaw(assets, _exitFeeBasisPoints()); + address recipient = _exitFeeRecipient(); + + super._withdraw(caller, receiver, owner, assets - fee, shares); + + if (fee > 0 && recipient != address(this)) { + IERC20(asset()).safeTransfer(recipient, fee); + } + } + + function _entryFeeBasisPoints() internal view virtual returns (uint256) { + return 0; // replace with e.g. 100 for 1% + } + + function _exitFeeBasisPoints() internal view virtual returns (uint256) { + return 0; // replace with e.g. 100 for 1% + } + + function _entryFeeRecipient() internal view virtual returns (address) { + return address(0); // replace with e.g. a treasury address + } + + function _exitFeeRecipient() internal view virtual returns (address) { + return address(0); // replace with e.g. a treasury address + } + + function _feeOnRaw(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { + return assets.mulDiv(feeBasisPoints, _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } + + function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { + return assets.mulDiv(feeBasisPoints, feeBasisPoints + _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } +} diff --git a/contracts/mocks/token/ERC7540FeesMock.sol b/contracts/mocks/token/ERC7540FeesMock.sol new file mode 100644 index 00000000000..c9717fdc313 --- /dev/null +++ b/contracts/mocks/token/ERC7540FeesMock.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC7540Fees} from "../docs/ERC7540Fees.sol"; +import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; +import {IERC20} from "../../token/ERC20/IERC20.sol"; + +abstract contract ERC7540FeesMock is ERC7540Fees { + uint256 private immutable _entryFeeBasisPointValue; + address private immutable _entryFeeRecipientValue; + uint256 private immutable _exitFeeBasisPointValue; + address private immutable _exitFeeRecipientValue; + + constructor( + IERC20 asset, + uint256 entryFeeBasisPoints, + address entryFeeRecipient, + uint256 exitFeeBasisPoints, + address exitFeeRecipient + ) ERC7540(asset) { + _entryFeeBasisPointValue = entryFeeBasisPoints; + _entryFeeRecipientValue = entryFeeRecipient; + _exitFeeBasisPointValue = exitFeeBasisPoints; + _exitFeeRecipientValue = exitFeeRecipient; + } + + function _entryFeeBasisPoints() internal view virtual override returns (uint256) { + return _entryFeeBasisPointValue; + } + + function _entryFeeRecipient() internal view virtual override returns (address) { + return _entryFeeRecipientValue; + } + + function _exitFeeBasisPoints() internal view virtual override returns (uint256) { + return _exitFeeBasisPointValue; + } + + function _exitFeeRecipient() internal view virtual override returns (address) { + return _exitFeeRecipientValue; + } +} From deb2490dc0c56561107755e8e7af6572129b66e9 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Sun, 26 Jan 2025 02:43:12 +0100 Subject: [PATCH 05/30] ERC-7540 - Add ERC7540 - add additional Events --- contracts/interfaces/IERC7540.sol | 4 ++++ contracts/token/ERC20/extensions/ERC7540.sol | 22 +++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 335d5d26c9e..19974adbb86 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -28,6 +28,10 @@ interface IERC7540 is IERC4626 { event OperatorSet(address indexed controller, address indexed operator, bool approved); + event DepositProcessed(address indexed controller, uint256 indexed requestId, uint256 amount); + + event RedeemProcessed(address indexed controller, uint256 indexed requestId, uint256 amount); + // Methods /** diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index a0d823dd20b..dc5c9f26cc9 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -2,16 +2,17 @@ pragma solidity ^0.8.20; -import {ERC4626} from "./ERC4626.sol"; import {IERC7540} from "../../../interfaces/IERC7540.sol"; -import {SafeERC20} from "../utils/SafeERC20.sol"; import {IERC20} from "../../../interfaces/IERC20.sol"; +import {ERC4626} from "./ERC4626.sol"; +import {SafeERC20} from "../utils/SafeERC20.sol"; import {Math} from "../../../utils/math/Math.sol"; +import {ReentrancyGuard} from "../../../utils/ReentrancyGuard.sol"; /** * @dev Abstract implementation of the ERC-7540 standard, extending ERC-4626. */ -abstract contract ERC7540 is ERC4626, IERC7540 { +abstract contract ERC7540 is ERC4626, IERC7540, ReentrancyGuard { using SafeERC20 for IERC20; using Math for uint256; @@ -21,11 +22,16 @@ abstract contract ERC7540 is ERC4626, IERC7540 { } // Mappings to track pending and claimable requests - mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; - mapping(address => mapping(uint256 => Request)) private _pendingRedeemRequests; + mapping(address => mapping(uint256 => Request)) internal _pendingDepositRequests; + mapping(address => mapping(uint256 => Request)) internal _pendingRedeemRequests; mapping(address => mapping(address => bool)) private _operators; + /** + * @dev Set the underlying asset contract. + */ + constructor(IERC20 asset) ERC4626(asset) {} + /** * @dev Creates a new deposit request. */ @@ -33,7 +39,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { uint256 assets, address controller, address owner - ) external override returns (uint256 requestId) { + ) external override nonReentrant returns (uint256 requestId) { require(assets > 0, "ERC7540: assets must be greater than zero"); require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); @@ -53,7 +59,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { uint256 shares, address controller, address owner - ) external override returns (uint256 requestId) { + ) external override nonReentrant returns (uint256 requestId) { require(shares > 0, "ERC7540: shares must be greater than zero"); require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); @@ -114,7 +120,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { * @dev Internal function to generate a unique request ID. */ function _generateRequestId(address controller, uint256 input) internal view returns (uint256) { - return uint256(keccak256(abi.encodePacked(block.timestamp, controller, input))); + return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, msg.sender, controller, input))); } /** From ff2d631301f241c7c9e5c909543d4a3d8e019aca Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Sun, 26 Jan 2025 02:44:01 +0100 Subject: [PATCH 06/30] ERC-7540 - Add ERC7540 - add ERC7540LimitsMock.sol --- contracts/mocks/token/ERC7540LimitsMock.sol | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 contracts/mocks/token/ERC7540LimitsMock.sol diff --git a/contracts/mocks/token/ERC7540LimitsMock.sol b/contracts/mocks/token/ERC7540LimitsMock.sol new file mode 100644 index 00000000000..08acabba73a --- /dev/null +++ b/contracts/mocks/token/ERC7540LimitsMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "../../token/ERC20/IERC20.sol"; +import {IERC4626} from "../../interfaces/IERC4626.sol"; +import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; +import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC7540LimitsMock is ERC7540 { + uint256 private immutable _maxDeposit; + uint256 private immutable _maxMint; + + constructor(IERC20 asset, uint256 maxDepositValue, uint256 maxMintValue) ERC7540(asset) { + _maxDeposit = maxDepositValue; + _maxMint = maxMintValue; + } + + function maxDeposit(address) public view override(IERC4626, ERC4626) returns (uint256) { + return _maxDeposit; + } + + function maxMint(address) public view override(IERC4626, ERC4626) returns (uint256) { + return _maxMint; + } +} From 37d62521a3f6630ae31c03b60618e2fa27d70c19 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Sun, 26 Jan 2025 02:49:01 +0100 Subject: [PATCH 07/30] ERC-7540 - Add ERC7540 - add ERC7540Mock.sol --- contracts/mocks/token/ERC7540Mock.sol | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 contracts/mocks/token/ERC7540Mock.sol diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol new file mode 100644 index 00000000000..e7ecbf5fbca --- /dev/null +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "../../token/ERC20/IERC20.sol"; +import {ERC20} from "../../token/ERC20/ERC20.sol"; +import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; +import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; + +contract ERC7540Mock is ERC7540 { + constructor(address asset) ERC20("ERC4626Mock", "E4626M") ERC7540(IERC20(asset)) {} + + function processPendingDeposit(uint256 requestId, address controller) external { + Request storage request = _pendingDepositRequests[controller][requestId]; + require(request.amount > 0, "No pending deposit request"); + + request.claimable += request.amount; + request.amount = 0; + } + + function processPendingRedeem(uint256 requestId, address controller) external { + Request storage request = _pendingRedeemRequests[controller][requestId]; + require(request.amount > 0, "No pending redeem request"); + + request.claimable += request.amount; + request.amount = 0; + } + + function _processPendingRequests(uint256 requestId, address controller) internal override { + Request storage depositRequest = _pendingDepositRequests[controller][requestId]; + if (depositRequest.amount > 0) { + depositRequest.claimable += depositRequest.amount; + emit DepositProcessed(controller, requestId, depositRequest.amount); + depositRequest.amount = 0; + } + + Request storage redeemRequest = _pendingRedeemRequests[controller][requestId]; + if (redeemRequest.amount > 0) { + redeemRequest.claimable += redeemRequest.amount; + emit RedeemProcessed(controller, requestId, redeemRequest.amount); + redeemRequest.amount = 0; + } + } +} From 6c2d9f6a7f10ed770f030db98fed33d11c1f0e91 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Sun, 26 Jan 2025 02:49:21 +0100 Subject: [PATCH 08/30] ERC-7540 - Add ERC7540 - add ERC7540OffsetMock.sol --- contracts/mocks/token/ERC7540OffsetMock.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 contracts/mocks/token/ERC7540OffsetMock.sol diff --git a/contracts/mocks/token/ERC7540OffsetMock.sol b/contracts/mocks/token/ERC7540OffsetMock.sol new file mode 100644 index 00000000000..d37f5fa3f4d --- /dev/null +++ b/contracts/mocks/token/ERC7540OffsetMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "../../token/ERC20/IERC20.sol"; +import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; + +abstract contract ERC7540OffsetMock is ERC7540 { + uint8 private immutable _offset; + + constructor(IERC20 asset, uint8 offset) ERC7540(asset) { + _offset = offset; + } + + function _decimalsOffset() internal view override returns (uint8) { + return _offset; + } +} From d2c8762cf6964675dd44bfac7d62a6fe87779f0b Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 09:18:00 +0100 Subject: [PATCH 09/30] ERC-7540 - Add ERC7540 - fix ordering --- contracts/interfaces/IERC7540.sol | 12 ++++++------ contracts/token/ERC20/extensions/ERC7540.sol | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 19974adbb86..036d5bfaaa2 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -61,20 +61,20 @@ interface IERC7540 is IERC4626 { function pendingDepositRequest(uint256 requestId, address controller) external view returns (uint256 assets); /** - * @dev Gets the claimable deposit request amount for a given controller and requestId. + * @dev Gets the pending redeem request amount for a given controller and requestId. * @param requestId The unique identifier for the request. * @param controller The address of the controller. - * @return assets The amount of assets in the claimable state. + * @return shares The amount of shares in the pending state. */ - function claimableDepositRequest(uint256 requestId, address controller) external view returns (uint256 assets); + function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares); /** - * @dev Gets the pending redeem request amount for a given controller and requestId. + * @dev Gets the claimable deposit request amount for a given controller and requestId. * @param requestId The unique identifier for the request. * @param controller The address of the controller. - * @return shares The amount of shares in the pending state. + * @return assets The amount of assets in the claimable state. */ - function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 shares); + function claimableDepositRequest(uint256 requestId, address controller) external view returns (uint256 assets); /** * @dev Gets the claimable redeem request amount for a given controller and requestId. diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index dc5c9f26cc9..c9d3a4ebcf4 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -80,17 +80,17 @@ abstract contract ERC7540 is ERC4626, IERC7540, ReentrancyGuard { } /** - * @dev Gets the claimable deposit request amount. + * @dev Gets the pending redeem request amount. */ - function claimableDepositRequest(uint256 requestId, address controller) external view override returns (uint256) { - return _pendingDepositRequests[controller][requestId].claimable; + function pendingRedeemRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingRedeemRequests[controller][requestId].amount; } /** - * @dev Gets the pending redeem request amount. + * @dev Gets the claimable deposit request amount. */ - function pendingRedeemRequest(uint256 requestId, address controller) external view override returns (uint256) { - return _pendingRedeemRequests[controller][requestId].amount; + function claimableDepositRequest(uint256 requestId, address controller) external view override returns (uint256) { + return _pendingDepositRequests[controller][requestId].claimable; } /** From 5780ff00d52e3eb31b19a1c404277c1c4634e5c0 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 09:23:31 +0100 Subject: [PATCH 10/30] ERC-7540 - Add ERC7540 - remove reentrancy guard --- contracts/token/ERC20/extensions/ERC7540.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index c9d3a4ebcf4..c7ed563a1b4 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -7,12 +7,11 @@ import {IERC20} from "../../../interfaces/IERC20.sol"; import {ERC4626} from "./ERC4626.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; import {Math} from "../../../utils/math/Math.sol"; -import {ReentrancyGuard} from "../../../utils/ReentrancyGuard.sol"; /** * @dev Abstract implementation of the ERC-7540 standard, extending ERC-4626. */ -abstract contract ERC7540 is ERC4626, IERC7540, ReentrancyGuard { +abstract contract ERC7540 is ERC4626, IERC7540 { using SafeERC20 for IERC20; using Math for uint256; @@ -39,7 +38,7 @@ abstract contract ERC7540 is ERC4626, IERC7540, ReentrancyGuard { uint256 assets, address controller, address owner - ) external override nonReentrant returns (uint256 requestId) { + ) external override returns (uint256 requestId) { require(assets > 0, "ERC7540: assets must be greater than zero"); require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); @@ -59,7 +58,7 @@ abstract contract ERC7540 is ERC4626, IERC7540, ReentrancyGuard { uint256 shares, address controller, address owner - ) external override nonReentrant returns (uint256 requestId) { + ) external override returns (uint256 requestId) { require(shares > 0, "ERC7540: shares must be greater than zero"); require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); From 7146c97864b9f3ba74061a3fb1f01f39efb39e5f Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 09:46:54 +0100 Subject: [PATCH 11/30] ERC-7540 - Add ERC7540 - fixing state variables and methods visibility --- contracts/mocks/token/ERC7540Mock.sol | 12 +++++----- contracts/token/ERC20/extensions/ERC7540.sol | 25 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index e7ecbf5fbca..f25e5687e70 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -10,16 +10,16 @@ import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; contract ERC7540Mock is ERC7540 { constructor(address asset) ERC20("ERC4626Mock", "E4626M") ERC7540(IERC20(asset)) {} - function processPendingDeposit(uint256 requestId, address controller) external { - Request storage request = _pendingDepositRequests[controller][requestId]; + function processPendingDeposit(uint256 requestId, address controller) external view { + Request memory request = _getPendingDepositRequest(controller, requestId); require(request.amount > 0, "No pending deposit request"); request.claimable += request.amount; request.amount = 0; } - function processPendingRedeem(uint256 requestId, address controller) external { - Request storage request = _pendingRedeemRequests[controller][requestId]; + function processPendingRedeem(uint256 requestId, address controller) external view { + Request memory request = _getPendingRedeemRequest(controller, requestId); require(request.amount > 0, "No pending redeem request"); request.claimable += request.amount; @@ -27,14 +27,14 @@ contract ERC7540Mock is ERC7540 { } function _processPendingRequests(uint256 requestId, address controller) internal override { - Request storage depositRequest = _pendingDepositRequests[controller][requestId]; + Request memory depositRequest = _getPendingDepositRequest(controller, requestId); if (depositRequest.amount > 0) { depositRequest.claimable += depositRequest.amount; emit DepositProcessed(controller, requestId, depositRequest.amount); depositRequest.amount = 0; } - Request storage redeemRequest = _pendingRedeemRequests[controller][requestId]; + Request memory redeemRequest = _getPendingRedeemRequest(controller, requestId); if (redeemRequest.amount > 0) { redeemRequest.claimable += redeemRequest.amount; emit RedeemProcessed(controller, requestId, redeemRequest.amount); diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index c7ed563a1b4..65b7a28d7c8 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -21,8 +21,8 @@ abstract contract ERC7540 is ERC4626, IERC7540 { } // Mappings to track pending and claimable requests - mapping(address => mapping(uint256 => Request)) internal _pendingDepositRequests; - mapping(address => mapping(uint256 => Request)) internal _pendingRedeemRequests; + mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; + mapping(address => mapping(uint256 => Request)) private _pendingRedeemRequests; mapping(address => mapping(address => bool)) private _operators; @@ -115,6 +115,27 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _operators[controller][operator]; } + /** + * @dev Internal function to return pending deposit request. + */ + function _getPendingDepositRequest(address controller, uint256 requestId) internal view returns (Request memory) { + return _pendingDepositRequests[controller][requestId]; + } + + /** + * @dev Internal function to return pending redeem request. + */ + function _getPendingRedeemRequest(address controller, uint256 requestId) internal view returns (Request memory) { + return _pendingRedeemRequests[controller][requestId]; + } + + /** + * @dev Internal function to return operator. + */ + function _getOperator(address controller, address operator) internal view returns (bool) { + return _operators[controller][operator]; + } + /** * @dev Internal function to generate a unique request ID. */ From 41688b8493ff839cf87c0a3de44be27c836ee137 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 11:09:27 +0100 Subject: [PATCH 12/30] ERC-7540 - Add ERC7540 - fixing state variables and methods visibility --- contracts/interfaces/IERC7540.sol | 35 ++++++++++++++++++++ contracts/mocks/token/ERC7540Mock.sol | 8 ++--- contracts/token/ERC20/extensions/ERC7540.sol | 19 ++++------- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 036d5bfaaa2..faaf7285db1 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -9,6 +9,11 @@ import {IERC4626} from "../interfaces/IERC4626.sol"; * https://eips.ethereum.org/EIPS/eip-7540[ERC-7540] */ interface IERC7540 is IERC4626 { + struct Request { + uint256 amount; + uint256 claimable; + } + // Events event DepositRequest( address indexed controller, @@ -99,4 +104,34 @@ interface IERC7540 is IERC4626 { * @return status Whether the operator is approved. */ function isOperator(address controller, address operator) external view returns (bool status); + + /** + * @dev Gets the details of a pending deposit request. + * @param controller The address of the controller. + * @param requestId The unique identifier for the deposit request. + * @return request The request object containing amount and claimable details. + */ + function getPendingDepositRequest( + address controller, + uint256 requestId + ) external view returns (Request memory request); + + /** + * @dev Gets the details of a pending redeem request. + * @param controller The address of the controller. + * @param requestId The unique identifier for the redeem request. + * @return request The request object containing amount and claimable details. + */ + function getPendingRedeemRequest( + address controller, + uint256 requestId + ) external view returns (Request memory request); + + /** + * @dev Gets the status of an operator for a given controller. + * @param controller The address of the controller. + * @param operator The address of the operator. + * @return status Whether the operator is approved. + */ + function getOperator(address controller, address operator) external view returns (bool status); } diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index f25e5687e70..166b0c0a716 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -11,7 +11,7 @@ contract ERC7540Mock is ERC7540 { constructor(address asset) ERC20("ERC4626Mock", "E4626M") ERC7540(IERC20(asset)) {} function processPendingDeposit(uint256 requestId, address controller) external view { - Request memory request = _getPendingDepositRequest(controller, requestId); + Request memory request = getPendingDepositRequest(controller, requestId); require(request.amount > 0, "No pending deposit request"); request.claimable += request.amount; @@ -19,7 +19,7 @@ contract ERC7540Mock is ERC7540 { } function processPendingRedeem(uint256 requestId, address controller) external view { - Request memory request = _getPendingRedeemRequest(controller, requestId); + Request memory request = getPendingRedeemRequest(controller, requestId); require(request.amount > 0, "No pending redeem request"); request.claimable += request.amount; @@ -27,14 +27,14 @@ contract ERC7540Mock is ERC7540 { } function _processPendingRequests(uint256 requestId, address controller) internal override { - Request memory depositRequest = _getPendingDepositRequest(controller, requestId); + Request memory depositRequest = getPendingDepositRequest(controller, requestId); if (depositRequest.amount > 0) { depositRequest.claimable += depositRequest.amount; emit DepositProcessed(controller, requestId, depositRequest.amount); depositRequest.amount = 0; } - Request memory redeemRequest = _getPendingRedeemRequest(controller, requestId); + Request memory redeemRequest = getPendingRedeemRequest(controller, requestId); if (redeemRequest.amount > 0) { redeemRequest.claimable += redeemRequest.amount; emit RedeemProcessed(controller, requestId, redeemRequest.amount); diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 65b7a28d7c8..74b75a5ebe6 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -15,11 +15,6 @@ abstract contract ERC7540 is ERC4626, IERC7540 { using SafeERC20 for IERC20; using Math for uint256; - struct Request { - uint256 amount; - uint256 claimable; - } - // Mappings to track pending and claimable requests mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; mapping(address => mapping(uint256 => Request)) private _pendingRedeemRequests; @@ -116,23 +111,23 @@ abstract contract ERC7540 is ERC4626, IERC7540 { } /** - * @dev Internal function to return pending deposit request. + * @dev Function to return pending deposit request. */ - function _getPendingDepositRequest(address controller, uint256 requestId) internal view returns (Request memory) { + function getPendingDepositRequest(address controller, uint256 requestId) public view returns (Request memory) { return _pendingDepositRequests[controller][requestId]; } /** - * @dev Internal function to return pending redeem request. + * @dev Function to return pending redeem request. */ - function _getPendingRedeemRequest(address controller, uint256 requestId) internal view returns (Request memory) { + function getPendingRedeemRequest(address controller, uint256 requestId) public view returns (Request memory) { return _pendingRedeemRequests[controller][requestId]; } /** - * @dev Internal function to return operator. + * @dev Function to return operator. */ - function _getOperator(address controller, address operator) internal view returns (bool) { + function getOperator(address controller, address operator) public view returns (bool) { return _operators[controller][operator]; } @@ -140,7 +135,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { * @dev Internal function to generate a unique request ID. */ function _generateRequestId(address controller, uint256 input) internal view returns (uint256) { - return uint256(keccak256(abi.encodePacked(block.timestamp, block.number, msg.sender, controller, input))); + return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, controller, input))); } /** From d6b15690860896071ad53ba895617c80f8818f8a Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 11:23:25 +0100 Subject: [PATCH 13/30] ERC-7540 - Add ERC7540 - fixing reentrancy --- contracts/token/ERC20/extensions/ERC7540.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 74b75a5ebe6..403f98afb05 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -39,10 +39,10 @@ abstract contract ERC7540 is ERC4626, IERC7540 { requestId = _generateRequestId(controller, assets); - IERC20(asset()).safeTransferFrom(owner, address(this), assets); - _pendingDepositRequests[controller][requestId].amount += assets; + IERC20(asset()).safeTransferFrom(owner, address(this), assets); + emit DepositRequest(controller, owner, requestId, msg.sender, assets); } From 4015f6f17a733b0fd233d9d9e821fb264ba9f26a Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 11:26:47 +0100 Subject: [PATCH 14/30] ERC-7540 - Add ERC7540 - add virtual to _generateRequestId --- contracts/token/ERC20/extensions/ERC7540.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 403f98afb05..cdb469ba558 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -134,7 +134,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { /** * @dev Internal function to generate a unique request ID. */ - function _generateRequestId(address controller, uint256 input) internal view returns (uint256) { + function _generateRequestId(address controller, uint256 input) internal virtual returns (uint256) { return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, controller, input))); } From e5cd1ab4afeec51bf1d3630a1de99807e9bad7c6 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 11:40:38 +0100 Subject: [PATCH 15/30] ERC-7540 - Add ERC7540 - fixing state variables and methods visibility --- contracts/mocks/token/ERC7540Mock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index 166b0c0a716..00b8694d631 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -8,7 +8,7 @@ import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; contract ERC7540Mock is ERC7540 { - constructor(address asset) ERC20("ERC4626Mock", "E4626M") ERC7540(IERC20(asset)) {} + constructor(IERC20 asset) ERC20("ERC4626Mock", "E4626M") ERC7540(asset) {} function processPendingDeposit(uint256 requestId, address controller) external view { Request memory request = getPendingDepositRequest(controller, requestId); From 3a4937254f53c00edd97fd3e2b42816286bfb86f Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 11:45:24 +0100 Subject: [PATCH 16/30] ERC-7540 - Add ERC7540 - dont revert, but instead just do no-op --- contracts/token/ERC20/extensions/ERC7540.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index cdb469ba558..75b32357328 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -34,7 +34,10 @@ abstract contract ERC7540 is ERC4626, IERC7540 { address controller, address owner ) external override returns (uint256 requestId) { - require(assets > 0, "ERC7540: assets must be greater than zero"); + if (assets == 0) { + return 0; + } + require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); requestId = _generateRequestId(controller, assets); From 36eb007c8ed3981fd5f4e39f01487b7964d33e35 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 12:02:38 +0100 Subject: [PATCH 17/30] ERC-7540 - Add ERC7540 - adding custom errors --- contracts/interfaces/IERC7540.sol | 7 ++++++ contracts/token/ERC20/extensions/ERC7540.sol | 23 +++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index faaf7285db1..aeb9a06a157 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -37,6 +37,13 @@ interface IERC7540 is IERC4626 { event RedeemProcessed(address indexed controller, uint256 indexed requestId, uint256 amount); + /** + * @dev Indicates an error related to the current `shares` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param shares Current shares for the interacting account. + */ + error ERC7540InsufficientShares(address sender, uint256 shares); + // Methods /** diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 75b32357328..b456718a309 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -34,11 +34,13 @@ abstract contract ERC7540 is ERC4626, IERC7540 { address controller, address owner ) external override returns (uint256 requestId) { + address sender = _msgSender(); + if (assets == 0) { return 0; } - require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); + require(owner == sender || isOperator(owner, sender), "ERC7540: unauthorized"); requestId = _generateRequestId(controller, assets); @@ -46,7 +48,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { IERC20(asset()).safeTransferFrom(owner, address(this), assets); - emit DepositRequest(controller, owner, requestId, msg.sender, assets); + emit DepositRequest(controller, owner, requestId, sender, assets); } /** @@ -57,8 +59,11 @@ abstract contract ERC7540 is ERC4626, IERC7540 { address controller, address owner ) external override returns (uint256 requestId) { - require(shares > 0, "ERC7540: shares must be greater than zero"); - require(owner == msg.sender || isOperator(owner, msg.sender), "ERC7540: unauthorized"); + address sender = _msgSender(); + if (shares <= 0) { + revert ERC7540InsufficientShares(sender, shares); + } + require(owner == sender || isOperator(owner, sender), "ERC7540: unauthorized"); requestId = _generateRequestId(controller, shares); @@ -66,7 +71,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { _pendingRedeemRequests[controller][requestId].amount += shares; - emit RedeemRequest(controller, owner, requestId, msg.sender, shares); + emit RedeemRequest(controller, owner, requestId, sender, shares); } /** @@ -101,8 +106,9 @@ abstract contract ERC7540 is ERC4626, IERC7540 { * @dev Sets or revokes an operator for the given controller. */ function setOperator(address operator, bool approved) external override returns (bool) { - _operators[msg.sender][operator] = approved; - emit OperatorSet(msg.sender, operator, approved); + address sender = _msgSender(); + _operators[sender][operator] = approved; + emit OperatorSet(sender, operator, approved); return true; } @@ -138,7 +144,8 @@ abstract contract ERC7540 is ERC4626, IERC7540 { * @dev Internal function to generate a unique request ID. */ function _generateRequestId(address controller, uint256 input) internal virtual returns (uint256) { - return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender, controller, input))); + address sender = _msgSender(); + return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, sender, controller, input))); } /** From 31500c7004a38c48b9ed42ba3c11f88e4f52932e Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 12:12:06 +0100 Subject: [PATCH 18/30] ERC-7540 - Add ERC7540 - adding custom errors --- contracts/interfaces/IERC7540.sol | 9 ++++++++- contracts/token/ERC20/extensions/ERC7540.sol | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index aeb9a06a157..50dcf3857ec 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -42,7 +42,14 @@ interface IERC7540 is IERC4626 { * @param sender Address whose tokens are being transferred. * @param shares Current shares for the interacting account. */ - error ERC7540InsufficientShares(address sender, uint256 shares); + error ERC7540ZeroSharesNotAllowed(address sender, uint256 shares); + + /** + * @dev Indicates an error related to the current `assets` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param owner Address of the owner. + */ + error ERC7540Unauthorized(address sender, address owner); // Methods diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index b456718a309..80190c261e7 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -40,7 +40,9 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return 0; } - require(owner == sender || isOperator(owner, sender), "ERC7540: unauthorized"); + if (owner != sender && !isOperator(owner, sender)) { + revert ERC7540Unauthorized(sender, owner); + } requestId = _generateRequestId(controller, assets); @@ -60,8 +62,8 @@ abstract contract ERC7540 is ERC4626, IERC7540 { address owner ) external override returns (uint256 requestId) { address sender = _msgSender(); - if (shares <= 0) { - revert ERC7540InsufficientShares(sender, shares); + if (shares == 0) { + revert ERC7540ZeroSharesNotAllowed(sender, shares); } require(owner == sender || isOperator(owner, sender), "ERC7540: unauthorized"); From dc5529f061a064c520e38e0979ddc63dd6d6549e Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 12:33:20 +0100 Subject: [PATCH 19/30] ERC-7540 - Add ERC7540 - remove duplicated method --- contracts/interfaces/IERC7540.sol | 8 -------- contracts/token/ERC20/extensions/ERC7540.sol | 12 ++++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 50dcf3857ec..0c139ad7125 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -140,12 +140,4 @@ interface IERC7540 is IERC4626 { address controller, uint256 requestId ) external view returns (Request memory request); - - /** - * @dev Gets the status of an operator for a given controller. - * @param controller The address of the controller. - * @param operator The address of the operator. - * @return status Whether the operator is approved. - */ - function getOperator(address controller, address operator) external view returns (bool status); } diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 80190c261e7..2c289eeae50 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -65,7 +65,10 @@ abstract contract ERC7540 is ERC4626, IERC7540 { if (shares == 0) { revert ERC7540ZeroSharesNotAllowed(sender, shares); } - require(owner == sender || isOperator(owner, sender), "ERC7540: unauthorized"); + + if (owner != sender && !isOperator(owner, sender)) { + revert ERC7540Unauthorized(sender, owner); + } requestId = _generateRequestId(controller, shares); @@ -135,13 +138,6 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _pendingRedeemRequests[controller][requestId]; } - /** - * @dev Function to return operator. - */ - function getOperator(address controller, address operator) public view returns (bool) { - return _operators[controller][operator]; - } - /** * @dev Internal function to generate a unique request ID. */ From bcb8becd368b4c86ffcfd7eb2b75e469e3ef28db Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 14:02:56 +0100 Subject: [PATCH 20/30] ERC-7540 - Add ERC7540 - removing events which are not under the ERC --- contracts/interfaces/IERC7540.sol | 4 ---- contracts/mocks/token/ERC7540Mock.sol | 2 -- 2 files changed, 6 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 0c139ad7125..c7d1fcf79bb 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -33,10 +33,6 @@ interface IERC7540 is IERC4626 { event OperatorSet(address indexed controller, address indexed operator, bool approved); - event DepositProcessed(address indexed controller, uint256 indexed requestId, uint256 amount); - - event RedeemProcessed(address indexed controller, uint256 indexed requestId, uint256 amount); - /** * @dev Indicates an error related to the current `shares` of a `sender`. Used in transfers. * @param sender Address whose tokens are being transferred. diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index 00b8694d631..37e58176f16 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -30,14 +30,12 @@ contract ERC7540Mock is ERC7540 { Request memory depositRequest = getPendingDepositRequest(controller, requestId); if (depositRequest.amount > 0) { depositRequest.claimable += depositRequest.amount; - emit DepositProcessed(controller, requestId, depositRequest.amount); depositRequest.amount = 0; } Request memory redeemRequest = getPendingRedeemRequest(controller, requestId); if (redeemRequest.amount > 0) { redeemRequest.claimable += redeemRequest.amount; - emit RedeemProcessed(controller, requestId, redeemRequest.amount); redeemRequest.amount = 0; } } From 981069c3f3d4cc868cc912b16201409ca7f8c94f Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 14:04:13 +0100 Subject: [PATCH 21/30] ERC-7540 - Add ERC7540 - removing events which are not under the ERC --- contracts/mocks/token/ERC7540Mock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index 37e58176f16..993edabbce4 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -26,7 +26,7 @@ contract ERC7540Mock is ERC7540 { request.amount = 0; } - function _processPendingRequests(uint256 requestId, address controller) internal override { + function _processPendingRequests(uint256 requestId, address controller) internal view override { Request memory depositRequest = getPendingDepositRequest(controller, requestId); if (depositRequest.amount > 0) { depositRequest.claimable += depositRequest.amount; From f4092f39f171fd1293b174a382c22ccd4c15a70c Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 14:11:58 +0100 Subject: [PATCH 22/30] ERC-7540 - Add ERC7540 - cumulative generate request id approach --- contracts/token/ERC20/extensions/ERC7540.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 2c289eeae50..f1969f64dcc 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -143,7 +143,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { */ function _generateRequestId(address controller, uint256 input) internal virtual returns (uint256) { address sender = _msgSender(); - return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, sender, controller, input))); + return uint256(keccak256(abi.encodePacked(block.number, sender, controller, input))); } /** From ff8585d020dd0e9cbb6819119a412fa3083ded9d Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 14:14:13 +0100 Subject: [PATCH 23/30] ERC-7540 - Add ERC7540 - cumulative generate request id approach - adding a description --- contracts/token/ERC20/extensions/ERC7540.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index f1969f64dcc..b78b0bdb4de 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -139,7 +139,13 @@ abstract contract ERC7540 is ERC4626, IERC7540 { } /** - * @dev Internal function to generate a unique request ID. + * @dev Internal function to generates a request ID. Requests created within the same block, + * for the same controller, input, and sender, are cumulative. + * + * Using only `block.number` ensures consistent behavior on L2s, where + * `block.timestamp` might vary slightly between operators. This approach + * defines "fungibility" of requests generated within the same block and + * with identical parameters. */ function _generateRequestId(address controller, uint256 input) internal virtual returns (uint256) { address sender = _msgSender(); From 891b3a0931da140d64b8c6676f6faace99174724 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Tue, 28 Jan 2025 14:14:34 +0100 Subject: [PATCH 24/30] ERC-7540 - Add ERC7540 - cumulative generate request id approach - adding a description --- contracts/token/ERC20/extensions/ERC7540.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index b78b0bdb4de..81d5dc3ad4e 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -141,7 +141,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { /** * @dev Internal function to generates a request ID. Requests created within the same block, * for the same controller, input, and sender, are cumulative. - * + * * Using only `block.number` ensures consistent behavior on L2s, where * `block.timestamp` might vary slightly between operators. This approach * defines "fungibility" of requests generated within the same block and From 94ce32b6762ad5905af678c57cf326476a8ae9c6 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Thu, 30 Jan 2025 03:17:46 +0100 Subject: [PATCH 25/30] ERC-7540 - Add ERC7540 - review updates --- contracts/interfaces/IERC7540.sol | 22 ----------- contracts/mocks/token/ERC7540Mock.sol | 28 +------------ contracts/token/ERC20/extensions/ERC7540.sol | 41 ++++++++++++-------- 3 files changed, 25 insertions(+), 66 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index c7d1fcf79bb..dc91935c7ab 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -114,26 +114,4 @@ interface IERC7540 is IERC4626 { * @return status Whether the operator is approved. */ function isOperator(address controller, address operator) external view returns (bool status); - - /** - * @dev Gets the details of a pending deposit request. - * @param controller The address of the controller. - * @param requestId The unique identifier for the deposit request. - * @return request The request object containing amount and claimable details. - */ - function getPendingDepositRequest( - address controller, - uint256 requestId - ) external view returns (Request memory request); - - /** - * @dev Gets the details of a pending redeem request. - * @param controller The address of the controller. - * @param requestId The unique identifier for the redeem request. - * @return request The request object containing amount and claimable details. - */ - function getPendingRedeemRequest( - address controller, - uint256 requestId - ) external view returns (Request memory request); } diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index 993edabbce4..011e928bedc 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -10,33 +10,7 @@ import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; contract ERC7540Mock is ERC7540 { constructor(IERC20 asset) ERC20("ERC4626Mock", "E4626M") ERC7540(asset) {} - function processPendingDeposit(uint256 requestId, address controller) external view { - Request memory request = getPendingDepositRequest(controller, requestId); - require(request.amount > 0, "No pending deposit request"); - - request.claimable += request.amount; - request.amount = 0; - } - - function processPendingRedeem(uint256 requestId, address controller) external view { - Request memory request = getPendingRedeemRequest(controller, requestId); - require(request.amount > 0, "No pending redeem request"); - - request.claimable += request.amount; - request.amount = 0; - } - function _processPendingRequests(uint256 requestId, address controller) internal view override { - Request memory depositRequest = getPendingDepositRequest(controller, requestId); - if (depositRequest.amount > 0) { - depositRequest.claimable += depositRequest.amount; - depositRequest.amount = 0; - } - - Request memory redeemRequest = getPendingRedeemRequest(controller, requestId); - if (redeemRequest.amount > 0) { - redeemRequest.claimable += redeemRequest.amount; - redeemRequest.amount = 0; - } + // ToDo } } diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 81d5dc3ad4e..4ecdd2056ca 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {IERC7540} from "../../../interfaces/IERC7540.sol"; +import {IERC4626} from "../../../interfaces/IERC4626.sol"; import {IERC20} from "../../../interfaces/IERC20.sol"; import {ERC4626} from "./ERC4626.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; @@ -15,11 +16,13 @@ abstract contract ERC7540 is ERC4626, IERC7540 { using SafeERC20 for IERC20; using Math for uint256; + uint256 public constant VESTING_DURATION = 48 hours; + // Mappings to track pending and claimable requests mapping(address => mapping(uint256 => Request)) private _pendingDepositRequests; mapping(address => mapping(uint256 => Request)) private _pendingRedeemRequests; - mapping(address => mapping(address => bool)) private _operators; + mapping(uint256 => uint256) private _vestingTimestamps; /** * @dev Set the underlying asset contract. @@ -46,7 +49,9 @@ abstract contract ERC7540 is ERC4626, IERC7540 { requestId = _generateRequestId(controller, assets); - _pendingDepositRequests[controller][requestId].amount += assets; + uint256 shares = previewDeposit(assets); + + _pendingDepositRequests[controller][requestId].amount += shares; IERC20(asset()).safeTransferFrom(owner, address(this), assets); @@ -62,6 +67,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { address owner ) external override returns (uint256 requestId) { address sender = _msgSender(); + if (shares == 0) { revert ERC7540ZeroSharesNotAllowed(sender, shares); } @@ -72,9 +78,14 @@ abstract contract ERC7540 is ERC4626, IERC7540 { requestId = _generateRequestId(controller, shares); + uint256 assets = previewRedeem(shares); + _burn(owner, shares); - _pendingRedeemRequests[controller][requestId].amount += shares; + _pendingRedeemRequests[controller][requestId].amount += assets; + + // Set vesting unlock time + _vestingTimestamps[requestId] = block.timestamp + VESTING_DURATION; emit RedeemRequest(controller, owner, requestId, sender, shares); } @@ -107,6 +118,16 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _pendingRedeemRequests[controller][requestId].claimable; } + /** @dev See {IERC4626-maxWithdraw}. */ + function maxWithdraw(address) public pure override(ERC4626, IERC4626) returns (uint256) { + return 0; // Withdrawals are only possible through requestRedeem + } + + /** @dev See {IERC4626-maxRedeem}. */ + function maxRedeem(address) public pure override(ERC4626, IERC4626) returns (uint256) { + return 0; // Redemptions are only possible through requestRedeem + } + /** * @dev Sets or revokes an operator for the given controller. */ @@ -124,20 +145,6 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _operators[controller][operator]; } - /** - * @dev Function to return pending deposit request. - */ - function getPendingDepositRequest(address controller, uint256 requestId) public view returns (Request memory) { - return _pendingDepositRequests[controller][requestId]; - } - - /** - * @dev Function to return pending redeem request. - */ - function getPendingRedeemRequest(address controller, uint256 requestId) public view returns (Request memory) { - return _pendingRedeemRequests[controller][requestId]; - } - /** * @dev Internal function to generates a request ID. Requests created within the same block, * for the same controller, input, and sender, are cumulative. From f46e0f58871a78564158da6502d607725ec9efde Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Thu, 30 Jan 2025 10:44:00 +0100 Subject: [PATCH 26/30] ERC-7540 - Add ERC7540 - refactoring the contracts to align more with the standard --- contracts/interfaces/IERC7540.sol | 11 +- contracts/mocks/docs/ERC7540Fees.sol | 19 +-- contracts/mocks/token/ERC7540LimitsMock.sol | 4 +- contracts/token/ERC20/extensions/ERC7540.sol | 126 ++++++++++++++++--- 4 files changed, 131 insertions(+), 29 deletions(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index dc91935c7ab..6adacdef179 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -34,19 +34,26 @@ interface IERC7540 is IERC4626 { event OperatorSet(address indexed controller, address indexed operator, bool approved); /** - * @dev Indicates an error related to the current `shares` of a `sender`. Used in transfers. + * @dev Indicates an error related to the current `shares` of a `sender`. * @param sender Address whose tokens are being transferred. * @param shares Current shares for the interacting account. */ error ERC7540ZeroSharesNotAllowed(address sender, uint256 shares); /** - * @dev Indicates an error related to the current `assets` of a `sender`. Used in transfers. + * @dev Indicates an error related to the current `assets` of a `sender`. * @param sender Address whose tokens are being transferred. * @param owner Address of the owner. */ error ERC7540Unauthorized(address sender, address owner); + /** + * @dev Indicates an error related to the current insufficient `claimable amount` of a `sender`. + * @param shares Current shares for the interacting account. + * @param amount Amount to be claimed. + */ + error ERC7540InsufficientClaimable(uint256 shares, uint256 amount); + // Methods /** diff --git a/contracts/mocks/docs/ERC7540Fees.sol b/contracts/mocks/docs/ERC7540Fees.sol index b80a47a3fa9..da35e3bd927 100644 --- a/contracts/mocks/docs/ERC7540Fees.sol +++ b/contracts/mocks/docs/ERC7540Fees.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import {IERC4626} from "../../interfaces/IERC4626.sol"; +import {IERC7540} from "../../interfaces/IERC7540.sol"; import {IERC20} from "../../token/ERC20/IERC20.sol"; import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; @@ -24,16 +25,16 @@ abstract contract ERC7540Fees is ERC7540 { uint256 private constant _BASIS_POINT_SCALE = 1e4; - function previewDeposit(uint256 assets) public view virtual override(ERC4626, IERC4626) returns (uint256) { - uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); - return super.previewDeposit(assets - fee); - } +// function previewDeposit(uint256 assets) public view virtual override(ERC7540) returns (uint256) { +// uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); +// return super.previewDeposit(assets - fee); +// } - function previewRedeem(uint256 shares) public view virtual override(ERC4626, IERC4626) returns (uint256) { - uint256 assets = super.previewRedeem(shares); - uint256 fee = _feeOnTotal(assets, _exitFeeBasisPoints()); - return assets - fee; - } +// function previewRedeem(uint256 shares) public view virtual override(ERC7540) returns (uint256) { +// uint256 assets = super.previewRedeem(shares); +// uint256 fee = _feeOnTotal(assets, _exitFeeBasisPoints()); +// return assets - fee; +// } function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); diff --git a/contracts/mocks/token/ERC7540LimitsMock.sol b/contracts/mocks/token/ERC7540LimitsMock.sol index 08acabba73a..4b8d48dc802 100644 --- a/contracts/mocks/token/ERC7540LimitsMock.sol +++ b/contracts/mocks/token/ERC7540LimitsMock.sol @@ -16,11 +16,11 @@ abstract contract ERC7540LimitsMock is ERC7540 { _maxMint = maxMintValue; } - function maxDeposit(address) public view override(IERC4626, ERC4626) returns (uint256) { + function maxDeposit(address) public view override(ERC7540) returns (uint256) { return _maxDeposit; } - function maxMint(address) public view override(IERC4626, ERC4626) returns (uint256) { + function maxMint(address) public view override(ERC4626, IERC4626) returns (uint256) { return _maxMint; } } diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 4ecdd2056ca..162ccfef446 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -6,13 +6,14 @@ import {IERC7540} from "../../../interfaces/IERC7540.sol"; import {IERC4626} from "../../../interfaces/IERC4626.sol"; import {IERC20} from "../../../interfaces/IERC20.sol"; import {ERC4626} from "./ERC4626.sol"; +import {IERC165} from "../../../interfaces/IERC165.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; import {Math} from "../../../utils/math/Math.sol"; /** * @dev Abstract implementation of the ERC-7540 standard, extending ERC-4626. */ -abstract contract ERC7540 is ERC4626, IERC7540 { +abstract contract ERC7540 is ERC4626, IERC7540, IERC165 { using SafeERC20 for IERC20; using Math for uint256; @@ -36,7 +37,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { uint256 assets, address controller, address owner - ) external override returns (uint256 requestId) { + ) public virtual override returns (uint256 requestId) { address sender = _msgSender(); if (assets == 0) { @@ -49,9 +50,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { requestId = _generateRequestId(controller, assets); - uint256 shares = previewDeposit(assets); - - _pendingDepositRequests[controller][requestId].amount += shares; + _pendingDepositRequests[controller][requestId].amount += assets; IERC20(asset()).safeTransferFrom(owner, address(this), assets); @@ -65,7 +64,7 @@ abstract contract ERC7540 is ERC4626, IERC7540 { uint256 shares, address controller, address owner - ) external override returns (uint256 requestId) { + ) public virtual override returns (uint256 requestId) { address sender = _msgSender(); if (shares == 0) { @@ -78,11 +77,9 @@ abstract contract ERC7540 is ERC4626, IERC7540 { requestId = _generateRequestId(controller, shares); - uint256 assets = previewRedeem(shares); - _burn(owner, shares); - _pendingRedeemRequests[controller][requestId].amount += assets; + _pendingRedeemRequests[controller][requestId].amount += shares; // Set vesting unlock time _vestingTimestamps[requestId] = block.timestamp + VESTING_DURATION; @@ -90,6 +87,20 @@ abstract contract ERC7540 is ERC4626, IERC7540 { emit RedeemRequest(controller, owner, requestId, sender, shares); } + /* + * @dev Overrides maxDeposit to allow deposits up to claimable request amount + */ + function maxDeposit(address controller) public view virtual override(ERC4626, IERC4626) returns (uint256) { + return _pendingDepositRequests[controller][0].claimable; + } + + /* + * @dev Overrides maxRedeem to allow redemptions up to claimable request amount + */ + function maxRedeem(address controller) public view virtual override(ERC4626, IERC4626) returns (uint256) { + return _pendingRedeemRequests[controller][0].claimable; + } + /** * @dev Gets the pending deposit request amount. */ @@ -118,14 +129,57 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _pendingRedeemRequests[controller][requestId].claimable; } - /** @dev See {IERC4626-maxWithdraw}. */ - function maxWithdraw(address) public pure override(ERC4626, IERC4626) returns (uint256) { - return 0; // Withdrawals are only possible through requestRedeem + /** + * @dev Implements ERC-7540 deposit by allowing users to deposit through calling ERC4626 deposit. + */ + function deposit(uint256 assets, address receiver, address controller) public virtual returns (uint256 shares) { + address sender = _msgSender(); + + if (sender != controller && !isOperator(controller, sender)) { + revert ERC7540Unauthorized(sender, controller); + } + + Request storage request = _pendingDepositRequests[controller][0]; + if (request.claimable < assets) { + revert ERC7540InsufficientClaimable(assets, request.claimable); + } + + request.claimable -= assets; + + uint256 maxAssets = maxDeposit(receiver); + if (assets > maxAssets) { + revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); + } + + shares = super.deposit(assets, receiver); + + return shares; } - /** @dev See {IERC4626-maxRedeem}. */ - function maxRedeem(address) public pure override(ERC4626, IERC4626) returns (uint256) { - return 0; // Redemptions are only possible through requestRedeem + /** + * @dev Implements ERC-7540 claim by allowing users to redeem claimable shares. + */ + function redeem(uint256 shares, address receiver, address controller) public virtual override(ERC4626, IERC4626) returns (uint256 assets) { + address sender = _msgSender(); + if (sender != controller && !isOperator(controller, sender)) { + revert ERC7540Unauthorized(sender, controller); + } + + Request storage request = _pendingRedeemRequests[controller][0]; + if (request.claimable < shares) { + revert ERC7540InsufficientClaimable(shares, request.claimable); + } + + request.claimable -= shares; + + uint256 maxShares = maxRedeem(controller); + if (shares > maxShares) { + revert ERC4626ExceededMaxRedeem(controller, shares, maxShares); + } + + assets = super.redeem(shares, receiver, controller); + + return assets; } /** @@ -145,6 +199,34 @@ abstract contract ERC7540 is ERC4626, IERC7540 { return _operators[controller][operator]; } + function previewDeposit(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { + revert("ERC7540: previewDeposit not supported"); + } + + function previewMint(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { + revert("ERC7540: previewMint not supported"); + } + + function previewRedeem(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { + revert("ERC7540: previewRedeem not supported"); + } + + function previewWithdraw(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { + revert("ERC7540: previewWithdraw not supported"); + } + + /** + * @dev Implements ERC-165 interface detection. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == 0xe3bc4e65 || // ERC-7540 operator methods + interfaceId == 0x2f0a18c5 || // ERC-7575 interface + interfaceId == 0xce3bbe50 || // Asynchronous deposit Vault + interfaceId == 0x620ee8e4; // Asynchronous redemption Vault + } + /** * @dev Internal function to generates a request ID. Requests created within the same block, * for the same controller, input, and sender, are cumulative. @@ -162,5 +244,17 @@ abstract contract ERC7540 is ERC4626, IERC7540 { /** * @dev Abstract function for transitioning requests from Pending to Claimable. */ - function _processPendingRequests(uint256 requestId, address controller) internal virtual; + function _processPendingRequests(uint256 requestId, address controller) internal virtual { + Request storage depositRequest = _pendingDepositRequests[controller][requestId]; + if (depositRequest.amount > 0) { + depositRequest.claimable += depositRequest.amount; + depositRequest.amount = 0; + } + + Request storage redeemRequest = _pendingRedeemRequests[controller][requestId]; + if (redeemRequest.amount > 0) { + redeemRequest.claimable += redeemRequest.amount; + redeemRequest.amount = 0; + } + } } From 859ab4d80f765988704a91f5886dc768704c44d5 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Thu, 30 Jan 2025 10:46:47 +0100 Subject: [PATCH 27/30] ERC-7540 - Add ERC7540 - fixing state variables and methods visibility --- contracts/interfaces/IERC7540.sol | 7 +++++++ contracts/token/ERC20/extensions/ERC7540.sol | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/interfaces/IERC7540.sol b/contracts/interfaces/IERC7540.sol index 6adacdef179..6b311aad0e8 100644 --- a/contracts/interfaces/IERC7540.sol +++ b/contracts/interfaces/IERC7540.sol @@ -40,6 +40,13 @@ interface IERC7540 is IERC4626 { */ error ERC7540ZeroSharesNotAllowed(address sender, uint256 shares); + /** + * @dev Indicates an error related to the current `assets` of a `sender`. + * @param sender Address whose tokens are being transferred. + * @param assets Current assets for the interacting account. + */ + error ERC7540ZeroAssetsNotAllowed(address sender, uint256 assets); + /** * @dev Indicates an error related to the current `assets` of a `sender`. * @param sender Address whose tokens are being transferred. diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 162ccfef446..24bb63c0c5f 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -41,7 +41,7 @@ abstract contract ERC7540 is ERC4626, IERC7540, IERC165 { address sender = _msgSender(); if (assets == 0) { - return 0; + revert ERC7540ZeroAssetsNotAllowed(sender, assets); } if (owner != sender && !isOperator(owner, sender)) { From ef006854755d2d2eb8c659bb776ea1cdd9191b25 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 31 Jan 2025 13:36:58 +0100 Subject: [PATCH 28/30] ERC-7540 - Add ERC7540 - add changeset --- .changeset/lazy-birds-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-birds-push.md diff --git a/.changeset/lazy-birds-push.md b/.changeset/lazy-birds-push.md new file mode 100644 index 00000000000..0ae7adb212e --- /dev/null +++ b/.changeset/lazy-birds-push.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +ERC-7540: Asynchronous ERC-4626 Tokenized Vaults From 30776adf38fc96adac27c211e9d79ac62aafcd3c Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 31 Jan 2025 13:43:47 +0100 Subject: [PATCH 29/30] ERC-7540 - Add ERC7540 - fixing state variables and methods visibility --- contracts/mocks/docs/ERC7540Fees.sol | 24 ++++++++-------- contracts/token/ERC20/extensions/ERC7540.sol | 30 ++++++-------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/contracts/mocks/docs/ERC7540Fees.sol b/contracts/mocks/docs/ERC7540Fees.sol index da35e3bd927..87c5de76a51 100644 --- a/contracts/mocks/docs/ERC7540Fees.sol +++ b/contracts/mocks/docs/ERC7540Fees.sol @@ -23,18 +23,18 @@ abstract contract ERC7540Fees is ERC7540 { using SafeERC20 for IERC20; using Math for uint256; - uint256 private constant _BASIS_POINT_SCALE = 1e4; + uint256 private constant BASIS_POINT_SCALE = 1e4; -// function previewDeposit(uint256 assets) public view virtual override(ERC7540) returns (uint256) { -// uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); -// return super.previewDeposit(assets - fee); -// } + // function previewDeposit(uint256 assets) public view virtual override(ERC7540) returns (uint256) { + // uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); + // return super.previewDeposit(assets - fee); + // } -// function previewRedeem(uint256 shares) public view virtual override(ERC7540) returns (uint256) { -// uint256 assets = super.previewRedeem(shares); -// uint256 fee = _feeOnTotal(assets, _exitFeeBasisPoints()); -// return assets - fee; -// } + // function previewRedeem(uint256 shares) public view virtual override(ERC7540) returns (uint256) { + // uint256 assets = super.previewRedeem(shares); + // uint256 fee = _feeOnTotal(assets, _exitFeeBasisPoints()); + // return assets - fee; + // } function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); @@ -81,10 +81,10 @@ abstract contract ERC7540Fees is ERC7540 { } function _feeOnRaw(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { - return assets.mulDiv(feeBasisPoints, _BASIS_POINT_SCALE, Math.Rounding.Ceil); + return assets.mulDiv(feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Ceil); } function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { - return assets.mulDiv(feeBasisPoints, feeBasisPoints + _BASIS_POINT_SCALE, Math.Rounding.Ceil); + return assets.mulDiv(feeBasisPoints, feeBasisPoints + BASIS_POINT_SCALE, Math.Rounding.Ceil); } } diff --git a/contracts/token/ERC20/extensions/ERC7540.sol b/contracts/token/ERC20/extensions/ERC7540.sol index 24bb63c0c5f..592f45d06c6 100644 --- a/contracts/token/ERC20/extensions/ERC7540.sol +++ b/contracts/token/ERC20/extensions/ERC7540.sol @@ -150,16 +150,20 @@ abstract contract ERC7540 is ERC4626, IERC7540, IERC165 { if (assets > maxAssets) { revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); } - + shares = super.deposit(assets, receiver); - + return shares; } /** * @dev Implements ERC-7540 claim by allowing users to redeem claimable shares. */ - function redeem(uint256 shares, address receiver, address controller) public virtual override(ERC4626, IERC4626) returns (uint256 assets) { + function redeem( + uint256 shares, + address receiver, + address controller + ) public virtual override(ERC4626, IERC4626) returns (uint256 assets) { address sender = _msgSender(); if (sender != controller && !isOperator(controller, sender)) { revert ERC7540Unauthorized(sender, controller); @@ -176,9 +180,9 @@ abstract contract ERC7540 is ERC4626, IERC7540, IERC165 { if (shares > maxShares) { revert ERC4626ExceededMaxRedeem(controller, shares, maxShares); } - + assets = super.redeem(shares, receiver, controller); - + return assets; } @@ -199,22 +203,6 @@ abstract contract ERC7540 is ERC4626, IERC7540, IERC165 { return _operators[controller][operator]; } - function previewDeposit(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { - revert("ERC7540: previewDeposit not supported"); - } - - function previewMint(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { - revert("ERC7540: previewMint not supported"); - } - - function previewRedeem(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { - revert("ERC7540: previewRedeem not supported"); - } - - function previewWithdraw(uint256) public view virtual override(ERC4626, IERC4626) returns (uint256) { - revert("ERC7540: previewWithdraw not supported"); - } - /** * @dev Implements ERC-165 interface detection. */ From f3a616aee15738bb618300bc2e9bfc441fa7e279 Mon Sep 17 00:00:00 2001 From: Aleksandar Djordjevic Date: Fri, 31 Jan 2025 14:16:57 +0100 Subject: [PATCH 30/30] ERC-7540 - Add ERC7540 - add unit tests --- contracts/mocks/token/ERC7540Mock.sol | 3 +- test/token/ERC20/extensions/ERC7540.test.js | 183 ++++++++++++++++++++ 2 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 test/token/ERC20/extensions/ERC7540.test.js diff --git a/contracts/mocks/token/ERC7540Mock.sol b/contracts/mocks/token/ERC7540Mock.sol index 011e928bedc..69914dd1929 100644 --- a/contracts/mocks/token/ERC7540Mock.sol +++ b/contracts/mocks/token/ERC7540Mock.sol @@ -5,10 +5,9 @@ pragma solidity ^0.8.20; import {IERC20} from "../../token/ERC20/IERC20.sol"; import {ERC20} from "../../token/ERC20/ERC20.sol"; import {ERC7540} from "../../token/ERC20/extensions/ERC7540.sol"; -import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; contract ERC7540Mock is ERC7540 { - constructor(IERC20 asset) ERC20("ERC4626Mock", "E4626M") ERC7540(asset) {} + constructor(IERC20 asset) ERC20("ERC7540Mock", "E7540M") ERC7540(asset) {} function _processPendingRequests(uint256 requestId, address controller) internal view override { // ToDo diff --git a/test/token/ERC20/extensions/ERC7540.test.js b/test/token/ERC20/extensions/ERC7540.test.js new file mode 100644 index 00000000000..4a6af2f6863 --- /dev/null +++ b/test/token/ERC20/extensions/ERC7540.test.js @@ -0,0 +1,183 @@ +const { ethers } = require("hardhat"); +const { expect } = require("chai"); +const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers"); + +const name = "TestToken"; +const symbol = "TTKN"; +const decimals = 18n; +const initialSupply = ethers.parseUnits("1000000", decimals); + +async function fixture() { + const [deployer, holder, recipient, controller, operator, other] = await ethers.getSigners(); + + const AssetToken = await ethers.getContractFactory("ERC20Mock"); + const assetToken = await AssetToken.deploy(name, symbol, decimals); + await assetToken.mint(holder.address, initialSupply); + + const ERC7540Vault = await ethers.getContractFactory("ERC7540Mock"); + const vault = await ERC7540Vault.deploy(assetToken.address); + + return { deployer, holder, recipient, controller, operator, other, assetToken, vault }; +} + +describe("ERC7540", function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe("Deployment", function () { + it("Should initialize the correct asset token", async function () { + expect(await this.vault.asset()).to.equal(this.assetToken.target); + }); + + it("Should set the correct initial decimals", async function () { + expect(await this.vault.decimals()).to.equal(decimals); + }); + }); + + describe("Request Deposit", function () { + it("Should create a deposit request", async function () { + const depositAmount = ethers.parseUnits("100", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount); + + const requestId = await this.vault + .connect(this.holder) + .requestDeposit(depositAmount, this.controller.address, this.holder.address); + + expect(await this.vault.pendingDepositRequest(requestId, this.controller.address)).to.equal(depositAmount); + }); + + it("Should revert if deposit amount is zero", async function () { + await expect( + this.vault.connect(this.holder).requestDeposit(0, this.controller.address, this.holder.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540ZeroAssetsNotAllowed"); + }); + + it("Should revert if sender is not authorized", async function () { + const depositAmount = ethers.parseUnits("100", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount); + + await expect( + this.vault.connect(this.other).requestDeposit(depositAmount, this.controller.address, this.holder.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized"); + }); + }); + + describe("Request Redeem", function () { + it("Should create a redeem request", async function () { + const redeemAmount = ethers.parseUnits("50", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, redeemAmount); + await this.vault.connect(this.holder).requestDeposit(redeemAmount, this.controller.address, this.holder.address); + + const requestId = await this.vault + .connect(this.holder) + .requestRedeem(redeemAmount, this.controller.address, this.holder.address); + + expect(await this.vault.pendingRedeemRequest(requestId, this.controller.address)).to.equal(redeemAmount); + }); + + it("Should revert if redeem amount is zero", async function () { + await expect( + this.vault.connect(this.holder).requestRedeem(0, this.controller.address, this.holder.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540ZeroSharesNotAllowed"); + }); + + it("Should revert if sender is not authorized", async function () { + const redeemAmount = ethers.parseUnits("50", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, redeemAmount); + await this.vault.connect(this.holder).requestDeposit(redeemAmount, this.controller.address, this.holder.address); + + await expect( + this.vault.connect(this.other).requestRedeem(redeemAmount, this.controller.address, this.holder.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized"); + }); + }); + + describe("Claim Deposits and Redemptions", function () { + beforeEach(async function () { + this.depositAmount = ethers.parseUnits("200", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, this.depositAmount); + await this.vault.connect(this.holder).requestDeposit(this.depositAmount, this.controller.address, this.holder.address); + }); + + it("Should allow a controller to claim a deposit", async function () { + await expect( + this.vault.connect(this.controller).deposit(this.depositAmount, this.holder.address, this.controller.address) + ).to.not.be.reverted; + }); + + it("Should revert if the controller tries to claim more than claimable amount", async function () { + await expect( + this.vault.connect(this.controller).deposit(this.depositAmount + 1n, this.holder.address, this.controller.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540InsufficientClaimable"); + }); + }); + + describe("Operator Permissions", function () { + it("Should allow a user to set an operator", async function () { + await expect(this.vault.connect(this.holder).setOperator(this.operator.address, true)) + .to.emit(this.vault, "OperatorSet") + .withArgs(this.holder.address, this.operator.address, true); + + expect(await this.vault.isOperator(this.holder.address, this.operator.address)).to.be.true; + }); + + it("Should allow an operator to perform actions on behalf of the owner", async function () { + await this.vault.connect(this.holder).setOperator(this.operator.address, true); + + const depositAmount = ethers.parseUnits("50", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, depositAmount); + + await expect( + this.vault.connect(this.operator).requestDeposit(depositAmount, this.controller.address, this.holder.address) + ).to.not.be.reverted; + }); + + it("Should revert if an unapproved operator tries to perform an action", async function () { + await expect( + this.vault.connect(this.other).requestDeposit(100, this.controller.address, this.holder.address) + ).to.be.revertedWithCustomError(this.vault, "ERC7540Unauthorized"); + }); + }); + + describe("ERC-165 Interface Support", function () { + it("Should support ERC-165", async function () { + expect(await this.vault.supportsInterface("0x01ffc9a7")).to.be.true; // ERC-165 ID + }); + + it("Should support ERC-7540 operator methods", async function () { + expect(await this.vault.supportsInterface("0xe3bc4e65")).to.be.true; + }); + + it("Should support ERC-7575", async function () { + expect(await this.vault.supportsInterface("0x2f0a18c5")).to.be.true; + }); + + it("Should support async deposit vault", async function () { + expect(await this.vault.supportsInterface("0xce3bbe50")).to.be.true; + }); + + it("Should support async redemption vault", async function () { + expect(await this.vault.supportsInterface("0x620ee8e4")).to.be.true; + }); + + it("Should return false for unsupported interfaces", async function () { + expect(await this.vault.supportsInterface("0x00000000")).to.be.false; + }); + }); + + describe("Reentrancy and Edge Cases", function () { + it("Should prevent reentrancy attacks", async function () { + // Implement tests to check for reentrancy if using a vulnerable function + }); + + it("Should handle large request amounts", async function () { + const largeAmount = ethers.parseUnits("1000000000", decimals); + await this.assetToken.connect(this.holder).approve(this.vault.target, largeAmount); + + await expect( + this.vault.connect(this.holder).requestDeposit(largeAmount, this.controller.address, this.holder.address) + ).to.not.be.reverted; + }); + }); +});