From 1d8e4b8dcc34f42e352ab729a3979de29b16f631 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 16 Sep 2024 21:01:45 +0100 Subject: [PATCH 01/13] feat: add HookTargetFirewall contract --- src/HookTarget/HookTargetFirewall.sol | 405 ++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 src/HookTarget/HookTargetFirewall.sol diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol new file mode 100644 index 00000000..78bb5664 --- /dev/null +++ b/src/HookTarget/HookTargetFirewall.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +import {StorageSlot} from "openzeppelin-contracts/utils/StorageSlot.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {Set, SetStorage} from "ethereum-vault-connector/Set.sol"; +import {AmountCap} from "evk/EVault/shared/types/AmountCap.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; + +/// @notice An interface for the SecurityValidator singleton contract. +interface ISecurityValidator { + struct Attestation { + uint256 deadline; + bytes32[] executionHashes; + } + + function getCurrentAttester() external view returns (address); + function executeCheckpoint(bytes32 checkpointHash) external returns (bytes32); + function hashAttestation(Attestation calldata attestation) external view returns (bytes32); + function executionHashFrom(bytes32 checkpointHash, address caller, bytes32 executionHash) + external + view + returns (bytes32); + function saveAttestation(Attestation calldata attestation, bytes calldata attestationSignature) external; + function storeAttestation(Attestation calldata attestation, bytes calldata attestationSignature) external; + function validateFinalState() external view; +} + +/// @title HookTargetFirewall +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice A hook target that integrates with the SecurityValidator contract. +contract HookTargetFirewall is IHookTarget, EVCUtil { + using Set for SetStorage; + + /// @notice Struct to store policy information for a vault. + struct PolicyStorage { + /// @notice The set of accepted attesters. + SetStorage attesters; + /// @notice The constant threshold for incoming transfers. + AmountCap inConstantThreshold; + /// @notice The accumulated threshold for incoming transfers. + AmountCap inAccumulatedThreshold; + /// @notice The constant threshold for outgoing transfers. + AmountCap outConstantThreshold; + /// @notice The accumulated threshold for outgoing transfers. + AmountCap outAccumulatedThreshold; + } + + /// @notice Enum representing the type of transfer. + enum TransferType { + /// @notice Represents an incoming transfer. + In, + /// @notice Represents an outgoing transfer. + Out + } + + /// @notice The security validator contract. + ISecurityValidator internal immutable validator; + + /// @notice The immutable ID of the attester controller + bytes32 internal immutable controllerId; + + /// @notice Mapping of vault addresses to their policy storage. + mapping(address => PolicyStorage) internal policies; + + /// @notice Mapping of address prefixes to their operation counter. + mapping(bytes19 => uint256) internal operationCounters; + + /// @notice Emitted when the attester controller ID is updated + /// @param attesterControllerId The new attester controller ID + event AttesterControllerUpdated(bytes32 attesterControllerId); + + /// @notice Emitted when accepted attesters are inserted for a vault. + /// @param vault The address of the vault. + /// @param attester The address of the attester. + event AddPolicyAttester(address indexed vault, address attester); + + /// @notice Emitted when an accepted attester is removed for a vault. + /// @param vault The address of the vault. + /// @param attester The address of the attester. + event RemovePolicyAttester(address indexed vault, address attester); + + /// @notice Emitted when a policy thresholds are set for a vault. + /// @param vault The address of the vault. + /// @param inConstantThreshold The constant threshold for incoming transfers. + /// @param inAccumulatedThreshold The accumulated threshold for incoming transfers. + /// @param outConstantThreshold The constant threshold for outgoing transfers. + /// @param outAccumulatedThreshold The accumulated threshold for outgoing transfers. + event SetPolicyThresholds( + address indexed vault, + uint16 inConstantThreshold, + uint16 inAccumulatedThreshold, + uint16 outConstantThreshold, + uint16 outAccumulatedThreshold + ); + + /// @notice Error thrown when the caller is not authorized to perform an operation. + error HTA_Unauthorized(); + + /// @notice Constructor to initialize the contract with the EVC and validator addresses. + /// @param _evc The address of the EVC contract. + /// @param _securityValidator The address of the security validator contract. + constructor(address _evc, address _securityValidator, bytes32 _controllerId) EVCUtil(_evc) { + validator = ISecurityValidator(_securityValidator); + controllerId = _controllerId; + emit AttesterControllerUpdated(_controllerId); + } + + /// @notice Fallback function to handle unexpected calls. + fallback() external {} + + /// @notice Modifier to restrict access to only the governor of the specified vault. + /// @param vault The address of the vault. + modifier onlyGovernor(address vault) { + if (_msgSender() != IEVault(vault).governorAdmin()) revert HTA_Unauthorized(); + _; + } + + /// @inheritdoc IHookTarget + function isHookTarget() external pure override returns (bytes4) { + return this.isHookTarget.selector; + } + + /// @notice Retrieves the address of the security validator contract + /// @return The address of the security validator contract + function getSecurityValidator() external view returns (address) { + return address(validator); + } + + /// @notice Retrieves the attester controller ID + /// @return The bytes32 representation of the attester controller ID + function getAttesterControllerId() external view returns (bytes32) { + return controllerId; + } + + /// @notice Adds an accepted attester to the policy for a given vault. + /// @param vault The address of the vault. + /// @param attester The address of the attester to be added. + function addPolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { + if (policies[vault].attesters.insert(attester)) { + emit AddPolicyAttester(vault, attester); + } + } + + /// @notice Removes an accepted attester from the policy for a given vault. + /// @param vault The address of the vault. + /// @param attester The address of the attester to be removed. + function removePolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { + if (policies[vault].attesters.remove(attester)) { + emit RemovePolicyAttester(vault, attester); + } + } + + /// @notice Retrieves the list of accepted attesters for a given vault. + /// @param vault The address of the vault. + /// @return An array of addresses representing the accepted attesters for the specified vault. + function getPolicyAttesters(address vault) external view returns (address[] memory) { + return policies[vault].attesters.get(); + } + + /// @notice Sets the policy thresholds for a given vault. + /// @param vault The address of the vault. + /// @param inConstantThreshold The constant threshold for incoming transfers. + /// @param inAccumulatedThreshold The accumulated threshold for incoming transfers. + /// @param outConstantThreshold The constant threshold for outgoing transfers. + /// @param outAccumulatedThreshold The accumulated threshold for outgoing transfers. + function setPolicyThresholds( + address vault, + uint16 inConstantThreshold, + uint16 inAccumulatedThreshold, + uint16 outConstantThreshold, + uint16 outAccumulatedThreshold + ) external onlyEVCAccountOwner onlyGovernor(vault) { + PolicyStorage storage policyStorage = policies[vault]; + policyStorage.inConstantThreshold = AmountCap.wrap(inConstantThreshold); + policyStorage.inAccumulatedThreshold = AmountCap.wrap(inAccumulatedThreshold); + policyStorage.outConstantThreshold = AmountCap.wrap(outConstantThreshold); + policyStorage.outAccumulatedThreshold = AmountCap.wrap(outAccumulatedThreshold); + + emit SetPolicyThresholds( + vault, inConstantThreshold, inAccumulatedThreshold, outConstantThreshold, outAccumulatedThreshold + ); + } + + /// @notice Retrieves the policy thresholds for a given vault. + /// @param vault The address of the vault. + /// @return inConstantThreshold The constant threshold for incoming transfers. + /// @return inAccumulatedThreshold The accumulated threshold for incoming transfers. + /// @return outConstantThreshold The constant threshold for outgoing transfers. + /// @return outAccumulatedThreshold The accumulated threshold for outgoing transfers. + function getPolicyThresholds(address vault) external view returns (uint16, uint16, uint16, uint16) { + PolicyStorage storage policyStorage = policies[vault]; + return ( + AmountCap.unwrap(policyStorage.inConstantThreshold), + AmountCap.unwrap(policyStorage.inAccumulatedThreshold), + AmountCap.unwrap(policyStorage.outConstantThreshold), + AmountCap.unwrap(policyStorage.outAccumulatedThreshold) + ); + } + + /// @notice Retrieves the resolved policy thresholds for a given vault. + /// @param vault The address of the vault. + /// @return inConstantThreshold The constant threshold for incoming transfers. + /// @return inAccumulatedThreshold The accumulated threshold for incoming transfers. + /// @return outConstantThreshold The constant threshold for outgoing transfers. + /// @return outAccumulatedThreshold The accumulated threshold for outgoing transfers. + function getPolicyThresholdsResolved(address vault) external view returns (uint256, uint256, uint256, uint256) { + PolicyStorage storage policyStorage = policies[vault]; + return ( + policyStorage.inConstantThreshold.resolve(), + policyStorage.inAccumulatedThreshold.resolve(), + policyStorage.outConstantThreshold.resolve(), + policyStorage.outAccumulatedThreshold.resolve() + ); + } + + /// @notice Saves an attestation. + /// @param attestation The attestation data. + /// @param attestationSignature The signature of the attestation. + function saveAttestation(ISecurityValidator.Attestation calldata attestation, bytes calldata attestationSignature) + external + { + validator.saveAttestation(attestation, attestationSignature); + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function deposit(uint256 amount, address receiver) external returns (uint256) { + if (amount == type(uint256).max) { + amount = IEVault(IEVault(msg.sender).asset()).balanceOf(caller()); + } + executeCheckpoint(TransferType.In, amount, abi.encode(receiver)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function mint(uint256 amount, address receiver) external returns (uint256) { + amount = IEVault(msg.sender).convertToAssets(amount); + executeCheckpoint(TransferType.In, amount, abi.encode(receiver)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function skim(uint256 amount, address receiver) external returns (uint256) { + if (amount == type(uint256).max) { + uint256 balance = IEVault(IEVault(msg.sender).asset()).balanceOf(msg.sender); + uint256 cash = IEVault(msg.sender).cash(); + amount = balance > cash ? balance - cash : 0; + } + executeCheckpoint(TransferType.In, amount, abi.encode(receiver)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function withdraw(uint256 amount, address receiver, address owner) external returns (uint256) { + executeCheckpoint(TransferType.Out, amount, abi.encode(receiver, owner)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function redeem(uint256 amount, address receiver, address owner) external returns (uint256) { + if (amount == type(uint256).max) { + amount = IEVault(msg.sender).convertToAssets(IEVault(msg.sender).balanceOf(owner)); + } + executeCheckpoint(TransferType.Out, amount, abi.encode(receiver, owner)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function borrow(uint256 amount, address receiver) external returns (uint256) { + if (amount == type(uint256).max) amount = IEVault(msg.sender).cash(); + executeCheckpoint(TransferType.Out, amount, abi.encode(receiver)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function repay(uint256 amount, address receiver) external returns (uint256) { + if (amount == type(uint256).max) amount = IEVault(msg.sender).debtOf(receiver); + executeCheckpoint(TransferType.In, amount, abi.encode(receiver)); + return 0; + } + + /// @notice Overriden function of IEVault in order to be intercepted by the hook target. + function checkVaultStatus() external view returns (bytes4) { + validator.validateFinalState(); + return 0; + } + + /// @notice Executes a checkpoint for a given transfer type and reference amount. + /// @param transferType The type of transfer (In or Out). + /// @param referenceAmount The reference amount for the transfer. + /// @param hashable Additional data to be hashed. + function executeCheckpoint(TransferType transferType, uint256 referenceAmount, bytes memory hashable) internal { + address sender = caller(); + uint256 operationCounter = updateOperationCounter(sender); + uint256 accumulatedAmount = updateAccumulatedAmount(transferType, msg.sender, referenceAmount); + (uint256 constantThreshold, uint256 accumulatedThreshold) = resolveThresholds(msg.sender, transferType); + + if ( + (referenceAmount >= constantThreshold || accumulatedAmount >= accumulatedThreshold) + && !evc.isSimulationInProgress() + ) { + // to prevent replay attacks, the hash must depend on: + // - the vault address that is a caller of the hook target + // - the operation type executed (function selector) + // - the quantized reference amount that allows for runtime changes within an acceptable range + // - the static parameters of the operation + // - the authenticated account that executes the operation + // - the operation counter associated with the authenticated account + validator.executeCheckpoint( + keccak256( + abi.encode( + msg.sender, + bytes4(msg.data), + log1_01(int256(referenceAmount)), + hashable, + sender, + operationCounter + ) + ) + ); + + // this check must be done after the checkpoint is executed so that at this point, in case the + // storeAttestation function is used instead of the saveAttestation function, the current attester must be + // already defined by the validator contract + if (!isAttestationInProgress()) { + revert HTA_Unauthorized(); + } + } + } + + /// @notice Updates the operation counter for a given account. + /// @param account The account for which the operation counter is updated. + /// @return The updated operation counter. + function updateOperationCounter(address account) internal returns (uint256) { + bytes19 prefix = _getAddressPrefix(account); + uint256 counter = operationCounters[prefix] + 1; + operationCounters[prefix] = counter; + return counter; + } + + /// @notice Updates the accumulated amount for a given transfer type and vault. + /// @param transferType The type of transfer (In or Out). + /// @param vault The vault address. + /// @param referenceAmount The reference amount for the transfer. + /// @return The updated accumulated amount. + function updateAccumulatedAmount(TransferType transferType, address vault, uint256 referenceAmount) + internal + returns (uint256) + { + StorageSlot.Uint256SlotType slot = StorageSlot.asUint256(keccak256(abi.encode(transferType, vault))); + uint256 accumulatedAmount = StorageSlot.tload(slot) + referenceAmount; + StorageSlot.tstore(slot, accumulatedAmount); + return accumulatedAmount; + } + + /// @notice Checks if an attestation is in progress. + /// @return True if an attestation is in progress, false otherwise. + function isAttestationInProgress() internal view returns (bool) { + address currentAttester = validator.getCurrentAttester(); + return currentAttester != address(0) && policies[msg.sender].attesters.contains(currentAttester); + } + + /// @notice Resolves the constant and accumulated thresholds for a given vault and transfer type. + /// @param vault The address of the vault. + /// @param transferType The type of transfer (In or Out). + /// @return constantThreshold The resolved constant threshold. + /// @return accumulatedThreshold The resolved accumulated threshold. + function resolveThresholds(address vault, TransferType transferType) + internal + view + returns (uint256 constantThreshold, uint256 accumulatedThreshold) + { + PolicyStorage storage policy = policies[vault]; + + if (transferType == TransferType.In) { + constantThreshold = policy.inConstantThreshold.resolve(); + accumulatedThreshold = policy.inAccumulatedThreshold.resolve(); + } else { + constantThreshold = policy.outConstantThreshold.resolve(); + accumulatedThreshold = policy.outAccumulatedThreshold.resolve(); + } + } + + /// @notice Calculates the logarithm base 1.01 of a given number. + /// @param x The number to calculate the logarithm for. + /// @return The logarithm base 1.01 of the given number. + function log1_01(int256 x) internal pure returns (int256) { + if (x == 0) return type(int256).max; + + // log1.01(x) = ln(x) / ln(1.01) = lnWad(x * 1e18) / lnWad(1.01 * 1e18) + return FixedPointMathLib.lnWad(x * 1e18) / 9950330853168082; + } + + /// @notice Retrieves the caller address from the calldata. + /// @return _caller The address of the caller. + function caller() internal pure returns (address _caller) { + assembly { + _caller := shr(96, calldataload(sub(calldatasize(), 20))) + } + } +} From a30aa13790a1f84a4cb0b4d40edb5fd31d84d14d Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 16 Sep 2024 21:20:24 +0100 Subject: [PATCH 02/13] forge install: solady v0.0.227 --- .gitmodules | 3 +++ lib/solady | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/solady diff --git a/.gitmodules b/.gitmodules index e07acdc9..5758cb41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/euler-vault-kit"] path = lib/euler-vault-kit url = https://github.com/euler-xyz/euler-vault-kit +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..1f43cc80 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 1f43cc8005cc3b3c8361dd7dbdd2cdeaf0f99e66 From c99a9002453c249869884617642268144652b089 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 16 Sep 2024 21:22:28 +0100 Subject: [PATCH 03/13] chore: add solady to remappings --- remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/remappings.txt b/remappings.txt index e3f894c5..f4cefdff 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,3 +8,4 @@ euler-price-oracle-test/=lib/euler-price-oracle/test/ fee-flow/=lib/fee-flow/src/ reward-streams/=lib/reward-streams/src/ @openzeppelin/contracts/utils/math/=lib/euler-price-oracle/lib/openzeppelin-contracts/contracts/utils/math/ +solady/=lib/euler-price-oracle/lib/solady/src/ From 620f9f036018e11186b1b83114204c1801e1bb70 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 16 Sep 2024 22:34:23 +0100 Subject: [PATCH 04/13] chore: add HookTargetFirewall docs --- docs/firewall.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/firewall.md diff --git a/docs/firewall.md b/docs/firewall.md new file mode 100644 index 00000000..a0b5b2a3 --- /dev/null +++ b/docs/firewall.md @@ -0,0 +1,50 @@ +# Firewall + +The `HookTargetFirewall` is a sophisticated security mechanism designed to integrate with Forta's `SecurityValidator` contract and the Euler Vault Kit (EVK) ecosystem. It acts as a hook target for EVK vaults, providing an additional layer of security by implementing firewall-like functionality. + +## Purpose + +The primary purpose of the `HookTargetFirewall` is to enforce security policies on vault operations, particularly for high-value or sensitive transactions. It works in conjunction with Forta's `SecurityValidator` contract to ensure that certain operations are properly attested before execution, bringing off-chain exploit detection to on-chain DeFi protocol. + +## Key Features + +1. Each vault can have its own security policy, including: + - A set of accepted attesters + - Thresholds for incoming and outgoing transfers (both constant and accumulated within a transaction) +2. The contract intercepts key vault operations like `deposit`, `withdraw`, `mint`, `redeem`, `borrow`, and `repay`, validating them against the stored policy. +3. For transactions exceeding defined thresholds, `HookTargetFirewall` requires an appropriate attestation to be obtained and saved in the `SecurityValidator` contract prior to the operation being executed. +4. The contract implements an operation counter to prevent replay attacks and preserve the integrity of operations even if they do not require checkpoints to be executed. Operation counter is incremented for each intercepted operation. + +## How It Works + +1. When a vault operation is called and the hooked operations are configured appropriately on the vault, the operation is intercepted by the `HookTargetFirewall`. +2. The contract checks if the operation exceeds the defined thresholds. +3. If thresholds are exceeded, a checkpoint is executed through the `SecurityValidator`. +4. The `SecurityValidator` ensures that the operation has been properly attested by checking for a matching attestation. This attestation can be saved before the transaction (using SSTORE) or at the beginning of the transaction (using TSTORE and the EVC's batching mechanism). +5. The attestation includes: + - A deadline timestamp + - An ordered list of execution hashes, which are derived from checkpoint hashes and additional inputs to ensure specificity and proper ordering +6. If a valid attestation exists, the operation proceeds; otherwise, it's blocked. + +## Caveats + +### Checkpoint Hash Computation + +The checkpoint hash is a crucial element in the security mechanism of the `HookTargetFirewall`. It is computed using the following components: + +1. The vault address (caller of the hook target) +2. The function selector of the operation being executed +3. A quantized reference amount +4. Static parameters of the operation +5. The authenticated account executing the operation +6. An operation counter associated with the authenticated account + +This composition ensures that the checkpoint is unique for each operation (also cross-vault), allows for small runtime changes in the reference amount, and prevents replay attacks. + +### Reference Amount Quantization + +The reference amount is quantized using a logarithmic function (`log1.01`) before being included in the checkpoint hash. This quantization is necessary because the asset amounts that will be processed can fluctuate slightly between the time the attestation is generated off-chain and when the user transaction is executed on-chain. Without quantization, these small fluctuations could cause different hashes to be produced during the real execution, resulting in a mismatch with the values in the attestation. By quantizing the reference value used in checkpoint hash computation, the `HookTargetFirewall` allows for small variations in asset amounts without invalidating the attestation. + +### Handling of Maximum Values + +When operations involve `type(uint256).max` as an amount (often used to represent "all available" in token operations), special handling is required. The `HookTargetFirewall` resolves these maximum values to concrete asset amounts before applying thresholds and computing checkpoint hashes. From ea581b62e63ca3551e0a654a90d6c6a4a6c69aef Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Mon, 23 Sep 2024 09:39:02 +0200 Subject: [PATCH 05/13] fix: yAudit audit Issue #1 and #2 --- docs/firewall.md | 17 +- src/HookTarget/HookTargetFirewall.sol | 323 ++++++++++++++++++-------- 2 files changed, 237 insertions(+), 103 deletions(-) diff --git a/docs/firewall.md b/docs/firewall.md index a0b5b2a3..3d48cdb4 100644 --- a/docs/firewall.md +++ b/docs/firewall.md @@ -11,9 +11,12 @@ The primary purpose of the `HookTargetFirewall` is to enforce security policies 1. Each vault can have its own security policy, including: - A set of accepted attesters - Thresholds for incoming and outgoing transfers (both constant and accumulated within a transaction) + - An operation counter threshold to limit the frequency of operations that do not require attestation 2. The contract intercepts key vault operations like `deposit`, `withdraw`, `mint`, `redeem`, `borrow`, and `repay`, validating them against the stored policy. 3. For transactions exceeding defined thresholds, `HookTargetFirewall` requires an appropriate attestation to be obtained and saved in the `SecurityValidator` contract prior to the operation being executed. -4. The contract implements an operation counter to prevent replay attacks and preserve the integrity of operations even if they do not require checkpoints to be executed. Operation counter is incremented for each intercepted operation. +4. The contract implements a sliding window mechanism to track frequency of operations that do not require attestation, using bit manipulation for gas-efficient storage and calculation. +5. The contract implements an operation counter to prevent replay attacks and preserve the integrity of operations even if they do not require attestation. Operation counter is incremented for each intercepted operation. +6. The firewall ensures that only authorized vaults (proxies deployed by the recognized EVault factory) can use it. ## How It Works @@ -48,3 +51,15 @@ The reference amount is quantized using a logarithmic function (`log1.01`) befor ### Handling of Maximum Values When operations involve `type(uint256).max` as an amount (often used to represent "all available" in token operations), special handling is required. The `HookTargetFirewall` resolves these maximum values to concrete asset amounts before applying thresholds and computing checkpoint hashes. + +### Operation Counter Mechanism + +The `HookTargetFirewall` uses a sliding window approach to track frequency of operations that do not require attestation: + +1. It uses a `uint96` to store three 32-bit counters, each representing a 1-minute window. +2. As time passes, the counters are shifted, and new operations increment the current window's counter. +3. The total operation count over the last 3 minutes is used to determine if the operation frequency threshold has been exceeded. + +### Vault Authentication + +The `HookTargetFirewall` ensures that only authorized vaults can use its services. It uses the `GenericFactory` contract to verify that the calling vault is a proxy deployed by the recognized EVault factory. diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol index 78bb5664..481c2e5a 100644 --- a/src/HookTarget/HookTargetFirewall.sol +++ b/src/HookTarget/HookTargetFirewall.sol @@ -6,6 +6,7 @@ import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {StorageSlot} from "openzeppelin-contracts/utils/StorageSlot.sol"; import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {Set, SetStorage} from "ethereum-vault-connector/Set.sol"; +import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; import {AmountCap} from "evk/EVault/shared/types/AmountCap.sol"; import {IEVault} from "evk/EVault/IEVault.sol"; import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; @@ -38,16 +39,24 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @notice Struct to store policy information for a vault. struct PolicyStorage { - /// @notice The set of accepted attesters. - SetStorage attesters; - /// @notice The constant threshold for incoming transfers. - AmountCap inConstantThreshold; - /// @notice The accumulated threshold for incoming transfers. - AmountCap inAccumulatedThreshold; - /// @notice The constant threshold for outgoing transfers. - AmountCap outConstantThreshold; - /// @notice The accumulated threshold for outgoing transfers. - AmountCap outAccumulatedThreshold; + /// @notice Whether the vault is authenticated. + bool isAuthenticated; + /// @notice The max operations counter threshold per 3 minutes that can be executed without attestation. + uint32 operationCounterThreshold; + /// @notice The normalized timestamp of the last update, rounded down to the nearest one minute interval. + uint48 updateTimestampNormalized; + /// @notice Packed operation counters for the last three one minute intervals. The least significant 32 bits + /// represent the current one minute window. + /// @dev Structured as [32 bits window][32 bits window][32 bits window]. + uint96 operationCountersPacked; + /// @notice The constant amount threshold for incoming transfers. + AmountCap inConstantAmountThreshold; + /// @notice The accumulated amount threshold for incoming transfers. + AmountCap inAccumulatedAmountThreshold; + /// @notice The constant amount threshold for outgoing transfers. + AmountCap outConstantAmountThreshold; + /// @notice The accumulated amount threshold for outgoing transfers. + AmountCap outAccumulatedAmountThreshold; } /// @notice Enum representing the type of transfer. @@ -58,22 +67,38 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { Out } + /// @notice The number of bits used to represent each window in the packed operation counters. + uint256 internal constant WINDOW_BITS = 32; + + /// @notice The duration of a single time window for operation counting. + uint256 internal constant WINDOW_PERIOD = 60; + + /// @notice The EVault factory contract. + GenericFactory internal immutable eVaultFactory; + /// @notice The security validator contract. ISecurityValidator internal immutable validator; /// @notice The immutable ID of the attester controller bytes32 internal immutable controllerId; + /// @notice Mapping of vault addresses to their set of accepted attesters. + mapping(address vault => SetStorage) internal attesters; + /// @notice Mapping of vault addresses to their policy storage. - mapping(address => PolicyStorage) internal policies; + mapping(address vault => PolicyStorage) internal policies; /// @notice Mapping of address prefixes to their operation counter. - mapping(bytes19 => uint256) internal operationCounters; + mapping(bytes19 addressPrefix => uint256) internal operationCounters; /// @notice Emitted when the attester controller ID is updated /// @param attesterControllerId The new attester controller ID event AttesterControllerUpdated(bytes32 attesterControllerId); + /// @notice Emitted when the vault is authenticated. + /// @param vault The address of the vault. + event AuthenticateVault(address indexed vault); + /// @notice Emitted when accepted attesters are inserted for a vault. /// @param vault The address of the vault. /// @param attester The address of the attester. @@ -86,16 +111,19 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @notice Emitted when a policy thresholds are set for a vault. /// @param vault The address of the vault. - /// @param inConstantThreshold The constant threshold for incoming transfers. - /// @param inAccumulatedThreshold The accumulated threshold for incoming transfers. - /// @param outConstantThreshold The constant threshold for outgoing transfers. - /// @param outAccumulatedThreshold The accumulated threshold for outgoing transfers. + /// @param operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed + /// without attestation. + /// @param inConstantAmountThreshold The constant amount threshold for incoming transfers. + /// @param inAccumulatedAmountThreshold The accumulated amount threshold for incoming transfers. + /// @param outConstantAmountThreshold The constant amount threshold for outgoing transfers. + /// @param outAccumulatedAmountThreshold The accumulated amount threshold for outgoing transfers. event SetPolicyThresholds( address indexed vault, - uint16 inConstantThreshold, - uint16 inAccumulatedThreshold, - uint16 outConstantThreshold, - uint16 outAccumulatedThreshold + uint32 operationCounterThreshold, + uint16 inConstantAmountThreshold, + uint16 inAccumulatedAmountThreshold, + uint16 outConstantAmountThreshold, + uint16 outAccumulatedAmountThreshold ); /// @notice Error thrown when the caller is not authorized to perform an operation. @@ -103,8 +131,12 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @notice Constructor to initialize the contract with the EVC and validator addresses. /// @param _evc The address of the EVC contract. + /// @param _eVaultFactory The address of the EVault factory contract. /// @param _securityValidator The address of the security validator contract. - constructor(address _evc, address _securityValidator, bytes32 _controllerId) EVCUtil(_evc) { + constructor(address _evc, address _eVaultFactory, address _securityValidator, bytes32 _controllerId) + EVCUtil(_evc) + { + eVaultFactory = GenericFactory(_eVaultFactory); validator = ISecurityValidator(_securityValidator); controllerId = _controllerId; emit AttesterControllerUpdated(_controllerId); @@ -121,8 +153,19 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { } /// @inheritdoc IHookTarget - function isHookTarget() external pure override returns (bytes4) { - return this.isHookTarget.selector; + /// @dev This function returns the expected magic value only if the caller is a proxy deployed by the recognized + /// EVault factory. + function isHookTarget() external view override returns (bytes4) { + if (eVaultFactory.isProxy(msg.sender)) { + return this.isHookTarget.selector; + } + return 0; + } + + /// @notice Retrieves the address of the EVault factory contract + /// @return The address of the EVault factory contract + function getEVaultFactory() external view returns (address) { + return address(eVaultFactory); } /// @notice Retrieves the address of the security validator contract @@ -141,7 +184,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @param attester The address of the attester to be added. function addPolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { - if (policies[vault].attesters.insert(attester)) { + if (attesters[vault].insert(attester)) { emit AddPolicyAttester(vault, attester); } } @@ -150,7 +193,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @param attester The address of the attester to be removed. function removePolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { - if (policies[vault].attesters.remove(attester)) { + if (attesters[vault].remove(attester)) { emit RemovePolicyAttester(vault, attester); } } @@ -159,65 +202,92 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @return An array of addresses representing the accepted attesters for the specified vault. function getPolicyAttesters(address vault) external view returns (address[] memory) { - return policies[vault].attesters.get(); + return attesters[vault].get(); } /// @notice Sets the policy thresholds for a given vault. /// @param vault The address of the vault. - /// @param inConstantThreshold The constant threshold for incoming transfers. - /// @param inAccumulatedThreshold The accumulated threshold for incoming transfers. - /// @param outConstantThreshold The constant threshold for outgoing transfers. - /// @param outAccumulatedThreshold The accumulated threshold for outgoing transfers. + /// @param operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed + /// without attestation. + /// @param inConstantAmountThreshold The constant amount threshold for incoming transfers. + /// @param inAccumulatedAmountThreshold The accumulated amount threshold for incoming transfers. + /// @param outConstantAmountThreshold The constant amount threshold for outgoing transfers. + /// @param outAccumulatedAmountThreshold The accumulated amount threshold for outgoing transfers. function setPolicyThresholds( address vault, - uint16 inConstantThreshold, - uint16 inAccumulatedThreshold, - uint16 outConstantThreshold, - uint16 outAccumulatedThreshold + uint32 operationCounterThreshold, + uint16 inConstantAmountThreshold, + uint16 inAccumulatedAmountThreshold, + uint16 outConstantAmountThreshold, + uint16 outAccumulatedAmountThreshold ) external onlyEVCAccountOwner onlyGovernor(vault) { PolicyStorage storage policyStorage = policies[vault]; - policyStorage.inConstantThreshold = AmountCap.wrap(inConstantThreshold); - policyStorage.inAccumulatedThreshold = AmountCap.wrap(inAccumulatedThreshold); - policyStorage.outConstantThreshold = AmountCap.wrap(outConstantThreshold); - policyStorage.outAccumulatedThreshold = AmountCap.wrap(outAccumulatedThreshold); + policyStorage.operationCounterThreshold = operationCounterThreshold; + policyStorage.inConstantAmountThreshold = AmountCap.wrap(inConstantAmountThreshold); + policyStorage.inAccumulatedAmountThreshold = AmountCap.wrap(inAccumulatedAmountThreshold); + policyStorage.outConstantAmountThreshold = AmountCap.wrap(outConstantAmountThreshold); + policyStorage.outAccumulatedAmountThreshold = AmountCap.wrap(outAccumulatedAmountThreshold); emit SetPolicyThresholds( - vault, inConstantThreshold, inAccumulatedThreshold, outConstantThreshold, outAccumulatedThreshold + vault, + operationCounterThreshold, + inConstantAmountThreshold, + inAccumulatedAmountThreshold, + outConstantAmountThreshold, + outAccumulatedAmountThreshold ); } /// @notice Retrieves the policy thresholds for a given vault. /// @param vault The address of the vault. - /// @return inConstantThreshold The constant threshold for incoming transfers. - /// @return inAccumulatedThreshold The accumulated threshold for incoming transfers. - /// @return outConstantThreshold The constant threshold for outgoing transfers. - /// @return outAccumulatedThreshold The accumulated threshold for outgoing transfers. - function getPolicyThresholds(address vault) external view returns (uint16, uint16, uint16, uint16) { + /// @return operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed + /// without attestation. + /// @return inConstantAmountThreshold The constant amount threshold for incoming transfers. + /// @return inAccumulatedAmountThreshold The accumulated amount threshold for incoming transfers. + /// @return outConstantAmountThreshold The constant amount threshold for outgoing transfers. + /// @return outAccumulatedAmountThreshold The accumulated amount threshold for outgoing transfers. + function getPolicyThresholds(address vault) external view returns (uint32, uint16, uint16, uint16, uint16) { PolicyStorage storage policyStorage = policies[vault]; return ( - AmountCap.unwrap(policyStorage.inConstantThreshold), - AmountCap.unwrap(policyStorage.inAccumulatedThreshold), - AmountCap.unwrap(policyStorage.outConstantThreshold), - AmountCap.unwrap(policyStorage.outAccumulatedThreshold) + policyStorage.operationCounterThreshold, + AmountCap.unwrap(policyStorage.inConstantAmountThreshold), + AmountCap.unwrap(policyStorage.inAccumulatedAmountThreshold), + AmountCap.unwrap(policyStorage.outConstantAmountThreshold), + AmountCap.unwrap(policyStorage.outAccumulatedAmountThreshold) ); } /// @notice Retrieves the resolved policy thresholds for a given vault. /// @param vault The address of the vault. - /// @return inConstantThreshold The constant threshold for incoming transfers. - /// @return inAccumulatedThreshold The accumulated threshold for incoming transfers. - /// @return outConstantThreshold The constant threshold for outgoing transfers. - /// @return outAccumulatedThreshold The accumulated threshold for outgoing transfers. - function getPolicyThresholdsResolved(address vault) external view returns (uint256, uint256, uint256, uint256) { + /// @return operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed + /// without attestation. + /// @return inConstantAmountThreshold The constant amount threshold for incoming transfers. + /// @return inAccumulatedAmountThreshold The accumulated amount threshold for incoming transfers. + /// @return outConstantAmountThreshold The constant amount threshold for outgoing transfers. + /// @return outAccumulatedAmountThreshold The accumulated amount threshold for outgoing transfers. + function getPolicyThresholdsResolved(address vault) + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { PolicyStorage storage policyStorage = policies[vault]; return ( - policyStorage.inConstantThreshold.resolve(), - policyStorage.inAccumulatedThreshold.resolve(), - policyStorage.outConstantThreshold.resolve(), - policyStorage.outAccumulatedThreshold.resolve() + policyStorage.operationCounterThreshold, + policyStorage.inConstantAmountThreshold.resolve(), + policyStorage.inAccumulatedAmountThreshold.resolve(), + policyStorage.outConstantAmountThreshold.resolve(), + policyStorage.outAccumulatedAmountThreshold.resolve() ); } + /// @notice Retrieves the current operation counter for a given vault over the last 3 minutes. + /// @dev This operation counter only counts operations that have been executed without attestation. + /// @param vault The address of the vault + /// @return The current operation counter for the vault + function getOperationCounter(address vault) external view returns (uint256) { + return updateVaultOperationCounter(policies[vault]) - 1; + } + /// @notice Saves an attestation. /// @param attestation The attestation data. /// @param attestationSignature The signature of the attestation. @@ -294,48 +364,71 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param referenceAmount The reference amount for the transfer. /// @param hashable Additional data to be hashed. function executeCheckpoint(TransferType transferType, uint256 referenceAmount, bytes memory hashable) internal { + PolicyStorage memory policy = policies[msg.sender]; + if (!policy.isAuthenticated) { + authenticateVault(msg.sender); + } + address sender = caller(); - uint256 operationCounter = updateOperationCounter(sender); + uint256 accountOperationCounter = updateAccountOperationCounter(sender); + uint256 vaultOperationCounter = updateVaultOperationCounter(policy); uint256 accumulatedAmount = updateAccumulatedAmount(transferType, msg.sender, referenceAmount); - (uint256 constantThreshold, uint256 accumulatedThreshold) = resolveThresholds(msg.sender, transferType); + (uint256 operationCounterThreshold, uint256 constantAmountThreshold, uint256 accumulatedAmountThreshold) = + resolveThresholds(transferType, policy); if ( - (referenceAmount >= constantThreshold || accumulatedAmount >= accumulatedThreshold) - && !evc.isSimulationInProgress() + vaultOperationCounter >= operationCounterThreshold || referenceAmount >= constantAmountThreshold + || accumulatedAmount >= accumulatedAmountThreshold ) { - // to prevent replay attacks, the hash must depend on: - // - the vault address that is a caller of the hook target - // - the operation type executed (function selector) - // - the quantized reference amount that allows for runtime changes within an acceptable range - // - the static parameters of the operation - // - the authenticated account that executes the operation - // - the operation counter associated with the authenticated account - validator.executeCheckpoint( - keccak256( - abi.encode( - msg.sender, - bytes4(msg.data), - log1_01(int256(referenceAmount)), - hashable, - sender, - operationCounter + if (!evc.isSimulationInProgress()) { + // to prevent replay attacks, the hash must depend on: + // - the vault address that is a caller of the hook target + // - the operation type executed (function selector) + // - the quantized reference amount that allows for runtime changes within an acceptable range + // - the static parameters of the operation + // - the authenticated account that executes the operation + // - the operation counter associated with the authenticated account + validator.executeCheckpoint( + keccak256( + abi.encode( + msg.sender, + bytes4(msg.data), + log1_01(referenceAmount), + hashable, + sender, + accountOperationCounter + ) ) - ) - ); - - // this check must be done after the checkpoint is executed so that at this point, in case the - // storeAttestation function is used instead of the saveAttestation function, the current attester must be - // already defined by the validator contract - if (!isAttestationInProgress()) { - revert HTA_Unauthorized(); + ); + + // this check must be done after the checkpoint is executed so that at this point, in case the + // storeAttestation function is used instead of the saveAttestation function, the current attester must + // be + // already defined by the validator contract + if (!isAttestationInProgress()) { + revert HTA_Unauthorized(); + } } + } else { + // apply the operation counter update only if the checkpoint does not need to be executed + policies[msg.sender] = policy; + } + } + + /// @notice Authenticates the vault if it is a proxy deployed by the recognized eVault factory. + function authenticateVault(address vault) internal { + if (eVaultFactory.isProxy(msg.sender)) { + policies[vault].isAuthenticated = true; + emit AuthenticateVault(msg.sender); + } else { + revert HTA_Unauthorized(); } } /// @notice Updates the operation counter for a given account. /// @param account The account for which the operation counter is updated. /// @return The updated operation counter. - function updateOperationCounter(address account) internal returns (uint256) { + function updateAccountOperationCounter(address account) internal returns (uint256) { bytes19 prefix = _getAddressPrefix(account); uint256 counter = operationCounters[prefix] + 1; operationCounters[prefix] = counter; @@ -357,42 +450,68 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { return accumulatedAmount; } + /// @notice Updates the vault operation counter. + /// @dev The update is only applied in policy memory. For the update to be persisted, update the policy storage. + /// @param policy The policy for the vault. + /// @return The total updated vault operation counter over all the windows. + function updateVaultOperationCounter(PolicyStorage memory policy) internal view returns (uint256) { + uint256 timeElapsed = block.timestamp - policy.updateTimestampNormalized; + uint256 counters = policy.operationCountersPacked; + + if (timeElapsed >= WINDOW_PERIOD) { + // shift based on windows passed (if > 2, this will zero out all counters) + counters = (counters << ((timeElapsed / WINDOW_PERIOD) * WINDOW_BITS)) | 1; + } else { + // increment the counter for the current window + counters = counters + 1; + } + + policy.updateTimestampNormalized = uint48(block.timestamp - (block.timestamp % WINDOW_PERIOD)); + policy.operationCountersPacked = uint96(counters); + + return uint32(counters) + uint32(counters >> WINDOW_BITS) + uint32(counters >> (2 * WINDOW_BITS)); + } + /// @notice Checks if an attestation is in progress. /// @return True if an attestation is in progress, false otherwise. function isAttestationInProgress() internal view returns (bool) { address currentAttester = validator.getCurrentAttester(); - return currentAttester != address(0) && policies[msg.sender].attesters.contains(currentAttester); + return currentAttester != address(0) && attesters[msg.sender].contains(currentAttester); } - /// @notice Resolves the constant and accumulated thresholds for a given vault and transfer type. - /// @param vault The address of the vault. + /// @notice Resolves the thresholds for a given vault and transfer type. /// @param transferType The type of transfer (In or Out). - /// @return constantThreshold The resolved constant threshold. - /// @return accumulatedThreshold The resolved accumulated threshold. - function resolveThresholds(address vault, TransferType transferType) + /// @param policy The policy for the vault. + /// @return operationCounterThreshold The operation counter threshold. + /// @return constantAmountThreshold The resolved constant amount threshold. + /// @return accumulatedAmountThreshold The resolved accumulated amount threshold. + function resolveThresholds(TransferType transferType, PolicyStorage memory policy) internal - view - returns (uint256 constantThreshold, uint256 accumulatedThreshold) + pure + returns (uint256 operationCounterThreshold, uint256 constantAmountThreshold, uint256 accumulatedAmountThreshold) { - PolicyStorage storage policy = policies[vault]; + operationCounterThreshold = policy.operationCounterThreshold; + if (operationCounterThreshold == 0) { + operationCounterThreshold = type(uint256).max; + } if (transferType == TransferType.In) { - constantThreshold = policy.inConstantThreshold.resolve(); - accumulatedThreshold = policy.inAccumulatedThreshold.resolve(); + constantAmountThreshold = policy.inConstantAmountThreshold.resolve(); + accumulatedAmountThreshold = policy.inAccumulatedAmountThreshold.resolve(); } else { - constantThreshold = policy.outConstantThreshold.resolve(); - accumulatedThreshold = policy.outAccumulatedThreshold.resolve(); + constantAmountThreshold = policy.outConstantAmountThreshold.resolve(); + accumulatedAmountThreshold = policy.outAccumulatedAmountThreshold.resolve(); } } /// @notice Calculates the logarithm base 1.01 of a given number. /// @param x The number to calculate the logarithm for. /// @return The logarithm base 1.01 of the given number. - function log1_01(int256 x) internal pure returns (int256) { - if (x == 0) return type(int256).max; + function log1_01(uint256 x) internal pure returns (uint256) { + if (x == 0) return type(uint256).max; // log1.01(x) = ln(x) / ln(1.01) = lnWad(x * 1e18) / lnWad(1.01 * 1e18) - return FixedPointMathLib.lnWad(x * 1e18) / 9950330853168082; + return uint256(FixedPointMathLib.lnWad(int256(x * 1e18))) / 9950330853168082; } /// @notice Retrieves the caller address from the calldata. From 56799e0f2999b2fa360b87c448a523ce431901a4 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 16 Oct 2024 15:26:17 +0200 Subject: [PATCH 06/13] feat: HookTargetFirewall unstructured storage --- src/HookTarget/HookTargetFirewall.sol | 56 +++++++++++++++++---------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol index 481c2e5a..b85ce46f 100644 --- a/src/HookTarget/HookTargetFirewall.sol +++ b/src/HookTarget/HookTargetFirewall.sol @@ -67,6 +67,20 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { Out } + /// @custom:storage-location erc7201:euler.storage.HookTargetFirewall + struct HookTargetFirewallStorage { + /// @notice Mapping of vault addresses to their set of accepted attesters. + mapping(address vault => SetStorage) attesters; + /// @notice Mapping of vault addresses to their policy storage. + mapping(address vault => PolicyStorage) policies; + /// @notice Mapping of address prefixes to their operation counter. + mapping(bytes19 addressPrefix => uint256) operationCounters; + } + + // keccak256(abi.encode(uint256(keccak256("euler.storage.HookTargetFirewall")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 internal constant HookTargetFirewallStorageLocation = + 0xd3e74b2efd7e77af7296587b6de98af243f03ea83f111b434fed08f9d95e5500; + /// @notice The number of bits used to represent each window in the packed operation counters. uint256 internal constant WINDOW_BITS = 32; @@ -82,15 +96,6 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @notice The immutable ID of the attester controller bytes32 internal immutable controllerId; - /// @notice Mapping of vault addresses to their set of accepted attesters. - mapping(address vault => SetStorage) internal attesters; - - /// @notice Mapping of vault addresses to their policy storage. - mapping(address vault => PolicyStorage) internal policies; - - /// @notice Mapping of address prefixes to their operation counter. - mapping(bytes19 addressPrefix => uint256) internal operationCounters; - /// @notice Emitted when the attester controller ID is updated /// @param attesterControllerId The new attester controller ID event AttesterControllerUpdated(bytes32 attesterControllerId); @@ -184,7 +189,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @param attester The address of the attester to be added. function addPolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { - if (attesters[vault].insert(attester)) { + if (getHookTargetFirewallStorage().attesters[vault].insert(attester)) { emit AddPolicyAttester(vault, attester); } } @@ -193,7 +198,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @param attester The address of the attester to be removed. function removePolicyAttester(address vault, address attester) external onlyEVCAccountOwner onlyGovernor(vault) { - if (attesters[vault].remove(attester)) { + if (getHookTargetFirewallStorage().attesters[vault].remove(attester)) { emit RemovePolicyAttester(vault, attester); } } @@ -202,7 +207,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault. /// @return An array of addresses representing the accepted attesters for the specified vault. function getPolicyAttesters(address vault) external view returns (address[] memory) { - return attesters[vault].get(); + return getHookTargetFirewallStorage().attesters[vault].get(); } /// @notice Sets the policy thresholds for a given vault. @@ -221,7 +226,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { uint16 outConstantAmountThreshold, uint16 outAccumulatedAmountThreshold ) external onlyEVCAccountOwner onlyGovernor(vault) { - PolicyStorage storage policyStorage = policies[vault]; + PolicyStorage storage policyStorage = getHookTargetFirewallStorage().policies[vault]; policyStorage.operationCounterThreshold = operationCounterThreshold; policyStorage.inConstantAmountThreshold = AmountCap.wrap(inConstantAmountThreshold); policyStorage.inAccumulatedAmountThreshold = AmountCap.wrap(inAccumulatedAmountThreshold); @@ -247,7 +252,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @return outConstantAmountThreshold The constant amount threshold for outgoing transfers. /// @return outAccumulatedAmountThreshold The accumulated amount threshold for outgoing transfers. function getPolicyThresholds(address vault) external view returns (uint32, uint16, uint16, uint16, uint16) { - PolicyStorage storage policyStorage = policies[vault]; + PolicyStorage storage policyStorage = getHookTargetFirewallStorage().policies[vault]; return ( policyStorage.operationCounterThreshold, AmountCap.unwrap(policyStorage.inConstantAmountThreshold), @@ -270,7 +275,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { view returns (uint256, uint256, uint256, uint256, uint256) { - PolicyStorage storage policyStorage = policies[vault]; + PolicyStorage storage policyStorage = getHookTargetFirewallStorage().policies[vault]; return ( policyStorage.operationCounterThreshold, policyStorage.inConstantAmountThreshold.resolve(), @@ -285,7 +290,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param vault The address of the vault /// @return The current operation counter for the vault function getOperationCounter(address vault) external view returns (uint256) { - return updateVaultOperationCounter(policies[vault]) - 1; + return updateVaultOperationCounter(getHookTargetFirewallStorage().policies[vault]) - 1; } /// @notice Saves an attestation. @@ -359,12 +364,20 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { return 0; } + /// @notice Retrieves the storage struct for HookTargetFirewall + /// @return $ The HookTargetFirewallStorage struct storage slot + function getHookTargetFirewallStorage() internal pure returns (HookTargetFirewallStorage storage $) { + assembly { + $.slot := HookTargetFirewallStorageLocation + } + } + /// @notice Executes a checkpoint for a given transfer type and reference amount. /// @param transferType The type of transfer (In or Out). /// @param referenceAmount The reference amount for the transfer. /// @param hashable Additional data to be hashed. function executeCheckpoint(TransferType transferType, uint256 referenceAmount, bytes memory hashable) internal { - PolicyStorage memory policy = policies[msg.sender]; + PolicyStorage memory policy = getHookTargetFirewallStorage().policies[msg.sender]; if (!policy.isAuthenticated) { authenticateVault(msg.sender); } @@ -411,14 +424,14 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { } } else { // apply the operation counter update only if the checkpoint does not need to be executed - policies[msg.sender] = policy; + getHookTargetFirewallStorage().policies[msg.sender] = policy; } } /// @notice Authenticates the vault if it is a proxy deployed by the recognized eVault factory. function authenticateVault(address vault) internal { if (eVaultFactory.isProxy(msg.sender)) { - policies[vault].isAuthenticated = true; + getHookTargetFirewallStorage().policies[vault].isAuthenticated = true; emit AuthenticateVault(msg.sender); } else { revert HTA_Unauthorized(); @@ -429,6 +442,8 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param account The account for which the operation counter is updated. /// @return The updated operation counter. function updateAccountOperationCounter(address account) internal returns (uint256) { + mapping(bytes19 prefix => uint256 counter) storage operationCounters = + getHookTargetFirewallStorage().operationCounters; bytes19 prefix = _getAddressPrefix(account); uint256 counter = operationCounters[prefix] + 1; operationCounters[prefix] = counter; @@ -476,7 +491,8 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @return True if an attestation is in progress, false otherwise. function isAttestationInProgress() internal view returns (bool) { address currentAttester = validator.getCurrentAttester(); - return currentAttester != address(0) && attesters[msg.sender].contains(currentAttester); + return currentAttester != address(0) + && getHookTargetFirewallStorage().attesters[msg.sender].contains(currentAttester); } /// @notice Resolves the thresholds for a given vault and transfer type. From 06bf153fe14445302463d514d5860e1d6bb8780a Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 16 Oct 2024 15:39:06 +0200 Subject: [PATCH 07/13] fix: typo --- src/HookTarget/HookTargetFirewall.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol index 481c2e5a..afb3fd47 100644 --- a/src/HookTarget/HookTargetFirewall.sol +++ b/src/HookTarget/HookTargetFirewall.sol @@ -127,7 +127,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { ); /// @notice Error thrown when the caller is not authorized to perform an operation. - error HTA_Unauthorized(); + error HTF_Unauthorized(); /// @notice Constructor to initialize the contract with the EVC and validator addresses. /// @param _evc The address of the EVC contract. @@ -148,7 +148,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @notice Modifier to restrict access to only the governor of the specified vault. /// @param vault The address of the vault. modifier onlyGovernor(address vault) { - if (_msgSender() != IEVault(vault).governorAdmin()) revert HTA_Unauthorized(); + if (_msgSender() != IEVault(vault).governorAdmin()) revert HTF_Unauthorized(); _; } @@ -406,7 +406,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { // be // already defined by the validator contract if (!isAttestationInProgress()) { - revert HTA_Unauthorized(); + revert HTF_Unauthorized(); } } } else { @@ -421,7 +421,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { policies[vault].isAuthenticated = true; emit AuthenticateVault(msg.sender); } else { - revert HTA_Unauthorized(); + revert HTF_Unauthorized(); } } From e5240d6087388cb9c9a677cbbe3723d429704fdf Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 16 Oct 2024 23:29:37 +0200 Subject: [PATCH 08/13] feat: add HookTargetSplitter --- src/HookTarget/HookTargetSplitter.sol | 99 ++++++++++++ test/HookTarget/HookTargetSplitter.t.sol | 185 +++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 src/HookTarget/HookTargetSplitter.sol create mode 100644 test/HookTarget/HookTargetSplitter.t.sol diff --git a/src/HookTarget/HookTargetSplitter.sol b/src/HookTarget/HookTargetSplitter.sol new file mode 100644 index 00000000..6d642b84 --- /dev/null +++ b/src/HookTarget/HookTargetSplitter.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {Set, SetStorage} from "ethereum-vault-connector/Set.sol"; +import {RevertBytes} from "evk/EVault/shared/lib/RevertBytes.sol"; +import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; + +/// @title HookTargetSplitter +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice A hook target that delegates calls to a list of other hook targets. +contract HookTargetSplitter is IHookTarget { + using Set for SetStorage; + + /// @notice Storage for the set of hook target addresses + SetStorage internal hookTargetsSet; + + /// @notice Error thrown when an unexpected hook target is encountered + error HTS_UnexpectedHookTarget(); + + /// @notice Constructor to initialize the contract with the hook targets. + /// @param hookTargets The addresses of the hook targets. + constructor(address[] memory hookTargets) { + for (uint256 i = 0; i < hookTargets.length; ++i) { + hookTargetsSet.insert(hookTargets[i]); + } + } + + /// @notice Fallback function that delegates calls to all hook targets + fallback() external { + address[] memory hookTargets = hookTargetsSet.get(); + + for (uint256 i = 0; i < hookTargets.length; ++i) { + (bool success, bytes memory result) = hookTargets[i].delegatecall(msg.data); + if (!success) RevertBytes.revertBytes(result); + } + } + + /// @inheritdoc IHookTarget + /// @dev This function checks if all the hook targets are valid. Some hook targets might rely on the caller + /// address, so this function must delegatecall to the hook targets. + function isHookTarget() external view override returns (bytes4) { + address[] memory hookTargets = hookTargetsSet.get(); + function (address) internal view returns (bool) isHookTargetPtr = asView(isHookTarget); + + for (uint256 i = 0; i < hookTargets.length; ++i) { + if (!isHookTargetPtr(hookTargets[i])) return 0; + } + + return this.isHookTarget.selector; + } + + /// @notice Delegates a call to a specific hook target + /// @param hookTarget The address of the hook target to delegate the call to + /// @param data The calldata to be passed to the hook target + /// @return The result of the delegatecall + function delegatecallHookTarget(address hookTarget, bytes calldata data) external returns (bytes memory) { + if (!hookTargetsSet.contains(hookTarget)) revert HTS_UnexpectedHookTarget(); + + (bool success, bytes memory result) = hookTarget.delegatecall(data); + if (!success) RevertBytes.revertBytes(result); + + return result; + } + + /// @notice Retrieves the list of hook targets + /// @return An array of addresses representing the hook targets + function getHookTargets() external view returns (address[] memory) { + return hookTargetsSet.get(); + } + + /// @notice Checks if the given address is a valid hook target + /// @param hookTarget The address of the hook target to check + /// @return A boolean indicating whether the address is a valid hook target + function isHookTarget(address hookTarget) internal returns (bool) { + (bool success, bytes memory result) = hookTarget.delegatecall(abi.encodeCall(IHookTarget.isHookTarget, ())); + + if (success && result.length == 32 && abi.decode(result, (bytes4)) == this.isHookTarget.selector) { + return true; + } + + return false; + } + + /// @notice Cast the state mutability of a function pointer from `non-view` to `view`. + /// @dev Credit to [z0age](https://twitter.com/z0age/status/1654922202930888704) for this trick. + /// @param fn A pointer to a function with `non-view` (default) state mutability. + /// @return fnAsView A pointer to the same function with its state mutability cast to `view`. + function asView(function (address) internal returns (bool) fn) + internal + pure + returns (function (address) internal view returns (bool) fnAsView) + { + assembly { + fnAsView := fn + } + } +} diff --git a/test/HookTarget/HookTargetSplitter.t.sol b/test/HookTarget/HookTargetSplitter.t.sol new file mode 100644 index 00000000..9d5e4304 --- /dev/null +++ b/test/HookTarget/HookTargetSplitter.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.23; + +import {EVaultTestBase} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; +import {HookTargetSplitter} from "../../src/HookTarget/HookTargetSplitter.sol"; +import "evk/EVault/shared/Constants.sol"; + +contract HookTargetMock is IHookTarget { + bytes32 internal constant expectedVaultLocation = keccak256("expectedVault"); + bytes32 internal constant expectedCalldataHashLocation = keccak256("expectedCalldataHash"); + + function setExpectedVault(address vault) external { + bytes32 slot = expectedVaultLocation; + assembly { + sstore(slot, vault) + } + } + + function setExpectedCalldataHash(bytes32 dataHash) external { + bytes32 slot = expectedCalldataHashLocation; + assembly { + sstore(slot, dataHash) + } + } + + function getExpectedVault() public view returns (address _expectedVault) { + bytes32 slot = expectedVaultLocation; + assembly { + _expectedVault := sload(slot) + } + } + + function getExpectedCalldataHash() public view returns (bytes32 _expectedCalldataHash) { + bytes32 slot = expectedCalldataHashLocation; + assembly { + _expectedCalldataHash := sload(slot) + } + } + + function isHookTarget() external view override returns (bytes4) { + require(msg.sender == getExpectedVault(), "isHookTarget: Invalid vault"); + return this.isHookTarget.selector; + } + + fallback() external { + require(msg.sender == getExpectedVault(), "fallback: Invalid vault"); + require(keccak256(msg.data) == getExpectedCalldataHash(), "fallback: Invalid calldata"); + } +} + +contract HookTargetMockFaulty is IHookTarget { + function isHookTarget() external pure override returns (bytes4) { + return 0; + } +} + +contract HookTargetSplitterTest is EVaultTestBase { + HookTargetSplitter hookTargetSplitter; + HookTargetSplitter hookTargetSplitterFaulty; + HookTargetMock hookTargetMock1; + HookTargetMock hookTargetMock2; + HookTargetMockFaulty hookTargetMockFaulty; + + function setUp() public virtual override { + super.setUp(); + + hookTargetMock1 = new HookTargetMock(); + hookTargetMock2 = new HookTargetMock(); + hookTargetMockFaulty = new HookTargetMockFaulty(); + + address[] memory hookTargets = new address[](2); + hookTargets[0] = address(hookTargetMock1); + hookTargets[1] = address(hookTargetMock2); + hookTargetSplitter = new HookTargetSplitter(hookTargets); + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock1), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) + ); + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock2), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) + ); + + hookTargets = new address[](1); + hookTargets[0] = address(hookTargetMockFaulty); + hookTargetSplitterFaulty = new HookTargetSplitter(hookTargets); + } + + function test_constructor() public { + address[] memory hookTargets = new address[](11); + for (uint160 i = 0; i < hookTargets.length; ++i) { + hookTargets[i] = address(i); + } + vm.expectRevert(); + new HookTargetSplitter(hookTargets); + + hookTargets = new address[](1); + new HookTargetSplitter(hookTargets); + } + + function test_isHookTarget() public { + eTST.setHookConfig(address(hookTargetSplitter), 5 | 10); + (address hookTarget, uint32 hookedOps) = eTST.hookConfig(); + assertEq(hookTarget, address(hookTargetSplitter)); + assertEq(hookedOps, 5 | 10); + + vm.expectRevert(); + eTST.setHookConfig(address(hookTargetSplitterFaulty), 5 | 10); + } + + function test_fallback() public { + eTST.setHookConfig(address(hookTargetSplitter), OP_SKIM | OP_TOUCH); + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock1), + abi.encodeCall( + HookTargetMock.setExpectedCalldataHash, + ( + keccak256( + abi.encodePacked( + abi.encodeCall(eTST.touch, ()), + abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), + address(this) + ) + ) + ) + ) + ); + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock2), + abi.encodeCall( + HookTargetMock.setExpectedCalldataHash, + ( + keccak256( + abi.encodePacked( + abi.encodeCall(eTST.touch, ()), + abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), + address(this) + ) + ) + ) + ) + ); + + vm.expectRevert(); + hookTargetSplitter.delegatecallHookTarget(address(hookTargetSplitterFaulty), ""); + + vm.expectRevert(); + eTST.skim(0, address(0)); + + eTST.touch(); + + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock1), + abi.encodeCall( + HookTargetMock.setExpectedCalldataHash, + ( + keccak256( + abi.encodePacked( + abi.encodeCall(eTST.skim, (0, address(0))), + abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), + address(this) + ) + ) + ) + ) + ); + hookTargetSplitter.delegatecallHookTarget( + address(hookTargetMock2), + abi.encodeCall( + HookTargetMock.setExpectedCalldataHash, + ( + keccak256( + abi.encodePacked( + abi.encodeCall(eTST.skim, (0, address(0))), + abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), + address(this) + ) + ) + ) + ) + ); + + eTST.skim(0, address(0)); + } +} From 6a882e63ed027c07a38ffdcbe9a679684ef4a01d Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Tue, 22 Oct 2024 18:06:58 +0200 Subject: [PATCH 09/13] refactor: HookTargetSplitter --- src/HookTarget/HookTargetSplitter.sol | 43 ++++++++++++---- test/HookTarget/HookTargetSplitter.t.sol | 65 +++++++++++++++--------- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/HookTarget/HookTargetSplitter.sol b/src/HookTarget/HookTargetSplitter.sol index 6d642b84..3ba25056 100644 --- a/src/HookTarget/HookTargetSplitter.sol +++ b/src/HookTarget/HookTargetSplitter.sol @@ -3,8 +3,10 @@ pragma solidity ^0.8.0; import {Set, SetStorage} from "ethereum-vault-connector/Set.sol"; +import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; import {RevertBytes} from "evk/EVault/shared/lib/RevertBytes.sol"; import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; /// @title HookTargetSplitter /// @custom:security-contact security@euler.xyz @@ -13,17 +15,34 @@ import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; contract HookTargetSplitter is IHookTarget { using Set for SetStorage; + /// @notice The vault associated with this contract + IEVault internal immutable vault; + /// @notice Storage for the set of hook target addresses SetStorage internal hookTargetsSet; + /// @notice Error thrown when the caller is not authorized to perform an operation + error HTS_Unauthorized(); + /// @notice Error thrown when an unexpected hook target is encountered error HTS_UnexpectedHookTarget(); + /// @notice Modifier to restrict access to only the governor of the vault + modifier onlyGovernor() { + if (msg.sender != vault.governorAdmin()) revert HTS_Unauthorized(); + _; + } + /// @notice Constructor to initialize the contract with the hook targets. - /// @param hookTargets The addresses of the hook targets. - constructor(address[] memory hookTargets) { - for (uint256 i = 0; i < hookTargets.length; ++i) { - hookTargetsSet.insert(hookTargets[i]); + /// @param _eVaultFactory The address of the EVault factory contract. + /// @param _vault The address of the vault associated with this contract. + /// @param _hookTargets The addresses of the hook targets. + constructor(address _eVaultFactory, address _vault, address[] memory _hookTargets) { + require(GenericFactory(_eVaultFactory).isProxy(_vault), "HTS: Invalid vault"); + vault = IEVault(_vault); + + for (uint256 i = 0; i < _hookTargets.length; ++i) { + hookTargetsSet.insert(_hookTargets[i]); } } @@ -41,8 +60,8 @@ contract HookTargetSplitter is IHookTarget { /// @dev This function checks if all the hook targets are valid. Some hook targets might rely on the caller /// address, so this function must delegatecall to the hook targets. function isHookTarget() external view override returns (bytes4) { - address[] memory hookTargets = hookTargetsSet.get(); function (address) internal view returns (bool) isHookTargetPtr = asView(isHookTarget); + address[] memory hookTargets = hookTargetsSet.get(); for (uint256 i = 0; i < hookTargets.length; ++i) { if (!isHookTargetPtr(hookTargets[i])) return 0; @@ -51,11 +70,11 @@ contract HookTargetSplitter is IHookTarget { return this.isHookTarget.selector; } - /// @notice Delegates a call to a specific hook target - /// @param hookTarget The address of the hook target to delegate the call to - /// @param data The calldata to be passed to the hook target + /// @notice Forwards the call by delegatecalling to a specific hook target + /// @param hookTarget The address of the hook target to delegatecall + /// @param data The calldata to be called on the hook target /// @return The result of the delegatecall - function delegatecallHookTarget(address hookTarget, bytes calldata data) external returns (bytes memory) { + function forwardCall(address hookTarget, bytes calldata data) external onlyGovernor returns (bytes memory) { if (!hookTargetsSet.contains(hookTarget)) revert HTS_UnexpectedHookTarget(); (bool success, bytes memory result) = hookTarget.delegatecall(data); @@ -64,6 +83,12 @@ contract HookTargetSplitter is IHookTarget { return result; } + /// @notice Retrieves the address of the vault associated with this contract + /// @return The address of the vault + function getVault() external view returns (address) { + return address(vault); + } + /// @notice Retrieves the list of hook targets /// @return An array of addresses representing the hook targets function getHookTargets() external view returns (address[] memory) { diff --git a/test/HookTarget/HookTargetSplitter.t.sol b/test/HookTarget/HookTargetSplitter.t.sol index 9d5e4304..c8ccac95 100644 --- a/test/HookTarget/HookTargetSplitter.t.sol +++ b/test/HookTarget/HookTargetSplitter.t.sol @@ -73,29 +73,38 @@ contract HookTargetSplitterTest is EVaultTestBase { address[] memory hookTargets = new address[](2); hookTargets[0] = address(hookTargetMock1); hookTargets[1] = address(hookTargetMock2); - hookTargetSplitter = new HookTargetSplitter(hookTargets); - hookTargetSplitter.delegatecallHookTarget( + hookTargetSplitter = new HookTargetSplitter(address(factory), address(eTST), hookTargets); + hookTargetSplitter.forwardCall( address(hookTargetMock1), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) ); - hookTargetSplitter.delegatecallHookTarget( + hookTargetSplitter.forwardCall( address(hookTargetMock2), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) ); hookTargets = new address[](1); hookTargets[0] = address(hookTargetMockFaulty); - hookTargetSplitterFaulty = new HookTargetSplitter(hookTargets); + hookTargetSplitterFaulty = new HookTargetSplitter(address(factory), address(eTST), hookTargets); } function test_constructor() public { - address[] memory hookTargets = new address[](11); + address[] memory hookTargets = new address[](1); + + vm.expectRevert(); + new HookTargetSplitter(address(0), address(eTST), hookTargets); + + vm.expectRevert(); + new HookTargetSplitter(address(factory), address(0), hookTargets); + + // succeeds + new HookTargetSplitter(address(factory), address(eTST), hookTargets); + + hookTargets = new address[](11); for (uint160 i = 0; i < hookTargets.length; ++i) { hookTargets[i] = address(i); } - vm.expectRevert(); - new HookTargetSplitter(hookTargets); - hookTargets = new address[](1); - new HookTargetSplitter(hookTargets); + vm.expectRevert(); + new HookTargetSplitter(address(factory), address(eTST), hookTargets); } function test_isHookTarget() public { @@ -110,22 +119,28 @@ contract HookTargetSplitterTest is EVaultTestBase { function test_fallback() public { eTST.setHookConfig(address(hookTargetSplitter), OP_SKIM | OP_TOUCH); - hookTargetSplitter.delegatecallHookTarget( - address(hookTargetMock1), - abi.encodeCall( - HookTargetMock.setExpectedCalldataHash, - ( - keccak256( - abi.encodePacked( - abi.encodeCall(eTST.touch, ()), - abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), - address(this) - ) + bytes memory data = abi.encodeCall( + HookTargetMock.setExpectedCalldataHash, + ( + keccak256( + abi.encodePacked( + abi.encodeCall(eTST.touch, ()), + abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), + address(this) ) ) ) ); - hookTargetSplitter.delegatecallHookTarget( + + // fails if non-vault governor calls + vm.prank(address(1)); + vm.expectRevert(); + hookTargetSplitter.forwardCall(address(hookTargetMock1), data); + + // succeeds if vault governor calls + hookTargetSplitter.forwardCall(address(hookTargetMock1), data); + + hookTargetSplitter.forwardCall( address(hookTargetMock2), abi.encodeCall( HookTargetMock.setExpectedCalldataHash, @@ -142,14 +157,14 @@ contract HookTargetSplitterTest is EVaultTestBase { ); vm.expectRevert(); - hookTargetSplitter.delegatecallHookTarget(address(hookTargetSplitterFaulty), ""); + hookTargetSplitter.forwardCall(address(hookTargetSplitterFaulty), ""); vm.expectRevert(); eTST.skim(0, address(0)); - + eTST.touch(); - hookTargetSplitter.delegatecallHookTarget( + hookTargetSplitter.forwardCall( address(hookTargetMock1), abi.encodeCall( HookTargetMock.setExpectedCalldataHash, @@ -164,7 +179,7 @@ contract HookTargetSplitterTest is EVaultTestBase { ) ) ); - hookTargetSplitter.delegatecallHookTarget( + hookTargetSplitter.forwardCall( address(hookTargetMock2), abi.encodeCall( HookTargetMock.setExpectedCalldataHash, From 3ff225d5edd8640edbe416ed070e51bab13c9ef0 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 20 Nov 2024 18:40:15 +0900 Subject: [PATCH 10/13] feat: allow trusted origin to bypass attestation --- src/HookTarget/HookTargetFirewall.sol | 43 +++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol index b85ce46f..808c8d8c 100644 --- a/src/HookTarget/HookTargetFirewall.sol +++ b/src/HookTarget/HookTargetFirewall.sol @@ -41,6 +41,8 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { struct PolicyStorage { /// @notice Whether the vault is authenticated. bool isAuthenticated; + /// @notice Whether the vault allows trusted origin address to bypass the attestation check. + bool allowTrustedOrigin; /// @notice The max operations counter threshold per 3 minutes that can be executed without attestation. uint32 operationCounterThreshold; /// @notice The normalized timestamp of the last update, rounded down to the nearest one minute interval. @@ -70,6 +72,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @custom:storage-location erc7201:euler.storage.HookTargetFirewall struct HookTargetFirewallStorage { /// @notice Mapping of vault addresses to their set of accepted attesters. + /// @dev This set is also used to store the trusted origin addresses. mapping(address vault => SetStorage) attesters; /// @notice Mapping of vault addresses to their policy storage. mapping(address vault => PolicyStorage) policies; @@ -114,6 +117,12 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { /// @param attester The address of the attester. event RemovePolicyAttester(address indexed vault, address attester); + /// @notice Emitted when the allowed trusted origin address is set for a vault. + /// @param vault The address of the vault. + /// @param allowTrustedOrigin A boolean indicating whether trusted origin is allowed to bypass the attestation + /// check. + event SetAllowTrustedOrigin(address indexed vault, bool allowTrustedOrigin); + /// @notice Emitted when a policy thresholds are set for a vault. /// @param vault The address of the vault. /// @param operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed @@ -210,6 +219,25 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { return getHookTargetFirewallStorage().attesters[vault].get(); } + /// @notice Sets whether the vault allows trusted origin address to bypass the attestation check. + /// @param vault The address of the vault. + /// @param allowTrustedOrigin A boolean indicating whether to allow trusted origin to bypass the attestation check. + function setAllowTrustedOrigin(address vault, bool allowTrustedOrigin) + external + onlyEVCAccountOwner + onlyGovernor(vault) + { + getHookTargetFirewallStorage().policies[vault].allowTrustedOrigin = allowTrustedOrigin; + emit SetAllowTrustedOrigin(vault, allowTrustedOrigin); + } + + /// @notice Retrieves whether the vault allows trusted origin address to bypass the attestation check. + /// @param vault The address of the vault. + /// @return A boolean indicating whether trusted origin is allowed to bypass the attestation check. + function getAllowTrustedOrigin(address vault) external view returns (bool) { + return getHookTargetFirewallStorage().policies[vault].allowTrustedOrigin; + } + /// @notice Sets the policy thresholds for a given vault. /// @param vault The address of the vault. /// @param operationCounterThreshold The max operations counter threshold per 3 minutes that can be executed @@ -393,7 +421,7 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { vaultOperationCounter >= operationCounterThreshold || referenceAmount >= constantAmountThreshold || accumulatedAmount >= accumulatedAmountThreshold ) { - if (!evc.isSimulationInProgress()) { + if (!evc.isSimulationInProgress() && (!policy.allowTrustedOrigin || !isTrustedOrigin())) { // to prevent replay attacks, the hash must depend on: // - the vault address that is a caller of the hook target // - the operation type executed (function selector) @@ -415,15 +443,14 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { ); // this check must be done after the checkpoint is executed so that at this point, in case the - // storeAttestation function is used instead of the saveAttestation function, the current attester must - // be - // already defined by the validator contract + // storeAttestation function is used on the validator instead of the saveAttestation function, + // the current attester must be already defined by the validator contract if (!isAttestationInProgress()) { revert HTA_Unauthorized(); } } } else { - // apply the operation counter update only if the checkpoint does not need to be executed + // apply the vault operation counter update only if the checkpoint does not need to be executed getHookTargetFirewallStorage().policies[msg.sender] = policy; } } @@ -495,6 +522,12 @@ contract HookTargetFirewall is IHookTarget, EVCUtil { && getHookTargetFirewallStorage().attesters[msg.sender].contains(currentAttester); } + /// @notice Checks if the transaction origin is trusted. + /// @return True if the transaction origin is trusted, false otherwise. + function isTrustedOrigin() internal view returns (bool) { + return getHookTargetFirewallStorage().attesters[msg.sender].contains(tx.origin); + } + /// @notice Resolves the thresholds for a given vault and transfer type. /// @param transferType The type of transfer (In or Out). /// @param policy The policy for the vault. From c1ff96f0eb8c1657800240f99a0ce535fbdc99c9 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 20 Nov 2024 19:40:04 +0900 Subject: [PATCH 11/13] factor out HookTargetSplitter --- src/HookTarget/HookTargetSplitter.sol | 124 -------------- test/HookTarget/HookTargetSplitter.t.sol | 200 ----------------------- 2 files changed, 324 deletions(-) delete mode 100644 src/HookTarget/HookTargetSplitter.sol delete mode 100644 test/HookTarget/HookTargetSplitter.t.sol diff --git a/src/HookTarget/HookTargetSplitter.sol b/src/HookTarget/HookTargetSplitter.sol deleted file mode 100644 index 3ba25056..00000000 --- a/src/HookTarget/HookTargetSplitter.sol +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.8.0; - -import {Set, SetStorage} from "ethereum-vault-connector/Set.sol"; -import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; -import {RevertBytes} from "evk/EVault/shared/lib/RevertBytes.sol"; -import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; -import {IEVault} from "evk/EVault/IEVault.sol"; - -/// @title HookTargetSplitter -/// @custom:security-contact security@euler.xyz -/// @author Euler Labs (https://www.eulerlabs.com/) -/// @notice A hook target that delegates calls to a list of other hook targets. -contract HookTargetSplitter is IHookTarget { - using Set for SetStorage; - - /// @notice The vault associated with this contract - IEVault internal immutable vault; - - /// @notice Storage for the set of hook target addresses - SetStorage internal hookTargetsSet; - - /// @notice Error thrown when the caller is not authorized to perform an operation - error HTS_Unauthorized(); - - /// @notice Error thrown when an unexpected hook target is encountered - error HTS_UnexpectedHookTarget(); - - /// @notice Modifier to restrict access to only the governor of the vault - modifier onlyGovernor() { - if (msg.sender != vault.governorAdmin()) revert HTS_Unauthorized(); - _; - } - - /// @notice Constructor to initialize the contract with the hook targets. - /// @param _eVaultFactory The address of the EVault factory contract. - /// @param _vault The address of the vault associated with this contract. - /// @param _hookTargets The addresses of the hook targets. - constructor(address _eVaultFactory, address _vault, address[] memory _hookTargets) { - require(GenericFactory(_eVaultFactory).isProxy(_vault), "HTS: Invalid vault"); - vault = IEVault(_vault); - - for (uint256 i = 0; i < _hookTargets.length; ++i) { - hookTargetsSet.insert(_hookTargets[i]); - } - } - - /// @notice Fallback function that delegates calls to all hook targets - fallback() external { - address[] memory hookTargets = hookTargetsSet.get(); - - for (uint256 i = 0; i < hookTargets.length; ++i) { - (bool success, bytes memory result) = hookTargets[i].delegatecall(msg.data); - if (!success) RevertBytes.revertBytes(result); - } - } - - /// @inheritdoc IHookTarget - /// @dev This function checks if all the hook targets are valid. Some hook targets might rely on the caller - /// address, so this function must delegatecall to the hook targets. - function isHookTarget() external view override returns (bytes4) { - function (address) internal view returns (bool) isHookTargetPtr = asView(isHookTarget); - address[] memory hookTargets = hookTargetsSet.get(); - - for (uint256 i = 0; i < hookTargets.length; ++i) { - if (!isHookTargetPtr(hookTargets[i])) return 0; - } - - return this.isHookTarget.selector; - } - - /// @notice Forwards the call by delegatecalling to a specific hook target - /// @param hookTarget The address of the hook target to delegatecall - /// @param data The calldata to be called on the hook target - /// @return The result of the delegatecall - function forwardCall(address hookTarget, bytes calldata data) external onlyGovernor returns (bytes memory) { - if (!hookTargetsSet.contains(hookTarget)) revert HTS_UnexpectedHookTarget(); - - (bool success, bytes memory result) = hookTarget.delegatecall(data); - if (!success) RevertBytes.revertBytes(result); - - return result; - } - - /// @notice Retrieves the address of the vault associated with this contract - /// @return The address of the vault - function getVault() external view returns (address) { - return address(vault); - } - - /// @notice Retrieves the list of hook targets - /// @return An array of addresses representing the hook targets - function getHookTargets() external view returns (address[] memory) { - return hookTargetsSet.get(); - } - - /// @notice Checks if the given address is a valid hook target - /// @param hookTarget The address of the hook target to check - /// @return A boolean indicating whether the address is a valid hook target - function isHookTarget(address hookTarget) internal returns (bool) { - (bool success, bytes memory result) = hookTarget.delegatecall(abi.encodeCall(IHookTarget.isHookTarget, ())); - - if (success && result.length == 32 && abi.decode(result, (bytes4)) == this.isHookTarget.selector) { - return true; - } - - return false; - } - - /// @notice Cast the state mutability of a function pointer from `non-view` to `view`. - /// @dev Credit to [z0age](https://twitter.com/z0age/status/1654922202930888704) for this trick. - /// @param fn A pointer to a function with `non-view` (default) state mutability. - /// @return fnAsView A pointer to the same function with its state mutability cast to `view`. - function asView(function (address) internal returns (bool) fn) - internal - pure - returns (function (address) internal view returns (bool) fnAsView) - { - assembly { - fnAsView := fn - } - } -} diff --git a/test/HookTarget/HookTargetSplitter.t.sol b/test/HookTarget/HookTargetSplitter.t.sol deleted file mode 100644 index c8ccac95..00000000 --- a/test/HookTarget/HookTargetSplitter.t.sol +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.8.23; - -import {EVaultTestBase} from "evk-test/unit/evault/EVaultTestBase.t.sol"; -import {IHookTarget} from "evk/interfaces/IHookTarget.sol"; -import {HookTargetSplitter} from "../../src/HookTarget/HookTargetSplitter.sol"; -import "evk/EVault/shared/Constants.sol"; - -contract HookTargetMock is IHookTarget { - bytes32 internal constant expectedVaultLocation = keccak256("expectedVault"); - bytes32 internal constant expectedCalldataHashLocation = keccak256("expectedCalldataHash"); - - function setExpectedVault(address vault) external { - bytes32 slot = expectedVaultLocation; - assembly { - sstore(slot, vault) - } - } - - function setExpectedCalldataHash(bytes32 dataHash) external { - bytes32 slot = expectedCalldataHashLocation; - assembly { - sstore(slot, dataHash) - } - } - - function getExpectedVault() public view returns (address _expectedVault) { - bytes32 slot = expectedVaultLocation; - assembly { - _expectedVault := sload(slot) - } - } - - function getExpectedCalldataHash() public view returns (bytes32 _expectedCalldataHash) { - bytes32 slot = expectedCalldataHashLocation; - assembly { - _expectedCalldataHash := sload(slot) - } - } - - function isHookTarget() external view override returns (bytes4) { - require(msg.sender == getExpectedVault(), "isHookTarget: Invalid vault"); - return this.isHookTarget.selector; - } - - fallback() external { - require(msg.sender == getExpectedVault(), "fallback: Invalid vault"); - require(keccak256(msg.data) == getExpectedCalldataHash(), "fallback: Invalid calldata"); - } -} - -contract HookTargetMockFaulty is IHookTarget { - function isHookTarget() external pure override returns (bytes4) { - return 0; - } -} - -contract HookTargetSplitterTest is EVaultTestBase { - HookTargetSplitter hookTargetSplitter; - HookTargetSplitter hookTargetSplitterFaulty; - HookTargetMock hookTargetMock1; - HookTargetMock hookTargetMock2; - HookTargetMockFaulty hookTargetMockFaulty; - - function setUp() public virtual override { - super.setUp(); - - hookTargetMock1 = new HookTargetMock(); - hookTargetMock2 = new HookTargetMock(); - hookTargetMockFaulty = new HookTargetMockFaulty(); - - address[] memory hookTargets = new address[](2); - hookTargets[0] = address(hookTargetMock1); - hookTargets[1] = address(hookTargetMock2); - hookTargetSplitter = new HookTargetSplitter(address(factory), address(eTST), hookTargets); - hookTargetSplitter.forwardCall( - address(hookTargetMock1), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) - ); - hookTargetSplitter.forwardCall( - address(hookTargetMock2), abi.encodeCall(HookTargetMock.setExpectedVault, address(eTST)) - ); - - hookTargets = new address[](1); - hookTargets[0] = address(hookTargetMockFaulty); - hookTargetSplitterFaulty = new HookTargetSplitter(address(factory), address(eTST), hookTargets); - } - - function test_constructor() public { - address[] memory hookTargets = new address[](1); - - vm.expectRevert(); - new HookTargetSplitter(address(0), address(eTST), hookTargets); - - vm.expectRevert(); - new HookTargetSplitter(address(factory), address(0), hookTargets); - - // succeeds - new HookTargetSplitter(address(factory), address(eTST), hookTargets); - - hookTargets = new address[](11); - for (uint160 i = 0; i < hookTargets.length; ++i) { - hookTargets[i] = address(i); - } - - vm.expectRevert(); - new HookTargetSplitter(address(factory), address(eTST), hookTargets); - } - - function test_isHookTarget() public { - eTST.setHookConfig(address(hookTargetSplitter), 5 | 10); - (address hookTarget, uint32 hookedOps) = eTST.hookConfig(); - assertEq(hookTarget, address(hookTargetSplitter)); - assertEq(hookedOps, 5 | 10); - - vm.expectRevert(); - eTST.setHookConfig(address(hookTargetSplitterFaulty), 5 | 10); - } - - function test_fallback() public { - eTST.setHookConfig(address(hookTargetSplitter), OP_SKIM | OP_TOUCH); - bytes memory data = abi.encodeCall( - HookTargetMock.setExpectedCalldataHash, - ( - keccak256( - abi.encodePacked( - abi.encodeCall(eTST.touch, ()), - abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), - address(this) - ) - ) - ) - ); - - // fails if non-vault governor calls - vm.prank(address(1)); - vm.expectRevert(); - hookTargetSplitter.forwardCall(address(hookTargetMock1), data); - - // succeeds if vault governor calls - hookTargetSplitter.forwardCall(address(hookTargetMock1), data); - - hookTargetSplitter.forwardCall( - address(hookTargetMock2), - abi.encodeCall( - HookTargetMock.setExpectedCalldataHash, - ( - keccak256( - abi.encodePacked( - abi.encodeCall(eTST.touch, ()), - abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), - address(this) - ) - ) - ) - ) - ); - - vm.expectRevert(); - hookTargetSplitter.forwardCall(address(hookTargetSplitterFaulty), ""); - - vm.expectRevert(); - eTST.skim(0, address(0)); - - eTST.touch(); - - hookTargetSplitter.forwardCall( - address(hookTargetMock1), - abi.encodeCall( - HookTargetMock.setExpectedCalldataHash, - ( - keccak256( - abi.encodePacked( - abi.encodeCall(eTST.skim, (0, address(0))), - abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), - address(this) - ) - ) - ) - ) - ); - hookTargetSplitter.forwardCall( - address(hookTargetMock2), - abi.encodeCall( - HookTargetMock.setExpectedCalldataHash, - ( - keccak256( - abi.encodePacked( - abi.encodeCall(eTST.skim, (0, address(0))), - abi.encodePacked(bytes4(0), eTST.asset(), eTST.oracle(), eTST.unitOfAccount()), - address(this) - ) - ) - ) - ) - ); - - eTST.skim(0, address(0)); - } -} From 8d5eae9b071bdc7f3366124ddcf6d7c27ef01c70 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 20 Nov 2024 22:57:15 +0900 Subject: [PATCH 12/13] chore: improve docs --- docs/firewall.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/firewall.md b/docs/firewall.md index 3d48cdb4..52d50a4a 100644 --- a/docs/firewall.md +++ b/docs/firewall.md @@ -9,14 +9,15 @@ The primary purpose of the `HookTargetFirewall` is to enforce security policies ## Key Features 1. Each vault can have its own security policy, including: - - A set of accepted attesters + - A set of accepted attesters (including trusted origin addresses) - Thresholds for incoming and outgoing transfers (both constant and accumulated within a transaction) - An operation counter threshold to limit the frequency of operations that do not require attestation 2. The contract intercepts key vault operations like `deposit`, `withdraw`, `mint`, `redeem`, `borrow`, and `repay`, validating them against the stored policy. 3. For transactions exceeding defined thresholds, `HookTargetFirewall` requires an appropriate attestation to be obtained and saved in the `SecurityValidator` contract prior to the operation being executed. 4. The contract implements a sliding window mechanism to track frequency of operations that do not require attestation, using bit manipulation for gas-efficient storage and calculation. 5. The contract implements an operation counter to prevent replay attacks and preserve the integrity of operations even if they do not require attestation. Operation counter is incremented for each intercepted operation. -6. The firewall ensures that only authorized vaults (proxies deployed by the recognized EVault factory) can use it. +6. The contract allows to specify trusted origin addresses which are allowed to bypass the attestation checks. +7. The contract ensures that only authorized vaults (proxies deployed by the recognized EVault factory) can use it. ## How It Works From d77c6ae21a575de3beece6b7d090ffee4b500e1a Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Wed, 20 Nov 2024 19:38:32 +0900 Subject: [PATCH 13/13] chore: add test --- remappings.txt | 2 +- test/HookTarget/HookTargetFirewall.sol | 1329 ++++++++++++++++++++++++ 2 files changed, 1330 insertions(+), 1 deletion(-) create mode 100644 test/HookTarget/HookTargetFirewall.sol diff --git a/remappings.txt b/remappings.txt index 1419b5c3..d516b926 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,4 +8,4 @@ euler-price-oracle-test/=lib/euler-price-oracle/test/ fee-flow/=lib/fee-flow/src/ reward-streams/=lib/reward-streams/src/ @openzeppelin/contracts/=lib/euler-price-oracle/lib/openzeppelin-contracts/contracts/ -solady/=lib/euler-price-oracle/lib/solady/src/ +solady/=lib/solady/src/ \ No newline at end of file diff --git a/test/HookTarget/HookTargetFirewall.sol b/test/HookTarget/HookTargetFirewall.sol new file mode 100644 index 00000000..3965eb7b --- /dev/null +++ b/test/HookTarget/HookTargetFirewall.sol @@ -0,0 +1,1329 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol"; +import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; +import {IEVC} from "ethereum-vault-connector/interfaces/IEthereumVaultConnector.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; +import {EVaultTestBase} from "evk-test/unit/evault/EVaultTestBase.t.sol"; +import {HookTargetFirewall, ISecurityValidator} from "../../src/HookTarget/HookTargetFirewall.sol"; +import "evk/EVault/shared/Constants.sol"; + +contract HookTargetFirewallTest is EVaultTestBase { + ISecurityValidator internal securityValidator; + HookTargetFirewall internal hookTarget; + + function setUp() public virtual override { + super.setUp(); + + string memory FORK_RPC_URL = vm.envOr("FORK_RPC_URL", string("")); + if (bytes(FORK_RPC_URL).length != 0) vm.createSelectFork(FORK_RPC_URL); + + securityValidator = ISecurityValidator(0xc9b1AeD0895Dd647A82e35Cafff421B6CcFe690C); + hookTarget = + new HookTargetFirewall(address(evc), address(factory), address(securityValidator), keccak256("test")); + + eTST.setHookConfig( + address(hookTarget), + OP_DEPOSIT | OP_MINT | OP_WITHDRAW | OP_REDEEM | OP_SKIM | OP_BORROW | OP_REPAY | OP_VAULT_STATUS_CHECK + ); + + eTST2.setHookConfig( + address(hookTarget), + OP_DEPOSIT | OP_MINT | OP_WITHDRAW | OP_REDEEM | OP_SKIM | OP_BORROW | OP_REPAY | OP_VAULT_STATUS_CHECK + ); + } + + function log1_01(uint256 x) internal pure returns (uint256) { + if (x == 0) return type(uint256).max; + + // log1.01(x) = ln(x) / ln(1.01) = lnWad(x * 1e18) / lnWad(1.01 * 1e18) + return uint256(FixedPointMathLib.lnWad(int256(x * 1e18))) / 9950330853168082; + } + + function test_isHookTarget() public { + GenericFactory unrecognizedFactory = new GenericFactory(admin); + address eVaultImpl = factory.implementation(); + + vm.prank(admin); + unrecognizedFactory.setImplementation(eVaultImpl); + + IEVault unrecognizedEVault = IEVault( + unrecognizedFactory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(0), address(0)) + ) + ); + + vm.expectRevert(); + unrecognizedEVault.setHookConfig(address(hookTarget), 1); + } + + function test_saveAttestation( + uint256 privateKey, + uint40 timestamp, + uint40 timeout, + bytes32[] memory executionHashes + ) public { + vm.assume( + privateKey > 0 + && privateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 + ); + vm.assume(uint256(timestamp) + uint256(timeout) <= type(uint40).max); + vm.warp(timestamp); + + address attester = vm.addr(privateKey); + ISecurityValidator.Attestation memory attestation = + ISecurityValidator.Attestation({deadline: timestamp + timeout, executionHashes: executionHashes}); + + bytes32 hashOfAttestation = securityValidator.hashAttestation(attestation); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hashOfAttestation); + + uint256 snapshot = vm.snapshot(); + hookTarget.saveAttestation(attestation, abi.encodePacked(r, s, v)); + assertEq(securityValidator.getCurrentAttester(), attester); + + // incorrect signature + vm.revertTo(snapshot); + r = keccak256(abi.encode(r)); + try hookTarget.saveAttestation(attestation, abi.encodePacked(r, s, v)) { + assertNotEq(securityValidator.getCurrentAttester(), attester); + } catch (bytes memory err) { + assertEq(err, abi.encodeWithSelector(ECDSA.ECDSAInvalidSignature.selector)); + } + } + + function test_governanceFunctions( + address vault, + address governor, + address firstAttester, + uint8 numberOfAttesters, + uint16[] memory thresholds + ) public { + vm.assume(uint160(vault) > 255 && vault.code.length == 0 && vault != governor); + vm.assume(governor != address(0) && governor != address(evc) && governor != address(this)); + vm.assume(firstAttester != address(0)); + vm.assume(numberOfAttesters > 0 && numberOfAttesters <= 10); + vm.assume(thresholds.length >= 5); + vm.etch(vault, address(eTST).code); + address attester = firstAttester; + + // if non-governor calling, revert + vm.startPrank(governor); + vm.expectRevert(); + hookTarget.addPolicyAttester(vault, attester); + vm.expectRevert(); + hookTarget.removePolicyAttester(vault, attester); + vm.expectRevert(); + hookTarget.setAllowTrustedOrigin(vault, true); + vm.expectRevert(); + hookTarget.setPolicyThresholds(vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4]); + vm.stopPrank(); + + // set governor admin + vm.prank(address(0)); + IEVault(vault).setGovernorAdmin(governor); + + // succeeds if governor is calling + uint256 snapshot = vm.snapshot(); + vm.startPrank(governor); + hookTarget.addPolicyAttester(vault, attester); + hookTarget.removePolicyAttester(vault, attester); + hookTarget.setAllowTrustedOrigin(vault, true); + hookTarget.setPolicyThresholds(vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4]); + vm.stopPrank(); + + // succeeds if governor is calling also through the EVC + vm.revertTo(snapshot); + vm.startPrank(governor); + evc.call(address(hookTarget), governor, 0, abi.encodeCall(hookTarget.addPolicyAttester, (vault, attester))); + evc.call(address(hookTarget), governor, 0, abi.encodeCall(hookTarget.removePolicyAttester, (vault, attester))); + evc.call( + address(hookTarget), + governor, + 0, + abi.encodeCall(hookTarget.setAllowTrustedOrigin, (vault, true)) + ); + evc.call( + address(hookTarget), + governor, + 0, + abi.encodeCall( + hookTarget.setPolicyThresholds, + (vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4]) + ) + ); + vm.stopPrank(); + + // but fails if non-governor is calling through the EVC, i.e. if governor's sub-account + { + vm.revertTo(snapshot); + vm.startPrank(governor); + address governorsSubAccount = address(uint160(governor) ^ 1); + vm.expectRevert(); + evc.call( + address(hookTarget), + governorsSubAccount, + 0, + abi.encodeCall(hookTarget.addPolicyAttester, (vault, attester)) + ); + vm.expectRevert(); + evc.call( + address(hookTarget), + governorsSubAccount, + 0, + abi.encodeCall(hookTarget.setAllowTrustedOrigin, (vault, true)) + ); + vm.expectRevert(); + evc.call( + address(hookTarget), + governorsSubAccount, + 0, + abi.encodeCall(hookTarget.removePolicyAttester, (vault, attester)) + ); + vm.expectRevert(); + evc.call( + address(hookTarget), + governorsSubAccount, + 0, + abi.encodeCall( + hookTarget.setPolicyThresholds, + (vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4]) + ) + ); + vm.stopPrank(); + } + + // succeeds if governor is calling, verify the outcome + vm.revertTo(snapshot); + for (uint256 i = 0; i < numberOfAttesters; ++i) { + vm.expectEmit(true, true, true, true); + emit HookTargetFirewall.AddPolicyAttester(vault, attester); + vm.prank(governor); + hookTarget.addPolicyAttester(vault, attester); + + { + bool found; + address[] memory attesters = hookTarget.getPolicyAttesters(vault); + for (uint256 j = 0; j < attesters.length; ++j) { + if (attesters[j] == attester) { + found = true; + break; + } + } + assertTrue(found); + } + + vm.expectEmit(true, true, true, true); + emit HookTargetFirewall.SetAllowTrustedOrigin(vault, true); + vm.prank(governor); + hookTarget.setAllowTrustedOrigin(vault, true); + assertEq(hookTarget.getAllowTrustedOrigin(vault), true); + + vm.expectEmit(true, true, true, true); + emit HookTargetFirewall.SetAllowTrustedOrigin(vault, false); + vm.prank(governor); + hookTarget.setAllowTrustedOrigin(vault, false); + assertEq(hookTarget.getAllowTrustedOrigin(vault), false); + + vm.expectEmit(true, true, true, true); + emit HookTargetFirewall.SetPolicyThresholds( + vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4] + ); + vm.prank(governor); + hookTarget.setPolicyThresholds( + vault, thresholds[0], thresholds[1], thresholds[2], thresholds[3], thresholds[4] + ); + (uint32 th0, uint16 th1, uint16 th2, uint16 th3, uint16 th4) = hookTarget.getPolicyThresholds(vault); + assertEq(th0, thresholds[0]); + assertEq(th1, thresholds[1]); + assertEq(th2, thresholds[2]); + assertEq(th3, thresholds[3]); + assertEq(th4, thresholds[4]); + + attester = address(uint160(uint256(keccak256(abi.encode(attester))))); + } + + attester = firstAttester; + for (uint256 i = 0; i < numberOfAttesters; ++i) { + vm.expectEmit(true, true, true, true); + emit HookTargetFirewall.RemovePolicyAttester(vault, attester); + vm.prank(governor); + hookTarget.removePolicyAttester(vault, attester); + + bool found; + address[] memory attesters = hookTarget.getPolicyAttesters(vault); + for (uint256 j = 0; j < attesters.length; ++j) { + if (attesters[j] == attester) { + found = true; + break; + } + } + assertFalse(found); + + attester = address(uint160(uint256(keccak256(abi.encode(attester))))); + } + } + + function test_deposit() public { + vm.warp(100); + + uint256 privateKey = 1; + address attester = vm.addr(privateKey); + address receiver = vm.addr(privateKey + 1); + uint256 amount = 1e18; + uint16 amountThreshold = assetTST.decimals() | 100 << 6; + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.addPolicyAttester(address(eTST), attester); + assetTST.mint(address(this), type(uint112).max); + assetTST.approve(address(eTST), type(uint112).max); + + // no thresholds are set, operation succeeds without attestation + uint256 snapshot = vm.snapshot(); + eTST.deposit(amount, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // operation counter threshold is set, operation fails without attestation + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + vm.expectRevert(); + eTST.deposit(amount, receiver); + + // out amount thresholds are set, operation succeeds without attestation + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, 1, 1); + eTST.deposit(amount, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // in amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, amountThreshold, 0, 0, 0); + eTST.deposit(amount - 1, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // in amount threshold is set, operation fails because the amount is at the threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, amountThreshold, 0, 0, 0); + vm.expectRevert(); + eTST.deposit(amount, receiver); + + // in amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, amountThreshold, 0, 0); + eTST.deposit(amount - 1, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // operation counter threshold and in amount threshold are set, operation fails without attestation even though + // the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, amountThreshold, 0, 0); + vm.expectRevert(); + eTST.deposit(amount - 1, receiver); + + // in amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, amountThreshold, 0, 0); + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (amount / 2, receiver)) + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (amount / 2 - 1, receiver)) + }); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // operation counter threshold and in amount threshold are set, operation fails without attestation even though + // the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, amountThreshold, 0, 0); + vm.expectRevert(); + evc.batch(items); + + // however, the operation succeeds if simulated (no attestation required) + { + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, amountThreshold, 0, 0); + (IEVC.BatchItemResult[] memory batchItemsResult,, IEVC.StatusCheckResult[] memory vaultStatusCheckResult) = + evc.batchSimulation(items); + assertEq(batchItemsResult[0].success, true); + assertEq(batchItemsResult[1].success, true); + assertEq(vaultStatusCheckResult[0].isValid, true); + } + + // in amount threshold is set, operation fails because the amount is at the threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, amountThreshold, 0, 0); + items[1] = items[0]; + vm.expectRevert(); + evc.batch(items); + + // however, the operation succeeds if simulated (no attestation required) + { + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, amountThreshold, 0, 0); + (IEVC.BatchItemResult[] memory batchItemsResult,, IEVC.StatusCheckResult[] memory vaultStatusCheckResult) = + evc.batchSimulation(items); + assertEq(batchItemsResult[0].success, true); + assertEq(batchItemsResult[1].success, true); + assertEq(vaultStatusCheckResult[0].isValid, true); + } + + // in amount threshold is set, operation fails because the attestation provided is incorrect + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, amountThreshold, 0, 0); + snapshot = vm.snapshot(); + + ISecurityValidator.Attestation memory attestation; + attestation.deadline = block.timestamp; + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items = new IEVC.BatchItem[](3); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (amount / 2, receiver)) + }); + items[2] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (amount / 2, receiver)) + }); + vm.expectRevert(); + evc.batch(items); + + // operation fails because the attestation provided is signed by the wrong attester + vm.revertTo(snapshot); + attestation.executionHashes = new bytes32[](1); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + // execution hash as per HookTargetFirewall + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.deposit.selector), + log1_01(amount / 2), + abi.encode(receiver), + address(this), + 2 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + + (v, r, s) = vm.sign(privateKey + 1, securityValidator.hashAttestation(attestation)); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + vm.expectRevert(); + evc.batch(items); + + // operation succeeds now because the attestation provided is correct + vm.revertTo(snapshot); + + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // operation fails if the attestation signature is replayed + vm.expectRevert(); + evc.batch(items); + + // operation still succeeds even if the reference amount differs slightly + vm.revertTo(snapshot); + items[2].data = abi.encodeCall(eTST.deposit, (1001 * (amount / 2) / 1000, receiver)); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // but operation fails if the reference amount differs significantly + vm.revertTo(snapshot); + items[2].data = abi.encodeCall(eTST.deposit, (101 * (amount / 2) / 100, receiver)); + vm.expectRevert(); + evc.batch(items); + + // operation succeeds with attestation saved in a separate tx + vm.revertTo(snapshot); + vm.startPrank(address(this), address(this)); + securityValidator.storeAttestation(attestation, abi.encodePacked(r, s, v)); + items[0] = IEVC.BatchItem({targetContract: address(0), onBehalfOfAccount: address(this), value: 0, data: ""}); + items[2].data = abi.encodeCall(eTST.deposit, (amount / 2, receiver)); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // elapse 1 minute + vm.warp(100 + 1 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // elapse another 1 minute + vm.warp(100 + 2 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // elapse another 1 minute + vm.warp(100 + 3 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 0); + } + + function test_withdraw() public { + vm.warp(100); + + uint256 privateKey = 1; + address attester = vm.addr(privateKey); + address receiver = vm.addr(privateKey + 1); + uint256 amount = 1e18; + uint16 amountThreshold = assetTST.decimals() | 100 << 6; + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.addPolicyAttester(address(eTST), attester); + assetTST.mint(address(this), type(uint112).max); + assetTST.approve(address(eTST), type(uint112).max); + + // deposit first and elapse 1 minute + eTST.deposit(type(uint112).max, address(this)); + vm.warp(100 + 1 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // no thresholds are set, operation succeeds without attestation + uint256 snapshot = vm.snapshot(); + eTST.withdraw(amount, receiver, address(this)); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // operation counter threshold is set, operation fails without attestation + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + vm.expectRevert(); + eTST.withdraw(amount, receiver, address(this)); + + // in amount threshold is set, operation succeeds without attestation + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 1, 1, 0, 0); + eTST.withdraw(amount, receiver, address(this)); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // out amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, amountThreshold, 0); + eTST.withdraw(amount - 1, receiver, address(this)); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // out amount threshold is set, operation fails because the amount is at the threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, amountThreshold, 0); + vm.expectRevert(); + eTST.withdraw(amount, receiver, address(this)); + + // out amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, amountThreshold, 0); + eTST.withdraw(amount - 1, receiver, address(this)); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // operation counter threshold and out amount threshold are set, operation fails without attestation even though + // the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, amountThreshold, 0); + vm.expectRevert(); + eTST.withdraw(amount - 1, receiver, address(this)); + + // out amount threshold is set, operation succeeds without attestation because the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, amountThreshold, 0); + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.withdraw, (amount / 2, receiver, address(this))) + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.withdraw, (amount / 2 - 1, receiver, address(this))) + }); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 3); + + // operation counter threshold and out amount threshold are set, operation fails without attestation even though + // the amount is below threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, amountThreshold); + vm.expectRevert(); + evc.batch(items); + + // however, the operation succeeds if simulated (no attestation required) + { + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, amountThreshold); + (IEVC.BatchItemResult[] memory batchItemsResult,, IEVC.StatusCheckResult[] memory vaultStatusCheckResult) = + evc.batchSimulation(items); + assertEq(batchItemsResult[0].success, true); + assertEq(batchItemsResult[1].success, true); + assertEq(vaultStatusCheckResult[0].isValid, true); + } + + // out amount threshold is set, operation fails because the amount is at the threshold + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, 0, amountThreshold); + items[1] = items[0]; + vm.expectRevert(); + evc.batch(items); + + // however, the operation succeeds if simulated (no attestation required) + { + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, 0, amountThreshold); + (IEVC.BatchItemResult[] memory batchItemsResult,,) = evc.batchSimulation(items); + assertEq(batchItemsResult[0].success, true); + assertEq(batchItemsResult[1].success, true); + } + + // out amount threshold is set, operation fails because the attestation provided is incorrect + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, 0, amountThreshold); + snapshot = vm.snapshot(); + + ISecurityValidator.Attestation memory attestation; + attestation.deadline = block.timestamp; + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items = new IEVC.BatchItem[](3); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.withdraw, (amount / 2, receiver, address(this))) + }); + items[2] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.withdraw, (amount / 2, receiver, address(this))) + }); + vm.expectRevert(); + evc.batch(items); + + // operation fails because the attestation provided is signed by the wrong attester + vm.revertTo(snapshot); + attestation.executionHashes = new bytes32[](1); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + // execution hash as per HookTargetFirewall + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.withdraw.selector), + log1_01(amount / 2), + abi.encode(receiver, address(this)), + address(this), + 3 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + + (v, r, s) = vm.sign(privateKey + 1, securityValidator.hashAttestation(attestation)); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + vm.expectRevert(); + evc.batch(items); + + // operation succeeds now because the attestation provided is correct + vm.revertTo(snapshot); + + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // operation fails if the attestation signature is replayed + vm.expectRevert(); + evc.batch(items); + + // operation still succeeds even if the reference amount differs slightly + vm.revertTo(snapshot); + items[2].data = abi.encodeCall(eTST.withdraw, (1001 * (amount / 2) / 1000, receiver, address(this))); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // but operation fails if the reference amount differs significantly + vm.revertTo(snapshot); + items[2].data = abi.encodeCall(eTST.withdraw, (101 * (amount / 2) / 100, receiver, address(this))); + vm.expectRevert(); + evc.batch(items); + + // operation succeeds with attestation saved in a separate tx + vm.revertTo(snapshot); + vm.startPrank(address(this), address(this)); + securityValidator.storeAttestation(attestation, abi.encodePacked(r, s, v)); + items[0] = IEVC.BatchItem({targetContract: address(0), onBehalfOfAccount: address(this), value: 0, data: ""}); + items[2].data = abi.encodeCall(eTST.withdraw, (amount / 2, receiver, address(this))); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // elapse 1 minute + vm.warp(100 + 2 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + // elapse another 1 minute + vm.warp(100 + 3 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // elapse another 1 minute + vm.warp(100 + 4 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 0); + } + + function test_complexScenario() public { + vm.warp(100); + + uint256 privateKey = 1; + address attester = vm.addr(privateKey); + address subAccountOne = address(uint160(address(this)) ^ 1); + + oracle.setPrice(address(assetTST), unitOfAccount, 1e18); + oracle.setPrice(address(assetTST2), unitOfAccount, 1e18); + eTST.setLTV(address(eTST2), 0.5e4, 0.5e4, 0); + + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.setAllowTrustedOrigin(address(eTST2), true); + hookTarget.addPolicyAttester(address(eTST), address(0)); + hookTarget.addPolicyAttester(address(eTST), address(1)); + hookTarget.addPolicyAttester(address(eTST), attester); + hookTarget.addPolicyAttester(address(eTST2), address(0)); + hookTarget.addPolicyAttester(address(eTST2), address(1)); + hookTarget.addPolicyAttester(address(eTST2), attester); + + // operationCounterThreshold = 3 for eTST and 4 for eTST2 + // inConstantAmountThreshold = 3e18 + // inAccumulatedAmountThreshold = 4e18 + // outConstantAmountThreshold = 1e18 + // outAccumulatedAmountThreshold = 2e18 + hookTarget.setPolicyThresholds(address(eTST), 3, 18 | 300 << 6, 18 | 400 << 6, 18 | 100 << 6, 18 | 200 << 6); + hookTarget.setPolicyThresholds(address(eTST2), 4, 18 | 300 << 6, 18 | 400 << 6, 18 | 100 << 6, 18 | 200 << 6); + + assetTST.mint(address(this), type(uint112).max); + assetTST2.mint(address(this), type(uint112).max); + assetTST.approve(address(eTST), type(uint112).max); + assetTST2.approve(address(eTST2), type(uint112).max); + + // - save the attestation + // - deposit 2.5e18 of assetTST + // - deposit 2.5e18 of assetTST - executes checkpoint + // - deposit 5e18 of assetTST2 - executes checkpoint + // - borrow 1e18 of assetTST - executes checkpoint + // - borrow 0.5e18 of assetTST + // - withdraw 0.5e18 of assetTST2 + // - withdraw 0.5e18 of assetTST2 + // - withdraw 0.5e18 of assetTST2 + // - withdraw 0.5e18 of assetTST2 - executes checkpoint + // - repay 1.5e18 of assetTST - executes checkpoint + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](12); + items[0] = IEVC.BatchItem({ + targetContract: address(0), + onBehalfOfAccount: address(this), + value: 0, + data: "ff" // dummy call. this batch item is a placeholder for the attestation added later on + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (2.5e18, address(this))) + }); + items[2] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (2.5e18, address(this))) + }); + items[3] = IEVC.BatchItem({ + targetContract: address(eTST2), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (5e18, subAccountOne)) + }); + items[4] = IEVC.BatchItem({ + targetContract: address(evc), + onBehalfOfAccount: address(0), + value: 0, + data: abi.encodeCall(evc.enableController, (subAccountOne, address(eTST))) + }); + items[5] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST.borrow, (1e18, address(this))) + }); + items[6] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST.borrow, (0.5e18, address(this))) + }); + items[7] = IEVC.BatchItem({ + targetContract: address(eTST2), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST2.withdraw, (0.5e18, address(this), subAccountOne)) + }); + items[8] = IEVC.BatchItem({ + targetContract: address(eTST2), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST2.withdraw, (0.5e18, address(this), subAccountOne)) + }); + items[9] = IEVC.BatchItem({ + targetContract: address(eTST2), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST2.withdraw, (0.5e18, address(this), subAccountOne)) + }); + items[10] = IEVC.BatchItem({ + targetContract: address(eTST2), + onBehalfOfAccount: subAccountOne, + value: 0, + data: abi.encodeCall(eTST2.withdraw, (0.5e18, address(this), subAccountOne)) + }); + items[11] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.repay, (1.5e18, subAccountOne)) + }); + + // fails without the attestation + vm.expectRevert(); + evc.batch(items); + + // but it is possible to simulate without the attestation + { + ( + IEVC.BatchItemResult[] memory batchItemsResult, + IEVC.StatusCheckResult[] memory accountsStatusCheckResult, + IEVC.StatusCheckResult[] memory vaultStatusCheckResult + ) = evc.batchSimulation(items); + for (uint256 i = 0; i < batchItemsResult.length; i++) { + assertEq(batchItemsResult[i].success, true); + } + for (uint256 i = 0; i < accountsStatusCheckResult.length; i++) { + assertEq(accountsStatusCheckResult[i].isValid, true); + } + for (uint256 i = 0; i < vaultStatusCheckResult.length; i++) { + assertEq(vaultStatusCheckResult[i].isValid, true); + } + } + + // prepare the attestation + ISecurityValidator.Attestation memory attestation; + attestation.deadline = block.timestamp; + attestation.executionHashes = new bytes32[](5); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.deposit.selector), + log1_01(2.5e18), + abi.encode(address(this)), + address(this), + 2 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + attestation.executionHashes[1] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST2), + bytes4(eTST.deposit.selector), + log1_01(5e18), + abi.encode(subAccountOne), + address(this), + 3 + ) + ), + address(hookTarget), + attestation.executionHashes[0] + ); + attestation.executionHashes[2] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.borrow.selector), + log1_01(1e18), + abi.encode(address(this)), + subAccountOne, + 4 + ) + ), + address(hookTarget), + attestation.executionHashes[1] + ); + attestation.executionHashes[3] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST2), + bytes4(eTST.withdraw.selector), + log1_01(0.5e18), + abi.encode(address(this), subAccountOne), + subAccountOne, + 9 + ) + ), + address(hookTarget), + attestation.executionHashes[2] + ); + attestation.executionHashes[4] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.repay.selector), + log1_01(1.5e18), + abi.encode(subAccountOne), + address(this), + 10 + ) + ), + address(hookTarget), + attestation.executionHashes[3] + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + + // succeeds with the attestation + uint256 snapshot = vm.snapshot(); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + assertEq(hookTarget.getOperationCounter(address(eTST2)), 3); + + // not possible to replay + vm.expectRevert(); + evc.batch(items); + + // additional operation below the threshold requires an attestation due to the operationCounterThreshold + // exceeded + vm.expectRevert(); + eTST.deposit(0, address(0)); + + vm.expectRevert(); + eTST2.deposit(0, address(0)); + + { + // additional operations now succeed with a new attestation + ISecurityValidator.Attestation memory newAttestation; + newAttestation.deadline = block.timestamp; + newAttestation.executionHashes = new bytes32[](1); + newAttestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.deposit.selector), + log1_01(0), + abi.encode(address(0)), + address(this), + 11 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(newAttestation)); + + IEVC.BatchItem[] memory newItems = new IEVC.BatchItem[](2); + newItems[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (newAttestation, abi.encodePacked(r, s, v))) + }); + newItems[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (0, address(0))) + }); + evc.batch(newItems); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + + newAttestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST2), + bytes4(eTST.deposit.selector), + log1_01(0), + abi.encode(address(0)), + address(this), + 12 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(newAttestation)); + + vm.prank(address(this), address(this)); + securityValidator.storeAttestation(newAttestation, abi.encodePacked(r, s, v)); + + vm.prank(address(this), address(this)); + eTST2.deposit(0, address(0)); + assertEq(hookTarget.getOperationCounter(address(eTST2)), 3); + } + + // the amounts can be floating a bit + vm.revertTo(snapshot); + items[1].data = abi.encodeCall(eTST.deposit, (2.5e18 + 1000, address(this))); + items[2].data = abi.encodeCall(eTST.deposit, (2.5e18 - 1000, address(this))); + items[3].data = abi.encodeCall(eTST.deposit, (5e18 - 10000, subAccountOne)); + items[5].data = abi.encodeCall(eTST.borrow, (1e18 + 10000, address(this))); + items[6].data = abi.encodeCall(eTST.borrow, (0.5e18 + 10000, address(this))); + items[7].data = abi.encodeCall(eTST.withdraw, (0.5e18 - 1000, address(this), subAccountOne)); + items[8].data = abi.encodeCall(eTST.withdraw, (0.5e18 - 1000, address(this), subAccountOne)); + items[9].data = abi.encodeCall(eTST.withdraw, (0.5e18 + 1000, address(this), subAccountOne)); + items[10].data = abi.encodeCall(eTST.withdraw, (0.5e18 + 1000, address(this), subAccountOne)); + items[11].data = abi.encodeCall(eTST.repay, (type(uint256).max, subAccountOne)); + evc.batch(items); + assertEq(hookTarget.getOperationCounter(address(eTST)), 2); + assertEq(hookTarget.getOperationCounter(address(eTST2)), 3); + + // fails if the attestation not fully utilized + { + vm.revertTo(snapshot); + IEVC.BatchItem[] memory itemsSubset = new IEVC.BatchItem[](4); + for (uint256 i = 0; i < 4; i++) { + itemsSubset[i] = items[i]; + } + vm.expectRevert(); + evc.batch(itemsSubset); + } + } + + function test_trustedOrigin() public { + vm.warp(100); + + address trustedOrigin = makeAddr("TRUSTED_ORIGIN"); + address receiver = makeAddr("RECEIVER"); + uint256 amount = 1e18; + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.addPolicyAttester(address(eTST), trustedOrigin); + assetTST.mint(address(this), type(uint112).max); + assetTST.approve(address(eTST), type(uint112).max); + + // no thresholds are set, operation succeeds without attestation + uint256 snapshot = vm.snapshot(); + eTST.deposit(amount, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + + // operation counter threshold is set, operation fails without attestation + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + vm.expectRevert(); + eTST.deposit(amount, receiver); + + // operation succeeds if the transaction is coming from the trusted origin and the allowTrustedOrigin flag is set + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + vm.prank(address(this), trustedOrigin); + eTST.deposit(amount, receiver); + assertEq(hookTarget.getOperationCounter(address(eTST)), 0); + + // operation fails if the origin address is not trusted + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + hookTarget.removePolicyAttester(address(eTST), trustedOrigin); + vm.prank(address(this), trustedOrigin); + vm.expectRevert(); + eTST.deposit(amount, receiver); + + // operation fails if the allowTrustedOrigin flag is not set + vm.revertTo(snapshot); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + hookTarget.setAllowTrustedOrigin(address(eTST), false); + vm.prank(address(this), trustedOrigin); + vm.expectRevert(); + eTST.deposit(amount, receiver); + } + + function test_maxAmounts() public { + vm.warp(100); + + uint256 privateKey = 1; + address attester = vm.addr(privateKey); + address subAccountOne = address(uint160(address(this)) ^ 1); + + oracle.setPrice(address(assetTST), unitOfAccount, 1e18); + oracle.setPrice(address(assetTST2), unitOfAccount, 1e18); + eTST.setLTV(address(eTST2), 0.9e4, 0.9e4, 0); + + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.addPolicyAttester(address(eTST), attester); + hookTarget.setPolicyThresholds(address(eTST), 0, 1, 1, 1, 1); + eTST2.setHookConfig(address(0), 0); + + assetTST.mint(address(this), type(uint64).max); + assetTST2.mint(address(this), type(uint64).max); + assetTST.approve(address(eTST), type(uint256).max); + assetTST2.approve(address(eTST2), type(uint256).max); + + // test the deposit max amount + ISecurityValidator.Attestation memory attestation; + attestation.deadline = block.timestamp; + attestation.executionHashes = new bytes32[](1); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.deposit.selector), + log1_01(2 ** 64 - 1), + abi.encode(address(this)), + address(this), + 1 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0] = IEVC.BatchItem({ + targetContract: address(hookTarget), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))) + }); + items[1] = IEVC.BatchItem({ + targetContract: address(eTST), + onBehalfOfAccount: address(this), + value: 0, + data: abi.encodeCall(eTST.deposit, (type(uint256).max, address(this))) + }); + evc.batch(items); + + // test the redeem max amount + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.redeem.selector), + log1_01(2 ** 64 - 1), + abi.encode(address(this), address(this)), + address(this), + 2 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items[0].data = abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))); + items[1].data = abi.encodeCall(eTST.redeem, (type(uint256).max, address(this), address(this))); + evc.batch(items); + + // test the skim max amount + assetTST.transfer(address(eTST), type(uint32).max); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.skim.selector), + log1_01(2 ** 32 - 1), + abi.encode(address(this)), + address(this), + 3 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items[0].data = abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))); + items[1].data = abi.encodeCall(eTST.skim, (type(uint256).max, address(this))); + evc.batch(items); + + // test the borrow max amount + evc.enableController(subAccountOne, address(eTST)); + evc.enableCollateral(subAccountOne, address(eTST2)); + eTST2.deposit(type(uint64).max, subAccountOne); + + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.borrow.selector), + log1_01(2 ** 32 - 1), + abi.encode(address(this)), + subAccountOne, + 4 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items[0].data = abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))); + items[1].onBehalfOfAccount = subAccountOne; + items[1].data = abi.encodeCall(eTST.borrow, (type(uint256).max, address(this))); + evc.batch(items); + + // test the repay max amount + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), + bytes4(eTST.repay.selector), + log1_01(2 ** 32 - 1), + abi.encode(subAccountOne), + address(this), + 5 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + items[0].data = abi.encodeCall(HookTargetFirewall.saveAttestation, (attestation, abi.encodePacked(r, s, v))); + items[1].onBehalfOfAccount = address(this); + items[1].data = abi.encodeCall(eTST.repay, (type(uint256).max, subAccountOne)); + evc.batch(items); + } + + function test_unauthorizedCaller(address caller) public { + vm.assume(caller != address(0) && !factory.isProxy(caller)); + + vm.startPrank(caller); + vm.expectRevert(abi.encodeWithSelector(HookTargetFirewall.HTA_Unauthorized.selector)); + hookTarget.deposit(0, address(0)); + + // succeeds if function is called through the vault + eTST.deposit(0, address(0)); + assertEq(hookTarget.getOperationCounter(address(eTST)), 1); + } + + function test_vaultOperationCounters(uint32 timestamp, uint8 window1, uint8 window2, uint8 window3) public { + vm.assume(timestamp != 0); + vm.assume(window1 != 0 || window2 != 0 || window3 != 0); + hookTarget.setPolicyThresholds(address(eTST), 0, 0, 0, 0, 0); + + vm.warp(timestamp); + for (uint256 i = 0; i < window1; i++) { + eTST.deposit(0, address(0)); + assertEq(hookTarget.getOperationCounter(address(eTST)), i + 1); + } + + vm.warp(uint256(timestamp) + 1 minutes); + for (uint256 i = 0; i < window2; i++) { + eTST.deposit(0, address(0)); + assertEq(hookTarget.getOperationCounter(address(eTST)), uint256(window1) + i + 1); + } + + vm.warp(uint256(timestamp) + 2 minutes); + for (uint256 i = 0; i < window3; i++) { + eTST.deposit(0, address(0)); + assertEq(hookTarget.getOperationCounter(address(eTST)), uint256(window1) + uint256(window2) + i + 1); + } + assertEq(hookTarget.getOperationCounter(address(eTST)), uint256(window1) + uint256(window2) + uint256(window3)); + + hookTarget.setPolicyThresholds(address(eTST), uint32(window1) + uint32(window2) + uint32(window3), 0, 0, 0, 0); + vm.expectRevert(); + eTST.deposit(0, address(0)); + + vm.warp(uint256(timestamp) + 3 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), uint256(window2) + uint256(window3)); + + vm.warp(uint256(timestamp) + 4 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), uint256(window3)); + + vm.warp(uint256(timestamp) + 5 minutes); + assertEq(hookTarget.getOperationCounter(address(eTST)), 0); + } + + function test_multipleAttestationsInOneTx() public { + vm.warp(1); + + uint256 privateKey = 1; + address attester = vm.addr(privateKey); + hookTarget.setAllowTrustedOrigin(address(eTST), true); + hookTarget.addPolicyAttester(address(eTST), attester); + hookTarget.setPolicyThresholds(address(eTST), 1, 0, 0, 0, 0); + + vm.startPrank(address(this), address(this)); + ISecurityValidator.Attestation memory attestation; + attestation.deadline = block.timestamp; + attestation.executionHashes = new bytes32[](1); + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), bytes4(eTST.deposit.selector), log1_01(0), abi.encode(address(0)), address(this), 1 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + securityValidator.storeAttestation(attestation, abi.encodePacked(r, s, v)); + eTST.deposit(0, address(0)); + + attestation.executionHashes[0] = securityValidator.executionHashFrom( + keccak256( + abi.encode( + address(eTST), bytes4(eTST.deposit.selector), log1_01(0), abi.encode(address(0)), address(this), 2 + ) + ), + address(hookTarget), + bytes32(uint256(0)) + ); + (v, r, s) = vm.sign(privateKey, securityValidator.hashAttestation(attestation)); + + securityValidator.storeAttestation(attestation, abi.encodePacked(r, s, v)); + eTST.deposit(0, address(0)); + } +}