diff --git a/.gitmodules b/.gitmodules index e64afcdf..6b77612c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,6 +16,9 @@ [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 [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable diff --git a/docs/firewall.md b/docs/firewall.md new file mode 100644 index 00000000..52d50a4a --- /dev/null +++ b/docs/firewall.md @@ -0,0 +1,66 @@ +# 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 (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 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 + +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. + +### 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/lib/solady b/lib/solady new file mode 160000 index 00000000..1f43cc80 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 1f43cc8005cc3b3c8361dd7dbdd2cdeaf0f99e66 diff --git a/remappings.txt b/remappings.txt index d212744e..36c213dd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,6 +8,7 @@ euler-price-oracle/=lib/euler-price-oracle/src/ euler-price-oracle-test/=lib/euler-price-oracle/test/ fee-flow/=lib/fee-flow/src/ reward-streams/=lib/reward-streams/src/ +solady/=lib/solady/src/ lib/euler-price-oracle:@openzeppelin/contracts/=lib/euler-price-oracle/lib/openzeppelin-contracts/contracts/ @openzeppelin/=lib/openzeppelin-contracts/contracts/ euler-earn/=lib/euler-earn/src/ diff --git a/src/HookTarget/HookTargetFirewall.sol b/src/HookTarget/HookTargetFirewall.sol new file mode 100644 index 00000000..743bc854 --- /dev/null +++ b/src/HookTarget/HookTargetFirewall.sol @@ -0,0 +1,573 @@ +// 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 {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"; + +/// @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 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. + 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. + enum TransferType { + /// @notice Represents an incoming transfer. + In, + /// @notice Represents an outgoing transfer. + Out + } + + /// @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; + /// @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; + + /// @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 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. + 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 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 + /// 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, + uint32 operationCounterThreshold, + uint16 inConstantAmountThreshold, + uint16 inAccumulatedAmountThreshold, + uint16 outConstantAmountThreshold, + uint16 outAccumulatedAmountThreshold + ); + + /// @notice Error thrown when the caller is not authorized to perform an operation. + error HTF_Unauthorized(); + + /// @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 _eVaultFactory, address _securityValidator, bytes32 _controllerId) + EVCUtil(_evc) + { + eVaultFactory = GenericFactory(_eVaultFactory); + 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 HTF_Unauthorized(); + _; + } + + /// @inheritdoc IHookTarget + /// @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 + /// @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 (getHookTargetFirewallStorage().attesters[vault].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 (getHookTargetFirewallStorage().attesters[vault].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 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 + /// 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, + uint32 operationCounterThreshold, + uint16 inConstantAmountThreshold, + uint16 inAccumulatedAmountThreshold, + uint16 outConstantAmountThreshold, + uint16 outAccumulatedAmountThreshold + ) external onlyEVCAccountOwner onlyGovernor(vault) { + PolicyStorage storage policyStorage = getHookTargetFirewallStorage().policies[vault]; + 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, + operationCounterThreshold, + inConstantAmountThreshold, + inAccumulatedAmountThreshold, + outConstantAmountThreshold, + outAccumulatedAmountThreshold + ); + } + + /// @notice Retrieves the policy thresholds for a given vault. + /// @param vault The address of the vault. + /// @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 = getHookTargetFirewallStorage().policies[vault]; + return ( + 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 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 = getHookTargetFirewallStorage().policies[vault]; + return ( + 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(getHookTargetFirewallStorage().policies[vault]) - 1; + } + + /// @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 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 = getHookTargetFirewallStorage().policies[msg.sender]; + if (!policy.isAuthenticated) { + authenticateVault(msg.sender); + } + + address sender = caller(); + uint256 accountOperationCounter = updateAccountOperationCounter(sender); + uint256 vaultOperationCounter = updateVaultOperationCounter(policy); + uint256 accumulatedAmount = updateAccumulatedAmount(transferType, msg.sender, referenceAmount); + (uint256 operationCounterThreshold, uint256 constantAmountThreshold, uint256 accumulatedAmountThreshold) = + resolveThresholds(transferType, policy); + + if ( + vaultOperationCounter >= operationCounterThreshold || referenceAmount >= constantAmountThreshold + || accumulatedAmount >= accumulatedAmountThreshold + ) { + 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) + // - 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 on the validator instead of the saveAttestation function, + // the current attester must be already defined by the validator contract + if (!isAttestationInProgress()) { + revert HTF_Unauthorized(); + } + } + } else { + // apply the vault operation counter update only if the checkpoint does not need to be executed + 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)) { + getHookTargetFirewallStorage().policies[vault].isAuthenticated = true; + emit AuthenticateVault(msg.sender); + } else { + revert HTF_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 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; + 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 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) + && 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. + /// @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 + pure + returns (uint256 operationCounterThreshold, uint256 constantAmountThreshold, uint256 accumulatedAmountThreshold) + { + operationCounterThreshold = policy.operationCounterThreshold; + if (operationCounterThreshold == 0) { + operationCounterThreshold = type(uint256).max; + } + + if (transferType == TransferType.In) { + constantAmountThreshold = policy.inConstantAmountThreshold.resolve(); + accumulatedAmountThreshold = policy.inAccumulatedAmountThreshold.resolve(); + } else { + 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(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; + } + + /// @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))) + } + } +} diff --git a/test/HookTarget/HookTargetFirewall.sol b/test/HookTarget/HookTargetFirewall.sol new file mode 100644 index 00000000..9e9a2169 --- /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.HTF_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)); + } +}