JACKSettlementAdapter
Overview
JACKSettlementAdapter is a production-ready smart contract that settles user intents by executing token swaps through Uniswap v4 with integrated policy validation. It serves as the bridge between intent-based execution and on-chain settlement, enabling solvers to fulfill user intents while enforcing security and policy constraints.
Key Features
- EIP-712 Signature Validation: Cryptographically verifies user intent authenticity
- Solver Authorization: Whitelist-based access control for authorized solvers
- Policy Integration: Validates intents through
JACKPolicyHookbefore execution - Atomic Swaps: Leverages Uniswap v4's unlock/callback pattern for atomic execution
- Reentrancy Protection: Guards against reentrancy attacks
- Owner Management: Supports ownership transfer and solver authorization updates
Architecture
The settlement adapter integrates with two core components:
- JACKPolicyHook: Validates intent compliance with system policies
- Uniswap v4 PoolManager: Executes token swaps atomically
User → Signs Intent (EIP-712)
↓
Solver → settleIntent()
↓
Signature Validation
↓
Policy Check (JACKPolicyHook)
↓
poolManager.unlock()
↓
unlockCallback() → swap() → settle deltas
↓
Event: IntentSettled
Contract Details
- Location:
contracts/src/JACKSettlementAdapter.sol - Inheritance:
EIP712,ReentrancyGuard,IUnlockCallback - Dependencies: OpenZeppelin contracts, Uniswap v4 core
Settlement Flow
1. Intent Signing (Off-Chain)
Users create and sign intents using EIP-712:
const intent = {
id: ethers.utils.formatBytes32String("intent-123"),
user: userAddress,
tokenIn: "0x...",
tokenOut: "0x...",
amountIn: ethers.utils.parseEther("100"),
minAmountOut: ethers.utils.parseEther("95"),
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour
signature: "" // Will be populated after signing
};
const domain = {
name: "JACKSettlementAdapter",
version: "1",
chainId: 1,
verifyingContract: settlementAdapterAddress
};
const types = {
Intent: [
{ name: "id", type: "bytes32" },
{ name: "user", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
{ name: "amountIn", type: "uint256" },
{ name: "minAmountOut", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const signature = await signer._signTypedData(domain, types, intent);
intent.signature = signature;
2. Intent Settlement (On-Chain)
Authorized solvers call settleIntent() with swap parameters:
function settleIntent(
Intent calldata intent,
PoolKey calldata poolKey,
SwapParams calldata swapParams,
uint256 quotedAmountOut
) external nonReentrant onlySolver
Parameters:
intent: The signed user intentpoolKey: Uniswap v4 pool identificationswapParams: Swap execution parametersquotedAmountOut: Expected output amount for policy validation
Validation Steps:
- Verify intent deadline hasn't expired
- Check quoted output meets minimum requirement
- Validate EIP-712 signature
- Query policy hook for approval
- Execute swap via unlock/callback pattern
3. Atomic Execution
The contract uses Uniswap v4's unlock mechanism for atomic swap execution:
// Step 1: Call unlock with settlement data
poolManager.unlock(abi.encode(settlement));
// Step 2: PoolManager calls back
function unlockCallback(bytes calldata data) external override {
// Decode settlement parameters
SettlementData memory settlement = abi.decode(data, (SettlementData));
// Execute swap with policy metadata
bytes memory hookData = abi.encode(settlement.intent.id, settlement.quotedAmountOut);
BalanceDelta delta = poolManager.swap(settlement.poolKey, settlement.swapParams, hookData);
// Settle token transfers
_settleDeltas(settlement.intent.user, settlement.poolKey, delta);
}
Public Functions
settleIntent
function settleIntent(
Intent calldata intent,
PoolKey calldata poolKey,
SwapParams calldata swapParams,
uint256 quotedAmountOut
) external nonReentrant onlySolver
Settles a user intent by validating signatures and policy, then executing the swap.
Access: Only authorized solvers and owner
Emits: IntentSettled(intentId, solver)
Reverts:
IntentExpired: Intent deadline has passedQuotedAmountOutTooLow: Quoted output below minimumInvalidSignature: EIP-712 signature verification failedPolicyRejected: Policy hook rejected the intentUnauthorizedSolver: Caller not authorized
hashIntent
function hashIntent(Intent calldata intent) public view returns (bytes32)
Computes the EIP-712 hash of an intent for signature verification.
Returns: The typed data hash for the intent
transferOwnership
function transferOwnership(address newOwner) external onlyOwner
Transfers contract ownership to a new address.
Access: Only current owner
Emits: OwnershipTransferred(previousOwner, newOwner)
Reverts:
Unauthorized: Caller is not owner or newOwner is zero address
setAuthorizedSolver
function setAuthorizedSolver(address solver, bool authorized) external onlyOwner
Updates solver authorization status.
Access: Only owner
Emits: SolverAuthorizationUpdated(solver, authorized)
Example:
// Authorize a solver
settlementAdapter.setAuthorizedSolver(solverAddress, true);
// Revoke authorization
settlementAdapter.setAuthorizedSolver(solverAddress, false);
unlockCallback
function unlockCallback(bytes calldata data) external override returns (bytes memory)
Callback invoked by PoolManager during unlock to execute the swap.
Access: Only PoolManager
Internal: Not called directly by users
Reverts:
UnauthorizedPoolManager: Caller is not the PoolManager
Data Structures
Intent
struct Intent {
bytes32 id; // Unique intent identifier
address user; // Intent creator/signer
address tokenIn; // Input token address
address tokenOut; // Output token address
uint256 amountIn; // Input token amount
uint256 minAmountOut; // Minimum acceptable output
uint256 deadline; // Intent expiration timestamp
bytes signature; // EIP-712 signature
}
SettlementData
struct SettlementData {
Intent intent; // User intent being settled
PoolKey poolKey; // Uniswap v4 pool key
SwapParams swapParams; // Swap parameters
uint256 quotedAmountOut; // Expected output for policy check
address solver; // Solver executing settlement
}
Events
IntentSettled
event IntentSettled(bytes32 indexed intentId, address indexed solver)
Emitted when an intent is successfully settled.
Parameters:
intentId: Unique identifier of the settled intentsolver: Address of the solver that executed the settlement
OwnershipTransferred
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Emitted when contract ownership is transferred.
SolverAuthorizationUpdated
event SolverAuthorizationUpdated(address indexed solver, bool authorized)
Emitted when solver authorization status changes.
Parameters:
solver: Solver addressauthorized: New authorization status
Errors
PolicyRejected
error PolicyRejected(bytes32 intentId, bytes32 reason)
Thrown when the policy hook rejects an intent.
When: Policy validation fails during settlement
InvalidSignature
error InvalidSignature()
Thrown when EIP-712 signature verification fails.
When: Signature doesn't match intent hash or signer is not the intent user
UnauthorizedSolver
error UnauthorizedSolver(address solver)
Thrown when an unauthorized address attempts to settle an intent.
When: Caller is not owner and not in authorized solvers mapping
UnauthorizedPoolManager
error UnauthorizedPoolManager(address caller)
Thrown when unlockCallback is called by an address other than the PoolManager.
When: Prevents unauthorized callback execution
IntentExpired
error IntentExpired(uint256 deadline, uint256 currentTimestamp)
Thrown when attempting to settle an expired intent.
When: Current block timestamp exceeds intent deadline
QuotedAmountOutTooLow
error QuotedAmountOutTooLow(uint256 quotedAmountOut, uint256 minAmountOut)
Thrown when quoted output doesn't meet minimum requirements.
When: Solver's quoted amount is below user's minimum acceptable output
Unauthorized
error Unauthorized()
Thrown for general authorization failures.
When: Non-owner calls owner-only functions
Security Features
Reentrancy Protection
The contract uses OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks on the settleIntent function. The unlock/callback pattern is carefully designed to prevent nested calls.
Solver Authorization
Only addresses explicitly authorized by the owner can settle intents. The owner always has settlement privileges. This creates a permissioned solver network while maintaining centralized control.
modifier onlySolver() {
if (msg.sender != owner && !authorizedSolvers[msg.sender]) {
revert UnauthorizedSolver(msg.sender);
}
_;
}
Signature Verification
All intents must be signed by the user using EIP-712 typed data signatures. This ensures:
- Intent authenticity
- Protection against replay attacks
- User consent for settlement
Deadline Enforcement
Intents include a deadline timestamp, preventing stale intents from being settled. This protects users from executing trades at outdated prices.
Minimum Output Enforcement
Users specify minAmountOut in their intent, and solvers must quote at least this amount. This protects against slippage and manipulation.
Integration Guide
For Solvers
- Get Authorized: Contact the contract owner to be added as an authorized solver
- Monitor Intents: Listen for new user intents (off-chain infrastructure)
- Calculate Route: Determine optimal swap parameters via Uniswap v4
- Settle Intent: Call
settleIntent()with intent and swap parameters - Handle Errors: Implement retry logic for temporary failures
// Example solver implementation
async function settleUserIntent(intent: Intent) {
// Verify solver is authorized
const isAuthorized = await settlementAdapter.authorizedSolvers(solverAddress);
if (!isAuthorized) throw new Error("Not authorized");
// Calculate swap parameters
const { poolKey, swapParams, quotedAmountOut } = await calculateSwapRoute(intent);
// Execute settlement
const tx = await settlementAdapter.settleIntent(
intent,
poolKey,
swapParams,
quotedAmountOut
);
await tx.wait();
console.log(`Intent ${intent.id} settled in tx ${tx.hash}`);
}
For Users
- Create Intent: Define desired swap parameters
- Sign Intent: Use EIP-712 to sign the intent
- Submit Off-Chain: Send signed intent to solver network
- Monitor Settlement: Watch for
IntentSettledevent
For Protocol Operators
- Deploy Contract: Deploy with
JACKPolicyHookaddress - Authorize Solvers: Use
setAuthorizedSolver()to manage solver network - Monitor Events: Track settlement activity and policy rejections
- Update Policies: Work with policy hook to refine validation rules
Policy Hook Integration
The settlement adapter delegates policy validation to the immutable JACKPolicyHook:
(bool allowed, bytes32 reason) = policyHook.checkPolicy(intent.id, quotedAmountOut);
if (!allowed) revert PolicyRejected(intent.id, reason);
This design allows policy rules to evolve independently while maintaining a stable settlement interface. The policy hook can enforce:
- Volume limits
- Rate limiting
- Token allowlists
- Liquidity requirements
- Risk thresholds
Policy configuration details are available in the contract source code and deployment documentation.
Deployment
Constructor
constructor(address _policyHook) EIP712("JACKSettlementAdapter", "1")
Parameters:
_policyHook: Address of the deployed JACKPolicyHook contract
The constructor:
- Initializes EIP-712 domain with name "JACKSettlementAdapter" and version "1"
- Stores the policy hook reference
- Retrieves PoolManager address from policy hook
- Sets deployer as initial owner
Deployment Steps
- Deploy
JACKPolicyHookfirst - Deploy
JACKSettlementAdapterwith policy hook address - Authorize initial solvers via
setAuthorizedSolver() - Transfer ownership if needed via
transferOwnership()
Deployment Script Example
# Deploy using Foundry
forge script script/DeployJACKSettlementAdapter.s.sol:DeployJACKSettlementAdapter \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify
Testing
The contract includes a comprehensive test suite at contracts/test/JACKSettlementAdapter.t.sol covering:
- ✅ Signature validation (valid and invalid cases)
- ✅ Solver authorization enforcement
- ✅ Deadline expiration handling
- ✅ Minimum output validation
- ✅ Policy rejection scenarios
- ✅ Reentrancy attack prevention
- ✅ Ownership transfer
- ✅ Solver authorization updates
- ✅ Delta settlement logic
- ✅ Unlock callback security
Run tests with:
cd contracts
forge test --match-contract JACKSettlementAdapterTest -vv
Future Enhancements
Potential improvements for future versions:
- Multi-hop Swaps: Support complex routing through multiple pools
- Batch Settlement: Settle multiple intents atomically
- Partial Fills: Allow intents to be partially fulfilled
- Cancel Mechanism: Enable users to cancel pending intents
- Fee Collection: Implement protocol fee capture
- Solver Reputation: Track and reward reliable solvers
- Dynamic Slippage: Adjust slippage based on market conditions