PolicyExecutor

0x28fcdcd3bc4afb8ebaa5d172abb98b9e144c7e79

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
13,593 bytes
ABI entries
96
19 read · 28 write
License
none

Contract information

Address
0x28fcdcd3bc4afb8ebaa5d172abb98b9e144c7e79
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xb475a76b71…b053d2fd8c

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

adminDelay()uint256
adminEta(bytes32)uint256
agent()address
allowedTargets(address)bool
approvedDigests(bytes32)bool
approvedOrders(bytes32)address, address, address, uint256, uint64, bool
getTokenBalance(address, address)uint256
getVault(address)uint256, uint256, uint64, uint256, uint256, bool
isValidSignature(bytes32, bytes)bytes4
owner()address
pendingOwner()address
rfqTokenLocked(address, address)uint256
rfqWethLocked(address)uint256
tokenAccounted(address)uint256
tokenBalances(address, address)uint256
totalRfqWethLocked()uint256
uniswapRouter()address
vaults(address)uint256, uint256, uint64, uint256, uint256, bool
weth()address

Events (25)

AdminActionCancelledAdminActionExecutedAdminActionQueuedAdminDelayUpdatedAgentExecutedAgentUpdatedDepositedDigestApprovedLimitsUpdatedOrderApprovedOwnershipTransferStartedOwnershipTransferredPausedPolicyRejectedRfqSettledRfqTokenConsumedRfqTokenLockedRfqTokenUnlockedRfqWethReleasedRfqWethWrappedTargetAllowedTokenCreditedTokenSoldTokenWithdrawnWithdrawn

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "agent_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "uniswapRouter_",
        "type": "address"
      },
      {
        "internalType": "address[]",
        "name": "bootstrapTargets",
        "type": "address[]"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AdminNotQueued",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AdminNotReady",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AmountTooLarge",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "returndata",
        "type": "bytes"
      }
    ],
    "name": "CallFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsDailyLimit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsPerTxLimit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientEthOut",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientRfqLock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientTokenBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoTokenReceived",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAgent",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAgentOrUser",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPendingOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TargetNotAllowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedEth",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "VaultPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "actionId",
        "type": "bytes32"
      }
    ],
    "name": "AdminActionCancelled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "actionId",
        "type": "bytes32"
      }
    ],
    "name": "AdminActionExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "actionId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eta",
        "type": "uint256"
      }
    ],
    "name": "AdminActionQueued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "delay",
        "type": "uint256"
      }
    ],
    "name": "AdminDelayUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "AgentExecuted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "agent",
        "type": "address"
      }
    ],
    "name": "AgentUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Deposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "digest",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "DigestApproved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "maxEthPerTx",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "maxEthPerDay",
        "type": "uint256"
      }
    ],
    "name": "LimitsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "digest",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "expiry",
        "type": "uint64"
      }
    ],
    "name": "OrderApproved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "name": "PolicyRejected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      }
    ],
    "name": "RfqSettled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RfqTokenConsumed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RfqTokenLocked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RfqTokenUnlocked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RfqWethReleased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RfqWethWrapped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "TargetAllowed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenCredited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethReceived",
        "type": "uint256"
      }
    ],
    "name": "TokenSold",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Withdrawn",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "adminDelay",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "adminEta",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "agent",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "allowedTargets",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "permit2",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint160",
        "name": "amount",
        "type": "uint160"
      },
      {
        "internalType": "uint48",
        "name": "expiration",
        "type": "uint48"
      }
    ],
    "name": "approvePermit2Spender",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "approvedDigests",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "approvedOrders",
    "outputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "expiry",
        "type": "uint64"
      },
      {
        "internalType": "bool",
        "name": "active",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "actionId",
        "type": "bytes32"
      }
    ],
    "name": "cancelAdminAction",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "consumeRfqTokenLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "consumeRfqWethLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "creditRfqTokenOut",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deposit",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "execute",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "returndata",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "executeSell",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "permit2",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "executeSellViaPermit2",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "agent_",
        "type": "address"
      }
    ],
    "name": "executeSetAgent",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "executeSetAllowedTarget",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      },
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      }
    ],
    "name": "executeSwap",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "returndata",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "getTokenBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getVault",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "spentToday",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "dayStart",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "maxEthPerTx",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxEthPerDay",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "hash",
        "type": "bytes32"
      },
      {
        "internalType": "bytes",
        "name": "signature",
        "type": "bytes"
      }
    ],
    "name": "isValidSignature",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "lockTokenForRfq",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "agent_",
        "type": "address"
      }
    ],
    "name": "queueSetAgent",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "queueSetAllowedTarget",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "releaseRfqWeth",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "rfqTokenLocked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "rfqWethLocked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "delay",
        "type": "uint256"
      }
    ],
    "name": "setAdminDelay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "digest",
        "type": "bytes32"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setApprovedDigest",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "digest",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenIn",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenOut",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "expiry",
        "type": "uint64"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setApprovedOrder",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "maxEthPerTx",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxEthPerDay",
        "type": "uint256"
      }
    ],
    "name": "setMyLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "paused_",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "wethAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "settleRfqSellEth",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenAccounted",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenBalances",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalRfqWethLocked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "uniswapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "unlockTokenForRfq",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "vaults",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "spentToday",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "dayStart",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "maxEthPerTx",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxEthPerDay",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "wrapEthForRfq",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IWETH {
    function deposit() external payable;
    function withdraw(uint256 wad) external;
    function balanceOf(address account) external view returns (uint256);
}

