Making Blockchain Physical
Tap your NFC chip to execute any blockchain transaction—no wallet popups, no gas fees, no friction
- 8 Smart Contracts deployed across 2 testnets
- 5 Core Contracts for universal execution
- 3 Advanced Extensions (Bridge, Aave Flash Loans)
- Sub-3-second tap-to-pay experience
- $0 gas fees for users (relay-sponsored)
- 100% gasless UX for end users
Current blockchain UX creates insurmountable friction:
Traditional Web3 Transaction:
┌────────────────────────────────────────────┐
│ 1. Open wallet app (5s) │
│ 2. Switch to correct network (10s) │
│ 3. Approve token spending (15s) │
│ 4. Confirm transaction (10s) │
│ 5. Wait for confirmation (30s) │
│ 6. Check if it worked (5s) │
│ │
│ Total: 75 seconds, 6 steps, 4 popups │
└────────────────────────────────────────────┘
Result: Users abandon transactions, developers compromise security for UX, mainstream adoption stalls.
Physical tap authorization replaces the entire flow:
TapThat X Transaction:
┌────────────────────────────────────────────┐
│ 1. Tap phone on NFC chip (2s) │
│ 2. Transaction executes (1s) │
│ │
│ Total: 3 seconds, 1 step, 0 popups │
└────────────────────────────────────────────┘
- One-time setup: Register NFC chip + pre-approve spending limits (5 minutes)
- Every transaction: Tap phone → chip signs authorization → relay executes (3 seconds)
- Result: Physical blockchain interactions with zero ongoing friction
┌─────────────────────────────────────────────────────────────────────────┐
│ TapThat X Protocol │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────┐
│ NFC Chip (HaLo) │ ← Secure element with private key
│ Signs EIP-712 msgs │ Keys never leave chip
└──────────┬──────────┘
│ tap (Web NFC API)
│ 2-3 seconds
┌──────────▼──────────┐
│ Mobile Device │ ← @arx-research/libhalo
│ (Android/iOS) │ Browser-based NFC reading
└──────────┬──────────┘
│ HTTPS POST
│ /api/relay-execute-tap
┌──────────▼──────────────────────────────────────┐
│ Gasless Relay (Next.js API) │ ← Backend relayer
│ - Validates request structure │ Pays gas for users
│ - Detects operation type │ Signs with private key
│ - Sets dynamic gas limits │
│ - Submits transaction on-chain │
└──────────┬──────────────────────────────────────┘
│ writeContract()
│ TapThatXExecutor.executeTap()
┌──────────▼──────────────────────────────────────┐
│ TapThatXExecutor (Orchestrator) │
│ 1. Fetch configuration from storage │
│ 2. Validate config active │
│ 3. Delegate to Protocol for execution │
└──────────┬──────────────────────────────────────┘
│
┌──────┴──────────────────────────────┐
│ │
┌───▼────────────────────┐ ┌───────────▼──────────────────────┐
│ TapThatXConfiguration │ │ TapThatXProtocol │
│ (Action Storage) │ │ (Validation & Execution Engine) │
│ │ │ │
│ getConfiguration() │ │ executeAuthorizedCall() │
│ → ActionConfig { │ │ ├─ Verify nonce not used │
│ target, │ │ ├─ Recover chip from signature │
│ callData, │ │ ├─ Validate timestamp <5 min │
│ description │ │ ├─ Check chip ownership │
│ } │ │ ├─ Mark nonce used │
│ │ │ └─ Execute target.call() │
└────────────────────────┘ └───────────┬──────────────────────┘
│
┌──────────┴─────────┐
│ │
┌────────▼──────────┐ ┌─────▼──────────────┐
│ TapThatXRegistry │ │ Target Contract │
│ (Chip Ownership) │ │ (Action Execute) │
│ │ │ │
│ hasChip() │ │ • ERC20 transfer │
│ → validates owner │ │ • Bridge ETH │
│ owns chip │ │ • Aave rebalance │
└───────────────────┘ │ • Uniswap swap │
│ • Custom call │
└────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Contract Architecture │
└─────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ TapThatXRegistry │ ← No dependencies
│ (Chip ↔ Owner map) │ Foundation layer
└──────────┬───────────┘
│ depends on
┌──────────────┴──────────────┐
│ │
┌─────────▼──────────┐ ┌──────────▼──────────────┐
│ TapThatXProtocol │ │ TapThatXConfiguration │
│ (Core execution) │ │ (Action storage) │
└─────────┬──────────┘ └──────────┬──────────────┘
│ │
└──────────┬──────────────────┘
│ both required
┌──────────▼──────────────┐
│ TapThatXExecutor │
│ (Simplified interface) │
└──────────┬──────────────┘
│ required by extensions
┌──────────────┼──────────────┐
│ │ │
┌───────────▼──────┐ ┌─────▼──────┐ ┌────▼─────────────────┐
│ TapThatXBridge │ │ TapThatX │ │ TapThatXAave │
│ ETHViaWETH │ │ Aave │ │ PositionCloser │
│ (Dual L2 bridge) │ │ Rebalancer │ │ (Flash loan close) │
└──────────────────┘ └────────────┘ └──────────────────────┘
Extension Extension Extension
(Sepolia only) (Base only) (Base only)
Purpose: Chip ownership registry with EIP-712 proof of possession
Deployed At:
- Base Sepolia:
0x91D05d5B8913BCdA59f1923dC6831B108154Df22 - Sepolia:
0x...(see deployedContracts.ts)
Key Data Structures:
// Bidirectional mappings for O(1) lookups
mapping(address => address[]) private ownerToChips;
mapping(address => address[]) private chipToOwners;
mapping(address => mapping(address => bool)) public ownerHasChip;Key Functions:
function registerChip(address chipAddress, bytes memory chipSignature) external- Purpose: Register NFC chip to owner with cryptographic proof
- Validation: Verifies chip signed EIP-712
ChipRegistrationmessage - Security: Uses chain-agnostic domain separator (no
chainId) - Gas: ~150k
function getOwnerChips(address owner) external view returns (address[] memory)- Purpose: Get all chips registered to an owner
- Use case: Display user's registered chips in UI
function hasChip(address owner, address chip) external view returns (bool)- Purpose: Fast ownership validation
- Use case: Called by Protocol before every execution
EIP-712 Domain (Chain-Agnostic):
{
name: "TapThatXRegistry",
version: "1",
verifyingContract: address(this)
// Deliberately excludes chainId for cross-chain chip reuse
}Innovation: Chips registered once work across all EVM chains where protocol is deployed.
Purpose: Core execution engine for chip-authorized contract calls
Deployed At:
- Base Sepolia:
0x0F917750db157D65c6c14e5Ce5828a250569afE1
Security Features:
- ✅ Nonce-based replay protection (
mapping(bytes32 => bool) usedNonces) - ✅ Timestamp validation (5-minute expiration window)
- ✅ Chip ownership verification (via Registry)
- ✅ ReentrancyGuard protection
- ✅ EIP-712 signature validation
Key Function:
function executeAuthorizedCall(
address owner, // Chip owner (transaction originator)
address target, // Contract to call (e.g., USDC, Aave)
bytes calldata callData, // Encoded function call
uint256 value, // ETH value (0 for most ops)
bytes memory chipSignature, // Chip's EIP-712 signature
uint256 timestamp, // Authorization timestamp
bytes32 nonce // Unique nonce (prevents replay)
) external payable nonReentrant returns (bool success, bytes memory returnData)Execution Flow:
1. Validate nonce unused require(!usedNonces[nonce])
2. Recover chip from signature TapThatXAuth.recoverChipFromCallAuth()
3. Validate timestamp fresh require(block.timestamp - timestamp <= 300)
4. Verify chip ownership require(registry.hasChip(owner, chip))
5. Mark nonce used usedNonces[nonce] = true
6. Execute call target.call{value}(callData)
7. Emit event AuthorizedCallExecuted(...)
CallAuthorization Struct (EIP-712):
struct CallAuthorization {
address owner; // Chip owner
address target; // Contract to call
bytes callData; // Function call data
uint256 value; // ETH value
uint256 timestamp; // Authorization time
bytes32 nonce; // Unique nonce
}Gas Cost: 130-150k (simple transfers) to 3M (complex DeFi)
Purpose: On-chain storage for pre-configured chip actions
Deployed At:
- Base Sepolia:
0x...(see deployedContracts.ts)
Data Structure:
struct ActionConfig {
address targetContract; // e.g., 0xUSDC
bytes staticCallData; // Pre-encoded function call
uint256 value; // ETH to send (0 for ERC20)
string description; // "Send 100 USDC to Alice"
bool isActive; // Enable/disable toggle
}
mapping(address => mapping(address => ActionConfig)) public configurations;
// Structure: owner → chip → ActionConfigKey Functions:
function setConfiguration(
address chip,
address targetContract,
bytes calldata staticCallData,
uint256 value,
string calldata description
) external- Access Control: Only chip owner can configure
- Validation:
require(registry.hasChip(msg.sender, chip)) - Storage: Saves to
configurations[msg.sender][chip]
function getConfiguration(address owner, address chip)
external view returns (ActionConfig memory)- Purpose: Fetch pre-configured action
- Used by: TapThatXExecutor before every execution
function toggleConfiguration(address chip) external- Purpose: Enable/disable without deleting configuration
- Use case: Pause payments while keeping configuration
Purpose: Simplified interface that combines configuration fetch + execution
Deployed At:
- Base Sepolia:
0x...
Key Function:
function executeTap(
address owner,
address chip,
bytes memory chipSignature,
uint256 timestamp,
bytes32 nonce
) external payable nonReentrant returns (bool, bytes memory)Execution Steps:
// 1. Fetch configuration
ActionConfig memory config = configuration.getConfiguration(owner, chip);
// 2. Validate configuration
require(config.targetContract != address(0), "No configuration");
require(config.isActive, "Configuration inactive");
// 3. Execute via protocol
(bool success, bytes memory data) = protocol.executeAuthorizedCall{value: config.value}(
owner,
config.targetContract,
config.staticCallData,
config.value,
chipSignature,
timestamp,
nonce
);
// 4. Emit event
emit TapExecuted(owner, chip, config.targetContract, nonce, success, config.description);Why Use Executor?
- Simpler: Single function call instead of fetching config separately
- Safer: Validates config exists and is active
- Gas-efficient: Optimized for relay usage
Purpose: Signature verification utilities for EIP-712
Key Functions:
function recoverChipFromCallAuth(
bytes32 domainSeparator,
CallAuthorization memory auth,
bytes memory signature
) internal pure returns (address chipAddress)Implementation:
// 1. Build struct hash
bytes32 structHash = keccak256(abi.encode(
CALL_AUTH_TYPEHASH,
auth.owner,
auth.target,
keccak256(auth.callData), // Hash callData
auth.value,
auth.timestamp,
auth.nonce
));
// 2. Build EIP-712 digest
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
domainSeparator,
structHash
));
// 3. Recover signer
return digest.recover(signature);function validateTimestamp(uint256 timestamp, uint256 maxWindow)
internal view returns (bool)- Validation:
timestamp <= block.timestamp && (block.timestamp - timestamp) <= maxWindow - Window: 300 seconds (5 minutes)
Purpose: Gasless cross-chain ETH bridging to Base + Optimism Sepolia
Deployed At:
- Sepolia:
0x...(L1 only)
Why This Extension?
Problem: Users need ETH on L2s but lack gas to bridge. Solution: Use WETH (ERC20) approval → unwrap → bridge atomically.
Key Function:
function unwrapAndBridgeDual(
address owner,
uint32 minGasLimitOP,
uint32 minGasLimitBase
) externalExecution Flow:
1. Pull pre-approved WETH weth.transferFrom(owner, this, amount)
2. Unwrap WETH → ETH weth.withdraw(amount)
3. Split amount for 2 chains toBase = amount / 2; toOP = amount - toBase
4. Bridge to Base Sepolia bridgeBase.depositETHTo{value: toBase}(...)
5. Bridge to OP Sepolia bridgeOP.depositETHTo{value: toOP}(...)
6. Emit event ETHBridgedViaWETH(...)
Gas Requirements: 3M gas
- OP bridge: ~913k
- Base bridge: ~650k
- Cross-chain messaging: ~200k
- WETH operations: ~150k
- Safety buffer: ~1.1M
Network Addresses (Sepolia):
WETH = 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9
BRIDGE_OP = 0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1 // L1StandardBridge
BRIDGE_BASE = 0xfd0Bf71F60660E2f608ed56e1659C450eB113120 // L1StandardBridgeUser Flow:
Setup (once):
1. Approve WETH to bridge extension
Execute (anytime):
1. Tap chip
2. Extension pulls WETH, unwraps, bridges
3. User receives ETH on both L2s
Purpose: Atomically rebalance Aave positions using flash loans
Deployed At:
- Base Sepolia:
0x...
The Impossible Problem:
To rebalance an Aave position (reduce debt, improve health factor), users need:
- Option 1: Deposit more collateral (requires capital)
- Option 2: Repay debt (requires debt tokens)
- Option 3: Flash loan + complex execution (requires technical knowledge)
TapThat X Solution: One tap executes flash loan rebalancing.
Optimal Flash Loan Formula:
Health Factor: HF = (totalCollateral × liquidationThreshold) / totalDebt
Target: targetHF = (totalCollateral × LT) / (totalDebt - debtRepaid)
Solving for debtRepaid:
debtRepaid = (targetHF × totalDebt - totalCollateral × LT) / (targetHF - costFactor × LT)
Where:
costFactor = 1.0000 + 0.0009 (flash fee) + 0.0030 (margin) + maxSlippage
Implementation:
function calculateOptimalFlashLoan(address owner, RebalanceConfig memory config)
public view returns (uint256 flashLoanAmount)
{
// Get Aave position data (USD, 8 decimals)
(uint256 collateralUSD, uint256 debtUSD, , uint256 ltBps, , uint256 hf) =
POOL.getUserAccountData(owner);
if (hf >= config.targetHealthFactor) return 0;
// Normalize to 18 decimals for precision
uint256 collateral18 = collateralUSD * 1e10;
uint256 debt18 = debtUSD * 1e10;
uint256 lt18 = (ltBps * 1e18) / 10000;
// Cost factor (basis points → 18 decimals)
// 10000 + 9 (flash) + 30 (margin) + slippage
uint256 costFactorBps = 10000 + 9 + 30 + config.maxSlippage;
uint256 costFactor18 = (costFactorBps * 1e18) / 10000;
// Formula application
uint256 numerator = (config.targetHealthFactor * debt18 / 1e18) -
(collateral18 * lt18 / 1e18);
uint256 denominator = config.targetHealthFactor - (costFactor18 * lt18 / 1e18);
uint256 debtRepaidUSD = (numerator * 1e18 / denominator) / 1e10; // Back to 8 decimals
// Convert USD → token amount via Aave oracle
address oracle = IPoolAddressesProvider(POOL.ADDRESSES_PROVIDER()).getPriceOracle();
uint256 price = IAaveOracle(oracle).getAssetPrice(config.debtAsset);
uint256 decimals = IERC20Metadata(config.debtAsset).decimals();
return (debtRepaidUSD * (10 ** decimals)) / price;
}Flash Loan Callback Execution (All Atomic):
function executeOperation(...) external returns (bool) {
// Step 1: Repay debt with flash loan
IERC20(debtAsset).approve(address(POOL), flashAmount);
POOL.repay(debtAsset, flashAmount, 2, owner); // Variable rate
// Step 2: Withdraw collateral (aToken)
address aToken = POOL.getReserveData(collateralAsset).aTokenAddress;
IERC20(aToken).transferFrom(owner, address(this), collateralAmount);
POOL.withdraw(collateralAsset, collateralAmount, address(this));
// Step 3: Swap collateral → debt asset (Uniswap V2)
uint256 totalRepayment = flashAmount + premium;
uint256 swapOutput = _swapV2(collateralAsset, debtAsset, collateralAmount, totalRepayment);
// Step 4: Approve Aave to pull repayment
IERC20(debtAsset).approve(address(POOL), totalRepayment);
// Step 5: Return excess to user
uint256 excess = swapOutput - totalRepayment;
if (excess > 0) IERC20(debtAsset).transfer(owner, excess);
// Step 6: Verify health factor improved
(, , , , , uint256 hfAfter) = POOL.getUserAccountData(owner);
require(hfAfter > hfBefore, "Health factor not improved");
return true; // Aave auto-pulls totalRepayment
}Uniswap V2 Calculation:
// Constant product formula: x × y = k
// Given output (totalRepayment), calculate input needed:
// amountIn = (reserveIn × amountOut × 1000) / ((reserveOut - amountOut) × 997)
uint256 numerator = uint256(reserveCollateral) * totalRepayment * 1000;
uint256 denominator = (uint256(reserveDebt) - totalRepayment) * 997; // 0.3% fee
uint256 amountIn = (numerator / denominator) + 1;
// Add slippage buffer
uint256 slippageMultiplier = 10000 + config.maxSlippage;
return (amountIn * slippageMultiplier) / 10000;Gas Requirements: 1.5M
- Flash loan callback: 200k
- Aave operations: 350k
- Uniswap swap: 300k
- Verifications: 200k
- Buffer: 450k
Example: Health factor 1.05 → 2.5 in one tap
Purpose: Fully close Aave positions (repay all debt, withdraw all collateral)
Key Difference from Rebalancer:
- Rebalancer: Reduces debt to improve health factor
- Closer: Repays ALL debt and withdraws ALL collateral
Execution Flow:
1. Calculate total debt flashLoanAmount = total variable debt
2. Flash loan exact amount POOL.flashLoanSimple(debtAsset, amount, ...)
3. Repay ALL debt POOL.repay(debtAsset, amount, 2, owner)
4. Withdraw ALL collateral POOL.withdraw(collateralAsset, aTokenBalance, ...)
5. Swap collateral → debt swapOutput = _swapV2(collateral, debt, ...)
6. Repay flash loan IERC20(debt).approve(POOL, repayment)
7. Return excess to user transfer(owner, remainingCollateral + excessDebt)
8. Verify fully closed require(remainingDebt == 0)
┌─────────────────────────────────────────────────────────────────┐
│ Chip Registration │
└─────────────────────────────────────────────────────────────────┘
User Frontend NFC Chip Blockchain
│ │ │ │
│ Navigate to │ │ │
│ /register │ │ │
├────────────────────>│ │ │
│ │ │ │
│ Click "Register" │ │ │
├────────────────────>│ │ │
│ │ │ │
│ │ Step 1: Detect Chip │ │
│ │ "Hold device near │ │
│ │ NFC chip..." │ │
│ │ │ │
│ Tap phone on chip │ │ │
├────────────────────>│─ signMessage("init") ─> │
│ │ │ │
│ │<─ {address, sig} ─────┤ │
│ │ │ │
│ │ chipAddress detected │ │
│ │ 0x742d... │ │
│ │ │ │
│ │ Step 2: Sign Auth │ │
│ │ "Tap again to │ │
│ │ authorize..." │ │
│ │ │ │
│ Tap chip again │ │ │
├────────────────────>│─ signTypedData({ │ │
│ │ domain: { │ │
│ │ name: "TapThatX │ │
│ │ Registry",│ │
│ │ version: "1" │ │
│ │ }, │ │
│ │ message: { │ │
│ │ owner: 0xUser, │ │
│ │ chip: 0x742d │ │
│ │ } │ │
│ │ }) ──────────────────> │
│ │ │ │
│ │ │ Chip signs EIP-712 │
│ │ │ with private key │
│ │ │ │
│ │<─ chipSignature ──────┤ │
│ │ │ │
│ │ Step 3: Submit Tx │ │
│ │ "Confirm in wallet" │ │
│ │ │ │
│ Approve in wallet │ │ │
├────────────────────>│─ writeContract({ │ │
│ │ fn: "registerChip",│ │
│ │ args: [ │ │
│ │ 0x742d, │ │
│ │ signature │ │
│ │ ] │ │
│ │ }) ──────────────────────────────────────>│
│ │ │ │
│ │ │ TapThatXRegistry │
│ │ │ 1. Recover signer │
│ │ │ 2. Verify == chip │
│ │ │ 3. Store mapping │
│ │ │ owner→chips[] │
│ │ │ chip→owners[] │
│ │ │ │
│ │<────── tx hash ───────────────────────────────┤
│ │ │ │
│ ✅ Success! │ │ │
│ "Chip registered" │ │ │
│<────────────────────┤ │ │
│ │ │ │
Total Time: ~30 seconds
User Actions: 3 taps, 1 wallet approval
Result: Chip → Owner mapping stored on-chain
┌─────────────────────────────────────────────────────────────────────┐
│ Tap-to-Execute Flow │
└─────────────────────────────────────────────────────────────────────┘
User Frontend NFC Chip Relay API Blockchain
│ │ │ │ │
│ Tap phone │ │ │ │
│ on chip │ │ │ │
├─────────────>│ │ │ │
│ │ Step 1: │ │ │
│ │ Detect chip │ │ │
│ ├─ signMessage ─> │ │
│ │ ("init") │ │ │
│ │<─ {address} ──┤ │ │
│ │ │ │ │
│ │ Verify chip │ │ │
│ │ ownership │ │ │
│ ├─ registry.hasChip() ─────────────────────────>│
│ │<─ true ──────────────────────────────────────┤
│ │ │ │ │
│ │ Fetch config │ │ │
│ ├─ config.getConfiguration() ──────────────────>│
│ │<─ ActionConfig { │ │
│ │ target: USDC, │ │
│ │ callData: transferFrom() │ │
│ │ } ──────────────────────────────────────────┤
│ │ │ │ │
│ │ Preview: │ │ │
│ │ "Send 10 USDC │ │ │
│ │ to Alice" │ │ │
│ │ │ │ │
│ │ Step 2: │ │ │
│ │ Authorize │ │ │
│ Tap again │ │ │ │
├─────────────>│─ signTypedData({ │ │
│ │ CallAuth: {│ │ │
│ │ owner, │ │ │
│ │ target, │ │ │
│ │ callData,│ │ │
│ │ timestamp, │ │
│ │ nonce │ │ │
│ │ } │ │ │
│ │ }) ──────────> │ │
│ │<─ signature ──┤ │ │
│ │ │ │ │
│ │ Step 3: │ │ │
│ │ Send to relay │ │ │
│ ├─ POST /api/relay-execute-tap ─> │
│ │ { │ │ │
│ │ owner, │ │ │
│ │ chip, │ │ │
│ │ signature,│ │ │
│ │ timestamp,│ │ │
│ │ nonce │ │ │
│ │ } │ │ │
│ │ │ │ │
│ │ │ │ Relay validates │
│ │ │ │ input structure │
│ │ │ │ │
│ │ │ │ Detect operation│
│ │ │ │ (simple/Aave/ │
│ │ │ │ bridge) │
│ │ │ │ │
│ │ │ │ Set gas limit: │
│ │ │ │ - Simple: auto │
│ │ │ │ - Aave: 1.5M │
│ │ │ │ - Bridge: 3M │
│ │ │ │ │
│ │ │ │ Submit tx │
│ │ │ ├─ executeTap() ──>
│ │ │ │ │
│ │ │ │ Executor: │
│ │ │ │ 1. Fetch config
│ │ │ │ 2. Validate │
│ │ │ │ │
│ │ │ │ Protocol: │
│ │ │ │ 3. Nonce check│
│ │ │ │ 4. Recover chip
│ │ │ │ 5. Timestamp │
│ │ │ │ 6. Ownership │
│ │ │ │ 7. Execute │
│ │ │ │ target.call│
│ │ │ │ │
│ │ │ │<─ success ──────┤
│ │ │ │ │
│ │<────── {success, txHash} ────┤ │
│ │ │ │ │
│ ✅ Success! │ │ │ │
│ "10 USDC sent │ │ │ │
│ to Alice" │ │ │ │
│<──────────────┤ │ │ │
Total Time: 3 seconds
User Actions: 2 taps
Gas Paid By: Relay
User Gas Cost: $0
┌──────────────────────────────────────────────────────────────────────┐
│ Aave Position Rebalancing (Flash Loan) │
└──────────────────────────────────────────────────────────────────────┘
Scenario: User's health factor is 1.05 (5% from liquidation)
Goal: Improve to 2.5 (safe zone)
Tap → Relay → TapThatXExecutor → TapThatXAaveRebalancer
│
│ calculateOptimalFlashLoan()
│ Returns: 1000 USDT needed
│
▼
┌─────────────────────────┐
│ Aave V3 Pool │
│ flashLoanSimple() │
└───────────┬─────────────┘
│
Flash 1000 USDT (0.09% fee = 0.9 USDT)
│
▼
┌─────────────────────────────────────┐
│ executeOperation() Callback │
│ (All steps atomic) │
└─────────────────────────────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Step 1: │ │ Step 2: │ │ Step 3: │
│ Repay Debt │ │ Withdraw │ │ Swap │
│ │ │ Collateral │ │ │
│ POOL.repay( │ │ │ │ Uniswap V2 │
│ USDT, │ │ 1. transferFrom │ │ │
│ 1000, │ │ user's aToken │ │ collateral │
│ owner │ │ 2. POOL.withdraw │ │ ↓ │
│ ) │ │ collateral │ │ USDT │
│ │ │ │ │ │
│ Health: 1.05 │ │ Health: improved │ │ Output: │
│ → 2.5+ │ │ (debt reduced) │ │ 1000.9 USDT │
└──────────────┘ └──────────────────┘ └─────────────┘
│
▼
┌─────────────────────────┐
│ Step 4: │
│ Repay Flash Loan │
│ │
│ Approve: 1000.9 USDT │
│ Aave auto-pulls at │
│ function end │
└─────────┬───────────────┘
│
▼
┌─────────────────────────┐
│ Step 5: │
│ Return Excess │
│ │
│ Excess collateral → user│
│ Excess USDT → user │
└─────────┬───────────────┘
│
▼
┌─────────────────────────┐
│ Step 6: │
│ Verify │
│ │
│ require(hfAfter > 1.05) │
│ require(hfAfter >= 2.5) │
└─────────────────────────┘
│
▼
✅ Position Rebalanced
Health Factor: 1.05 → 2.5
User paid: $0 (gasless)
Time: ~60 seconds
Key Innovation: Repay debt FIRST (improves health) → THEN withdraw collateral (safe)
Traditional approach (withdraw → swap → repay) would trigger liquidation
Standard EIP-712 (chain-locked):
keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("TapThatXRegistry")),
keccak256(bytes("1")),
block.chainid, // ← Locks to specific chain
address(this)
))TapThat X (cross-chain compatible):
keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,address verifyingContract)"),
keccak256(bytes("TapThatXRegistry")),
keccak256(bytes("1")),
address(this)
// No chainId ← Chip works on any chain
))Benefit: Register chip once on Sepolia → use on Base, OP, Mainnet, Arbitrum without re-registration.
Security: Maintained via per-chain nonce tracking in TapThatXProtocol.
Problem: HaLo library uses JSON (no BigInt support), but Viem uses BigInt for uint256 precision.
Solution: Recursive BigInt → string conversion before NFC signing.
Implementation (useHaloChip.ts):
const serializeBigInt = (obj: any): any => {
if (typeof obj === "bigint") return obj.toString();
if (obj === null || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(serializeBigInt);
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, serializeBigInt(v)])
);
};
// Usage in signTypedData
const typedDataPayload = {
domain,
types,
primaryType,
value: serializeBigInt(message), // Recursive conversion
};
const result = await execHaloCmdWeb({
name: "sign",
keyNo: 1,
typedData: typedDataPayload,
});Example:
// Input
{ owner: "0x123", value: 1000000000000000000n, timestamp: 1698765432n }
// After serialization
{ owner: "0x123", value: "1000000000000000000", timestamp: "1698765432" }Traditional Approach (fails):
1. Withdraw collateral from Aave
2. Swap collateral for debt token
3. Repay debt
❌ Fails: Withdrawing before repaying triggers liquidation
TapThat X Approach (succeeds):
1. Flash loan debt token
2. Repay debt (health factor improves)
3. Withdraw collateral (now safe)
4. Swap collateral for debt token
5. Repay flash loan
✅ Succeeds: Debt repayment improves health before withdrawal
Key Insight: Improving health factor BEFORE withdrawing enables safe collateral removal.
Traditional Gasless (requires relayer trust):
User sends tokens to relayer → Relayer submits transaction
❌ Risk: Relayer could steal tokens
TapThat X Gasless (trustless):
// User approves once
IERC20(token).approve(extensionAddress, maxUint256);
// Extension pulls during execution (relayer can't access)
IERC20(token).transferFrom(owner, address(this), amount);
// Smart contract validates chip signature (relayer can't forge)
require(registry.hasChip(owner, chip), "Ownership validation");Security: Relay pays gas but cannot steal funds. On-chain validation prevents forgery.
┌─────────────────────────────────────────────────────────────┐
│ Frontend Structure │
└─────────────────────────────────────────────────────────────┘
app/
├── page.tsx → Landing page (/)
├── register/page.tsx → Chip registration (/register)
├── approve/page.tsx → Token approval (/approve)
├── configure/page.tsx → Action configuration (/configure)
└── execute/page.tsx → Tap-to-execute (/execute)
hooks/
├── useHaloChip.ts → NFC hardware integration
└── useGaslessRelay.ts → Backend relay API
utils/
└── actionTemplates.ts → CallData builders
├── erc20TransferTemplate
├── aaveRebalanceTemplate
└── bridgeETHTemplate
api/
└── relay-execute-tap/route.ts → Gasless relay endpoint
Purpose: Interface to HaLo NFC chip hardware via Web NFC API
Functions:
const { signMessage, signTypedData, isLoading, error } = useHaloChip();
// Chip detection (returns address)
const { address, signature } = await signMessage({
message: "init",
format: "text"
});
// EIP-712 signing
const { address, signature } = await signTypedData({
domain: { name: "TapThatXProtocol", version: "1", verifyingContract },
types: { CallAuthorization: [...] },
primaryType: "CallAuthorization",
message: { owner, target, callData, value, timestamp, nonce }
});Library: @arx-research/libhalo@1.3.2
Platforms:
- ✅ Android Chrome (native Web NFC)
⚠️ iOS Safari (requires NFC-enabled browser or app)
Purpose: Submit transactions to gasless relay API
Function:
const { relayExecuteTap } = useGaslessRelay();
const result = await relayExecuteTap({
owner: "0x...",
chip: "0x...",
chipSignature: "0x...",
timestamp: 1234567890,
nonce: "0x...",
});
// Returns: { success: true, transactionHash: "0x...", blockNumber: "12345" }Endpoint: POST /api/relay-execute-tap
Purpose: Build callData for common DeFi operations
ERC20 Transfer Template:
const erc20TransferTemplate = {
id: "erc20-transfer",
name: "ERC20 Transfer",
buildCallData: (params: {
tokenAddress: `0x${string}`;
from: `0x${string}`;
to: `0x${string}`;
amount: bigint;
}) => {
const callData = encodeFunctionData({
abi: ERC20_ABI,
functionName: "transferFrom",
args: [params.from, params.to, params.amount],
});
return { target: params.tokenAddress, callData, value: 0n };
},
};Aave Rebalance Template:
const aaveRebalanceTemplate = {
id: "aave-rebalance",
buildCallData: (params: {
rebalancerAddress: `0x${string}`;
owner: `0x${string}`;
collateralAsset: `0x${string}`;
debtAsset: `0x${string}`;
targetHealthFactor: bigint;
maxSlippage: bigint;
}) => {
const callData = encodeFunctionData({
abi: AAVE_REBALANCER_ABI,
functionName: "executeRebalance",
args: [
params.owner,
{
collateralAsset: params.collateralAsset,
debtAsset: params.debtAsset,
targetHealthFactor: params.targetHealthFactor,
maxSlippage: params.maxSlippage,
},
],
});
return { target: params.rebalancerAddress, callData, value: 0n };
},
};| Network | Chain ID | Purpose | Contracts |
|---|---|---|---|
| Ethereum Sepolia | 11155111 | Bridge testing | Core (5) + Bridge (1) |
| Base Sepolia | 84532 | Full features | Core (5) + DeFi Extensions (3) + MockUSDC |
| Contract | Address |
|---|---|
| MockUSDC | 0xb090ae6dd89b25d1b79718853c439d1354bf62c5 |
| TapThatXRegistry | 0x91D05d5B8913BCdA59f1923dC6831B108154Df22 |
| TapThatXProtocol | 0x0F917750db157D65c6c14e5Ce5828a250569afE1 |
| TapThatXConfiguration | See deployedContracts.ts |
| TapThatXExecutor | See deployedContracts.ts |
| TapThatXAaveRebalancer | See deployedContracts.ts |
| TapThatXAavePositionCloser | See deployedContracts.ts |
| Contract | Address |
|---|---|
| TapThatXRegistry | See deployedContracts.ts |
| TapThatXProtocol | See deployedContracts.ts |
| TapThatXConfiguration | See deployedContracts.ts |
| TapThatXExecutor | See deployedContracts.ts |
| TapThatXBridgeETHViaWETH | See deployedContracts.ts |
- ✅ ERC20 transfers
- ✅ Aave V3 flash loan rebalancing
- ✅ Aave V3 position closing
- ✅ MockUSDC for testing
- ✅ Sub-2-second block times
- ✅ <$0.01 gas costs
- ✅ Bridge to Base Sepolia
- ✅ Bridge to OP Sepolia
- ✅ WETH unwrapping
- ✅ Dual L2 deposits
| Operation | Gas | Time | Cost (1 gwei) | User Cost |
|---|---|---|---|---|
| Register Chip | 150k | ~30s | 0.00015 ETH | Paid once |
| Configure Action | 80k | ~30s | 0.00008 ETH | Paid once |
| Execute ERC20 Transfer | 130k | ~3s | 0.00013 ETH | $0 (relayed) |
| Aave Rebalance (Flash Loan) | 1.5M | ~60s | 0.0015 ETH | $0 (relayed) |
| Bridge to 2 L2s | 3M | ~2min | 0.003 ETH | $0 (relayed) |
Key Insight: Users pay setup costs once, then execute unlimited actions gaslessly.
┌────────────────────────────────────────────────────────────┐
│ Security Validation Layers │
└────────────────────────────────────────────────────────────┘
Request from User
│
▼
┌─────────────────────┐
│ Layer 1: Signature │
│ - EIP-712 format │
│ - Recover chip addr │
│ - Verify signature │
└──────┬──────────────┘
│ ✅ Valid signature
▼
┌─────────────────────┐
│ Layer 2: Ownership │
│ - registry.hasChip()│
│ - owner → chip map │
│ - Bidirectional │
└──────┬──────────────┘
│ ✅ Owner has chip
▼
┌─────────────────────┐
│ Layer 3: Timestamp │
│ - Not in future │
│ - Within 5 min │
│ - Prevents stale │
└──────┬──────────────┘
│ ✅ Fresh authorization
▼
┌─────────────────────┐
│ Layer 4: Nonce │
│ - Check unused │
│ - Mark used │
│ - Replay protection │
└──────┬──────────────┘
│ ✅ Unique transaction
▼
┌─────────────────────┐
│ Layer 5: Execution │
│ - ReentrancyGuard │
│ - target.call() │
│ - Return success │
└─────────────────────┘
│
▼
✅ Transaction executes
| Feature | Implementation | Purpose |
| -------------------------- | ----------------------------------------------------------------------------- | ---------------------------- | --------------------- | ----------------------- |
| Replay Protection | mapping(bytes32 => bool) usedNonces | Prevent signature reuse |
| Timestamp Validation | require(timestamp <= block.timestamp && block.timestamp - timestamp <= 300) | Prevent stale signatures |
| Ownership Verification | require(registry.hasChip(owner, chip)) | Ensure chip belongs to owner |
| ReentrancyGuard | OpenZeppelin protection | Prevent reentrancy attacks |
| Access Control | require(msg.sender == PROTOCOL | | msg.sender == owner) | Limit extension callers |
| Secure Element | HaLo chip private key never leaves chip | Hardware security |
- Foundry - Development framework
- Solidity ^0.8.19
- OpenZeppelin - EIP712, ECDSA, ReentrancyGuard, Ownable
- Aave V3 - Flash loans (Base Sepolia:
0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27) - Uniswap V2 - Token swaps
- Next.js 15 (App Router)
- React 19
- TypeScript 5
- Wagmi 2.16 - React hooks for Ethereum
- Viem 2.34 - TypeScript Ethereum library
- RainbowKit 2.2 - Wallet connection
- @arx-research/libhalo 1.3.2 - NFC chip communication
- TailwindCSS 4 - Utility-first CSS
- DaisyUI 5 - Component library
- Lucide React - Icon library
- Glassmorphism design aesthetic
- Base Sepolia - L2 deployment (2s blocks, <$0.01 gas)
- Optimism Sepolia - L2 deployment
- Ethereum Sepolia - L1 deployment (bridge base)
- Alchemy - RPC provider (optional)
- Arx HaLo NFC Chips - Secure element with EIP-712 signing
- Web NFC API - Browser-based NFC reading (Android Chrome)
- Node.js >= 22.14.0
- npm or yarn
- MetaMask or compatible Web3 wallet
- NFC-capable mobile device (Android recommended)
- Arx HaLo NFC chip (order here)
# Clone repository
git clone https://github.com/0xgeorgemathew/tap-that-x.git
cd tap-that-x
# Install dependencies
npm installCreate .env file in packages/nextjs/:
# Relayer account (must be funded with testnet ETH)
RELAYER_PRIVATE_KEY=0x...
# Alchemy API key (optional, improves RPC reliability)
ALCHEMY_API_KEY=...
# Or use public name
NEXT_PUBLIC_ALCHEMY_API_KEY=...Fund Relayer Account:
- Generate new wallet or use existing private key
- Get testnet ETH from Base Sepolia Faucet
- Add private key to
.env
# Start Next.js development server
npm run start
# Visit http://localhost:3000# Run Foundry tests
npm run foundry:test
# Run with verbosity
npm run foundry:test -- -vvv
# Test specific contract
npm run foundry:test -- --match-contract TapThatXTestTest Coverage:
- ✅ Chip registration
- ✅ ERC20 authorized calls
- ✅ Nonce replay protection
- ✅ Timestamp expiration
- ✅ Ownership validation
- ✅ Aave rebalancing
- ✅ Flash loan calculations
# Deploy to Base Sepolia
npm run deploy
# Deploy to Sepolia (bridge testing)
npm run deploy -- --network sepoliaDeployment Script: packages/foundry/script/Deploy.s.sol
Deployment Order:
- TapThatXRegistry (no dependencies)
- TapThatXProtocol (requires Registry)
- TapThatXConfiguration (requires Registry)
- TapThatXExecutor (requires Protocol + Configuration)
- Extensions (network-specific)
1. Navigate to /register
2. Click "Start Registration"
3. Tap chip to detect address (2-3 sec)
4. Tap again to authorize registration (2-3 sec)
5. Confirm transaction in wallet (5-10 sec)
✅ Chip registered on-chain
1. Navigate to /approve
2. Click "Approve USDC Spending"
3. Confirm in wallet
✅ Protocol can spend your USDC (you control via allowance)
1. Navigate to /configure
2. Select your registered chip
3. Choose action template:
- ERC20 Transfer
- Aave Rebalance
- Bridge ETH
4. Enter parameters (recipient, amount, etc.)
5. Save configuration on-chain
✅ Chip now executes this action on tap
1. Navigate to /execute
2. Tap chip to detect (2 sec)
3. Tap again to authorize (2 sec)
4. Transaction executes gaslessly (1 sec)
✅ Action completed, no wallet popup needed
# 1. Start local Foundry chain
npm run chain
# 2. Deploy contracts locally
npm run deploy -- --network localhost
# 3. Start frontend
npm run start
# 4. Test with mock chip address
# Use any address as "chip" for local testing# 1. Deploy to Base Sepolia
npm run deploy
# 2. Fund relayer account
# Get ETH from https://www.alchemy.com/faucets/base-sepolia
# 3. Get HaLo chip
# Order from https://arx.org/
# 4. Test full flow
# register → approve → configure → execute- NFC chip registration with EIP-712
- Generic executeAuthorizedCall for any contract
- Gasless execution via relay
- ERC20 token transfers
- Aave V3 flash loan rebalancing
- Aave V3 position closing
- Cross-chain bridging (Sepolia → Base/OP)
- Chain-agnostic chip signatures
- Dynamic gas limit detection
- Mobile-first UI with glassmorphism
- Mainnet deployment
- Additional DeFi integrations (Uniswap V3, Compound)
- Multi-action batching (execute multiple actions in one tap)
- Spending limits per chip
- Emergency pause functionality
- Avail Nexus Integration - Cross-chain payments with unified liquidity
- Merchant Terminals - Physical POS systems accepting stablecoin payments
- NFT Ticketing - Tap-to-claim POAPs and event tickets
- DAO Voting - Physical presence voting with chips
- DCA Strategies - Automated dollar-cost averaging via taps
- Yield Optimization - Auto-rebalance across protocols
- Limit Orders - DEX limit orders with tap triggers
Contributions welcome! Please read CONTRIBUTING.md first.
# 1. Fork repository
# 2. Create feature branch
git checkout -b feature/amazing-feature
# 3. Make changes
# 4. Run tests
npm run foundry:test
# 5. Commit changes
git commit -m "Add amazing feature"
# 6. Push to branch
git push origin feature/amazing-feature
# 7. Open Pull RequestThis project is licensed under the MIT License - see the LICENSE file for details.
- Arx Research for HaLo NFC chips and libhalo library
- Base for fast, cheap L2 infrastructure
- Optimism for L2StandardBridge specification
- Aave for V3 flash loan functionality
- Scaffold-ETH for development framework
- OpenZeppelin for battle-tested smart contract libraries
- Twitter: @0xgeorgemathew
- GitHub: @0xgeorgemathew
- Project: TapThat X
Built with ❤️ for ETHOnline 2025 Hackathon
Making blockchain physical, one tap at a time