From ca9dfc0f75beb57fefabd085b83f944ee8e650e7 Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 08:49:12 +0530 Subject: [PATCH 1/6] feat: PRO-2631 MultiTokenSessionkeyValidator V1 --- .../interfaces/IAggregatorV3Interface.sol | 20 + .../interfaces/IERC20.sol | 93 +++++ .../IMultiTokenSessionKeyValidator.sol | 137 +++++++ .../MultiTokenSessionKeyValidator.sol | 378 ++++++++++++++++++ 4 files changed, 628 insertions(+) create mode 100644 src/modular-etherspot-wallet/interfaces/IAggregatorV3Interface.sol create mode 100644 src/modular-etherspot-wallet/interfaces/IERC20.sol create mode 100644 src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol create mode 100644 src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol diff --git a/src/modular-etherspot-wallet/interfaces/IAggregatorV3Interface.sol b/src/modular-etherspot-wallet/interfaces/IAggregatorV3Interface.sol new file mode 100644 index 00000000..c2b94b1f --- /dev/null +++ b/src/modular-etherspot-wallet/interfaces/IAggregatorV3Interface.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// solhint-disable-next-line interface-starts-with-i +interface IAggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} \ No newline at end of file diff --git a/src/modular-etherspot-wallet/interfaces/IERC20.sol b/src/modular-etherspot-wallet/interfaces/IERC20.sol new file mode 100644 index 00000000..620fcfea --- /dev/null +++ b/src/modular-etherspot-wallet/interfaces/IERC20.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() external view returns (uint8); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} \ No newline at end of file diff --git a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol new file mode 100644 index 00000000..5a67c524 --- /dev/null +++ b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IValidator} from "../../../src/modular-etherspot-wallet/erc7579-ref-impl/interfaces/IERC7579Module.sol"; +import {IERC7579Account} from "../../../src/modular-etherspot-wallet/erc7579-ref-impl/interfaces/IERC7579Account.sol"; +import {PackedUserOperation} from "../../../account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +/// @title MultiTokenSessionKeyValidator Interface +/// @author Etherspot +/// @notice This interface defines the functions and events of the MultiTokenSessionKeyValidator contract. +interface IMultiTokenSessionKeyValidator is IValidator { + /// @notice Emitted when the MultiToken Session Key Validator module is installed for a wallet. + /// @param wallet The address of the wallet for which the module is installed. + event ERC20SKV_ModuleInstalled(address wallet); + + /// @notice Emitted when the MultiToken Session Key Validator module is uninstalled from a wallet. + /// @param wallet The address of the wallet from which the module is uninstalled. + event ERC20SKV_ModuleUninstalled(address wallet); + + /// @notice Emitted when a new session key is enabled for a wallet. + /// @param sessionKey The address of the session key. + /// @param wallet The address of the wallet for which the session key is enabled. + event ERC20SKV_SessionKeyEnabled(address sessionKey, address wallet); + + /// @notice Emitted when a session key is disabled for a wallet. + /// @param sessionKey The address of the session key. + /// @param wallet The address of the wallet for which the session key is disabled. + event ERC20SKV_SessionKeyDisabled(address sessionKey, address wallet); + + /// @notice Emitted when a session key is paused for a wallet. + /// @param sessionKey The address of the session key. + /// @param wallet The address of the wallet for which the session key is paused. + event ERC20SKV_SessionKeyPaused(address sessionKey, address wallet); + + /// @notice Emitted when a session key is unpaused for a wallet. + /// @param sessionKey The address of the session key. + /// @param wallet The address of the wallet for which the session key is unpaused. + event ERC20SKV_SessionKeyUnpaused(address sessionKey, address wallet); + + /// @notice Struct representing the data associated with a session key. + struct MultiTokenSessionData { + address[] tokens; + bytes4 funcSelector; // The function selector for the allowed operation (e.g., transfer, transferFrom). + uint256 spendingLimit; // The maximum amount that can be spent with this session key. + uint48 validAfter; // The timestamp after which the session key is valid. + uint48 validUntil; // The timestamp until which the session key is valid. + bool live; // Flag indicating whether the session key is paused or not. + } + + /// @notice Enables a new session key for the caller's wallet. + /// @param _sessionData The encoded session data containing the session key address, token address, interface ID, function selector, spending limit, valid after timestamp, and valid until timestamp. + function enableSessionKey(bytes calldata _sessionData) external; + + /// @notice Disables a session key for the caller's wallet. + /// @param _session The address of the session key to disable. + function disableSessionKey(address _session) external; + + /// @notice Rotates a session key by disabling the old one and enabling a new one. + /// @param _oldSessionKey The address of the old session key to disable. + /// @param _newSessionData The encoded session data for the new session key. + function rotateSessionKey( + address _oldSessionKey, + bytes calldata _newSessionData + ) external; + + /// @notice Toggles the pause state of a session key for the caller's wallet. + /// @param _sessionKey The address of the session key to toggle the pause state for. + function toggleSessionKeyPause(address _sessionKey) external; + + /// @notice Checks if a session key is paused for the caller's wallet. + /// @param _sessionKey The address of the session key to check. + /// @return paused True if the session key is paused, false otherwise. + function isSessionKeyLive( + address _sessionKey + ) external view returns (bool paused); + + /// @notice Validates the parameters of a session key for a given user operation. + /// @param _sessionKey The address of the session key. + /// @param userOp The packed user operation containing the call data. + /// @return True if the session key parameters are valid for the user operation, false otherwise. + function validateSessionKeyParams( + address _sessionKey, + PackedUserOperation calldata userOp + ) external returns (bool); + + /// @notice Returns the list of associated session keys for the caller's wallet. + /// @return keys The array of associated session key addresses. + function getAssociatedSessionKeys() + external + view + returns (address[] memory keys); + + /// @notice Returns the session data for a given session key and the caller's wallet. + /// @param _sessionKey The address of the session key. + /// @return data The session data struct. + function getSessionKeyData( + address _sessionKey + ) external view returns (MultiTokenSessionData memory data); + + /// @notice Validates a user operation using a session key. + /// @param userOp The packed user operation. + /// @param userOpHash The hash of the user operation. + /// @return validationData The validation data containing the expiration time and valid after timestamp of the session key. + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external returns (uint256 validationData); + + /// @notice Checks if the module type matches the validator module type. + /// @param moduleTypeId The module type ID to check. + /// @return True if the module type matches the validator module type, false otherwise. + function isModuleType(uint256 moduleTypeId) external pure returns (bool); + + /// @notice Placeholder function for module installation. + /// @param data The data to pass during installation. + function onInstall(bytes calldata data) external; + + /// @notice Placeholder function for module uninstallation. + /// @param data The data to pass during uninstallation. + function onUninstall(bytes calldata data) external; + + /// @notice Reverts with a "NotImplemented" error. + /// @param sender The address of the sender. + /// @param hash The hash of the message. + /// @param data The data associated with the message. + /// @return A bytes4 value indicating the function is not implemented. + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata data + ) external view returns (bytes4); + + /// @notice Reverts with a "NotImplemented" error. + /// @param smartAccount The address of the smart account. + /// @return True if the smart account is initialized, false otherwise. + function isInitialized(address smartAccount) external view returns (bool); +} diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol new file mode 100644 index 00000000..c06caa7e --- /dev/null +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAggregatorV3Interface} from "../../interfaces/IAggregatorV3Interface.sol"; +import {IERC20SessionKeyValidator} from "../../interfaces/IERC20SessionKeyValidator.sol"; +import {ArrayLib} from "../../libraries/ArrayLib.sol"; +import {ECDSA} from "solady/src/utils/ECDSA.sol"; +import {IERC20} from "../../interfaces/IERC20.sol"; +import {PackedUserOperation} from "../../../../account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import "../../erc7579-ref-impl/interfaces/IERC7579Account.sol"; +import {MODULE_TYPE_VALIDATOR, VALIDATION_FAILED, VALIDATION_SUCCESS} from "../../erc7579-ref-impl/interfaces/IERC7579Module.sol"; +import "../../../../account-abstraction/contracts/core/Helpers.sol"; +import "../../erc7579-ref-impl/libs/ModeLib.sol"; +import "../../erc7579-ref-impl/libs/ExecutionLib.sol"; +import {IMultiTokenSessionKeyValidator} from "../../interfaces/IMultiTokenSessionKeyValidator.sol"; +import {ArrayLib} from "../../libraries/ArrayLib.sol"; + +contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { + using ModeLib for ModeCode; + using ExecutionLib for bytes; + using ArrayLib for address[]; + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* CONSTANTS */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + string constant NAME = "MultiTokenSessionKeyValidator"; + string constant VERSION = "1.0.0"; + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* ERRORS */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + error ERC20SKV_ModuleAlreadyInstalled(); + error ERC20SKV_ModuleNotInstalled(); + error ERC20SKV_InvalidSessionKey(); + error ERC20SKV_InvalidToken(); + error ERC20SKV_InvalidFunctionSelector(); + error ERC20SKV_InvalidSpendingLimit(); + error ERC20SKV_InvalidValidAfter(uint48 validAfter); + error ERC20SKV_InvalidValidUntil(uint48 validUntil); + error ERC20SKV_SessionKeyAlreadyExists(address sessionKey); + error ERC20SKV_SessionKeyDoesNotExist(address session); + error ERC20SKV_SessionPaused(address sessionKey); + error NotImplemented(); + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* MAPPINGS */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + mapping(address => bool) public initialized; + mapping(address => mapping(address => MultiTokenSessionData)) public multiTokenSessionData; + mapping(address wallet => address[] assocSessionKeys) public walletSessionKeys; + // tokenAddress to spentAmount + mapping(address => mapping(address => uint256)) spentAmounts; + + IAggregatorV3Interface internal priceFeed; + + constructor(address _priceFeed) { + priceFeed = IAggregatorV3Interface(_priceFeed); + } + + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* PUBLIC/EXTERNAL */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + // @inheritdoc IMultiTokenSessionKeyValidator + function enableSessionKey(bytes calldata _sessionData) public { + address sessionKey = address(bytes20(_sessionData[0:20])); + if (sessionKey == address(0)) revert ERC20SKV_InvalidSessionKey(); + if ( + multiTokenSessionData[sessionKey][msg.sender].validUntil != 0 && + ArrayLib._contains(getAssociatedSessionKeys(), sessionKey) + ) revert ERC20SKV_SessionKeyAlreadyExists(sessionKey); + + uint256 numTokens = uint256(uint8(_sessionData[20])); + address[] memory tokens = new address[](numTokens); + for (uint256 i = 0; i < numTokens; i++) { + tokens[i] = address(bytes20(_sessionData[21 + i * 20:41 + i * 20])); + if (tokens[i] == address(0)) revert ERC20SKV_InvalidToken(); + } + + bytes4 funcSelector = bytes4(_sessionData[21 + numTokens * 20:25 + numTokens * 20]); + if (funcSelector == bytes4(0)) revert ERC20SKV_InvalidFunctionSelector(); + + uint256 cumulativeSpendingLimitUSD = uint256(bytes32(_sessionData[25 + numTokens * 20:57 + numTokens * 20])); + if (cumulativeSpendingLimitUSD == 0) revert ERC20SKV_InvalidSpendingLimit(); + + uint48 validAfter = uint48(bytes6(_sessionData[57 + numTokens * 20:63 + numTokens * 20])); + if (validAfter == 0) revert ERC20SKV_InvalidValidAfter(validAfter); + + uint48 validUntil = uint48(bytes6(_sessionData[63 + numTokens * 20:69 + numTokens * 20])); + if (validUntil == 0) revert ERC20SKV_InvalidValidUntil(validUntil); + + multiTokenSessionData[sessionKey][msg.sender] = MultiTokenSessionData( + tokens, + funcSelector, + cumulativeSpendingLimitUSD, + validAfter, + validUntil, + true + ); + walletSessionKeys[msg.sender].push(sessionKey); + } + + // @inheritdoc IERC20SessionKeyValidator + function disableSessionKey(address _session) public { + if (multiTokenSessionData[_session][msg.sender].validUntil == 0) + revert ERC20SKV_SessionKeyDoesNotExist(_session); + delete multiTokenSessionData[_session][msg.sender]; + walletSessionKeys[msg.sender] = ArrayLib._removeElement( + getAssociatedSessionKeys(), + _session + ); + emit ERC20SKV_SessionKeyDisabled(_session, msg.sender); + } + + // @inheritdoc IERC20SessionKeyValidator + function rotateSessionKey( + address _oldSessionKey, + bytes calldata _newSessionData + ) external { + disableSessionKey(_oldSessionKey); + enableSessionKey(_newSessionData); + } + + // @inheritdoc IERC20SessionKeyValidator + function toggleSessionKeyPause(address _sessionKey) external { + MultiTokenSessionData storage sd = multiTokenSessionData[_sessionKey][msg.sender]; + if (sd.validUntil == 0) + revert ERC20SKV_SessionKeyDoesNotExist(_sessionKey); + if (sd.live) { + sd.live = false; + emit ERC20SKV_SessionKeyPaused(_sessionKey, msg.sender); + } else { + sd.live = true; + emit ERC20SKV_SessionKeyUnpaused(_sessionKey, msg.sender); + } + } + + + + function isSessionKeyLive(address sessionKey) public view returns (bool) { + MultiTokenSessionData storage data = multiTokenSessionData[sessionKey][msg.sender]; + return (data.validAfter <= block.timestamp && data.validUntil >= block.timestamp); + } + + + function validateSessionKeyParams( + address _sessionKey, + PackedUserOperation calldata userOp + ) public view returns (bool) { + MultiTokenSessionData storage sd = multiTokenSessionData[_sessionKey][msg.sender]; + + // Check if the session key is live + if (!isSessionKeyLive(_sessionKey)) { + return false; + } + + bytes calldata callData = userOp.callData; + bytes4 sel = bytes4(callData[:4]); + + // Validate function selector (e.g., execute function) + if (sel == IERC7579Account.execute.selector) { + ModeCode mode = ModeCode.wrap(bytes32(callData[4:36])); + (CallType calltype, , , ) = ModeLib.decode(mode); + + if (calltype == CALLTYPE_SINGLE) { + return _validateSingleCall(_sessionKey, sd, callData); + } + + if (calltype == CALLTYPE_BATCH) { + return _validateBatchCall(_sessionKey, sd, callData); + } + } + return false; + } + + function _validateSingleCall( + address _sessionKey, + MultiTokenSessionData storage sd, + bytes calldata callData + ) internal view returns (bool) { + (, , bytes calldata execData) = ExecutionLib.decodeSingle(callData[100:]); + + (bytes4 selector, address target, , uint256 amount) = _digest(execData); + + // Ensure the target token is in the allowed tokens + if (!ArrayLib._contains(sd.tokens, target)) { + return false; + } + + // Ensure the function selector matches + if (selector != sd.funcSelector) { + return false; + } + + // Ensure the amount doesn't exceed the spending limit + if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { + return false; + } + + return true; + } + + function _validateBatchCall( + address _sessionKey, + MultiTokenSessionData storage sd, + bytes calldata callData + ) internal view returns (bool) { + Execution[] calldata execs = ExecutionLib.decodeBatch(callData[100:]); + + for (uint256 i; i < execs.length; i++) { + (bytes4 selector, address target, , uint256 amount) = _digest(execs[i].callData); + + // Ensure the target token is in the allowed tokens + if (!ArrayLib._contains(sd.tokens, target)) { + return false; + } + + // Ensure the function selector matches + if (selector != sd.funcSelector) { + return false; + } + + // Ensure the amount doesn't exceed the spending limit + if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { + return false; + } + } + + return true; + } + + // @inheritdoc IMultiTokenSessionKeyValidator + function getAssociatedSessionKeys() public view returns (address[] memory) { + return walletSessionKeys[msg.sender]; + } + + // @inheritdoc IMultiTokenSessionKeyValidator + function getSessionKeyData( + address _sessionKey + ) public view returns (MultiTokenSessionData memory) { + return multiTokenSessionData[_sessionKey][msg.sender]; + } + + // @inheritdoc IERC20SessionKeyValidator + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external override returns (uint256) { + // Recover the session key signer from the signature + address sessionKeySigner = ECDSA.recover( + ECDSA.toEthSignedMessageHash(userOpHash), + userOp.signature + ); + + // Validate the session key parameters + if (!validateSessionKeyParams(sessionKeySigner, userOp)) { + return VALIDATION_FAILED; + } + + // Fetch session data to return validation data + MultiTokenSessionData storage sd = multiTokenSessionData[sessionKeySigner][msg.sender]; + + // Return validation data with expiration info + return _packValidationData(false, sd.validUntil, sd.validAfter); + } + + + // @inheritdoc IERC20SessionKeyValidator + function isModuleType( + uint256 moduleTypeId + ) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + // @inheritdoc IERC20SessionKeyValidator + function onInstall(bytes calldata data) external override { + if (initialized[msg.sender] == true) + revert ERC20SKV_ModuleAlreadyInstalled(); + initialized[msg.sender] = true; + emit ERC20SKV_ModuleInstalled(msg.sender); + } + + // @inheritdoc IERC20SessionKeyValidator + function onUninstall(bytes calldata data) external override { + if (initialized[msg.sender] == false) + revert ERC20SKV_ModuleNotInstalled(); + address[] memory sessionKeys = getAssociatedSessionKeys(); + uint256 sessionKeysLength = sessionKeys.length; + for (uint256 i; i < sessionKeysLength; i++) { + delete multiTokenSessionData[sessionKeys[i]][msg.sender]; + } + delete walletSessionKeys[msg.sender]; + initialized[msg.sender] = false; + emit ERC20SKV_ModuleUninstalled(msg.sender); + } + + // @inheritdoc IERC20SessionKeyValidator + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata data + ) external view returns (bytes4) { + revert NotImplemented(); + } + + // @inheritdoc IERC20SessionKeyValidator + function isInitialized(address smartAccount) external view returns (bool) { + return initialized[smartAccount]; + } + + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* INTERNAL */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + function _digest( + bytes calldata _data + ) + internal + pure + returns (bytes4 selector, address from, address to, uint256 amount) + { + selector = bytes4(_data[0:4]); + if ( + selector == IERC20.approve.selector || + selector == IERC20.transfer.selector + ) { + to = address(bytes20(_data[16:36])); + amount = uint256(bytes32(_data[36:68])); + return (selector, address(0), to, amount); + } else if (selector == IERC20.transferFrom.selector) { + from = address(bytes20(_data[16:36])); + to = address(bytes20(_data[48:68])); + amount = uint256(bytes32(_data[68:100])); + return (selector, from, to, amount); + } else { + return (bytes4(0), address(0), address(0), 0); + } + } + + + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + /* VIEW */ + /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + + function getTokenPriceUSD(address token) internal view returns (uint256) { + (, int256 price, , , ) = priceFeed.latestRoundData(); + require(price > 0, "Invalid price from oracle"); + return uint256(price); + } + + function getTokenDecimals(address token) internal view returns (uint8) { + return IERC20(token).decimals(); + } + + function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) public view returns (bool) { + MultiTokenSessionData storage data = multiTokenSessionData[sessionKey][user]; + uint256 tokenPriceUSD = getTokenPriceUSD(token); + uint8 tokenDecimals = getTokenDecimals(token); + uint256 amountInUSD = (amount * tokenPriceUSD) / (10 ** tokenDecimals); + uint256 totalSpentUSD = 0; + + for (uint256 i = 0; i < data.tokens.length; i++) { + address currentToken = data.tokens[i]; + uint256 spentAmount = spentAmounts[sessionKey][currentToken]; + uint256 currentTokenPriceUSD = getTokenPriceUSD(currentToken); + uint8 currentTokenDecimals = getTokenDecimals(currentToken); + totalSpentUSD += (spentAmount * currentTokenPriceUSD) / (10 ** currentTokenDecimals); + } + + return (totalSpentUSD + amountInUSD) <= data.spendingLimit; + } + +} \ No newline at end of file From 716244f1654444ef57e9908d25e6c1cd7ae704d0 Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 10:12:44 +0530 Subject: [PATCH 2/6] feat: PRO-2631 pricefeeds for multi-tokens in sessionkey-validator --- .../IMultiTokenSessionKeyValidator.sol | 12 +- .../MultiTokenSessionKeyValidator.sol | 155 +++++++++++------- 2 files changed, 100 insertions(+), 67 deletions(-) diff --git a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol index 5a67c524..dfb38ee4 100644 --- a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol @@ -11,31 +11,31 @@ import {PackedUserOperation} from "../../../account-abstraction/contracts/interf interface IMultiTokenSessionKeyValidator is IValidator { /// @notice Emitted when the MultiToken Session Key Validator module is installed for a wallet. /// @param wallet The address of the wallet for which the module is installed. - event ERC20SKV_ModuleInstalled(address wallet); + event MTSKV_ModuleInstalled(address wallet); /// @notice Emitted when the MultiToken Session Key Validator module is uninstalled from a wallet. /// @param wallet The address of the wallet from which the module is uninstalled. - event ERC20SKV_ModuleUninstalled(address wallet); + event MTSKV_ModuleUninstalled(address wallet); /// @notice Emitted when a new session key is enabled for a wallet. /// @param sessionKey The address of the session key. /// @param wallet The address of the wallet for which the session key is enabled. - event ERC20SKV_SessionKeyEnabled(address sessionKey, address wallet); + event MTSKV_SessionKeyEnabled(address sessionKey, address wallet); /// @notice Emitted when a session key is disabled for a wallet. /// @param sessionKey The address of the session key. /// @param wallet The address of the wallet for which the session key is disabled. - event ERC20SKV_SessionKeyDisabled(address sessionKey, address wallet); + event MTSKV_SessionKeyDisabled(address sessionKey, address wallet); /// @notice Emitted when a session key is paused for a wallet. /// @param sessionKey The address of the session key. /// @param wallet The address of the wallet for which the session key is paused. - event ERC20SKV_SessionKeyPaused(address sessionKey, address wallet); + event MTSKV_SessionKeyPaused(address sessionKey, address wallet); /// @notice Emitted when a session key is unpaused for a wallet. /// @param sessionKey The address of the session key. /// @param wallet The address of the wallet for which the session key is unpaused. - event ERC20SKV_SessionKeyUnpaused(address sessionKey, address wallet); + event MTSKV_SessionKeyUnpaused(address sessionKey, address wallet); /// @notice Struct representing the data associated with a session key. struct MultiTokenSessionData { diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol index c06caa7e..54eb2976 100644 --- a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -14,11 +14,15 @@ import "../../erc7579-ref-impl/libs/ModeLib.sol"; import "../../erc7579-ref-impl/libs/ExecutionLib.sol"; import {IMultiTokenSessionKeyValidator} from "../../interfaces/IMultiTokenSessionKeyValidator.sol"; import {ArrayLib} from "../../libraries/ArrayLib.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { + +contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownable { using ModeLib for ModeCode; using ExecutionLib for bytes; using ArrayLib for address[]; + using EnumerableSet for EnumerableSet.AddressSet; /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* CONSTANTS */ @@ -31,17 +35,17 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { /* ERRORS */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ - error ERC20SKV_ModuleAlreadyInstalled(); - error ERC20SKV_ModuleNotInstalled(); - error ERC20SKV_InvalidSessionKey(); - error ERC20SKV_InvalidToken(); - error ERC20SKV_InvalidFunctionSelector(); - error ERC20SKV_InvalidSpendingLimit(); - error ERC20SKV_InvalidValidAfter(uint48 validAfter); - error ERC20SKV_InvalidValidUntil(uint48 validUntil); - error ERC20SKV_SessionKeyAlreadyExists(address sessionKey); - error ERC20SKV_SessionKeyDoesNotExist(address session); - error ERC20SKV_SessionPaused(address sessionKey); + error MTSKV_ModuleAlreadyInstalled(); + error MTSKV_ModuleNotInstalled(); + error MTSKV_InvalidSessionKey(); + error MTSKV_InvalidToken(); + error MTSKV_InvalidFunctionSelector(); + error MTSKV_InvalidSpendingLimit(); + error MTSKV_InvalidValidAfter(uint48 validAfter); + error MTSKV_InvalidValidUntil(uint48 validUntil); + error MTSKV_SessionKeyAlreadyExists(address sessionKey); + error MTSKV_SessionKeyDoesNotExist(address session); + error MTSKV_SessionPaused(address sessionKey); error NotImplemented(); /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ @@ -49,17 +53,53 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ mapping(address => bool) public initialized; + mapping(address => mapping(address => MultiTokenSessionData)) public multiTokenSessionData; + mapping(address wallet => address[] assocSessionKeys) public walletSessionKeys; - // tokenAddress to spentAmount - mapping(address => mapping(address => uint256)) spentAmounts; - IAggregatorV3Interface internal priceFeed; + // session-key to tokenAddress to spentAmount + mapping(address => mapping(address => uint256)) public spentAmounts; - constructor(address _priceFeed) { - priceFeed = IAggregatorV3Interface(_priceFeed); + EnumerableSet.AddressSet private allowedTokens; + + mapping(address token => IAggregatorV3Interface) internal priceFeeds; + + constructor(address[] memory tokens, address[] memory _priceFeeds) Ownable(msg.sender) { + _addAllowedTokens(tokens, _priceFeeds); } + function addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { + _addAllowedTokens(_tokens, _priceFeeds); + } + + function _addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) internal { + for (uint256 i = 0; i < _tokens.length; i++) { + allowedTokens.add(_tokens[i]); + priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); + } + } + + function removeAllowedTokens(address[] memory _tokens) external onlyOwner { + _removeAllowedTokens(_tokens); + } + + function _removeAllowedTokens(address[] memory _tokens) internal { + for (uint256 i = 0; i < _tokens.length; i++) { + allowedTokens.remove(_tokens[i]); + delete priceFeeds[_tokens[i]]; + } + } + + function updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { + _updatePriceFeeds(_tokens, _priceFeeds); + } + + function _updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) internal { + for (uint256 i = 0; i < _tokens.length; i++) { + priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); + } + } /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* PUBLIC/EXTERNAL */ @@ -68,30 +108,30 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { // @inheritdoc IMultiTokenSessionKeyValidator function enableSessionKey(bytes calldata _sessionData) public { address sessionKey = address(bytes20(_sessionData[0:20])); - if (sessionKey == address(0)) revert ERC20SKV_InvalidSessionKey(); + if (sessionKey == address(0)) revert MTSKV_InvalidSessionKey(); if ( multiTokenSessionData[sessionKey][msg.sender].validUntil != 0 && ArrayLib._contains(getAssociatedSessionKeys(), sessionKey) - ) revert ERC20SKV_SessionKeyAlreadyExists(sessionKey); + ) revert MTSKV_SessionKeyAlreadyExists(sessionKey); uint256 numTokens = uint256(uint8(_sessionData[20])); address[] memory tokens = new address[](numTokens); for (uint256 i = 0; i < numTokens; i++) { tokens[i] = address(bytes20(_sessionData[21 + i * 20:41 + i * 20])); - if (tokens[i] == address(0)) revert ERC20SKV_InvalidToken(); + if (tokens[i] == address(0)) revert MTSKV_InvalidToken(); } bytes4 funcSelector = bytes4(_sessionData[21 + numTokens * 20:25 + numTokens * 20]); - if (funcSelector == bytes4(0)) revert ERC20SKV_InvalidFunctionSelector(); + if (funcSelector == bytes4(0)) revert MTSKV_InvalidFunctionSelector(); uint256 cumulativeSpendingLimitUSD = uint256(bytes32(_sessionData[25 + numTokens * 20:57 + numTokens * 20])); - if (cumulativeSpendingLimitUSD == 0) revert ERC20SKV_InvalidSpendingLimit(); + if (cumulativeSpendingLimitUSD == 0) revert MTSKV_InvalidSpendingLimit(); uint48 validAfter = uint48(bytes6(_sessionData[57 + numTokens * 20:63 + numTokens * 20])); - if (validAfter == 0) revert ERC20SKV_InvalidValidAfter(validAfter); + if (validAfter == 0) revert MTSKV_InvalidValidAfter(validAfter); uint48 validUntil = uint48(bytes6(_sessionData[63 + numTokens * 20:69 + numTokens * 20])); - if (validUntil == 0) revert ERC20SKV_InvalidValidUntil(validUntil); + if (validUntil == 0) revert MTSKV_InvalidValidUntil(validUntil); multiTokenSessionData[sessionKey][msg.sender] = MultiTokenSessionData( tokens, @@ -102,18 +142,19 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { true ); walletSessionKeys[msg.sender].push(sessionKey); + emit MTSKV_SessionKeyEnabled(sessionKey, msg.sender); } // @inheritdoc IERC20SessionKeyValidator function disableSessionKey(address _session) public { if (multiTokenSessionData[_session][msg.sender].validUntil == 0) - revert ERC20SKV_SessionKeyDoesNotExist(_session); + revert MTSKV_SessionKeyDoesNotExist(_session); delete multiTokenSessionData[_session][msg.sender]; walletSessionKeys[msg.sender] = ArrayLib._removeElement( getAssociatedSessionKeys(), _session ); - emit ERC20SKV_SessionKeyDisabled(_session, msg.sender); + emit MTSKV_SessionKeyDisabled(_session, msg.sender); } // @inheritdoc IERC20SessionKeyValidator @@ -129,27 +170,25 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { function toggleSessionKeyPause(address _sessionKey) external { MultiTokenSessionData storage sd = multiTokenSessionData[_sessionKey][msg.sender]; if (sd.validUntil == 0) - revert ERC20SKV_SessionKeyDoesNotExist(_sessionKey); + revert MTSKV_SessionKeyDoesNotExist(_sessionKey); if (sd.live) { sd.live = false; - emit ERC20SKV_SessionKeyPaused(_sessionKey, msg.sender); + emit MTSKV_SessionKeyPaused(_sessionKey, msg.sender); } else { sd.live = true; - emit ERC20SKV_SessionKeyUnpaused(_sessionKey, msg.sender); + emit MTSKV_SessionKeyUnpaused(_sessionKey, msg.sender); } } - - - function isSessionKeyLive(address sessionKey) public view returns (bool) { - MultiTokenSessionData storage data = multiTokenSessionData[sessionKey][msg.sender]; + function isSessionKeyLive(address _sessionKey) public view returns (bool) { + MultiTokenSessionData storage data = multiTokenSessionData[_sessionKey][msg.sender]; return (data.validAfter <= block.timestamp && data.validUntil >= block.timestamp); } function validateSessionKeyParams( address _sessionKey, - PackedUserOperation calldata userOp + PackedUserOperation calldata _userOp ) public view returns (bool) { MultiTokenSessionData storage sd = multiTokenSessionData[_sessionKey][msg.sender]; @@ -158,7 +197,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { return false; } - bytes calldata callData = userOp.callData; + bytes calldata callData = _userOp.callData; bytes4 sel = bytes4(callData[:4]); // Validate function selector (e.g., execute function) @@ -179,24 +218,21 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { function _validateSingleCall( address _sessionKey, - MultiTokenSessionData storage sd, - bytes calldata callData + MultiTokenSessionData storage _mtsd, + bytes calldata _callData ) internal view returns (bool) { - (, , bytes calldata execData) = ExecutionLib.decodeSingle(callData[100:]); + (, , bytes calldata execData) = ExecutionLib.decodeSingle(_callData[100:]); (bytes4 selector, address target, , uint256 amount) = _digest(execData); - // Ensure the target token is in the allowed tokens - if (!ArrayLib._contains(sd.tokens, target)) { + if (!ArrayLib._contains(_mtsd.tokens, target)) { return false; } - // Ensure the function selector matches - if (selector != sd.funcSelector) { + if (selector != _mtsd.funcSelector) { return false; } - // Ensure the amount doesn't exceed the spending limit if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { return false; } @@ -206,25 +242,22 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { function _validateBatchCall( address _sessionKey, - MultiTokenSessionData storage sd, - bytes calldata callData + MultiTokenSessionData storage _mtsd, + bytes calldata _callData ) internal view returns (bool) { - Execution[] calldata execs = ExecutionLib.decodeBatch(callData[100:]); + Execution[] calldata execs = ExecutionLib.decodeBatch(_callData[100:]); for (uint256 i; i < execs.length; i++) { (bytes4 selector, address target, , uint256 amount) = _digest(execs[i].callData); - // Ensure the target token is in the allowed tokens - if (!ArrayLib._contains(sd.tokens, target)) { + if (!ArrayLib._contains(_mtsd.tokens, target)) { return false; } - // Ensure the function selector matches - if (selector != sd.funcSelector) { + if (selector != _mtsd.funcSelector) { return false; } - // Ensure the amount doesn't exceed the spending limit if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { return false; } @@ -277,17 +310,17 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { } // @inheritdoc IERC20SessionKeyValidator - function onInstall(bytes calldata data) external override { + function onInstall(bytes calldata) external override { if (initialized[msg.sender] == true) - revert ERC20SKV_ModuleAlreadyInstalled(); + revert MTSKV_ModuleAlreadyInstalled(); initialized[msg.sender] = true; - emit ERC20SKV_ModuleInstalled(msg.sender); + emit MTSKV_ModuleInstalled(msg.sender); } // @inheritdoc IERC20SessionKeyValidator - function onUninstall(bytes calldata data) external override { + function onUninstall(bytes calldata) external override { if (initialized[msg.sender] == false) - revert ERC20SKV_ModuleNotInstalled(); + revert MTSKV_ModuleNotInstalled(); address[] memory sessionKeys = getAssociatedSessionKeys(); uint256 sessionKeysLength = sessionKeys.length; for (uint256 i; i < sessionKeysLength; i++) { @@ -295,14 +328,14 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { } delete walletSessionKeys[msg.sender]; initialized[msg.sender] = false; - emit ERC20SKV_ModuleUninstalled(msg.sender); + emit MTSKV_ModuleUninstalled(msg.sender); } // @inheritdoc IERC20SessionKeyValidator function isValidSignatureWithSender( - address sender, - bytes32 hash, - bytes calldata data + address, + bytes32, + bytes memory ) external view returns (bytes4) { revert NotImplemented(); } @@ -348,7 +381,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator { /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ function getTokenPriceUSD(address token) internal view returns (uint256) { - (, int256 price, , , ) = priceFeed.latestRoundData(); + (, int256 price, , , ) = priceFeeds[token].latestRoundData(); require(price > 0, "Invalid price from oracle"); return uint256(price); } From da365cd7cd287bf134f7c7bc9e361316175e5afe Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 14:35:54 +0530 Subject: [PATCH 3/6] feat: PRO-2631 StalePrice check for Oracle query in MultiTokenSessionKeyValidator --- .../IMultiTokenSessionKeyValidator.sol | 2 +- .../MultiTokenSessionKeyValidator.sol | 68 +++++++++++-------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol index dfb38ee4..bd66e8ad 100644 --- a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol @@ -41,7 +41,7 @@ interface IMultiTokenSessionKeyValidator is IValidator { struct MultiTokenSessionData { address[] tokens; bytes4 funcSelector; // The function selector for the allowed operation (e.g., transfer, transferFrom). - uint256 spendingLimit; // The maximum amount that can be spent with this session key. + uint256 cumulativeSpendingLimitInUsd; // The total spending limit in USD. uint48 validAfter; // The timestamp after which the session key is valid. uint48 validUntil; // The timestamp until which the session key is valid. bool live; // Flag indicating whether the session key is paused or not. diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol index 54eb2976..436c306f 100644 --- a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -47,6 +47,9 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl error MTSKV_SessionKeyDoesNotExist(address session); error MTSKV_SessionPaused(address sessionKey); error NotImplemented(); + error MTSKV_InvalidTokenPrice(address token); + error MTSKV_InvalidStalenessThreshold(); + error MTSKV_StaleTokenPrice(address token); /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* MAPPINGS */ @@ -54,19 +57,25 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl mapping(address => bool) public initialized; - mapping(address => mapping(address => MultiTokenSessionData)) public multiTokenSessionData; + mapping(address sessionKey => mapping(address wallet => MultiTokenSessionData)) public multiTokenSessionData; mapping(address wallet => address[] assocSessionKeys) public walletSessionKeys; - // session-key to tokenAddress to spentAmount - mapping(address => mapping(address => uint256)) public spentAmounts; - EnumerableSet.AddressSet private allowedTokens; mapping(address token => IAggregatorV3Interface) internal priceFeeds; - constructor(address[] memory tokens, address[] memory _priceFeeds) Ownable(msg.sender) { + mapping(address sessionKey => uint256) public totalSpentInUsd; + + uint256 public stalenessThresholdInSeconds; + + constructor(address[] memory tokens, address[] memory _priceFeeds, uint256 _stalenessThresholdInSeconds) Ownable(msg.sender) { _addAllowedTokens(tokens, _priceFeeds); + + if(_stalenessThresholdInSeconds == 0) { + revert MTSKV_InvalidStalenessThreshold(); + } + stalenessThresholdInSeconds = _stalenessThresholdInSeconds; } function addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { @@ -124,8 +133,8 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl bytes4 funcSelector = bytes4(_sessionData[21 + numTokens * 20:25 + numTokens * 20]); if (funcSelector == bytes4(0)) revert MTSKV_InvalidFunctionSelector(); - uint256 cumulativeSpendingLimitUSD = uint256(bytes32(_sessionData[25 + numTokens * 20:57 + numTokens * 20])); - if (cumulativeSpendingLimitUSD == 0) revert MTSKV_InvalidSpendingLimit(); + uint256 cumulativeSpendingLimitInUsd = uint256(bytes32(_sessionData[25 + numTokens * 20:57 + numTokens * 20])); + if (cumulativeSpendingLimitInUsd == 0) revert MTSKV_InvalidSpendingLimit(); uint48 validAfter = uint48(bytes6(_sessionData[57 + numTokens * 20:63 + numTokens * 20])); if (validAfter == 0) revert MTSKV_InvalidValidAfter(validAfter); @@ -136,7 +145,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl multiTokenSessionData[sessionKey][msg.sender] = MultiTokenSessionData( tokens, funcSelector, - cumulativeSpendingLimitUSD, + cumulativeSpendingLimitInUsd, validAfter, validUntil, true @@ -380,32 +389,31 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl /* VIEW */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ - function getTokenPriceUSD(address token) internal view returns (uint256) { - (, int256 price, , , ) = priceFeeds[token].latestRoundData(); - require(price > 0, "Invalid price from oracle"); + function getTokenPriceInUsd(address token) internal view returns (uint256) { + + (, int256 price, ,uint256 updatedAt, ) = priceFeeds[token].latestRoundData(); + + if(price == 0) { + revert MTSKV_InvalidTokenPrice(token); + } + + if(block.timestamp - updatedAt >= stalenessThresholdInSeconds) { + revert MTSKV_StaleTokenPrice(token); + } + return uint256(price); } - function getTokenDecimals(address token) internal view returns (uint8) { - return IERC20(token).decimals(); - } + function estimateTotalSpentAmountInUsd(address sessionKey, address token, uint256 amount) public view returns (uint256) { + uint256 currentTokenPriceUSD = getTokenPriceInUsd(token); + uint8 currentTokenDecimals = IERC20(token).decimals(); + uint256 amountInUsd = (amount * currentTokenPriceUSD) / (10 ** currentTokenDecimals); + return amountInUsd + totalSpentInUsd[sessionKey]; + } function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) public view returns (bool) { - MultiTokenSessionData storage data = multiTokenSessionData[sessionKey][user]; - uint256 tokenPriceUSD = getTokenPriceUSD(token); - uint8 tokenDecimals = getTokenDecimals(token); - uint256 amountInUSD = (amount * tokenPriceUSD) / (10 ** tokenDecimals); - uint256 totalSpentUSD = 0; - - for (uint256 i = 0; i < data.tokens.length; i++) { - address currentToken = data.tokens[i]; - uint256 spentAmount = spentAmounts[sessionKey][currentToken]; - uint256 currentTokenPriceUSD = getTokenPriceUSD(currentToken); - uint8 currentTokenDecimals = getTokenDecimals(currentToken); - totalSpentUSD += (spentAmount * currentTokenPriceUSD) / (10 ** currentTokenDecimals); - } - - return (totalSpentUSD + amountInUSD) <= data.spendingLimit; + MultiTokenSessionData memory data = multiTokenSessionData[sessionKey][user]; + return estimateTotalSpentAmountInUsd(sessionKey, token, amount) <= data.cumulativeSpendingLimitInUsd; } -} \ No newline at end of file +} From e4c351e09acef7653e36f133ced05c7f84f05c16 Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 15:11:14 +0530 Subject: [PATCH 4/6] feat: PRO-2631 natspec and interface updates for MultiTokenSessionKeyValidator --- .../IMultiTokenSessionKeyValidator.sol | 10 ++++ .../MultiTokenSessionKeyValidator.sol | 55 ++++++++----------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol index bd66e8ad..f1e8b1b6 100644 --- a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol @@ -134,4 +134,14 @@ interface IMultiTokenSessionKeyValidator is IValidator { /// @param smartAccount The address of the smart account. /// @return True if the smart account is initialized, false otherwise. function isInitialized(address smartAccount) external view returns (bool); + + function addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) external; + + function removeAllowedTokens(address[] memory _tokens) external; + + function updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) external; + + function estimateTotalSpentAmountInUsd(address sessionKey, address token, uint256 amount) external view returns (uint256); + + function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) external view returns (bool); } diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol index 436c306f..f30ac50f 100644 --- a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -55,6 +55,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl /* MAPPINGS */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ + mapping(address => bool) public initialized; mapping(address sessionKey => mapping(address wallet => MultiTokenSessionData)) public multiTokenSessionData; @@ -78,38 +79,6 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl stalenessThresholdInSeconds = _stalenessThresholdInSeconds; } - function addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { - _addAllowedTokens(_tokens, _priceFeeds); - } - - function _addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) internal { - for (uint256 i = 0; i < _tokens.length; i++) { - allowedTokens.add(_tokens[i]); - priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); - } - } - - function removeAllowedTokens(address[] memory _tokens) external onlyOwner { - _removeAllowedTokens(_tokens); - } - - function _removeAllowedTokens(address[] memory _tokens) internal { - for (uint256 i = 0; i < _tokens.length; i++) { - allowedTokens.remove(_tokens[i]); - delete priceFeeds[_tokens[i]]; - } - } - - function updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { - _updatePriceFeeds(_tokens, _priceFeeds); - } - - function _updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) internal { - for (uint256 i = 0; i < _tokens.length; i++) { - priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); - } - } - /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* PUBLIC/EXTERNAL */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ @@ -354,6 +323,22 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl return initialized[smartAccount]; } + function addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { + _addAllowedTokens(_tokens, _priceFeeds); + } + + function removeAllowedTokens(address[] memory _tokens) external onlyOwner { + for (uint256 i = 0; i < _tokens.length; i++) { + allowedTokens.remove(_tokens[i]); + delete priceFeeds[_tokens[i]]; + } + } + + function updatePriceFeeds(address[] memory _tokens, address[] memory _priceFeeds) external onlyOwner { + for (uint256 i = 0; i < _tokens.length; i++) { + priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); + } + } /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* INTERNAL */ @@ -384,6 +369,12 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl } } + function _addAllowedTokens(address[] memory _tokens, address[] memory _priceFeeds) internal { + for (uint256 i = 0; i < _tokens.length; i++) { + allowedTokens.add(_tokens[i]); + priceFeeds[_tokens[i]] = IAggregatorV3Interface(_priceFeeds[i]); + } + } /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* VIEW */ From 018c13402f7bf9998521ad52fa5a4e4a38b8b65a Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 18:43:22 +0530 Subject: [PATCH 5/6] feat: PRO-2631 refactor tokenPriceUsd computation from oracle price and derive normalised UsdAmount with 18 decimals --- .../MultiTokenSessionKeyValidator.sol | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol index f30ac50f..91d08b35 100644 --- a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -19,6 +19,9 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownable { + + uint8 constant public USD_AMOUNT_DECIMALS = 18; + using ModeLib for ModeCode; using ExecutionLib for bytes; using ArrayLib for address[]; @@ -380,7 +383,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl /* VIEW */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ - function getTokenPriceInUsd(address token) internal view returns (uint256) { + function getTokenPriceInUsd(address token) internal view returns (uint256, uint8) { (, int256 price, ,uint256 updatedAt, ) = priceFeeds[token].latestRoundData(); @@ -392,15 +395,42 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl revert MTSKV_StaleTokenPrice(token); } - return uint256(price); + uint8 feedDecimals = priceFeeds[token].decimals(); + + return (uint256(price), feedDecimals); } - function estimateTotalSpentAmountInUsd(address sessionKey, address token, uint256 amount) public view returns (uint256) { - uint256 currentTokenPriceUSD = getTokenPriceInUsd(token); - uint8 currentTokenDecimals = IERC20(token).decimals(); - uint256 amountInUsd = (amount * currentTokenPriceUSD) / (10 ** currentTokenDecimals); + // @inheritdoc IMultiTokenSessionKeyValidator + /// @dev Estimates the total amount spent in USD for a given session key and token + /// @dev token decimals and feed decimals are different and to derive the USD amount in a fixed precision of 18 decimals + /// @dev scale the tokenPrice up or down by different to target precision and divide by 10 ** feedDecimals + /// @dev amountInUsd will be in decimal precision of 18 + function estimateTotalSpentAmountInUsd( + address sessionKey, + address token, + uint256 amount + ) public view returns (uint256) { + (uint256 tokenPriceUSD, uint8 feedDecimals) = getTokenPriceInUsd(token); + + uint8 tokenDecimals = IERC20(token).decimals(); + uint256 scaledAmount; + + if (tokenDecimals < USD_AMOUNT_DECIMALS) { + // If token has fewer than 18 decimals, scale the amount up to 18 decimals + scaledAmount = amount * (10 ** (USD_AMOUNT_DECIMALS - tokenDecimals)); + } else if(tokenDecimals > USD_AMOUNT_DECIMALS) { + // If token has more than 18 decimals, scale the amount down to 18 decimals + scaledAmount = amount / (10 ** (tokenDecimals - USD_AMOUNT_DECIMALS)); + } else { + scaledAmount = amount; + } + + uint256 amountInUsd = (scaledAmount * tokenPriceUSD) / (10 ** feedDecimals); + + // add the amountInUsd with the totalSpentInUsd for the session key return amountInUsd + totalSpentInUsd[sessionKey]; - } + } + function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) public view returns (bool) { MultiTokenSessionData memory data = multiTokenSessionData[sessionKey][user]; From dae74885b53fa8b0bd140573e16c39aaab6d041a Mon Sep 17 00:00:00 2001 From: kanth Date: Sun, 25 Aug 2024 18:52:59 +0530 Subject: [PATCH 6/6] feat: PRO-2631 refactor the check function for estimatedTotalUSDSpent with spendingLimit --- .../interfaces/IMultiTokenSessionKeyValidator.sol | 2 +- .../validators/MultiTokenSessionKeyValidator.sol | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol index f1e8b1b6..da51750a 100644 --- a/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/interfaces/IMultiTokenSessionKeyValidator.sol @@ -143,5 +143,5 @@ interface IMultiTokenSessionKeyValidator is IValidator { function estimateTotalSpentAmountInUsd(address sessionKey, address token, uint256 amount) external view returns (uint256); - function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) external view returns (bool); + function isEstimatedTotalUsdSpentWithInLimits(address sessionKey, address user, address token, uint256 amount) external view returns (bool); } diff --git a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol index 91d08b35..25147563 100644 --- a/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol +++ b/src/modular-etherspot-wallet/modules/validators/MultiTokenSessionKeyValidator.sol @@ -57,7 +57,6 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ /* MAPPINGS */ /*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*§*/ - mapping(address => bool) public initialized; @@ -214,11 +213,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl return false; } - if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { - return false; - } - - return true; + return isEstimatedTotalUsdSpentWithInLimits(_sessionKey, msg.sender, target, amount); } function _validateBatchCall( @@ -239,7 +234,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl return false; } - if (!checkSpendingLimit(_sessionKey, msg.sender, target, amount)) { + if (!isEstimatedTotalUsdSpentWithInLimits(_sessionKey, msg.sender, target, amount)) { return false; } } @@ -432,7 +427,7 @@ contract MultiTokenSessionKeyValidator is IMultiTokenSessionKeyValidator, Ownabl } - function checkSpendingLimit(address sessionKey, address user, address token, uint256 amount) public view returns (bool) { + function isEstimatedTotalUsdSpentWithInLimits(address sessionKey, address user, address token, uint256 amount) public view returns (bool) { MultiTokenSessionData memory data = multiTokenSessionData[sessionKey][user]; return estimateTotalSpentAmountInUsd(sessionKey, token, amount) <= data.cumulativeSpendingLimitInUsd; }