diff --git a/documents/TokenTransformationSystem.md b/documents/TokenTransformationSystem.md
new file mode 100644
index 00000000..e530ccff
--- /dev/null
+++ b/documents/TokenTransformationSystem.md
@@ -0,0 +1,435 @@
+# Token Transformation System
+
+## Overview
+
+The Token Transformation System enables delegations to track and control token transformations through DeFi protocol interactions. This system allows AI agents and other automated systems to use delegated tokens in lending protocols (like Aave) while maintaining granular control over the evolving token positions.
+
+## Problem Statement
+
+When tokens are used in DeFi protocols, they often transform into different tokens:
+
+- **Lending Protocols**: Depositing USDC into Aave yields wrapped aUSDC (a non-rebasing token)
+- **Token Evolution**: A single delegation may evolve to control multiple token types
+
+**Example Scenario:**
+
+1. User delegates 1000 USDC to an AI agent
+2. Agent deposits 500 USDC → receives 500 wrapped aUSDC (Aave)
+3. Final state: User has control over 300 USDC + 500 wrapped aUSDC
+
+The system maintains permission over **all tokens derived from the original delegation** until the delegation expires or is revoked.
+
+## Architecture
+
+The solution consists of three main components:
+
+### 1. TokenTransformationEnforcer
+
+A caveat enforcer that tracks multiple tokens per delegation hash.
+
+**State:**
+
+```solidity
+mapping(bytes32 delegationHash => mapping(address token => uint256 amount)) public availableAmounts;
+mapping(bytes32 delegationHash => bool initialized) public isInitialized;
+address public immutable adapterManager;
+```
+
+**Key Functions:**
+
+- `beforeHook()`: Validates token transfers and deducts from available amounts
+- `updateAssetState()`: Adds new tokens after protocol interactions (only callable by AdapterManager)
+- `getAvailableAmount()`: Public view function to query available amounts
+
+**Terms Format:**
+
+- Base (52 bytes): 20 bytes token address + 32 bytes initial amount
+- Extended (optional): 1 byte protocol count + N \* 20 bytes protocol addresses
+- Minimum: 52 bytes (no protocols)
+- Maximum: 52 + 1 + (255 \* 20) = 5162 bytes
+
+**Initialization:**
+
+- On first use of the initial token, amount is initialized from terms
+- Subsequent uses deduct from available amount
+- Protocol addresses in `_args` are validated against `allowedProtocols` from terms
+
+**Design Pattern: State Tracking**
+
+The enforcer uses a state tracking pattern to maintain multiple tokens per delegation:
+
+- Tracks what tokens are **available**, not what tokens exist
+- Allows tracking multiple token types simultaneously
+- Initializes lazily on first use to save gas
+- Prevents double initialization with `isInitialized` mapping
+
+### 2. AdapterManager
+
+Central coordinator that routes protocol interactions to adapters and updates enforcer state.
+
+**State:**
+
+```solidity
+IDelegationManager public immutable delegationManager;
+TokenTransformationEnforcer public tokenTransformationEnforcer; // Settable by owner
+mapping(address protocol => address adapter) public protocolAdapters;
+```
+
+**Key Functions:**
+
+- `executeProtocolActionByDelegation()`: Main entry point for protocol interactions
+- `registerProtocolAdapter()`: Register adapters for protocols (owner only)
+- `setTokenTransformationEnforcer()`: Set the enforcer address (owner only)
+
+**Flow:**
+
+1. User calls `executeProtocolActionByDelegation()` with delegations
+2. AdapterManager validates TokenTransformationEnforcer is at index 0 of root delegation
+3. Sets protocol address in enforcer caveat args
+4. Creates two executions via delegation redemption:
+ - Execution 1: Transfer tokens from delegator to AdapterManager
+ - Execution 2: Internal call to `executeProtocolActionInternal()`
+5. Routes to adapter based on protocol address
+6. Adapter executes protocol interaction and returns transformation info
+7. AdapterManager updates enforcer state (adds output tokens)
+8. Transfers output tokens to root delegator
+
+**Design Pattern: Two-Phase Execution**
+
+The system uses a two-phase execution pattern:
+
+**Phase 1 - Token Transfer**: Validated through delegation redemption
+
+- TokenTransformationEnforcer validates availability in `beforeHook()`
+- Tokens are transferred from delegator to AdapterManager
+- Amount is deducted from availableAmounts
+
+**Phase 2 - Protocol Action**: Executed internally
+
+- AdapterManager calls adapter to execute protocol interaction
+- Adapter returns transformation information
+- AdapterManager updates enforcer state with new tokens
+
+**Why:** Separating validation from execution allows us to use the delegation framework's validation while maintaining control over protocol interactions.
+
+**Security:**
+
+- Only DelegationManager can call `executeFromExecutor()`
+- Only AdapterManager can update enforcer state
+- TokenTransformationEnforcer must be first caveat in root delegation
+
+### 3. Protocol Adapters
+
+Protocol-specific adapters that handle interactions with lending protocols.
+
+**Current Adapters:**
+
+- **AaveAdapter**: Handles Aave V3 deposits/withdrawals
+
+**Adapter Interface:**
+
+```solidity
+interface IAdapter {
+ struct TransformationInfo {
+ address tokenFrom;
+ uint256 amountFrom;
+ address tokenTo;
+ uint256 amountTo;
+ }
+
+ function executeProtocolAction(
+ address _protocolAddress,
+ string calldata _action,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData
+ ) external returns (TransformationInfo memory);
+}
+```
+
+**AaveAdapter Details:**
+
+- Supports "deposit" and "withdraw" actions
+- Wraps rebasing aTokens into non-rebasing stataTokens (ERC-4626)
+- Dynamically detects if wrapper accepts aTokens or underlying tokens directly
+- Returns transformation info: tokenFrom → tokenTo with amounts
+
+**Design Pattern: Adapter Pattern**
+
+Different DeFi protocols have different interfaces and behaviors. The adapter pattern:
+
+- Encapsulates protocol-specific logic
+- Provides a consistent interface for the AdapterManager
+- Makes it easy to add support for new protocols
+
+Each protocol has an adapter contract implementing `IAdapter` that handles protocol-specific details (approvals, wrapping, etc.) and returns standardized `TransformationInfo` structs.
+
+## Contract Interaction Diagram
+
+```mermaid
+sequenceDiagram
+ participant Agent as Agent/User
+ participant AM as AdapterManager
+ participant DM as DelegationManager
+ participant TTE as TokenTransformationEnforcer
+ participant AA as AaveAdapter
+ participant Aave as Aave Pool
+
+ Agent->>AM: executeProtocolActionByDelegation(protocol, token, amount, delegations)
+
+ Note over AM: 1. Validate TokenTransformationEnforcer
is first caveat in root delegation
2. Set protocol address in caveat args
+
+ AM->>DM: redeemDelegations([delegations], [modes], [callDatas])
+
+ Note over DM: Execution 1: Token Transfer
+ DM->>TTE: beforeHook(terms, args, mode, transferCallData, delegationHash)
+ TTE->>TTE: Validate & deduct tokens
+ TTE-->>DM: ✓ Valid
+ DM->>AM: transfer(token, amount)
+
+ Note over DM: Execution 2: Internal Action
+ DM->>AM: executeFromExecutor(mode, internalCallData)
+ AM->>AM: executeProtocolActionInternal()
+
+ AM->>AA: transfer(token, amount)
+ AM->>AA: executeProtocolAction(protocol, action, token, amount, actionData)
+
+ AA->>Aave: supply(token, amount, ...)
+ Aave-->>AA: aTokens
+ AA->>Aave: wrap aTokens → stataTokens
+ Aave-->>AA: stataTokens
+ AA-->>AM: TransformationInfo(tokenFrom, amountFrom, tokenTo, amountTo)
+
+ AM->>TTE: updateAssetState(delegationHash, tokenTo, amountTo)
+ TTE->>TTE: Add tokens to availableAmounts
+
+ AM->>Agent: transfer(tokenTo, amountTo) to root delegator
+```
+
+## How It Works
+
+### Example Flow: Aave Deposit
+
+1. **Initial Delegation**:
+
+ ```
+ User delegates 1000 USDC with TokenTransformationEnforcer
+ Terms: [USDC address (20 bytes), 1000 (32 bytes), protocol count (1 byte), Aave Pool address (20 bytes)]
+ ```
+
+2. **Agent Initiates Deposit**:
+
+ ```
+ Agent calls AdapterManager.executeProtocolActionByDelegation(
+ protocol: Aave Pool,
+ tokenFrom: USDC,
+ amountFrom: 500,
+ delegations: [...]
+ )
+ ```
+
+3. **Delegation Redemption**:
+
+ - AdapterManager validates TokenTransformationEnforcer is at index 0
+ - Sets protocol address in enforcer caveat args
+ - DelegationManager validates delegations
+ - TokenTransformationEnforcer.beforeHook() validates 500 USDC is available
+ - Deducts 500 USDC from availableAmounts[delegationHash][USDC]
+ - Transfers 500 USDC to AdapterManager
+
+4. **Protocol Interaction**:
+
+ - AdapterManager transfers tokens to AaveAdapter
+ - AaveAdapter calls Aave Pool.supply(USDC, 500, ...)
+ - AaveAdapter wraps aUSDC → wrapped aUSDC (stataToken)
+ - Returns: tokenFrom=USDC, amountFrom=500, tokenTo=wrapped aUSDC, amountTo=500
+
+5. **State Update**:
+
+ - AdapterManager calls TokenTransformationEnforcer.updateAssetState(
+ delegationHash,
+ wrapped aUSDC,
+ 500
+ )
+ - Enforcer state: availableAmounts[delegationHash][wrapped aUSDC] = 500
+
+6. **Token Transfer**:
+
+ - AdapterManager transfers wrapped aUSDC to root delegator
+ - Final state:
+ - availableAmounts[delegationHash][USDC] = 500
+ - availableAmounts[delegationHash][wrapped aUSDC] = 500
+
+### Example Flow: Multiple Transformations
+
+**Initial**: 1000 USDC delegated
+
+**Step 1**: Deposit 500 USDC → Aave
+
+- Result: 500 USDC + 500 wrapped aUSDC tracked
+
+**Step 2**: Deposit 200 USDC → Aave
+
+- Result: 300 USDC + 700 wrapped aUSDC tracked
+
+**Step 3**: Withdraw 100 wrapped aUSDC → USDC
+
+- Result: 400 USDC + 600 wrapped aUSDC tracked
+
+All tokens remain under delegation control until expiration or revocation.
+
+## Key Design Decisions
+
+### 1. Wrapped Tokens for Rebasing Assets
+
+**Problem:** Rebasing tokens (like aTokens) change balance over time, making tracking difficult.
+
+**Solution:** Wrap rebasing tokens into non-rebasing tokens using ERC-4626 vaults.
+
+**Implementation:** AaveAdapter wraps aTokens into stataTokens (StaticAToken), which have fixed supply and are easier to track.
+
+### 2. AdapterManager as State Updater
+
+**Problem:** Who can update the enforcer state after protocol interactions?
+
+**Solution:** Only AdapterManager can call `updateAssetState()`.
+
+**Rationale:**
+
+- Ensures state updates only happen after verified protocol interactions
+- Prevents unauthorized state manipulation
+- Clear security boundary
+
+**Implementation:** TokenTransformationEnforcer validates `msg.sender == adapterManager` in `updateAssetState()`.
+
+### 3. Tokens Always Go to Root Delegator
+
+**Problem:** Where should tokens be stored after protocol interactions?
+
+**Solution:** All tokens are transferred to the root delegator.
+
+**Rationale:**
+
+- Clear ownership model
+- Tokens never stay in contracts
+- Simplifies accounting
+
+**Flow:** Root Delegator → AdapterManager → Protocol → AdapterManager → Root Delegator
+
+### 4. Terms Format Design
+
+**Problem:** How to encode initial token, amount, and optional protocol restrictions?
+
+**Solution:** Variable-length encoding with backward compatibility.
+
+**Format:**
+
+- Base (52 bytes): token address (20 bytes) + amount (32 bytes)
+- Extended (optional): protocol count (1 byte) + protocol addresses (20 bytes each)
+
+**Benefits:**
+
+- Backward compatible (52 bytes minimum)
+- Flexible (up to 255 protocols)
+- Efficient encoding
+
+### 5. Initialization on First Use
+
+**Problem:** When should initial token amounts be set from terms?
+
+**Solution:** Initialize on first use of the initial token.
+
+**Rationale:**
+
+- Lazy initialization saves gas
+- Only initializes when actually needed
+- Prevents double initialization
+
+**Implementation:** Check `isInitialized[delegationHash]` and `token == initialToken` before initializing.
+
+### 6. Protocol Validation Pattern
+
+**Why:** Users may want to restrict which protocols can be used with their delegations.
+
+**How it works:**
+
+- Terms can include an optional list of allowed protocol addresses
+- Protocol address is passed in `_args` when used with AdapterManager
+- TokenTransformationEnforcer validates protocol against allowed list
+- If no protocols specified, all protocols are allowed (backward compatible)
+
+**Example:** Terms encode `[token, amount, protocolCount, protocol1, protocol2, ...]`
+
+## Public API
+
+### Query Available Amounts
+
+```solidity
+uint256 available = tokenTransformationEnforcer.getAvailableAmount(
+ delegationHash,
+ tokenAddress
+);
+```
+
+### Check Protocol Adapters
+
+```solidity
+address adapter = adapterManager.protocolAdapters(protocolAddress);
+```
+
+## Security Considerations
+
+1. **State Updates**: Only AdapterManager can update enforcer state
+2. **Token Validation**: Enforcer validates all token transfers before execution
+3. **Protocol Validation**: Protocol addresses are validated against allowed list in terms
+4. **Ownership**: All tokens always belong to root delegator
+5. **Initialization Protection**: Initial amount only set once per delegationHash
+6. **Enforcer Position**: TokenTransformationEnforcer must be first caveat in root delegation
+
+## Extensibility
+
+### Adding New Protocols
+
+To add support for a new protocol:
+
+1. Create adapter contract implementing `IAdapter`
+2. Implement `executeProtocolAction()` returning `TransformationInfo`
+3. Register adapter in AdapterManager via `registerProtocolAdapter()`
+
+### Adding New Actions
+
+To add new actions to an existing adapter:
+
+1. Add action string constant (e.g., `ACTION_BORROW`)
+2. Add handler function (e.g., `_handleBorrow()`)
+3. Route action in `executeProtocolAction()`
+
+## Usage Example
+
+```solidity
+// 1. Create delegation with initial token
+Delegation memory delegation = Delegation({
+ delegator: user,
+ delegate: agent,
+ caveats: [Caveat({
+ enforcer: address(tokenTransformationEnforcer),
+ terms: abi.encodePacked(token, amount, protocolCount, protocolAddress),
+ args: hex""
+ })]
+});
+
+// 2. Agent calls AdapterManager
+adapterManager.executeProtocolActionByDelegation(
+ protocolAddress,
+ token,
+ amount,
+ actionData,
+ [delegation]
+);
+
+// 3. System handles:
+// - Validation through delegation redemption
+// - Protocol interaction via adapter
+// - State update in enforcer
+// - Token transfer to root delegator
+```
diff --git a/script/DeployTokenTransformationSystem.s.sol b/script/DeployTokenTransformationSystem.s.sol
new file mode 100644
index 00000000..bb81a97c
--- /dev/null
+++ b/script/DeployTokenTransformationSystem.s.sol
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import "forge-std/Script.sol";
+import { console2 } from "forge-std/console2.sol";
+
+import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol";
+import { TokenTransformationEnforcer } from "../src/enforcers/TokenTransformationEnforcer.sol";
+import { AdapterManager } from "../src/helpers/adapters/AdapterManager.sol";
+
+/**
+ * @title DeployTokenTransformationSystem
+ * @notice Deploys TokenTransformationEnforcer and AdapterManager together
+ * @dev Resolves circular dependency by deploying in correct order:
+ * 1. Deploy AdapterManager first (no enforcer in constructor)
+ * 2. Deploy TokenTransformationEnforcer with AdapterManager address
+ * 3. Owner sets enforcer in AdapterManager
+ * @dev Run the script with:
+ * forge script script/DeployTokenTransformationSystem.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast
+ */
+contract DeployTokenTransformationSystem is Script {
+ bytes32 salt;
+ IDelegationManager delegationManager;
+ address owner;
+ address deployer;
+
+ function setUp() public {
+ salt = bytes32(abi.encodePacked(vm.envString("SALT")));
+ delegationManager = IDelegationManager(vm.envAddress("DELEGATION_MANAGER_ADDRESS"));
+ owner = vm.envAddress("OWNER_ADDRESS");
+ deployer = msg.sender;
+
+ console2.log("~~~");
+ console2.log("Deployer: %s", address(deployer));
+ console2.log("Owner: %s", address(owner));
+ console2.log("DelegationManager: %s", address(delegationManager));
+ console2.log("Salt:");
+ console2.logBytes32(salt);
+ }
+
+ function run() public {
+ console2.log("~~~");
+ console2.log("Deploying Token Transformation System...");
+ vm.startBroadcast();
+
+ // Step 1: Deploy AdapterManager first (no enforcer in constructor)
+ address adapterManager = address(new AdapterManager{ salt: salt }(owner, delegationManager));
+ console2.log("AdapterManager: %s", adapterManager);
+
+ // Step 2: Deploy TokenTransformationEnforcer with the real AdapterManager address
+ address tokenTransformationEnforcer = address(new TokenTransformationEnforcer{ salt: salt }(adapterManager));
+ console2.log("TokenTransformationEnforcer: %s", tokenTransformationEnforcer);
+
+ vm.stopBroadcast();
+
+ // Step 3: Set the enforcer in AdapterManager (as owner)
+ // If deployer is the owner, set it now. Otherwise, owner must call setTokenTransformationEnforcer separately
+ if (deployer == owner) {
+ vm.startBroadcast();
+ AdapterManager(payable(adapterManager))
+ .setTokenTransformationEnforcer(TokenTransformationEnforcer(tokenTransformationEnforcer));
+ vm.stopBroadcast();
+ console2.log("Enforcer set in AdapterManager");
+ } else {
+ console2.log("WARNING: Deployer is not the owner.");
+ console2.log("Owner must call setTokenTransformationEnforcer separately:");
+ console2.log(
+ " AdapterManager(%s).setTokenTransformationEnforcer(TokenTransformationEnforcer(%s))",
+ adapterManager,
+ tokenTransformationEnforcer
+ );
+ }
+
+ // Step 4: Verify the deployment (read-only, no broadcast needed)
+ require(
+ TokenTransformationEnforcer(tokenTransformationEnforcer).adapterManager() == adapterManager,
+ "DeployTokenTransformationSystem: enforcer adapterManager mismatch"
+ );
+ if (deployer == owner) {
+ require(
+ address(AdapterManager(payable(adapterManager)).tokenTransformationEnforcer()) == tokenTransformationEnforcer,
+ "DeployTokenTransformationSystem: adapterManager enforcer mismatch"
+ );
+ console2.log("Deployment verified successfully");
+ } else {
+ console2.log("Note: Enforcer not yet set. Verification will pass after owner calls setTokenTransformationEnforcer.");
+ }
+
+ console2.log("~~~");
+ console2.log("Token Transformation System deployed successfully!");
+ console2.log("AdapterManager: %s", adapterManager);
+ console2.log("TokenTransformationEnforcer: %s", tokenTransformationEnforcer);
+ }
+}
+
diff --git a/src/enforcers/TokenTransformationEnforcer.sol b/src/enforcers/TokenTransformationEnforcer.sol
new file mode 100644
index 00000000..b6b97d09
--- /dev/null
+++ b/src/enforcers/TokenTransformationEnforcer.sol
@@ -0,0 +1,248 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
+
+import { CaveatEnforcer } from "./CaveatEnforcer.sol";
+import { ModeCode } from "../utils/Types.sol";
+
+/**
+ * @title TokenTransformationEnforcer
+ * @notice Tracks token transformations through protocol interactions (e.g., lending protocols).
+ * @dev This enforcer allows tracking multiple tokens per delegationHash, enabling delegation
+ * of an initial token amount and tracking what it transforms into through protocol interactions.
+ * @dev The enforcer validates that token usage doesn't exceed tracked amounts.
+ * @dev State updates can only be made by the AdapterManager.
+ */
+contract TokenTransformationEnforcer is CaveatEnforcer {
+ using ExecutionLib for bytes;
+
+ ////////////////////////////// State //////////////////////////////
+
+ /// @dev Mapping from delegationHash => token => available amount
+ mapping(bytes32 delegationHash => mapping(address token => uint256 amount)) public availableAmounts;
+
+ /// @dev Mapping to track if initial token has been initialized for a delegationHash
+ mapping(bytes32 delegationHash => bool initialized) public isInitialized;
+
+ /// @dev Address of the AdapterManager that can update state
+ address public immutable adapterManager;
+
+ ////////////////////////////// Events //////////////////////////////
+
+ /// @dev Emitted when asset state is updated for a delegation
+ event AssetStateUpdated(bytes32 indexed delegationHash, address indexed token, uint256 oldAmount, uint256 newAmount);
+
+ /// @dev Emitted when tokens are spent from a delegation
+ event TokensSpent(bytes32 indexed delegationHash, address indexed token, uint256 amount, uint256 remaining);
+
+ ////////////////////////////// Errors //////////////////////////////
+
+ /// @dev Error thrown when caller is not the AdapterManager
+ error NotAdapterManager();
+
+ /// @dev Error thrown when insufficient tokens are available
+ error InsufficientTokensAvailable(bytes32 delegationHash, address token, uint256 requested, uint256 available);
+
+ /// @dev Error thrown when invalid terms length is provided
+ error InvalidTermsLength();
+
+ /// @dev Error thrown when protocol is not allowed
+ error ProtocolNotAllowed(address protocol);
+
+ ////////////////////////////// Constructor //////////////////////////////
+
+ /**
+ * @notice Initializes the TokenTransformationEnforcer
+ * @param _adapterManager Address of the AdapterManager contract
+ */
+ constructor(address _adapterManager) {
+ if (_adapterManager == address(0)) revert("TokenTransformationEnforcer:invalid-adapter-manager");
+ adapterManager = _adapterManager;
+ }
+
+ ////////////////////////////// Modifiers //////////////////////////////
+
+ modifier onlyAdapterManager() {
+ if (msg.sender != adapterManager) revert NotAdapterManager();
+ _;
+ }
+
+ ////////////////////////////// Public Methods //////////////////////////////
+
+ /**
+ * @notice Validates that the requested token amount is available for the delegation
+ * @dev Expected delegation types:
+ * - Initial delegation: Grants access to an initial token amount (e.g., 1000 USDC)
+ * - Protocol interaction delegations: Used with AdapterManager to track token transformations
+ * through lending protocols (e.g., USDC -> aUSDC via Aave deposit)
+ * - Multi-token delegations: Tracks multiple tokens per delegationHash as tokens are transformed
+ * @dev When used with AdapterManager, _args must contain the protocol address (20 bytes)
+ * and the protocol will be validated against allowedProtocols from terms
+ * @param _terms Encoded initial token address and amount (52 bytes: 20 bytes token + 32 bytes amount)
+ * Extended format may include allowed protocol addresses
+ * @param _args Protocol address (20 bytes) when used with AdapterManager, empty otherwise
+ * @param _mode The execution mode (must be Single callType, Default execType)
+ * @param _executionCallData The execution call data containing the transfer
+ * @param _delegationHash The hash of the delegation
+ */
+ function beforeHook(
+ bytes calldata _terms,
+ bytes calldata _args,
+ ModeCode _mode,
+ bytes calldata _executionCallData,
+ bytes32 _delegationHash,
+ address,
+ address
+ )
+ public
+ override
+ onlySingleCallTypeMode(_mode)
+ onlyDefaultExecutionMode(_mode)
+ {
+ (address initialToken_, uint256 initialAmount_, address[] memory allowedProtocols_) = getTermsInfo(_terms);
+ _validateProtocol(_args, allowedProtocols_);
+
+ (address token_,, bytes calldata callData_) = _executionCallData.decodeSingle();
+
+ // Validate that this is an ERC20 transfer
+ require(callData_.length == 68, "TokenTransformationEnforcer:invalid-execution-length");
+ require(bytes4(callData_[0:4]) == IERC20.transfer.selector, "TokenTransformationEnforcer:invalid-method");
+
+ // Decode transfer amount
+ uint256 transferAmount_ = uint256(bytes32(callData_[36:68]));
+
+ // Get available amount
+ uint256 available_ = availableAmounts[_delegationHash][token_];
+
+ // Initialize from terms only if this is the first use of the initial token
+ // Only initialize if: token matches initial token AND delegationHash hasn't been initialized yet
+ if (available_ == 0 && !isInitialized[_delegationHash] && token_ == initialToken_) {
+ availableAmounts[_delegationHash][token_] = initialAmount_;
+ isInitialized[_delegationHash] = true;
+ available_ = initialAmount_;
+ }
+
+ if (transferAmount_ > available_) {
+ revert InsufficientTokensAvailable(_delegationHash, token_, transferAmount_, available_);
+ }
+
+ // Deduct from available amount
+ availableAmounts[_delegationHash][token_] = available_ - transferAmount_;
+
+ emit TokensSpent(_delegationHash, token_, transferAmount_, available_ - transferAmount_);
+ }
+
+ ////////////////////////////// Private/Internal Methods //////////////////////////////
+
+ /**
+ * @notice Validates that a protocol address from args is allowed according to the allowedProtocols list
+ * @param _args Protocol address (20 bytes) when used with AdapterManager, empty otherwise
+ * @param _allowedProtocols Array of allowed protocol addresses from terms
+ */
+ function _validateProtocol(bytes calldata _args, address[] memory _allowedProtocols) internal pure {
+ // If args is empty, no protocol validation needed (backward compatible)
+ // TODO: Validate if this is secure
+ if (_args.length == 0) {
+ return;
+ }
+
+ // If args is provided, it must be exactly 20 bytes (protocol address)
+ if (_args.length != 20) revert InvalidTermsLength();
+ address protocol_ = address(bytes20(_args[:20]));
+
+ // Validate protocol against allowed list
+ if (_allowedProtocols.length > 0) {
+ bool isAllowed_ = false;
+ for (uint256 i = 0; i < _allowedProtocols.length; i++) {
+ if (_allowedProtocols[i] == protocol_) {
+ isAllowed_ = true;
+ break;
+ }
+ }
+ if (!isAllowed_) {
+ revert ProtocolNotAllowed(protocol_);
+ }
+ }
+ // If no protocols specified in terms, allow all (backward compatible)
+ }
+
+ ////////////////////////////// Public Methods //////////////////////////////
+
+ /**
+ * @notice Updates the asset state for a delegation after a protocol interaction
+ * @dev Only callable by the AdapterManager
+ * @param _delegationHash The hash of the delegation
+ * @param _token The token address
+ * @param _amount The new amount available (adds to existing if token already tracked)
+ */
+ function updateAssetState(bytes32 _delegationHash, address _token, uint256 _amount) external onlyAdapterManager {
+ uint256 oldAmount_ = availableAmounts[_delegationHash][_token];
+ uint256 newAmount_ = oldAmount_ + _amount;
+ availableAmounts[_delegationHash][_token] = newAmount_;
+
+ emit AssetStateUpdated(_delegationHash, _token, oldAmount_, newAmount_);
+ }
+
+ /**
+ * @notice Gets the available amount for a specific token in a delegation
+ * @param _delegationHash The hash of the delegation
+ * @param _token The token address
+ * @return The available amount
+ */
+ function getAvailableAmount(bytes32 _delegationHash, address _token) external view returns (uint256) {
+ return availableAmounts[_delegationHash][_token];
+ }
+
+ /**
+ * @notice Decodes the terms used in this CaveatEnforcer
+ * @dev Terms format:
+ * - Base (52 bytes): 20 bytes token address + 32 bytes initial amount
+ * - Extended (optional): 1 byte protocol count + N * 20 bytes protocol addresses
+ * - Minimum length: 52 bytes (no protocols, backward compatible)
+ * - Maximum length: 52 + 1 + (255 * 20) = 5162 bytes
+ * @param _terms Encoded data
+ * @return token_ The initial token address
+ * @return amount_ The initial amount
+ * @return allowedProtocols_ Array of allowed protocol addresses (empty if none specified)
+ */
+ function getTermsInfo(bytes calldata _terms)
+ public
+ pure
+ returns (address token_, uint256 amount_, address[] memory allowedProtocols_)
+ {
+ if (_terms.length < 52) revert InvalidTermsLength();
+
+ token_ = address(bytes20(_terms[:20]));
+ amount_ = uint256(bytes32(_terms[20:52]));
+
+ // Check if protocols are specified
+ if (_terms.length == 52) {
+ // No protocols specified (backward compatible)
+ allowedProtocols_ = new address[](0);
+ } else {
+ // Must have at least 53 bytes (base + count byte)
+ if (_terms.length < 53) revert InvalidTermsLength();
+
+ uint8 protocolCount_ = uint8(_terms[52]);
+
+ // Expected length: 52 (base) + 1 (count) + protocolCount * 20 (addresses)
+ uint256 expectedLength_ = 53 + (protocolCount_ * 20);
+ if (_terms.length != expectedLength_) {
+ revert InvalidTermsLength();
+ }
+
+ if (protocolCount_ == 0) {
+ allowedProtocols_ = new address[](0);
+ } else {
+ allowedProtocols_ = new address[](protocolCount_);
+ for (uint8 i = 0; i < protocolCount_; i++) {
+ uint256 offset_ = 53 + (i * 20);
+ allowedProtocols_[i] = address(bytes20(_terms[offset_:offset_ + 20]));
+ }
+ }
+ }
+ }
+}
+
diff --git a/src/helpers/adapters/AaveAdapter.sol b/src/helpers/adapters/AaveAdapter.sol
new file mode 100644
index 00000000..5e754ce3
--- /dev/null
+++ b/src/helpers/adapters/AaveAdapter.sol
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
+import { IAdapter } from "../interfaces/IAdapter.sol";
+import { IAavePool, IAaveDataProvider, IStaticATokenFactory, IERC4626 } from "../interfaces/IAave.sol";
+
+/**
+ * @title AaveAdapter
+ * @notice Adapter for Aave lending protocol interactions
+ * @dev Handles deposit and withdraw actions for Aave V3
+ * @dev Wraps rebasing aTokens into static aTokens (stataTokens) using Aave's ERC-4626 wrapper
+ * This converts rebasing tokens into fixed-supply tokens for easier tracking
+ */
+contract AaveAdapter is IAdapter {
+ using SafeERC20 for IERC20;
+
+ ////////////////////////////// Constants //////////////////////////////
+
+ /// @dev Hash of "deposit" action string
+ bytes32 public constant ACTION_DEPOSIT = keccak256(bytes("deposit"));
+
+ /// @dev Hash of "withdraw" action string
+ bytes32 public constant ACTION_WITHDRAW = keccak256(bytes("withdraw"));
+
+ ////////////////////////////// State //////////////////////////////
+
+ /// @dev Aave Pool contract address
+ address public immutable aavePool;
+
+ /// @dev Aave Data Provider contract address
+ address public immutable aaveDataProvider;
+
+ /// @dev StaticATokenFactory contract address
+ address public immutable staticATokenFactory;
+
+ ////////////////////////////// Errors //////////////////////////////
+
+ /// @dev Error thrown when action is not supported
+ error UnsupportedAction(string action);
+
+ /// @dev Error thrown when protocol address doesn't match Aave Pool
+ error InvalidProtocolAddress();
+
+ /// @dev Error thrown when address is zero
+ error InvalidZeroAddress();
+
+ ////////////////////////////// Constructor //////////////////////////////
+
+ /**
+ * @notice Initializes the AaveAdapter
+ * @param _aavePool The Aave Pool contract address
+ * @param _aaveDataProvider The Aave Data Provider contract address
+ * @param _staticATokenFactory The StaticATokenFactory contract address
+ */
+ constructor(address _aavePool, address _aaveDataProvider, address _staticATokenFactory) {
+ if (_aavePool == address(0) || _aaveDataProvider == address(0) || _staticATokenFactory == address(0)) {
+ revert InvalidZeroAddress();
+ }
+ aavePool = _aavePool;
+ aaveDataProvider = _aaveDataProvider;
+ staticATokenFactory = _staticATokenFactory;
+ }
+
+ ////////////////////////////// Public Methods //////////////////////////////
+
+ /**
+ * @notice Executes an Aave protocol action and returns transformation info
+ * @param _protocolAddress The Aave Pool address (must match aavePool)
+ * @param _action The action to perform ("deposit" or "withdraw")
+ * @param _tokenFrom The input token address
+ * @param _amountFrom The amount of input tokens to use
+ * @param _actionData Additional data containing the AdapterManager address for balance measurement
+ * @return transformationInfo_ The transformation information
+ */
+ function executeProtocolAction(
+ address _protocolAddress,
+ string calldata _action,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData
+ )
+ external
+ override
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ if (_protocolAddress != aavePool) revert InvalidProtocolAddress();
+
+ address adapterManager_ = abi.decode(_actionData, (address));
+
+ bytes32 actionHash_ = keccak256(bytes(_action));
+
+ if (actionHash_ == ACTION_DEPOSIT) {
+ return _handleDeposit(_tokenFrom, _amountFrom, adapterManager_);
+ } else if (actionHash_ == ACTION_WITHDRAW) {
+ return _handleWithdraw(_tokenFrom, _amountFrom, _actionData, adapterManager_);
+ } else {
+ revert UnsupportedAction(_action);
+ }
+ }
+
+ ////////////////////////////// Private Methods //////////////////////////////
+
+ /**
+ * @notice Handles Aave deposit action
+ * @param _tokenFrom The underlying token to deposit (already transferred to this adapter)
+ * @param _amountFrom The amount to deposit
+ * @param _adapterManager The AdapterManager address (for returning wrapped tokens)
+ * @return transformationInfo_ The transformation information
+ */
+ function _handleDeposit(
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ address _adapterManager
+ )
+ private
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ address staticATokenAddress_ = IStaticATokenFactory(staticATokenFactory).getStataToken(address(_tokenFrom));
+ if (staticATokenAddress_ == address(0)) revert InvalidZeroAddress();
+
+ address wrapperAsset_ = IERC4626(staticATokenAddress_).asset();
+ (address aTokenAddress_,,) = IAaveDataProvider(aaveDataProvider).getReserveTokensAddresses(address(_tokenFrom));
+ uint256 wrappedShares_;
+
+ if (wrapperAsset_ == aTokenAddress_) {
+ // Deposit to Aave first, then wrap aTokens
+ uint256 currentAllowance_ = _tokenFrom.allowance(address(this), aavePool);
+ if (currentAllowance_ < _amountFrom) {
+ if (currentAllowance_ > 0) {
+ _tokenFrom.safeDecreaseAllowance(aavePool, currentAllowance_);
+ }
+ _tokenFrom.safeIncreaseAllowance(aavePool, _amountFrom);
+ }
+ IAavePool(aavePool).supply(address(_tokenFrom), _amountFrom, address(this), 0);
+
+ uint256 aTokenBalance_ = IERC20(aTokenAddress_).balanceOf(address(this));
+ IERC20(aTokenAddress_).safeIncreaseAllowance(staticATokenAddress_, aTokenBalance_);
+ wrappedShares_ = IERC4626(staticATokenAddress_).deposit(aTokenBalance_, _adapterManager);
+ } else if (wrapperAsset_ == address(_tokenFrom)) {
+ // Wrapper handles Aave deposit internally
+ _tokenFrom.safeIncreaseAllowance(staticATokenAddress_, _amountFrom);
+ wrappedShares_ = IERC4626(staticATokenAddress_).deposit(_amountFrom, _adapterManager);
+ } else {
+ revert InvalidProtocolAddress();
+ }
+
+ return TransformationInfo({
+ tokenFrom: address(_tokenFrom), amountFrom: _amountFrom, tokenTo: staticATokenAddress_, amountTo: wrappedShares_
+ });
+ }
+
+ /**
+ * @notice Handles Aave withdraw action
+ * @param _wrappedToken The wrapped static aToken to withdraw (already transferred to this adapter)
+ * @param _amountFrom The amount of wrapped token shares to withdraw
+ * @param _actionData Additional data containing underlying token address and AdapterManager address
+ * @param _adapterManager The AdapterManager address (for returning underlying tokens)
+ * @return transformationInfo_ The transformation information
+ */
+ function _handleWithdraw(
+ IERC20 _wrappedToken,
+ uint256 _amountFrom,
+ bytes calldata _actionData,
+ address _adapterManager
+ )
+ private
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ (, bytes memory originalActionData_) = abi.decode(_actionData, (address, bytes));
+ (, address underlyingToken_) = abi.decode(originalActionData_, (address, address));
+
+ address staticATokenAddress_ = IStaticATokenFactory(staticATokenFactory).getStataToken(underlyingToken_);
+ if (staticATokenAddress_ == address(0) || address(_wrappedToken) != staticATokenAddress_) {
+ revert InvalidProtocolAddress();
+ }
+
+ address wrapperAsset_ = IERC4626(staticATokenAddress_).asset();
+ (address aTokenAddress_,,) = IAaveDataProvider(aaveDataProvider).getReserveTokensAddresses(underlyingToken_);
+
+ uint256 underlyingAmount_;
+
+ if (wrapperAsset_ == aTokenAddress_) {
+ uint256 aTokenBalanceBefore_ = IERC20(aTokenAddress_).balanceOf(address(this));
+ IERC4626(staticATokenAddress_).redeem(_amountFrom, address(this), address(this));
+ uint256 aTokenAmount_ = IERC20(aTokenAddress_).balanceOf(address(this)) - aTokenBalanceBefore_;
+
+ uint256 underlyingBalanceBefore_ = IERC20(underlyingToken_).balanceOf(address(this));
+ IAavePool(aavePool).withdraw(underlyingToken_, aTokenAmount_, address(this));
+ underlyingAmount_ = IERC20(underlyingToken_).balanceOf(address(this)) - underlyingBalanceBefore_;
+ } else if (wrapperAsset_ == underlyingToken_) {
+ uint256 underlyingBalanceBefore_ = IERC20(underlyingToken_).balanceOf(address(this));
+ IERC4626(staticATokenAddress_).redeem(_amountFrom, address(this), address(this));
+ underlyingAmount_ = IERC20(underlyingToken_).balanceOf(address(this)) - underlyingBalanceBefore_;
+ } else {
+ revert InvalidProtocolAddress();
+ }
+
+ IERC20(underlyingToken_).safeTransfer(_adapterManager, underlyingAmount_);
+
+ transformationInfo_.tokenFrom = address(_wrappedToken);
+ transformationInfo_.amountFrom = _amountFrom;
+ transformationInfo_.tokenTo = underlyingToken_;
+ transformationInfo_.amountTo = underlyingAmount_;
+ }
+}
diff --git a/src/helpers/adapters/AdapterManager.sol b/src/helpers/adapters/AdapterManager.sol
new file mode 100644
index 00000000..b3597f10
--- /dev/null
+++ b/src/helpers/adapters/AdapterManager.sol
@@ -0,0 +1,520 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
+import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
+import { ModeLib } from "@erc7579/lib/ModeLib.sol";
+import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
+import { ExecutionHelper } from "@erc7579/core/ExecutionHelper.sol";
+
+import { IDelegationManager } from "../../interfaces/IDelegationManager.sol";
+import { Delegation, ModeCode, CallType, ExecType } from "../../utils/Types.sol";
+import { TokenTransformationEnforcer } from "../../enforcers/TokenTransformationEnforcer.sol";
+import { IAdapter } from "../interfaces/IAdapter.sol";
+import { IAavePool } from "../interfaces/IAave.sol";
+import { EncoderLib } from "../../libraries/EncoderLib.sol";
+import { CALLTYPE_SINGLE, EXECTYPE_DEFAULT } from "../../utils/Constants.sol";
+
+/**
+ * @title AdapterManager
+ * @notice Manages protocol adapters and coordinates token transformations through lending protocols
+ * @dev Routes protocol interactions to specific adapters and updates enforcer state
+ * @dev All tokens are transferred to the root delegator after protocol interactions
+ */
+contract AdapterManager is ExecutionHelper, Ownable2Step {
+ using ModeLib for ModeCode;
+ using ExecutionLib for bytes;
+ using SafeERC20 for IERC20;
+
+ ////////////////////////////// State //////////////////////////////
+
+ /// @dev The DelegationManager contract
+ IDelegationManager public immutable delegationManager;
+
+ /// @dev The TokenTransformationEnforcer contract (settable by owner)
+ TokenTransformationEnforcer public tokenTransformationEnforcer;
+
+ /// @dev Mapping from protocol address to adapter address
+ mapping(address protocol => address adapter) public protocolAdapters;
+
+ /// @dev Struct to reduce stack depth
+ struct ExecutionCallDataParams {
+ IERC20 tokenFrom;
+ uint256 amountFrom;
+ address protocolAddress;
+ bytes actionData;
+ address rootDelegator;
+ bytes32 rootDelegationHash;
+ uint256 balanceBefore;
+ }
+
+ ////////////////////////////// Events //////////////////////////////
+
+ /// @dev Emitted when the token transformation enforcer is set
+ event TokenTransformationEnforcerSet(address indexed oldEnforcer, address indexed newEnforcer);
+
+ /// @dev Emitted when a protocol adapter is registered
+ event ProtocolAdapterRegistered(address indexed protocol, address indexed adapter);
+
+ /// @dev Emitted when a protocol adapter is removed
+ event ProtocolAdapterRemoved(address indexed protocol);
+
+ /// @dev Emitted when tokens are transferred to root delegator
+ event TokensTransferredToDelegator(address indexed token, address indexed delegator, uint256 amount);
+
+ /// @dev Emitted when a protocol action is executed
+ event ProtocolActionExecuted(
+ bytes32 indexed delegationHash,
+ address indexed protocol,
+ string action,
+ address tokenFrom,
+ uint256 amountFrom,
+ address tokenTo,
+ uint256 amountTo
+ );
+
+ ////////////////////////////// Errors //////////////////////////////
+
+ /// @dev Error thrown when caller is not the DelegationManager
+ error NotDelegationManager();
+
+ /// @dev Error thrown when caller is not the leaf delegator
+ error NotLeafDelegator();
+
+ /// @dev Error thrown when no adapter is registered for the protocol
+ error NoAdapterForProtocol(address protocol);
+
+ /// @dev Error thrown when delegations array is empty
+ error InvalidEmptyDelegations();
+
+ /// @dev Error thrown when address is zero
+ error InvalidZeroAddress();
+
+ /// @dev Error thrown when native token transfer fails
+ error FailedNativeTokenTransfer(address recipient);
+
+ /// @dev Error thrown when call is not from self
+ error NotSelf();
+
+ /// @dev Error thrown when insufficient tokens are received
+ error InsufficientTokens();
+
+ /// @dev Error thrown when insufficient output tokens are received from adapter
+ error InsufficientOutputTokens();
+
+ /// @dev Error thrown when unsupported call type is used
+ error UnsupportedCallType(CallType callType);
+
+ /// @dev Error thrown when unsupported execution type is used
+ error UnsupportedExecType(ExecType execType);
+
+ /// @dev Error thrown when enforcer is not set
+ error EnforcerNotSet();
+
+ /// @dev Error thrown when TokenTransformationEnforcer is not found at index 0
+ error TokenTransformationEnforcerNotFound();
+
+ ////////////////////////////// Modifiers //////////////////////////////
+
+ modifier onlyDelegationManager() {
+ if (msg.sender != address(delegationManager)) revert NotDelegationManager();
+ _;
+ }
+
+ modifier onlySelf() {
+ if (msg.sender != address(this)) revert NotSelf();
+ _;
+ }
+
+ ////////////////////////////// Constructor //////////////////////////////
+
+ /**
+ * @notice Initializes the AdapterManager
+ * @param _owner The initial owner of the contract
+ * @param _delegationManager The DelegationManager contract address
+ */
+ constructor(address _owner, IDelegationManager _delegationManager) Ownable(_owner) {
+ if (address(_delegationManager) == address(0)) {
+ revert InvalidZeroAddress();
+ }
+ delegationManager = _delegationManager;
+ // tokenTransformationEnforcer is set later by owner via setTokenTransformationEnforcer()
+ }
+
+ ////////////////////////////// External Methods //////////////////////////////
+
+ /**
+ * @notice Allows this contract to receive native tokens
+ */
+ receive() external payable { }
+
+ /**
+ * @notice Sets the TokenTransformationEnforcer contract address
+ * @dev Only callable by the contract owner
+ * @dev Can be called multiple times to update the enforcer address
+ * @param _tokenTransformationEnforcer The TokenTransformationEnforcer contract address
+ */
+ function setTokenTransformationEnforcer(TokenTransformationEnforcer _tokenTransformationEnforcer) external onlyOwner {
+ if (address(_tokenTransformationEnforcer) == address(0)) {
+ revert InvalidZeroAddress();
+ }
+ address oldEnforcer_ = address(tokenTransformationEnforcer);
+ tokenTransformationEnforcer = _tokenTransformationEnforcer;
+ emit TokenTransformationEnforcerSet(oldEnforcer_, address(_tokenTransformationEnforcer));
+ }
+
+ /**
+ * @notice Executes a protocol action using delegations
+ * @dev The msg.sender must be the leaf delegator
+ * @param _protocolAddress The address of the lending protocol contract
+ * @param _tokenFrom The input token address
+ * @param _amountFrom The amount of input tokens to use
+ * @param _actionData Additional data needed for the specific action (first element should be the action string)
+ * @param _delegations Array of Delegation objects, sorted leaf to root
+ */
+ function executeProtocolActionByDelegation(
+ address _protocolAddress,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData,
+ Delegation[] memory _delegations
+ )
+ external
+ {
+ if (_delegations.length == 0) revert InvalidEmptyDelegations();
+ if (_delegations[0].delegator != msg.sender) revert NotLeafDelegator();
+ if (protocolAdapters[_protocolAddress] == address(0)) revert NoAdapterForProtocol(_protocolAddress);
+
+ _validateAndSetProtocol(_delegations, _protocolAddress);
+ _executeWithDelegations(_delegations, _protocolAddress, _tokenFrom, _amountFrom, _actionData);
+ }
+
+ /**
+ * @notice Executes the protocol action using delegations
+ * @param _delegations The delegation chain
+ * @param _protocolAddress The protocol address
+ * @param _tokenFrom The input token
+ * @param _amountFrom The amount to use
+ * @param _actionData Additional action data (first element should be the action string)
+ */
+ function _executeWithDelegations(
+ Delegation[] memory _delegations,
+ address _protocolAddress,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData
+ )
+ private
+ {
+ uint256 delegationsLength_ = _delegations.length;
+ Delegation memory rootDelegation_ = _delegations[delegationsLength_ - 1];
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+ address rootDelegator_ = rootDelegation_.delegator;
+ uint256 balanceBefore_ = _getSelfBalance(_tokenFrom);
+
+ ExecutionCallDataParams memory params_;
+ params_.tokenFrom = _tokenFrom;
+ params_.amountFrom = _amountFrom;
+ params_.protocolAddress = _protocolAddress;
+ params_.actionData = _actionData;
+ params_.rootDelegator = rootDelegator_;
+ params_.rootDelegationHash = rootDelegationHash_;
+ params_.balanceBefore = balanceBefore_;
+
+ delegationManager.redeemDelegations(_buildPermissionContexts(_delegations), _buildModes(), _buildCallDatas(params_));
+ }
+
+ /**
+ * @notice Builds execution call datas array
+ */
+ function _buildCallDatas(ExecutionCallDataParams memory _params) private view returns (bytes[] memory executionCallDatas_) {
+ executionCallDatas_ = new bytes[](2);
+ executionCallDatas_[0] = _encodeTokenTransfer(_params.tokenFrom, _params.amountFrom);
+ executionCallDatas_[1] = _encodeInternalActionCall(_params);
+ }
+
+ /**
+ * @notice Encodes token transfer call data
+ */
+ function _encodeTokenTransfer(IERC20 _tokenFrom, uint256 _amountFrom) private view returns (bytes memory) {
+ if (address(_tokenFrom) == address(0)) {
+ return ExecutionLib.encodeSingle(address(this), _amountFrom, hex"");
+ }
+ return ExecutionLib.encodeSingle(address(_tokenFrom), 0, abi.encodeCall(IERC20.transfer, (address(this), _amountFrom)));
+ }
+
+ /**
+ * @notice Encodes internal action call data
+ */
+ function _encodeInternalActionCall(ExecutionCallDataParams memory _params) private view returns (bytes memory) {
+ bytes memory callData_ = abi.encodeWithSelector(
+ this.executeProtocolActionInternal.selector,
+ _params.protocolAddress,
+ _params.tokenFrom,
+ _params.amountFrom,
+ _params.actionData,
+ _params.rootDelegator,
+ _params.rootDelegationHash,
+ _params.balanceBefore
+ );
+ return ExecutionLib.encodeSingle(address(this), 0, callData_);
+ }
+
+ /**
+ * @notice Builds permission contexts array
+ */
+ function _buildPermissionContexts(Delegation[] memory _delegations) private pure returns (bytes[] memory) {
+ bytes[] memory permissionContexts_ = new bytes[](2);
+ permissionContexts_[0] = abi.encode(_delegations);
+ permissionContexts_[1] = abi.encode(new Delegation[](0));
+ return permissionContexts_;
+ }
+
+ /**
+ * @notice Builds modes array
+ */
+ function _buildModes() private pure returns (ModeCode[] memory) {
+ ModeCode[] memory encodedModes_ = new ModeCode[](2);
+ encodedModes_[0] = ModeLib.encodeSimpleSingle();
+ encodedModes_[1] = ModeLib.encodeSimpleSingle();
+ return encodedModes_;
+ }
+
+ /**
+ * @notice Executes the protocol action internally and updates enforcer state
+ * @dev Only callable internally by this contract (`onlySelf`)
+ * @param _protocolAddress The protocol address
+ * @param _tokenFrom The input token
+ * @param _amountFrom The amount to use
+ * @param _actionData The action data (first element is the action string)
+ * @param _rootDelegator The root delegator address
+ * @param _rootDelegationHash The root delegation hash
+ * @param _balanceBefore The balance before receiving tokens
+ */
+ function executeProtocolActionInternal(
+ address _protocolAddress,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes memory _actionData,
+ address _rootDelegator,
+ bytes32 _rootDelegationHash,
+ uint256 _balanceBefore
+ )
+ external
+ onlySelf
+ {
+ address adapter_ = protocolAdapters[_protocolAddress];
+ if (adapter_ == address(0)) revert NoAdapterForProtocol(_protocolAddress);
+
+ (string memory action_, bytes memory originalActionData_) = abi.decode(_actionData, (string, bytes));
+
+ _handleTokenReceipt(_tokenFrom, _amountFrom, _balanceBefore, _rootDelegator);
+ _tokenFrom.safeTransfer(adapter_, _amountFrom);
+
+ IAdapter.TransformationInfo memory transformationInfo_ = IAdapter(adapter_)
+ .executeProtocolAction(
+ _protocolAddress, action_, _tokenFrom, _amountFrom, abi.encode(address(this), originalActionData_)
+ );
+
+ _handleTransformationResult(transformationInfo_, _rootDelegator, _rootDelegationHash, _protocolAddress, action_);
+ }
+
+ /**
+ * @notice Executes calls on behalf of this contract, authorized by the DelegationManager
+ * @dev Only callable by the DelegationManager
+ * @param _mode The encoded execution mode
+ * @param _executionCalldata The encoded call data (single) to be executed
+ * @return returnData_ An array of returned data from each executed call
+ */
+ function executeFromExecutor(
+ ModeCode _mode,
+ bytes calldata _executionCalldata
+ )
+ external
+ payable
+ onlyDelegationManager
+ returns (bytes[] memory returnData_)
+ {
+ (CallType callType_, ExecType execType_,,) = _mode.decode();
+
+ if (CallType.unwrap(CALLTYPE_SINGLE) != CallType.unwrap(callType_)) {
+ revert UnsupportedCallType(callType_);
+ }
+ if (ExecType.unwrap(EXECTYPE_DEFAULT) != ExecType.unwrap(execType_)) {
+ revert UnsupportedExecType(execType_);
+ }
+
+ (address target_, uint256 value_, bytes calldata callData_) = _executionCalldata.decodeSingle();
+ returnData_ = new bytes[](1);
+ returnData_[0] = _execute(target_, value_, callData_);
+ return returnData_;
+ }
+
+ /**
+ * @notice Registers a protocol adapter
+ * @dev Only callable by the contract owner
+ * @param _protocol The protocol contract address
+ * @param _adapter The adapter contract address
+ */
+ function registerProtocolAdapter(address _protocol, address _adapter) external onlyOwner {
+ if (_protocol == address(0) || _adapter == address(0)) revert InvalidZeroAddress();
+ protocolAdapters[_protocol] = _adapter;
+ emit ProtocolAdapterRegistered(_protocol, _adapter);
+ }
+
+ /**
+ * @notice Removes a protocol adapter
+ * @dev Only callable by the contract owner
+ * @param _protocol The protocol contract address
+ */
+ function removeProtocolAdapter(address _protocol) external onlyOwner {
+ delete protocolAdapters[_protocol];
+ emit ProtocolAdapterRemoved(_protocol);
+ }
+
+ ////////////////////////////// Private/Internal Methods //////////////////////////////
+
+ /**
+ * @notice Validates that TokenTransformationEnforcer is at index 0 of root delegation caveats
+ * and sets the protocol address in args
+ * @dev The TokenTransformationEnforcer must be the first caveat in the root delegation
+ * @param _delegations The delegation chain; the last delegation must include the TokenTransformationEnforcer
+ * @param _protocolAddress The protocol address to set in the enforcer's args
+ */
+ function _validateAndSetProtocol(Delegation[] memory _delegations, address _protocolAddress) private view {
+ // The TokenTransformationEnforcer must be the first caveat in the root delegation
+ uint256 lastIndex_ = _delegations.length - 1;
+ if (
+ _delegations[lastIndex_].caveats.length == 0
+ || _delegations[lastIndex_].caveats[0].enforcer != address(tokenTransformationEnforcer)
+ ) {
+ revert TokenTransformationEnforcerNotFound();
+ }
+
+ // Set protocol address in args of TokenTransformationEnforcer caveat
+ _delegations[lastIndex_].caveats[0].args = abi.encodePacked(_protocolAddress);
+ }
+
+ /**
+ * @notice Handles token receipt verification and excess token return
+ * @param _tokenFrom The input token
+ * @param _amountFrom The expected amount
+ * @param _balanceFromBefore The balance before receiving tokens
+ * @param _rootDelegator The root delegator to return excess tokens to
+ */
+ function _handleTokenReceipt(
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ uint256 _balanceFromBefore,
+ address _rootDelegator
+ )
+ private
+ {
+ uint256 tokenFromObtained_ = _getSelfBalance(_tokenFrom) - _balanceFromBefore;
+ if (tokenFromObtained_ < _amountFrom) revert InsufficientTokens();
+ if (tokenFromObtained_ > _amountFrom) {
+ _sendTokens(_tokenFrom, tokenFromObtained_ - _amountFrom, _rootDelegator);
+ }
+ }
+
+ /**
+ * @notice Approves protocol to spend tokens if needed
+ * @param _tokenFrom The input token
+ * @param _protocolAddress The protocol address
+ * @param _amountFrom The amount needed
+ */
+ function _approveProtocolIfNeeded(IERC20 _tokenFrom, address _protocolAddress, uint256 _amountFrom) private {
+ if (_tokenFrom.allowance(address(this), _protocolAddress) < _amountFrom) {
+ _tokenFrom.safeIncreaseAllowance(_protocolAddress, type(uint256).max);
+ }
+ }
+
+ /**
+ * @notice Approves adapter to transfer aTokens from this contract for wrapping
+ * @dev Only called for Aave deposits where aTokens need to be wrapped
+ * @param _adapter The adapter address
+ * @param _protocolAddress The Aave Pool address
+ * @param _tokenFrom The underlying token
+ * @param _amountFrom The amount to approve
+ */
+ function _approveAdapterForATokenTransfer(
+ address _adapter,
+ address _protocolAddress,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom
+ )
+ private
+ {
+ address aTokenAddress_ = IAavePool(_protocolAddress).getReserveAToken(address(_tokenFrom));
+ IERC20(aTokenAddress_).safeIncreaseAllowance(_adapter, _amountFrom);
+ }
+
+ /**
+ * @notice Handles the transformation result: verifies output, updates enforcer, transfers tokens, and emits event
+ * @param _transformationInfo The transformation information from the adapter
+ * @param _rootDelegator The root delegator address
+ * @param _rootDelegationHash The root delegation hash
+ * @param _protocolAddress The protocol address
+ * @param _action The action string that was performed
+ */
+ function _handleTransformationResult(
+ IAdapter.TransformationInfo memory _transformationInfo,
+ address _rootDelegator,
+ bytes32 _rootDelegationHash,
+ address _protocolAddress,
+ string memory _action
+ )
+ private
+ {
+ uint256 amountTo_ = _transformationInfo.amountTo;
+ if (_getSelfBalance(IERC20(_transformationInfo.tokenTo)) < amountTo_) {
+ revert InsufficientOutputTokens();
+ }
+
+ if (address(tokenTransformationEnforcer) == address(0)) revert EnforcerNotSet();
+ tokenTransformationEnforcer.updateAssetState(_rootDelegationHash, _transformationInfo.tokenTo, amountTo_);
+
+ if (amountTo_ > 0) {
+ _sendTokens(IERC20(_transformationInfo.tokenTo), amountTo_, _rootDelegator);
+ }
+
+ emit ProtocolActionExecuted(
+ _rootDelegationHash,
+ _protocolAddress,
+ _action,
+ _transformationInfo.tokenFrom,
+ _transformationInfo.amountFrom,
+ _transformationInfo.tokenTo,
+ amountTo_
+ );
+ }
+
+ /**
+ * @notice Sends tokens or native token to a specified recipient
+ * @param _token ERC20 token to send or address(0) for native token
+ * @param _amount Amount of tokens or native token to send
+ * @param _recipient Address to receive the funds
+ */
+ function _sendTokens(IERC20 _token, uint256 _amount, address _recipient) private {
+ if (address(_token) == address(0)) {
+ (bool success_,) = _recipient.call{ value: _amount }("");
+ if (!success_) revert FailedNativeTokenTransfer(_recipient);
+ } else {
+ _token.safeTransfer(_recipient, _amount);
+ }
+ emit TokensTransferredToDelegator(address(_token), _recipient, _amount);
+ }
+
+ /**
+ * @notice Returns this contract's balance of the specified ERC20 token
+ * @param _token The token to check balance for
+ * @return balance_ The balance of the specified token
+ */
+ function _getSelfBalance(IERC20 _token) private view returns (uint256 balance_) {
+ if (address(_token) == address(0)) return address(this).balance;
+ return _token.balanceOf(address(this));
+ }
+}
+
diff --git a/src/helpers/adapters/MorphoAdapter.sol b/src/helpers/adapters/MorphoAdapter.sol
new file mode 100644
index 00000000..b2da1f49
--- /dev/null
+++ b/src/helpers/adapters/MorphoAdapter.sol
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
+import { IAdapter } from "../interfaces/IAdapter.sol";
+
+/**
+ * @notice Simplified Morpho Market interface
+ * @dev In production, use the official Morpho interfaces
+ */
+interface IMorphoMarket {
+ function supply(address underlying, uint256 amount, address onBehalfOf, bytes calldata data) external;
+ function withdraw(address underlying, uint256 amount, address onBehalfOf, address receiver) external;
+}
+
+/**
+ * @notice Morpho Positions Manager interface to get market addresses
+ * @dev In production, use the official Morpho interfaces
+ */
+interface IMorphoPositionsManager {
+ function market(address underlying) external view returns (address marketAddress);
+}
+
+/**
+ * @title MorphoAdapter
+ * @notice Adapter for Morpho lending protocol interactions
+ * @dev Handles deposit and withdraw actions for Morpho markets
+ */
+contract MorphoAdapter is IAdapter {
+ using SafeERC20 for IERC20;
+
+ ////////////////////////////// State //////////////////////////////
+
+ /// @dev Morpho Positions Manager contract address
+ address public immutable morphoPositionsManager;
+
+ ////////////////////////////// Errors //////////////////////////////
+
+ /// @dev Error thrown when action is not supported
+ error UnsupportedAction(string action);
+
+ /// @dev Error thrown when protocol address doesn't match Morpho
+ error InvalidProtocolAddress();
+
+ /// @dev Error thrown when market is not found for underlying asset
+ error MarketNotFound(address underlying);
+
+ ////////////////////////////// Constructor //////////////////////////////
+
+ /**
+ * @notice Initializes the MorphoAdapter
+ * @param _morphoPositionsManager The Morpho Positions Manager contract address
+ */
+ constructor(address _morphoPositionsManager) {
+ if (_morphoPositionsManager == address(0)) {
+ revert("MorphoAdapter:invalid-address");
+ }
+ morphoPositionsManager = _morphoPositionsManager;
+ }
+
+ ////////////////////////////// Public Methods //////////////////////////////
+
+ /**
+ * @notice Executes a Morpho protocol action and returns transformation info
+ * @param _protocolAddress The Morpho Positions Manager address (must match morphoPositionsManager)
+ * @param _action The action to perform ("deposit" or "withdraw")
+ * @param _tokenFrom The input token address
+ * @param _amountFrom The amount of input tokens to use
+ * @param _actionData Additional data containing the AdapterManager address for balance measurement
+ * @return transformationInfo_ The transformation information
+ */
+ function executeProtocolAction(
+ address _protocolAddress,
+ string calldata _action,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData
+ )
+ external
+ override
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ if (_protocolAddress != morphoPositionsManager) revert InvalidProtocolAddress();
+
+ // Decode AdapterManager address from actionData
+ address adapterManager_ = abi.decode(_actionData, (address));
+
+ bytes32 actionHash_ = keccak256(bytes(_action));
+
+ if (actionHash_ == keccak256(bytes("deposit"))) {
+ return _handleDeposit(_tokenFrom, _amountFrom, adapterManager_);
+ } else if (actionHash_ == keccak256(bytes("withdraw"))) {
+ return _handleWithdraw(_tokenFrom, _amountFrom, _actionData, adapterManager_);
+ } else {
+ revert UnsupportedAction(_action);
+ }
+ }
+
+ ////////////////////////////// Private Methods //////////////////////////////
+
+ /**
+ * @notice Handles Morpho deposit action
+ * @param _tokenFrom The underlying token to deposit
+ * @param _amountFrom The amount to deposit
+ * @param _adapterManager The AdapterManager address to measure balances
+ * @return transformationInfo_ The transformation information
+ */
+ function _handleDeposit(
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ address _adapterManager
+ )
+ private
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ // Get Morpho market address for the underlying token
+ address marketAddress_ = IMorphoPositionsManager(morphoPositionsManager).market(address(_tokenFrom));
+ if (marketAddress_ == address(0)) revert MarketNotFound(address(_tokenFrom));
+
+ // In Morpho, the market token (mToken) represents the position
+ // For simplicity, we'll use the market address as the tokenTo
+ // In production, you'd query the market contract for the actual mToken address
+ IERC20 mToken_ = IERC20(marketAddress_);
+
+ // Measure mToken balance of AdapterManager before deposit
+ uint256 mTokenBalanceBefore_ = mToken_.balanceOf(_adapterManager);
+
+ // Execute deposit (supply) to Morpho on behalf of AdapterManager
+ IMorphoMarket(marketAddress_).supply(address(_tokenFrom), _amountFrom, _adapterManager, hex"");
+
+ // Measure mToken balance of AdapterManager after deposit
+ uint256 mTokenBalanceAfter_ = mToken_.balanceOf(_adapterManager);
+ uint256 mTokenAmount_ = mTokenBalanceAfter_ - mTokenBalanceBefore_;
+
+ return TransformationInfo({
+ tokenFrom: address(_tokenFrom), amountFrom: _amountFrom, tokenTo: marketAddress_, amountTo: mTokenAmount_
+ });
+ }
+
+ /**
+ * @notice Handles Morpho withdraw action
+ * @param _mToken The mToken to withdraw
+ * @param _amountFrom The amount of mToken to withdraw
+ * @param _actionData Additional data containing underlying token address and AdapterManager address
+ * @param _adapterManager The AdapterManager address to measure balances
+ * @return transformationInfo_ The transformation information
+ */
+ function _handleWithdraw(
+ IERC20 _mToken,
+ uint256 _amountFrom,
+ bytes calldata _actionData,
+ address _adapterManager
+ )
+ private
+ returns (TransformationInfo memory transformationInfo_)
+ {
+ // Decode underlying token address from actionData (skip first 32 bytes which is adapterManager)
+ address underlyingToken_ = abi.decode(_actionData[32:], (address));
+ IERC20 underlyingToken = IERC20(underlyingToken_);
+
+ // Measure underlying token balance of AdapterManager before withdraw
+ uint256 underlyingBalanceBefore_ = underlyingToken.balanceOf(_adapterManager);
+
+ // Execute withdraw from Morpho to AdapterManager
+ IMorphoMarket(address(_mToken)).withdraw(underlyingToken_, _amountFrom, _adapterManager, _adapterManager);
+
+ // Measure underlying token balance of AdapterManager after withdraw
+ uint256 underlyingBalanceAfter_ = underlyingToken.balanceOf(_adapterManager);
+ uint256 underlyingAmount_ = underlyingBalanceAfter_ - underlyingBalanceBefore_;
+
+ return TransformationInfo({
+ tokenFrom: address(_mToken), amountFrom: _amountFrom, tokenTo: underlyingToken_, amountTo: underlyingAmount_
+ });
+ }
+}
+
diff --git a/src/helpers/interfaces/IAave.sol b/src/helpers/interfaces/IAave.sol
new file mode 100644
index 00000000..ac45c96f
--- /dev/null
+++ b/src/helpers/interfaces/IAave.sol
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+/**
+ * @title IAavePool
+ * @notice Simplified Aave Pool interface for supply and withdraw
+ * @dev In production, use the official Aave IPool interface
+ */
+interface IAavePool {
+ function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
+ function withdraw(address asset, uint256 amount, address to) external returns (uint256);
+ function getReserveAToken(address asset) external view returns (address);
+}
+
+/**
+ * @title IAaveDataProvider
+ * @notice Aave Data Provider interface to get aToken address
+ * @dev In production, use the official Aave IProtocolDataProvider interface
+ */
+interface IAaveDataProvider {
+ function getReserveTokensAddresses(address asset) external view returns (address aTokenAddress, address, address);
+}
+
+/**
+ * @title IStaticATokenFactory
+ * @notice StaticATokenFactory interface to get wrapper addresses
+ * @dev Factory deployed by Aave DAO to manage stataToken instances
+ */
+interface IStaticATokenFactory {
+ function getStataToken(address underlying) external view returns (address);
+}
+
+/**
+ * @title IERC4626
+ * @notice ERC-4626 vault interface for StaticAToken wrapper
+ * @dev Wraps rebasing aTokens into fixed-supply tokens
+ */
+interface IERC4626 {
+ function deposit(uint256 assets, address receiver) external returns (uint256 shares);
+ function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
+ function convertToShares(uint256 assets) external view returns (uint256);
+ function convertToAssets(uint256 shares) external view returns (uint256);
+ function asset() external view returns (address);
+}
+
diff --git a/src/helpers/interfaces/IAdapter.sol b/src/helpers/interfaces/IAdapter.sol
new file mode 100644
index 00000000..20a35357
--- /dev/null
+++ b/src/helpers/interfaces/IAdapter.sol
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { Delegation } from "../../utils/Types.sol";
+
+/**
+ * @title IAdapter
+ * @notice Interface for protocol adapters that handle token transformations
+ */
+interface IAdapter {
+ /**
+ * @notice Struct representing token transformation information
+ */
+ struct TransformationInfo {
+ address tokenFrom;
+ uint256 amountFrom;
+ address tokenTo;
+ uint256 amountTo;
+ }
+
+ /**
+ * @notice Executes a protocol interaction and returns transformation info
+ * @param _protocolAddress The address of the protocol contract
+ * @param _action The action to perform (e.g., "deposit", "withdraw", "borrow", "repay")
+ * @param _tokenFrom The input token address
+ * @param _amountFrom The amount of input tokens to use
+ * @param _actionData Additional data needed for the specific action
+ * @return transformationInfo_ The transformation information (tokenFrom, amountFrom, tokenTo, amountTo)
+ */
+ function executeProtocolAction(
+ address _protocolAddress,
+ string calldata _action,
+ IERC20 _tokenFrom,
+ uint256 _amountFrom,
+ bytes calldata _actionData
+ )
+ external
+ returns (TransformationInfo memory transformationInfo_);
+}
+
diff --git a/test/enforcers/TokenTransformationEnforcer.t.sol b/test/enforcers/TokenTransformationEnforcer.t.sol
new file mode 100644
index 00000000..882e0819
--- /dev/null
+++ b/test/enforcers/TokenTransformationEnforcer.t.sol
@@ -0,0 +1,441 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import "forge-std/Test.sol";
+import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol";
+import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol";
+import { BasicERC20 } from "../utils/BasicERC20.t.sol";
+import { EncoderLib } from "../../src/libraries/EncoderLib.sol";
+import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol";
+import { Implementation, SignatureType } from "../utils/Types.t.sol";
+import { TokenTransformationEnforcer } from "../../src/enforcers/TokenTransformationEnforcer.sol";
+import { AdapterManager } from "../../src/helpers/adapters/AdapterManager.sol";
+import { ModeLib } from "@erc7579/lib/ModeLib.sol";
+
+/**
+ * @title TokenTransformationEnforcer Test
+ * @notice Tests for the TokenTransformationEnforcer that tracks multiple tokens per delegation
+ */
+contract TokenTransformationEnforcerTest is CaveatEnforcerBaseTest {
+ using ModeLib for ModeCode;
+
+ ////////////////////////////// State //////////////////////////////
+
+ TokenTransformationEnforcer public tokenTransformationEnforcer;
+ AdapterManager public adapterManager;
+ BasicERC20 public testToken;
+ BasicERC20 public testToken2;
+
+ uint256 public constant INITIAL_AMOUNT = 1000 ether;
+ uint256 public constant TRANSFER_AMOUNT = 500 ether;
+
+ ////////////////////////////// Setup //////////////////////////////
+
+ function setUp() public override {
+ super.setUp();
+
+ address owner_ = makeAddr("AdapterManager Owner");
+
+ // Deploy AdapterManager first (needed for enforcer constructor)
+ adapterManager = new AdapterManager(owner_, delegationManager);
+ vm.label(address(adapterManager), "AdapterManager");
+
+ // Deploy TokenTransformationEnforcer with the real AdapterManager address
+ tokenTransformationEnforcer = new TokenTransformationEnforcer(address(adapterManager));
+ vm.label(address(tokenTransformationEnforcer), "TokenTransformationEnforcer");
+
+ // Set the enforcer in the adapterManager (must be called by owner)
+ vm.prank(owner_);
+ adapterManager.setTokenTransformationEnforcer(tokenTransformationEnforcer);
+
+ // Deploy test tokens
+ testToken = new BasicERC20(address(users.alice.deleGator), "TestToken", "TEST", INITIAL_AMOUNT);
+ testToken2 = new BasicERC20(address(users.alice.deleGator), "TestToken2", "TEST2", INITIAL_AMOUNT);
+ vm.label(address(testToken), "TestToken");
+ vm.label(address(testToken2), "TestToken2");
+
+ // Fund wallets with ETH for gas
+ vm.deal(address(users.alice.deleGator), 10 ether);
+ vm.deal(address(users.bob.deleGator), 10 ether);
+ }
+
+ ////////////////////////////// Unit Tests //////////////////////////////
+
+ /// @notice Test that getTermsInfo correctly decodes terms
+ function test_getTermsInfo() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ (address token_, uint256 amount_, address[] memory allowedProtocols_) = tokenTransformationEnforcer.getTermsInfo(terms_);
+ assertEq(token_, address(testToken));
+ assertEq(amount_, INITIAL_AMOUNT);
+ assertEq(allowedProtocols_.length, 0, "Should have no protocols for base format");
+ }
+
+ /// @notice Test that getTermsInfo reverts on invalid terms length
+ function test_getTermsInfo_invalidLength() public {
+ bytes memory invalidTerms_ = abi.encodePacked(address(testToken));
+ vm.expectRevert(TokenTransformationEnforcer.InvalidTermsLength.selector);
+ tokenTransformationEnforcer.getTermsInfo(invalidTerms_);
+ }
+
+ /// @notice Test that initial amount is set on first use
+ function test_initializesOnFirstUse() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+
+ // Verify terms are correct by calling getTermsInfo directly (this works)
+ (address token_, uint256 amount_,) = tokenTransformationEnforcer.getTermsInfo(terms_);
+ assertEq(token_, address(testToken));
+ assertEq(amount_, INITIAL_AMOUNT);
+ assertEq(terms_.length, 52, "Terms must be exactly 52 bytes");
+
+ Execution memory execution_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), TRANSFER_AMOUNT)
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ // Before first use, available amount should be 0
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken)), 0);
+ assertEq(tokenTransformationEnforcer.isInitialized(delegationHash_), false);
+
+ // Execute beforeHook
+ vm.prank(address(delegationManager));
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+
+ // After first use, should be initialized and amount deducted
+ assertEq(tokenTransformationEnforcer.isInitialized(delegationHash_), true);
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken)), INITIAL_AMOUNT - TRANSFER_AMOUNT
+ );
+ }
+
+ /// @notice Test that transfer succeeds when amount is available
+ function test_transferSucceedsWhenAmountAvailable() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Execution memory execution_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), TRANSFER_AMOUNT)
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ // Execute beforeHook
+ vm.prank(address(delegationManager));
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken)), INITIAL_AMOUNT - TRANSFER_AMOUNT
+ );
+ }
+
+ /// @notice Test that transfer fails when amount exceeds available
+ function test_transferFailsWhenAmountExceedsAvailable() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ uint256 excessiveAmount_ = INITIAL_AMOUNT + 1;
+ Execution memory execution_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), excessiveAmount_)
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ vm.prank(address(delegationManager));
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ TokenTransformationEnforcer.InsufficientTokensAvailable.selector,
+ delegationHash_,
+ address(testToken),
+ excessiveAmount_,
+ INITIAL_AMOUNT
+ )
+ );
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+ }
+
+ /// @notice Test that updateAssetState can only be called by AdapterManager
+ function test_updateAssetState_onlyAdapterManager() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ // Try to update from non-AdapterManager address
+ vm.prank(address(users.bob.deleGator));
+ vm.expectRevert(TokenTransformationEnforcer.NotAdapterManager.selector);
+ tokenTransformationEnforcer.updateAssetState(delegationHash_, address(testToken2), 100 ether);
+
+ // Note: Calling from real adapterManager won't work in unit test because enforcer
+ // was deployed with placeholder address. This is tested in integration tests.
+ }
+
+ /// @notice Test that updateAssetState correctly adds new token amounts
+ /// @dev Note: This test uses the placeholder adapterManager address stored in enforcer
+ /// Full integration is tested in TokenTransformationSystemTest
+ function test_updateAssetState_addsNewToken() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ uint256 newTokenAmount_ = 200 ether;
+
+ // Update state from placeholder AdapterManager address (stored in enforcer)
+ // In real usage, this would be called from the actual AdapterManager
+ address storedAdapterManager_ = tokenTransformationEnforcer.adapterManager();
+ vm.prank(storedAdapterManager_);
+ tokenTransformationEnforcer.updateAssetState(delegationHash_, address(testToken2), newTokenAmount_);
+
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken2)), newTokenAmount_);
+ }
+
+ /// @notice Test that updateAssetState adds to existing amounts
+ function test_updateAssetState_addsToExisting() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ uint256 firstAmount_ = 100 ether;
+ uint256 secondAmount_ = 50 ether;
+
+ address storedAdapterManager_ = tokenTransformationEnforcer.adapterManager();
+
+ // Add first amount
+ vm.prank(storedAdapterManager_);
+ tokenTransformationEnforcer.updateAssetState(delegationHash_, address(testToken2), firstAmount_);
+
+ // Add second amount
+ vm.prank(storedAdapterManager_);
+ tokenTransformationEnforcer.updateAssetState(delegationHash_, address(testToken2), secondAmount_);
+
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken2)), firstAmount_ + secondAmount_);
+ }
+
+ /// @notice Test that initialization only happens once per delegationHash
+ function test_initializationOnlyOnce() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Execution memory execution1_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), 100 ether)
+ });
+ bytes memory executionCallData1_ = ExecutionLib.encodeSingle(execution1_.target, execution1_.value, execution1_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ // First use - should initialize
+ vm.prank(address(delegationManager));
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData1_, delegationHash_, address(0), address(0)
+ );
+
+ assertEq(tokenTransformationEnforcer.isInitialized(delegationHash_), true);
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken)), INITIAL_AMOUNT - 100 ether);
+
+ // Second use - should NOT re-initialize, should deduct from remaining
+ Execution memory execution2_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), 200 ether)
+ });
+ bytes memory executionCallData2_ = ExecutionLib.encodeSingle(execution2_.target, execution2_.value, execution2_.callData);
+
+ vm.prank(address(delegationManager));
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData2_, delegationHash_, address(0), address(0)
+ );
+
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(testToken)),
+ INITIAL_AMOUNT - 100 ether - 200 ether
+ );
+ }
+
+ /// @notice Test that initialization only happens for the initial token
+ function test_initializationOnlyForInitialToken() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Execution memory execution_ = Execution({
+ target: address(testToken2), // Different token
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), 100 ether)
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ // Try to use different token - should fail (no available amount)
+ vm.prank(address(delegationManager));
+ vm.expectRevert(
+ abi.encodeWithSelector(
+ TokenTransformationEnforcer.InsufficientTokensAvailable.selector, delegationHash_, address(testToken2), 100 ether, 0
+ )
+ );
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+ }
+
+ /// @notice Test that invalid execution length reverts
+ function test_invalidExecutionLength() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Execution memory execution_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.transferFrom.selector, address(0), address(0), 0) // Wrong length
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ vm.prank(address(delegationManager));
+ vm.expectRevert("TokenTransformationEnforcer:invalid-execution-length");
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+ }
+
+ /// @notice Test that invalid method selector reverts
+ function test_invalidMethod() public {
+ bytes memory terms_ = abi.encodePacked(address(testToken), INITIAL_AMOUNT);
+ Execution memory execution_ = Execution({
+ target: address(testToken),
+ value: 0,
+ callData: abi.encodeWithSelector(IERC20.approve.selector, address(users.bob.deleGator), 100 ether) // Wrong method
+ });
+ bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
+
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
+
+ vm.prank(address(delegationManager));
+ vm.expectRevert("TokenTransformationEnforcer:invalid-method");
+ tokenTransformationEnforcer.beforeHook(
+ terms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0)
+ );
+ }
+
+ ////////////////////////////// Helper Methods //////////////////////////////
+
+ function _getEnforcer() internal view override returns (ICaveatEnforcer) {
+ return ICaveatEnforcer(address(tokenTransformationEnforcer));
+ }
+}
+
diff --git a/test/helpers/TokenTransformationSystem.t.sol b/test/helpers/TokenTransformationSystem.t.sol
new file mode 100644
index 00000000..8a420c0b
--- /dev/null
+++ b/test/helpers/TokenTransformationSystem.t.sol
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: MIT AND Apache-2.0
+pragma solidity 0.8.23;
+
+import { Test } from "forge-std/Test.sol";
+import { ModeLib } from "@erc7579/lib/ModeLib.sol";
+import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
+import { BaseTest } from "../utils/BaseTest.t.sol";
+import { Implementation, SignatureType } from "../utils/Types.t.sol";
+import { Execution, Delegation, Caveat, ModeCode } from "../../src/utils/Types.sol";
+import { TokenTransformationEnforcer } from "../../src/enforcers/TokenTransformationEnforcer.sol";
+import { AdapterManager } from "../../src/helpers/adapters/AdapterManager.sol";
+import { AaveAdapter } from "../../src/helpers/adapters/AaveAdapter.sol";
+import { IAdapter } from "../../src/helpers/interfaces/IAdapter.sol";
+import { EncoderLib } from "../../src/libraries/EncoderLib.sol";
+import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol";
+import { TimestampEnforcer } from "../../src/enforcers/TimestampEnforcer.sol";
+import { IAavePool, IAaveDataProvider, IStaticATokenFactory, IERC4626 } from "../../src/helpers/interfaces/IAave.sol";
+
+// @dev Do not remove this comment below
+/// forge-config: default.evm_version = "shanghai"
+
+/**
+ * @title TokenTransformationSystem Test
+ * @notice Integration tests for the token transformation system with Aave V3
+ * @dev Uses a forked Ethereum mainnet environment to test real contract interactions
+ */
+contract TokenTransformationSystemTest is BaseTest {
+ using ModeLib for ModeCode;
+ using SafeERC20 for IERC20;
+
+ ////////////////////////////// Constants //////////////////////////////
+
+ IAavePool public constant AAVE_POOL = IAavePool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
+ IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
+ address public constant AAVE_DATA_PROVIDER = 0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3;
+ address public constant STATIC_A_TOKEN_FACTORY = 0xCb0b5cA20b6C5C02A9A3B2cE433650768eD2974F; // Aave V3 Ethereum
+ // StaticATokenFactory
+ address public constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341;
+
+ uint256 public constant MAINNET_FORK_BLOCK = 24029630;
+
+ uint256 public constant INITIAL_USDC_BALANCE = 10000000000; // 10k USDC (6 decimals)
+ uint256 public constant DEPOSIT_AMOUNT = 1000000000; // 1k USDC
+
+ ////////////////////////////// State //////////////////////////////
+
+ TokenTransformationEnforcer public tokenTransformationEnforcer;
+ AdapterManager public adapterManager;
+ AaveAdapter public aaveAdapter;
+ IERC20 public aUSDC;
+ IERC4626 public stataUSDC; // Wrapped aUSDC (static aToken)
+ TimestampEnforcer public timestampEnforcer;
+
+ ////////////////////////////// Setup //////////////////////////////
+
+ function setUp() public override {
+ // Create fork from mainnet at specific block
+ vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), MAINNET_FORK_BLOCK);
+
+ // Set implementation type
+ IMPLEMENTATION = Implementation.Hybrid;
+ SIGNATURE_TYPE = SignatureType.RawP256;
+
+ // Call parent setup to initialize delegation framework
+ super.setUp();
+
+ address owner_ = makeAddr("AdapterManager Owner");
+
+ adapterManager = new AdapterManager(owner_, delegationManager);
+ tokenTransformationEnforcer = new TokenTransformationEnforcer(address(adapterManager));
+
+ // Set the enforcer on AdapterManager
+ vm.prank(adapterManager.owner());
+ adapterManager.setTokenTransformationEnforcer(tokenTransformationEnforcer);
+
+ vm.label(address(tokenTransformationEnforcer), "TokenTransformationEnforcer");
+ vm.label(address(adapterManager), "AdapterManager");
+
+ // Deploy TimestampEnforcer for testing with multiple caveats (set to future so it doesn't restrict)
+ timestampEnforcer = new TimestampEnforcer();
+ vm.label(address(timestampEnforcer), "TimestampEnforcer");
+
+ // Deploy AaveAdapter with StaticATokenFactory
+ aaveAdapter = new AaveAdapter(address(AAVE_POOL), AAVE_DATA_PROVIDER, STATIC_A_TOKEN_FACTORY);
+ vm.label(address(aaveAdapter), "AaveAdapter");
+
+ // Register Aave adapter in AdapterManager
+ vm.prank(adapterManager.owner());
+ adapterManager.registerProtocolAdapter(address(AAVE_POOL), address(aaveAdapter));
+
+ // Get aUSDC address
+ aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC)));
+ vm.label(address(aUSDC), "aUSDC");
+
+ // Get stataUSDC wrapper address from factory
+ // Factory expects underlying token address (USDC), not aToken address
+ if (STATIC_A_TOKEN_FACTORY.code.length == 0) {
+ revert("StaticATokenFactory does not exist at fork block");
+ }
+
+ // Query factory for stataUSDC address using USDC address (not aUSDC)
+ address stataUSDCAddress_ = IStaticATokenFactory(STATIC_A_TOKEN_FACTORY).getStataToken(address(USDC));
+ if (stataUSDCAddress_ == address(0)) {
+ revert("stataUSDC not found in factory - wrapper may not be deployed at fork block");
+ }
+ if (stataUSDCAddress_.code.length == 0) {
+ revert("stataUSDC wrapper not deployed at fork block");
+ }
+ stataUSDC = IERC4626(stataUSDCAddress_);
+ vm.label(address(stataUSDC), "stataUSDC");
+
+ // Labels
+ vm.label(address(AAVE_POOL), "Aave Pool");
+ vm.label(address(USDC), "USDC");
+ vm.label(USDC_WHALE, "USDC Whale");
+
+ // Fund Alice's deleGator with USDC
+ vm.deal(address(users.alice.deleGator), 1 ether);
+ vm.prank(USDC_WHALE);
+ USDC.transfer(address(users.alice.deleGator), INITIAL_USDC_BALANCE);
+ }
+
+ ////////////////////////////// Helper Functions //////////////////////////////
+
+ /// @notice Creates a delegation with TokenTransformationEnforcer as the first caveat (required by AdapterManager)
+ /// @dev TokenTransformationEnforcer MUST be at index 0 of the root delegation's caveats
+ /// @dev For the leaf delegation, the caller must be the delegator
+ function _createTokenTransformationDelegation(
+ address _delegate,
+ address _token,
+ uint256 _amount
+ )
+ internal
+ view
+ returns (Delegation memory)
+ {
+ bytes memory terms_ = abi.encodePacked(_token, _amount);
+ Caveat[] memory caveats_ = new Caveat[](1);
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory delegation_ = Delegation({
+ delegate: _delegate,
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+
+ return signDelegation(users.alice, delegation_);
+ }
+
+ /// @notice Creates a leaf delegation where the caller is the delegator (required for executeProtocolActionByDelegation)
+ /// @dev The delegator must be the caller (msg.sender) for the first delegation in the chain
+ /// @dev Note: The leaf delegation doesn't need TokenTransformationEnforcer - it's only required in the root delegation
+ function _createLeafDelegation(
+ address _callerDeleGator,
+ bytes32 _parentDelegationHash
+ )
+ internal
+ pure
+ returns (Delegation memory)
+ {
+ // Leaf delegation has no caveats - validation happens at root level
+ Caveat[] memory caveats_ = new Caveat[](0);
+
+ Delegation memory delegation_ = Delegation({
+ delegate: address(0), // Will be set by caller
+ delegator: _callerDeleGator,
+ authority: _parentDelegationHash,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+
+ return delegation_;
+ }
+
+ /// @notice Asserts balances for Alice's deleGator
+ function _assertBalances(uint256 expectedUSDC, uint256 expectedAUSDC) internal {
+ uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator));
+ assertEq(aliceUSDCBalance_, expectedUSDC, "USDC balance mismatch");
+
+ uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator));
+ assertEq(aliceATokenBalance_, expectedAUSDC, "aUSDC balance mismatch");
+ }
+
+ /// @notice Asserts available amounts in enforcer
+ function _assertAvailableAmounts(bytes32 _delegationHash, address _token, uint256 _expectedAmount) internal {
+ uint256 available_ = tokenTransformationEnforcer.getAvailableAmount(_delegationHash, _token);
+ assertEq(available_, _expectedAmount, "Available amount mismatch");
+ }
+
+ ////////////////////////////// Tests //////////////////////////////
+
+ /// @notice Test direct deposit to Aave (baseline test)
+ function test_deposit_direct() public {
+ uint256 aliceUSDCInitialBalance_ = USDC.balanceOf(address(users.alice.deleGator));
+ assertEq(aliceUSDCInitialBalance_, INITIAL_USDC_BALANCE);
+
+ vm.prank(address(users.alice.deleGator));
+ USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT);
+ vm.prank(address(users.alice.deleGator));
+ AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0);
+
+ uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator));
+ assertEq(aliceUSDCBalance_, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT);
+
+ uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator));
+ // Aave may return slightly less due to interest accrual (allow 1 wei tolerance)
+ assertGe(aliceATokenBalance_, DEPOSIT_AMOUNT - 1, "aToken balance should be close to deposit amount");
+ assertLe(aliceATokenBalance_, DEPOSIT_AMOUNT, "aToken balance should not exceed deposit amount");
+ }
+
+ /// @notice Test token transformation flow: deposit USDC → get wrapped aUSDC (stataUSDC)
+ function test_depositViaDelegation_tracksTransformation() public {
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory rootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+ bytes32 delegationHash_ = rootDelegationHash_;
+
+ // Verify initial state
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)), 0);
+ assertEq(tokenTransformationEnforcer.isInitialized(delegationHash_), false);
+
+ // Execute protocol action via AdapterManager
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+
+ // Verify tokens were transferred to Alice (root delegator)
+ assertEq(USDC.balanceOf(address(users.alice.deleGator)), INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT);
+
+ // Verify wrapped tokens (stataUSDC) were received
+ uint256 wrappedBalance_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+ assertGt(wrappedBalance_, 0, "Wrapped token balance should be greater than 0");
+
+ // Verify enforcer state was updated
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)), 0, "USDC should be fully spent");
+ uint256 trackedAmount_ = tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(stataUSDC));
+ assertGe(trackedAmount_, wrappedBalance_ - 1, "Wrapped token should be tracked");
+ assertLe(trackedAmount_, wrappedBalance_ + 1, "Wrapped token should be tracked");
+ }
+
+ /// @notice Test partial deposit: use only part of delegated amount
+ function test_partialDeposit_tracksRemaining() public {
+ uint256 delegatedAmount_ = DEPOSIT_AMOUNT * 2; // 2000 USDC
+ uint256 depositAmount_ = DEPOSIT_AMOUNT; // 1000 USDC
+
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory rootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(USDC), delegatedAmount_);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+ bytes32 delegationHash_ = rootDelegationHash_;
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, depositAmount_, actionData_, delegations_);
+
+ // Verify remaining USDC is still available
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)),
+ delegatedAmount_ - depositAmount_,
+ "Remaining USDC should be tracked"
+ );
+
+ // Verify wrapped tokens (stataUSDC) were received
+ uint256 wrappedBalance_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+
+ // Verify wrapped tokens were tracked
+ uint256 trackedWrapped_ = tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(stataUSDC));
+ assertGe(trackedWrapped_, wrappedBalance_ - 1, "Wrapped token should be tracked");
+ assertLe(trackedWrapped_, wrappedBalance_ + 1, "Wrapped token should be tracked");
+ }
+
+ /// @notice Test withdraw flow: wrapped aUSDC (stataUSDC) → USDC
+ function test_withdrawViaDelegation_tracksTransformation() public {
+ // First, deposit to get wrapped tokens
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory depositRootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT);
+ bytes32 depositRootDelegationHash_ = EncoderLib._getDelegationHash(depositRootDelegation_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory depositLeafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), depositRootDelegationHash_);
+ depositLeafDelegation_.delegate = address(adapterManager);
+ depositLeafDelegation_ = signDelegation(users.bob, depositLeafDelegation_);
+
+ Delegation[] memory depositDelegations_ = new Delegation[](2);
+ depositDelegations_[0] = depositLeafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ depositDelegations_[1] = depositRootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalDepositActionData_ = abi.encode(address(adapterManager));
+ bytes memory depositActionData_ = abi.encode("deposit", originalDepositActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(
+ address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, depositActionData_, depositDelegations_
+ );
+
+ // Get actual wrapped token amount received
+ uint256 wrappedAmount_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+ require(wrappedAmount_ > 0, "Should have wrapped tokens");
+
+ // Now create delegation for withdrawal using wrapped tokens
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory withdrawRootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(stataUSDC), wrappedAmount_);
+ bytes32 withdrawRootDelegationHash_ = EncoderLib._getDelegationHash(withdrawRootDelegation_);
+ bytes32 withdrawDelegationHash_ = withdrawRootDelegationHash_;
+
+ // Note: In a real scenario, we'd use the same delegationHash, but for testing
+ // we'll simulate by updating the enforcer state manually first
+ vm.prank(address(adapterManager));
+ tokenTransformationEnforcer.updateAssetState(withdrawDelegationHash_, address(stataUSDC), wrappedAmount_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory withdrawLeafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), withdrawRootDelegationHash_);
+ withdrawLeafDelegation_.delegate = address(adapterManager);
+ withdrawLeafDelegation_ = signDelegation(users.bob, withdrawLeafDelegation_);
+
+ Delegation[] memory withdrawDelegations_ = new Delegation[](2);
+ withdrawDelegations_[0] = withdrawLeafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ withdrawDelegations_[1] = withdrawRootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalWithdrawActionData_ = abi.encode(address(adapterManager), address(USDC));
+ bytes memory withdrawActionData_ = abi.encode("withdraw", originalWithdrawActionData_);
+
+ uint256 usdcBalanceBefore_ = USDC.balanceOf(address(users.alice.deleGator));
+
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(
+ address(AAVE_POOL), IERC20(address(stataUSDC)), wrappedAmount_, withdrawActionData_, withdrawDelegations_
+ );
+
+ // Verify USDC was received
+ uint256 usdcBalanceAfter_ = USDC.balanceOf(address(users.alice.deleGator));
+ assertGt(usdcBalanceAfter_, usdcBalanceBefore_, "USDC should increase after withdraw");
+
+ // Verify wrapped tokens were deducted
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(withdrawDelegationHash_, address(stataUSDC)),
+ 0,
+ "Wrapped tokens should be fully spent"
+ );
+ }
+
+ /// @notice Test multiple transformations: USDC → wrapped aUSDC (stataUSDC) → USDC
+ function test_multipleTransformations_tracksAllTokens() public {
+ uint256 initialAmount_ = DEPOSIT_AMOUNT * 2; // 2000 USDC
+
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory rootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(USDC), initialAmount_);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+ bytes32 delegationHash_ = rootDelegationHash_;
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ // Step 1: Deposit 1000 USDC
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+
+ uint256 wrappedAmount1_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+
+ // Verify state after deposit
+ assertEq(
+ tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)),
+ DEPOSIT_AMOUNT,
+ "Remaining USDC should be tracked"
+ );
+ uint256 trackedWrapped1_ = tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(stataUSDC));
+ assertGe(trackedWrapped1_, wrappedAmount1_ - 1, "Wrapped tokens should be tracked");
+ assertLe(trackedWrapped1_, wrappedAmount1_ + 1, "Wrapped tokens should be tracked");
+
+ // Step 2: Use remaining 1000 USDC for another deposit
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+
+ uint256 wrappedAmount2_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+
+ // Verify final state
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)), 0, "All USDC should be spent");
+ uint256 trackedWrapped2_ = tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(stataUSDC));
+ assertGe(trackedWrapped2_, wrappedAmount2_ - 1, "All wrapped tokens should be tracked");
+ assertLe(trackedWrapped2_, wrappedAmount2_ + 1, "All wrapped tokens should be tracked");
+ }
+
+ /// @notice Test that exceeding available amount fails
+ function test_exceedingAvailableAmount_fails() public {
+ // Create root delegation: Alice delegates to Bob
+ Delegation memory rootDelegation_ =
+ _createTokenTransformationDelegation(address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ uint256 excessiveAmount_ = DEPOSIT_AMOUNT + 1;
+
+ vm.prank(address(users.bob.deleGator));
+ vm.expectRevert();
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, excessiveAmount_, actionData_, delegations_);
+ }
+
+ /// @notice Test that delegation without TokenTransformationEnforcer at index 0 fails
+ function test_missingTokenTransformationEnforcerAtIndex0_fails() public {
+ // Create root delegation with TokenTransformationEnforcer NOT at index 0
+ bytes memory terms_ = abi.encodePacked(address(USDC), DEPOSIT_AMOUNT);
+ Caveat[] memory caveats_ = new Caveat[](2);
+ // Put a dummy enforcer at index 0
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(0x1234), terms: hex"" });
+ // TokenTransformationEnforcer at index 1 (should fail)
+ caveats_[1] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+
+ Delegation memory rootDelegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ rootDelegation_ = signDelegation(users.alice, rootDelegation_);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ vm.expectRevert(AdapterManager.TokenTransformationEnforcerNotFound.selector);
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+ }
+
+ /// @notice Test that delegation with no caveats fails
+ function test_delegationWithNoCaveats_fails() public {
+ // Create root delegation with no caveats
+ Delegation memory rootDelegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: new Caveat[](0),
+ salt: 0,
+ signature: hex""
+ });
+ rootDelegation_ = signDelegation(users.alice, rootDelegation_);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ vm.expectRevert(AdapterManager.TokenTransformationEnforcerNotFound.selector);
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+ }
+
+ /// @notice Test that delegation with TokenTransformationEnforcer at index 0 but other caveats works
+ function test_tokenTransformationEnforcerAtIndex0WithOtherCaveats_succeeds() public {
+ // Create root delegation with TokenTransformationEnforcer at index 0 and another caveat
+ bytes memory terms_ = abi.encodePacked(address(USDC), DEPOSIT_AMOUNT);
+ Caveat[] memory caveats_ = new Caveat[](2);
+ // TokenTransformationEnforcer at index 0 (required)
+ caveats_[0] = Caveat({ args: hex"", enforcer: address(tokenTransformationEnforcer), terms: terms_ });
+ // Another caveat at index 1 - use TimestampEnforcer set to future timestamp (won't restrict)
+ bytes memory timestampTerms_ = abi.encodePacked(uint256(block.timestamp + 1 days));
+ caveats_[1] = Caveat({ args: hex"", enforcer: address(timestampEnforcer), terms: timestampTerms_ });
+
+ Delegation memory rootDelegation_ = Delegation({
+ delegate: address(users.bob.deleGator),
+ delegator: address(users.alice.deleGator),
+ authority: ROOT_AUTHORITY,
+ caveats: caveats_,
+ salt: 0,
+ signature: hex""
+ });
+ rootDelegation_ = signDelegation(users.alice, rootDelegation_);
+ bytes32 rootDelegationHash_ = EncoderLib._getDelegationHash(rootDelegation_);
+ bytes32 delegationHash_ = rootDelegationHash_;
+
+ // Create leaf delegation: Bob delegates to AdapterManager
+ Delegation memory leafDelegation_ = _createLeafDelegation(address(users.bob.deleGator), rootDelegationHash_);
+ leafDelegation_.delegate = address(adapterManager);
+ leafDelegation_ = signDelegation(users.bob, leafDelegation_);
+
+ Delegation[] memory delegations_ = new Delegation[](2);
+ delegations_[0] = leafDelegation_; // Leaf delegation (Bob -> AdapterManager)
+ delegations_[1] = rootDelegation_; // Root delegation (Alice -> Bob)
+
+ bytes memory originalActionData_ = abi.encode(address(adapterManager));
+ bytes memory actionData_ = abi.encode("deposit", originalActionData_);
+
+ vm.prank(address(users.bob.deleGator));
+ adapterManager.executeProtocolActionByDelegation(address(AAVE_POOL), USDC, DEPOSIT_AMOUNT, actionData_, delegations_);
+
+ // Verify transformation was tracked correctly
+ assertEq(tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(USDC)), 0);
+ uint256 wrappedBalance_ = IERC20(address(stataUSDC)).balanceOf(address(users.alice.deleGator));
+ uint256 trackedWrapped_ = tokenTransformationEnforcer.getAvailableAmount(delegationHash_, address(stataUSDC));
+ assertGe(trackedWrapped_, wrappedBalance_ - 1, "Wrapped token should be tracked");
+ assertLe(trackedWrapped_, wrappedBalance_ + 1, "Wrapped token should be tracked");
+ }
+}
+
+/**
+ * @title DummyContract
+ * @notice Dummy contract for address manipulation in tests
+ */
+contract DummyContract {
+ constructor() { }
+}
+