/// @notice Minimal Permit2 allowance interface used by Universal Router pulls.
interface IPermit2 {
    function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}

/// @title PolicyExecutor
/// @notice Per-user ETH vault with on-chain spend limits, target whitelist,
///         and ERC20 token custody for agent swaps.
///         Trusted agent (or the user) may execute calls that pass policy checks.
///         ERC-1271 lets the vault act as swapper for RFQ (Dutch V3) orders.
contract PolicyExecutor is ReentrancyGuard {
    using SafeERC20 for IERC20;

    /// @dev ERC-1271 magic value returned by isValidSignature on success.
    bytes4 private constant ERC1271_MAGICVALUE = 0x1626ba7e;

    struct UserVault {
        uint256 balance;
        uint256 spentToday;
        uint64 dayStart;
        uint256 maxEthPerTx;
        uint256 maxEthPerDay;
        bool paused;
    }

    /// @notice Bound metadata for a pre-approved RFQ digest (ERC-1271).
    struct ApprovedOrder {
        address user;
        address tokenIn;
        address tokenOut;
        uint256 amount;
        uint64 expiry;
        bool active;
    }

    address public owner;
    address public pendingOwner;
    address public agent;
    address public uniswapRouter;
    /// @dev Cached WETH for unwrap receive-path (resolved once in constructor).
    address public immutable weth;
    /// @notice Delay before owner can execute setAgent / setAllowedTarget.
    uint256 public adminDelay;

    mapping(address => UserVault) public vaults;
    mapping(address => bool) public allowedTargets;
    /// @notice Per-user ERC20 balances custodied by this vault.
    mapping(address => mapping(address => uint256)) public tokenBalances;
    /// @notice Aggregate accounted ERC20 (tokenBalances + rfqTokenLocked) for surplus checks.
    mapping(address => uint256) public tokenAccounted;
    /// @notice EIP-712 digests pre-approved by agent/owner for ERC-1271 (RFQ orders).
    mapping(bytes32 => bool) public approvedDigests;
    /// @notice Bound order metadata for approved digests.
    mapping(bytes32 => ApprovedOrder) public approvedOrders;
    /// @notice WETH locked for in-flight RFQ buys (not in tokenBalances).
    mapping(address => uint256) public rfqWethLocked;
    /// @notice Aggregate of rfqWethLocked across users (surplus WETH settle guard).
    uint256 public totalRfqWethLocked;
    /// @notice Tokens locked for in-flight RFQ sells (debited from tokenBalances).
    mapping(address => mapping(address => uint256)) public rfqTokenLocked;
    /// @notice Queued admin actions: actionId => earliest execution timestamp (0 = none).
    mapping(bytes32 => uint256) public adminEta;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event AgentExecuted(address indexed user, address indexed target, uint256 value, bytes data);
    event TokenCredited(address indexed user, address indexed token, uint256 amount);
    event TokenWithdrawn(address indexed user, address indexed token, uint256 amount);
    event TokenSold(address indexed user, address indexed token, uint256 tokenAmount, uint256 ethReceived);
    event PolicyRejected(address indexed user, string reason);
    event LimitsUpdated(address indexed user, uint256 maxEthPerTx, uint256 maxEthPerDay);
    event TargetAllowed(address indexed target, bool allowed);
    event AgentUpdated(address indexed agent);
    event Paused(address indexed user, bool paused);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    event DigestApproved(bytes32 indexed digest, bool approved);
    event OrderApproved(
        bytes32 indexed digest,
        address indexed user,
        address tokenIn,
        address tokenOut,
        uint256 amount,
        uint64 expiry
    );
    event RfqWethWrapped(address indexed user, uint256 amount);
    event RfqWethReleased(address indexed user, uint256 amount);
    event RfqTokenLocked(address indexed user, address indexed token, uint256 amount);
    event RfqTokenUnlocked(address indexed user, address indexed token, uint256 amount);
    event RfqTokenConsumed(address indexed user, address indexed token, uint256 amount);
    event RfqSettled(address indexed user, address indexed tokenOut, uint256 amountOut);
    event AdminActionQueued(bytes32 indexed actionId, uint256 eta);
    event AdminActionExecuted(bytes32 indexed actionId);
    event AdminActionCancelled(bytes32 indexed actionId);
    event AdminDelayUpdated(uint256 delay);

    error NotOwner();
    error NotPendingOwner();
    error NotAgentOrUser();
    error NotAgent();
    error ZeroAddress();
    error InsufficientBalance();
    error InsufficientTokenBalance();
    error TargetNotAllowed();
    error ExceedsPerTxLimit();
    error ExceedsDailyLimit();
    error VaultPaused(address user);
    error CallFailed(bytes returndata);
    error TransferFailed();
    error NoTokenReceived();
    error InsufficientEthOut();
    error InsufficientRfqLock();
    error UnexpectedEth();
    error AdminNotQueued();
    error AdminNotReady();
    error AmountTooLarge();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(
        address initialOwner,
        address agent_,
        address uniswapRouter_,
        address[] memory bootstrapTargets
    ) {
        if (initialOwner == address(0) || agent_ == address(0) || uniswapRouter_ == address(0)) {
            revert ZeroAddress();
        }
        owner = initialOwner;
        agent = agent_;
        uniswapRouter = uniswapRouter_;
        adminDelay = 1 days;
        allowedTargets[uniswapRouter_] = true;
        emit TargetAllowed(uniswapRouter_, true);
        for (uint256 i = 0; i < bootstrapTargets.length; i++) {
            address t = bootstrapTargets[i];
            if (t != address(0)) {
                allowedTargets[t] = true;
                emit TargetAllowed(t, true);
            }
        }
        weth = _lookupWeth(uniswapRouter_);
    }

    receive() external payable {
        // WETH.withdraw() refunds ETH here — do not attribute to the WETH contract.
        if (msg.sender == weth) return;
        // Force explicit deposit() so ETH cannot be mis-attributed via receive().
        revert UnexpectedEth();
    }

    function deposit() external payable nonReentrant {
        _deposit(msg.sender, msg.value);
    }

    function _deposit(address user, uint256 amount) internal {
        require(amount > 0, "zero");
        UserVault storage v = vaults[user];
        if (v.maxEthPerTx == 0 && v.maxEthPerDay == 0) {
            // defaults: 0.05 ETH / tx, 0.2 ETH / day
            v.maxEthPerTx = 0.05 ether;
            v.maxEthPerDay = 0.2 ether;
        }
        v.balance += amount;
        emit Deposited(user, amount);
    }

    function withdraw(uint256 amount) external nonReentrant {
        UserVault storage v = vaults[msg.sender];
        if (amount > v.balance) revert InsufficientBalance();
        v.balance -= amount;
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "withdraw failed");
        emit Withdrawn(msg.sender, amount);
    }

    /// @notice Withdraw custodied ERC20 tokens to the caller's wallet.
    function withdrawToken(address token, uint256 amount) external nonReentrant {
        if (token == address(0)) revert ZeroAddress();
        uint256 bal = tokenBalances[msg.sender][token];
        if (amount > bal) revert InsufficientTokenBalance();
        tokenBalances[msg.sender][token] = bal - amount;
        tokenAccounted[token] -= amount;
        IERC20(token).safeTransfer(msg.sender, amount);
        emit TokenWithdrawn(msg.sender, token, amount);
    }

    function setMyLimits(uint256 maxEthPerTx, uint256 maxEthPerDay) external {
        UserVault storage v = vaults[msg.sender];
        v.maxEthPerTx = maxEthPerTx;
        v.maxEthPerDay = maxEthPerDay;
        emit LimitsUpdated(msg.sender, maxEthPerTx, maxEthPerDay);
    }

    function setPaused(bool paused_) external {
        vaults[msg.sender].paused = paused_;
        emit Paused(msg.sender, paused_);
    }

    function setAdminDelay(uint256 delay) external onlyOwner {
        adminDelay = delay;
        emit AdminDelayUpdated(delay);
    }

    function queueSetAgent(address agent_) external onlyOwner {
        if (agent_ == address(0)) revert ZeroAddress();
        bytes32 id = keccak256(abi.encodePacked("setAgent", agent_));
        uint256 eta = block.timestamp + adminDelay;
        adminEta[id] = eta;
        emit AdminActionQueued(id, eta);
    }

    function executeSetAgent(address agent_) external onlyOwner {
        if (agent_ == address(0)) revert ZeroAddress();
        bytes32 id = keccak256(abi.encodePacked("setAgent", agent_));
        uint256 eta = adminEta[id];
        if (eta == 0) revert AdminNotQueued();
        if (block.timestamp < eta) revert AdminNotReady();
        delete adminEta[id];
        agent = agent_;
        emit AgentUpdated(agent_);
        emit AdminActionExecuted(id);
    }

    function queueSetAllowedTarget(address target, bool allowed) external onlyOwner {
        if (target == address(0)) revert ZeroAddress();
        bytes32 id = keccak256(abi.encodePacked("setAllowedTarget", target, allowed));
        uint256 eta = block.timestamp + adminDelay;
        adminEta[id] = eta;
        emit AdminActionQueued(id, eta);
    }

    function executeSetAllowedTarget(address target, bool allowed) external onlyOwner {
        if (target == address(0)) revert ZeroAddress();
        bytes32 id = keccak256(abi.encodePacked("setAllowedTarget", target, allowed));
        uint256 eta = adminEta[id];
        if (eta == 0) revert AdminNotQueued();
        if (block.timestamp < eta) revert AdminNotReady();
        delete adminEta[id];
        allowedTargets[target] = allowed;
        emit TargetAllowed(target, allowed);
        emit AdminActionExecuted(id);
    }

    function cancelAdminAction(bytes32 actionId) external onlyOwner {
        if (adminEta[actionId] == 0) revert AdminNotQueued();
        delete adminEta[actionId];
        emit AdminActionCancelled(actionId);
    }

    /// @notice Start two-step ownership transfer.
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert ZeroAddress();
        pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner, newOwner);
    }

    /// @notice Complete two-step ownership transfer.
    function acceptOwnership() external {
        if (msg.sender != pendingOwner) revert NotPendingOwner();
        address old = owner;
        owner = msg.sender;
        pendingOwner = address(0);
        emit OwnershipTransferred(old, msg.sender);
    }

    /// @notice ERC-1271: accept ECDSA signatures from `agent`/`owner`, or pre-approved digests.
    /// @dev Permit2 / Dutch V3 Reactor call this when the vault is the RFQ swapper.
    function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4) {
        ApprovedOrder storage ord = approvedOrders[hash];
        if (ord.active) {
            if (ord.expiry != 0 && block.timestamp > ord.expiry) {
                return bytes4(0xffffffff);
            }
            return ERC1271_MAGICVALUE;
        }
        if (approvedDigests[hash]) {
            return ERC1271_MAGICVALUE;
        }
        (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, signature);
        if (err == ECDSA.RecoverError.NoError && recovered != address(0)) {
            if (recovered == agent || recovered == owner) {
                return ERC1271_MAGICVALUE;
            }
        }
        return bytes4(0xffffffff);
    }

    /// @notice Agent/owner pre-approves an EIP-712 digest for ERC-1271 (legacy).
    function setApprovedDigest(bytes32 digest, bool approved) external {
        if (msg.sender != agent && msg.sender != owner) revert NotAgent();
        approvedDigests[digest] = approved;
        emit DigestApproved(digest, approved);
    }

    /// @notice Bind an RFQ digest to (user, tokenIn, tokenOut, amount, expiry).
    function setApprovedOrder(
        bytes32 digest,
        address user,
        address tokenIn,
        address tokenOut,
        uint256 amount,
        uint64 expiry,
        bool approved
    ) external {
        if (msg.sender != agent && msg.sender != owner) revert NotAgent();
        if (user == address(0) || tokenIn == address(0) || tokenOut == address(0)) revert ZeroAddress();
        if (approved) {
            approvedOrders[digest] = ApprovedOrder({
                user: user,
                tokenIn: tokenIn,
                tokenOut: tokenOut,
                amount: amount,
                expiry: expiry,
                active: true
            });
            approvedDigests[digest] = true;
            emit OrderApproved(digest, user, tokenIn, tokenOut, amount, expiry);
        } else {
            delete approvedOrders[digest];
            approvedDigests[digest] = false;
        }
        emit DigestApproved(digest, approved);
    }

    /// @notice Wrap vault ETH → WETH for an RFQ buy (input must be ERC20, not native).
    function wrapEthForRfq(address user, uint256 amount) external nonReentrant {
        if (msg.sender != agent && msg.sender != user) revert NotAgentOrUser();
        if (vaults[user].paused) revert VaultPaused(user);
        if (amount == 0) revert InsufficientBalance();
        if (amount > vaults[user].balance) revert InsufficientBalance();
        if (amount > vaults[user].maxEthPerTx) revert ExceedsPerTxLimit();

        UserVault storage v = vaults[user];
        _rollDay(v);
        if (v.spentToday + amount > v.maxEthPerDay) revert ExceedsDailyLimit();

        v.balance -= amount;
        v.spentToday += amount;
        rfqWethLocked[user] += amount;
        totalRfqWethLocked += amount;
        IWETH(weth).deposit{value: amount}();
        emit RfqWethWrapped(user, amount);
    }

    /// @notice Unwrap unused RFQ WETH back to the user's vault ETH (expired / cancelled order).
    function releaseRfqWeth(address user, uint256 amount) external nonReentrant {
        if (msg.sender != agent && msg.sender != user) revert NotAgentOrUser();
        if (amount == 0 || amount > rfqWethLocked[user]) revert InsufficientRfqLock();
        rfqWethLocked[user] -= amount;
        totalRfqWethLocked -= amount;
        IWETH(weth).withdraw(amount);
        vaults[user].balance += amount;
        emit RfqWethReleased(user, amount);
        emit Deposited(user, amount);
    }

    /// @notice Clear RFQ WETH lock after a successful buy fill (Permit2 already pulled WETH).
    function consumeRfqWethLock(address user, uint256 amount) external {
        if (msg.sender != agent) revert NotAgent();
        if (amount == 0 || amount > rfqWethLocked[user]) revert InsufficientRfqLock();
        rfqWethLocked[user] -= amount;
        totalRfqWethLocked -= amount;
        emit RfqWethReleased(user, amount);
    }

    /// @notice Debit custodied ERC20 for an RFQ sell (tokens stay in contract until Permit2 pull).
    function lockTokenForRfq(address user, address token, uint256 amount) external {
        if (msg.sender != agent && msg.sender != user) revert NotAgentOrUser();
        if (token == address(0)) revert ZeroAddress();
        if (vaults[user].paused) revert VaultPaused(user);
        if (amount == 0 || amount > tokenBalances[user][token]) revert InsufficientTokenBalance();
        tokenBalances[user][token] -= amount;
        rfqTokenLocked[user][token] += amount;
        // tokenAccounted unchanged: still held (locked) in this contract.
        emit RfqTokenLocked(user, token, amount);
    }

    /// @notice Restore locked ERC20 to user balance if RFQ order expires unused.
    /// @dev Agent-only; requires a matching prior lock (prevents free balance inflation).
    function unlockTokenForRfq(address user, address token, uint256 amount) external {
        if (msg.sender != agent) revert NotAgent();
        if (token == address(0)) revert ZeroAddress();
        if (amount == 0 || amount > rfqTokenLocked[user][token]) revert InsufficientRfqLock();
        rfqTokenLocked[user][token] -= amount;
        tokenBalances[user][token] += amount;
        emit RfqTokenUnlocked(user, token, amount);
        emit TokenCredited(user, token, amount);
    }

    /// @notice Clear RFQ ERC20 lock after a successful sell fill (Permit2 already pulled tokens).
    /// @dev Decrements tokenAccounted so future creditRfqTokenOut surplus checks stay correct.
    function consumeRfqTokenLock(address user, address token, uint256 amount) external {
        if (msg.sender != agent) revert NotAgent();
        if (token == address(0)) revert ZeroAddress();
        if (amount == 0 || amount > rfqTokenLocked[user][token]) revert InsufficientRfqLock();
        rfqTokenLocked[user][token] -= amount;
        tokenAccounted[token] -= amount;
        emit RfqTokenConsumed(user, token, amount);
    }

    /// @notice Approve Permit2 → spender (reactor / UR) for RFQ or classic pulls.
    /// @dev Both `permit2` and `spender` must be allowlisted; amount capped to uint160.
    function approvePermit2Spender(
        address permit2,
        address token,
        address spender,
        uint160 amount,
        uint48 expiration
    ) external {
        if (msg.sender != agent && msg.sender != owner) revert NotAgent();
        if (permit2 == address(0) || token == address(0) || spender == address(0)) revert ZeroAddress();
        if (!allowedTargets[permit2]) revert TargetNotAllowed();
        if (!allowedTargets[spender]) revert TargetNotAllowed();
        if (amount == type(uint160).max) revert AmountTooLarge();
        IERC20(token).forceApprove(permit2, amount);
        IPermit2(permit2).approve(token, spender, amount, expiration);
    }

    /// @notice After RFQ fill delivers `tokenOut` to this contract, credit the user.
    /// @dev Only credits up to unaccounted surplus (physical balance − tokenAccounted).
    function creditRfqTokenOut(address user, address tokenOut, uint256 amount) external nonReentrant {
        if (msg.sender != agent) revert NotAgent();
        if (tokenOut == address(0) || amount == 0) revert ZeroAddress();
        uint256 physical = IERC20(tokenOut).balanceOf(address(this));
        uint256 accounted = tokenAccounted[tokenOut];
        if (physical <= accounted) revert NoTokenReceived();
        uint256 surplus = physical - accounted;
        if (amount > surplus) revert NoTokenReceived();
        tokenBalances[user][tokenOut] += amount;
        tokenAccounted[tokenOut] += amount;
        emit TokenCredited(user, tokenOut, amount);
        emit RfqSettled(user, tokenOut, amount);
    }

    /// @notice After RFQ sell fill delivers WETH, unwrap and credit vault ETH.
    /// @dev Only settles WETH surplus beyond totalRfqWethLocked (buy-side locks).
    function settleRfqSellEth(address user, uint256 wethAmount, uint256 minEthOut)
        external
        nonReentrant
    {
        if (msg.sender != agent) revert NotAgent();
        if (wethAmount == 0) revert NoTokenReceived();
        uint256 physical = IWETH(weth).balanceOf(address(this));
        if (physical < totalRfqWethLocked) revert InsufficientBalance();
        uint256 surplus = physical - totalRfqWethLocked;
        if (wethAmount > surplus) revert InsufficientBalance();

        uint256 ethBefore = address(this).balance;
        IWETH(weth).withdraw(wethAmount);
        uint256 ethDelta = address(this).balance - ethBefore;
        if (ethDelta < minEthOut) revert InsufficientEthOut();

        vaults[user].balance += ethDelta;
        emit TokenSold(user, weth, wethAmount, ethDelta);
        emit Deposited(user, ethDelta);
        emit RfqSettled(user, address(0), ethDelta);
    }

    /// @notice Execute a call spending from `user` vault. Caller must be agent or the user.
    function execute(address user, address target, uint256 value, bytes calldata data)
        external
        nonReentrant
        returns (bytes memory returndata)
    {
        _policySpend(user, target, value);

        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) {
            emit PolicyRejected(user, "callFailed");
            revert CallFailed(ret);
        }

        emit AgentExecuted(user, target, value, data);
        return ret;
    }

    /// @notice Swap via router, credit `tokenOut` received by this contract to `user`.
    /// @dev Calldata must set Uniswap recipient to address(this).
    function executeSwap(
        address user,
        address router,
        uint256 value,
        bytes calldata data,
        address tokenOut
    ) external nonReentrant returns (bytes memory returndata) {
        if (tokenOut == address(0)) revert ZeroAddress();
        _policySpend(user, router, value);

        uint256 beforeBal = IERC20(tokenOut).balanceOf(address(this));
        (bool ok, bytes memory ret) = router.call{value: value}(data);
        if (!ok) {
            emit PolicyRejected(user, "callFailed");
            revert CallFailed(ret);
        }
        uint256 afterBal = IERC20(tokenOut).balanceOf(address(this));
        if (afterBal <= beforeBal) revert NoTokenReceived();
        uint256 delta = afterBal - beforeBal;
        tokenBalances[user][tokenOut] += delta;
        tokenAccounted[tokenOut] += delta;

        emit TokenCredited(user, tokenOut, delta);
        emit AgentExecuted(user, router, value, data);
        return ret;
    }

    /// @notice Sell custodied ERC20 via router for WETH, unwrap to ETH, credit vault.
    /// @dev Calldata must set Uniswap recipient to address(this). Does not consume ETH spend caps.
    function executeSell(
        address user,
        address router,
        address tokenIn,
        uint256 amountIn,
        uint256 minEthOut,
        bytes calldata data
    ) external nonReentrant returns (bytes memory) {
        _assertSellAuth(user, router, tokenIn, amountIn);
        tokenBalances[user][tokenIn] -= amountIn;
        tokenAccounted[tokenIn] -= amountIn;

        address wethToken = weth;
        uint256 ethBefore = address(this).balance;
        uint256 wethBefore = IERC20(wethToken).balanceOf(address(this));

        IERC20(tokenIn).forceApprove(router, amountIn);
        (bool ok, bytes memory ret) = router.call(data);
        if (!ok) {
            tokenBalances[user][tokenIn] += amountIn;
            tokenAccounted[tokenIn] += amountIn;
            IERC20(tokenIn).forceApprove(router, 0);
            emit PolicyRejected(user, "callFailed");
            revert CallFailed(ret);
        }

        uint256 wethDelta = IERC20(wethToken).balanceOf(address(this)) - wethBefore;
        if (wethDelta > 0) IWETH(wethToken).withdraw(wethDelta);

        uint256 ethDelta = address(this).balance - ethBefore;
        if (ethDelta == 0) revert NoTokenReceived();
        if (ethDelta < minEthOut) revert InsufficientEthOut();

        vaults[user].balance += ethDelta;
        IERC20(tokenIn).forceApprove(router, 0);

        emit TokenSold(user, tokenIn, amountIn, ethDelta);
        emit Deposited(user, ethDelta);
        emit AgentExecuted(user, router, 0, data);
        return ret;
    }

    /// @notice Sell via Universal Router (Permit2 pull). Approves Permit2 then UR calldata.
    /// @dev `router` and `permit2` must both be allowedTargets. Calldata recipient = address(this).
    function executeSellViaPermit2(
        address user,
        address router,
        address permit2,
        address tokenIn,
        uint256 amountIn,
        uint256 minEthOut,
        bytes calldata data
    ) external nonReentrant returns (bytes memory) {
        _assertSellAuth(user, router, tokenIn, amountIn);
        if (permit2 == address(0)) revert ZeroAddress();
        if (!allowedTargets[permit2]) revert TargetNotAllowed();
        if (amountIn > type(uint160).max) revert InsufficientTokenBalance();

        tokenBalances[user][tokenIn] -= amountIn;
        tokenAccounted[tokenIn] -= amountIn;

        address wethToken = weth;
        uint256 ethBefore = address(this).balance;
        uint256 wethBefore = IERC20(wethToken).balanceOf(address(this));

        IERC20(tokenIn).forceApprove(permit2, amountIn);
        IPermit2(permit2).approve(
            tokenIn,
            router,
            uint160(amountIn),
            uint48(block.timestamp + 1 hours)
        );

        (bool ok, bytes memory ret) = router.call(data);
        if (!ok) {
            tokenBalances[user][tokenIn] += amountIn;
            tokenAccounted[tokenIn] += amountIn;
            IERC20(tokenIn).forceApprove(permit2, 0);
            emit PolicyRejected(user, "callFailed");
            revert CallFailed(ret);
        }

        uint256 wethDelta = IERC20(wethToken).balanceOf(address(this)) - wethBefore;
        if (wethDelta > 0) IWETH(wethToken).withdraw(wethDelta);

        uint256 ethDelta = address(this).balance - ethBefore;
        if (ethDelta == 0) revert NoTokenReceived();
        if (ethDelta < minEthOut) revert InsufficientEthOut();

        vaults[user].balance += ethDelta;
        IERC20(tokenIn).forceApprove(permit2, 0);

        emit TokenSold(user, tokenIn, amountIn, ethDelta);
        emit Deposited(user, ethDelta);
        emit AgentExecuted(user, router, 0, data);
        return ret;
    }

    function _assertSellAuth(
        address user,
        address router,
        address tokenIn,
        uint256 amountIn
    ) internal view {
        if (tokenIn == address(0) || router == address(0)) revert ZeroAddress();
        if (msg.sender != agent && msg.sender != user) revert NotAgentOrUser();
        if (vaults[user].paused) revert VaultPaused(user);
        if (!allowedTargets[router]) revert TargetNotAllowed();
        if (amountIn > tokenBalances[user][tokenIn]) revert InsufficientTokenBalance();
    }

    function _lookupWeth(address router) internal view returns (address) {
        (bool ok, bytes memory data) =
            router.staticcall(abi.encodeWithSignature("WETH9()"));
        if (ok && data.length >= 32) {
            address w = abi.decode(data, (address));
            if (w != address(0)) return w;
        }
        (bool ok2, bytes memory data2) =
            router.staticcall(abi.encodeWithSignature("WETH()"));
        if (ok2 && data2.length >= 32) {
            address w2 = abi.decode(data2, (address));
            if (w2 != address(0)) return w2;
        }
        revert ZeroAddress();
    }

    function _policySpend(address user, address target, uint256 value) internal {
        if (msg.sender != agent && msg.sender != user) revert NotAgentOrUser();

        UserVault storage v = vaults[user];
        if (v.paused) {
            emit PolicyRejected(user, "paused");
            revert VaultPaused(user);
        }
        if (!allowedTargets[target]) {
            emit PolicyRejected(user, "target");
            revert TargetNotAllowed();
        }
        if (value > v.maxEthPerTx) {
            emit PolicyRejected(user, "perTx");
            revert ExceedsPerTxLimit();
        }

        _rollDay(v);
        if (v.spentToday + value > v.maxEthPerDay) {
            emit PolicyRejected(user, "daily");
            revert ExceedsDailyLimit();
        }
        if (value > v.balance) revert InsufficientBalance();

        v.balance -= value;
        v.spentToday += value;
    }

    function _rollDay(UserVault storage v) internal {
        uint64 startOfDay = uint64(block.timestamp - (block.timestamp % 1 days));
        if (v.dayStart != startOfDay) {
            v.dayStart = startOfDay;
            v.spentToday = 0;
        }
    }

    function getVault(address user)
        external
        view
        returns (
            uint256 balance,
            uint256 spentToday,
            uint64 dayStart,
            uint256 maxEthPerTx,
            uint256 maxEthPerDay,
            bool paused
        )
    {
        UserVault storage v = vaults[user];
        return (v.balance, v.spentToday, v.dayStart, v.maxEthPerTx, v.maxEthPerDay, v.paused);
    }

    function getTokenBalance(address user, address token) external view returns (uint256) {
        return tokenBalances[user][token];
    }
}
Chain explorer4092msChain node78ms