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
- Creator
- 0xE0045e0622…2731E16ecA
- 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