Usage
What You're Building
Think of TargetBase as your smart contract's security bouncer. It's a battle-tested foundation that gives you signature-based authorization, smart account enforcement, and all the protection you need for EIP-7702 delegated accounts—without having to reinvent the wheel.
This guide walks you through extending TargetBase to build your own secure contracts. We'll use the RealEstateInvestment contract as our real-world example because, well, nothing teaches better than actual code.
What TargetBase Does For You
Here's what you get out of the box:
Signature-based authorization – Only calls signed by your master key get through
Smart account validation – Enforces EIP-7702 compliance (no random EOAs sneaking in)
Replay attack protection – Nonces and hash tracking ensure each authorization works exactly once
Time-bound authorizations – Set expiry timestamps so old signatures can't haunt you
Emergency recovery – Because sometimes you need a backup plan
Multi-source execution chains – Perfect for when you need to aggregate data from multiple sources
Think of it as a security framework that handles all the hard parts so you can focus on your business logic.
The Core Building Blocks
AuthData
Every protected function needs one of these. It's like a VIP pass that proves the caller has permission:
struct AuthData {
uint256 nonce; // Prevents replay attacks
uint256 expiry; // "This signature expires on..."
bytes32 id; // Unique execution identifier
Execution[] executions; // For multi-step operations
bytes result; // The final data or parameters
bool sponsorExecutionFee; // Who pays the gas?
bytes signature; // The master key's signature
}The Authorization Players
Master Key – The VIP who signs everything. This is your authorization authority.
Recovery Key – Your emergency contact. Can rotate the master key if things go wrong.
Owner – The admin. Controls contract settings and upgrades.
DelegatedAccount – The only type of account allowed to call your protected functions (EIP-7702 smart accounts only)
How to Extend TargetBase (The Three-Step Implementation)
Step 1: Set Up Your Contract
Start by inheriting from TargetBase. You can mix in other contracts too—we're using ERC20 in our real estate example:
contract YourContract is TargetBase, [OtherContracts] {
constructor(
address _authKey,
address _recoveryKey,
address _owner,
address _delegatedAccountImpl,
// your additional parameters
) TargetBase(_authKey, _recoveryKey, _owner, _delegatedAccountImpl) {
// your initialization magic happens here
}
}Step 2: Define Your Data Structures
Before you do anything else, define the structs that will carry your data which is basically the result your are expected from the KRNL node as a part of the workflow execution. These are what you'll encode/decode in authData.result:
// What your protected function will actually use
struct YourResponseStruct {
uint256 someField; // 0-100
uint256 expectedAnnualYield; // In basis points
string investmentGrade; // "A+", "A", "B+", etc.
uint256 propertyValue; // In USD
string recommendation; // "INVEST", "HOLD", or "PASS"
}Important: The fields in your struct should be alphabetically ordered when you're working with external data sources. This ensures consistent encoding/decoding across different systems.
Step 2: Protect Your Functions
Now when you integrate KRNL to your smart contract, you use requireAuth(authData) modifier to do the basic validation check like your nonce, expiry, etc and make AuthData your first parameter:
function yourProtectedFunction(
AuthData calldata authData, // Always first!
// then your actual business parameters
) external requireAuth(authData) {
// By the time this runs, TargetBase has already verified:
// ✓ The caller is a legit EIP-7702 account
// ✓ The signature is valid
// ✓ The nonce is correct
// ✓ It hasn't expired
// ✓ It hasn't been used before
// Your business logic here
}Step 3: Decode Your Data (When You Need It)
If you're working with multi-source execution chains or need to extract data from the authorization:
function submitData(AuthData calldata authData)
external requireAuth(authData)
{
// Unpack the final result from your execution chain
YourResponseStruct memory response =
abi.decode(authData.result, (YourResponseStruct));
// Now validate it
require(response.someField > threshold, "Not good enough!");
// And use it
// ... your business logic
}Real-World Example: Tokenizing Real Estate
Let's look at how the RealEstateInvestment contract does it. This contract lets people buy fractional ownership of properties using USDC. It needs TargetBase because property analysis data comes from multiple sources (Zillow, Census data, AI analysis) and needs to be verified before allowing investments.
contract RealEstateInvestment is TargetBase, ERC20 {
// Step 1: Define the response struct for property analysis
struct PropertyAnalysisResponse {
uint256 confidence; // Fields alphabetically ordered
uint256 expectedAnnualYield;
string investmentGrade;
uint256 propertyValue;
string recommendation;
}
// Step 2: Set up the constructor
// Notice we're mixing TargetBase with ERC20—totally fine!
constructor(
address _authKey,
address _recoveryKey,
address _owner,
address _delegatedAccountImpl,
address _usdcToken,
string memory _propertyAddress
) TargetBase(_authKey, _recoveryKey, _owner, _delegatedAccountImpl)
ERC20("Real Estate Property Token", "REPT") {
// Initialize our business-specific stuff
usdc = IERC20(_usdcToken);
property.propertyAddress = _propertyAddress;
}
// Step 3: Create a protected function that handles multi-source data
function submitPropertyAnalysis(AuthData calldata authData)
external requireAuth(authData)
{
// This data came from a chain of executions:
// Zillow API → Census data → AI analysis
// We decode the final aggregated result
PropertyAnalysisResponse memory analysisResponse =
abi.decode(authData.result, (PropertyAnalysisResponse));
// Always validate before trusting!
if (analysisResponse.confidence < MIN_CONFIDENCE) {
revert ConfidenceTooLow();
}
// Now we can safely use it
property.totalValue = analysisResponse.propertyValue;
property.investmentGrade = analysisResponse.investmentGrade;
// Pro move: Emit events with authData context for audit trails
emit PropertyAnalyzed(
msg.sender,
authData.nonce,
authData.id,
property.propertyAddress,
analysisResponse.propertyValue,
// ... more event params
);
}
// Step 4: Another Protected function with business parameters
function purchaseTokens(
AuthData calldata authData, // Auth first
uint256 usdcAmount // Then your params
) external requireAuth(authData) {
// Authorization already verified, just do your thing
require(usdcAmount >= MIN_INVESTMENT, "Need at least 1000 USDC");
// Calculate tokens, transfer USDC, mint tokens, etc.
// ... your business logic
}
}What's happening here?
The constructor handles both TargetBase and ERC20 initialization
submitPropertyAnalysisdecodes complex multi-source data and validates itpurchaseTokenscombines authorization with business parametersEvents include
authData.nonceandauthData.idfor complete traceability
Things to Avoid
When Setting Up Your Constructor
No zero addresses – TargetBase will reject them. Every address parameter matters.
DelegatedAccount must be real – It needs to be an actual deployed contract with code. No empty addresses or EOAs.
Order matters – Pass the four TargetBase parameters first, in order:
_authKey, _recoveryKey, _owner, _delegatedAccountImpl
When Writing Protected Functions
AuthData always comes first – It's not just convention, it's how the pattern works.
function myFunc(AuthData calldata authData, ...)Use the modifier – Don't forget
requireAuth(authData). That's where all the magic happens.Include context in events – Always emit
authData.nonceandauthData.idin your events. Future you will thank present you when debugging!
The Security Guarantees
Here's what TargetBase enforces automatically:
EIP-7702 only – No EOAs allowed. Only properly delegated smart accounts get through.
Sequential nonces – Can't skip ahead or go backward. If Alice is on nonce 5, her next call must use nonce 5, then 6, then 7...
Expiry matters – That timestamp isn't just for show. Expired = rejected.
One-time use – Each authorization is like a ticket. Once it's used, it's burned. No replays, no exceptions.
Best Practices
Emit meaningful events
Include authData.nonce and authData.id in every event. Six months from now when you're debugging a production issue, you'll want that audit trail. Trust me.
Validate everything Just because data made it through authorization doesn't mean it's correct. Always validate decoded results before using them. Check ranges, verify checksums, enforce business rules.
Use custom errors
TargetBase uses them for a reason—they're gas-efficient and more descriptive. revert InsufficientFunds() beats require(balance > 0, "Insufficient funds") every time.
Protect against reentrancy
TargetBase includes nonReentrant protection. Use it on any function that changes state or moves value around.
Keep view functions simple
Don't use requireAuth on view or pure functions. They don't change state, so they don't need authorization. Save the gas.
Use Ownable for admin stuff
TargetBase inherits from Ownable, so you get onlyOwner for free. Use it for administrative functions that shouldn't go through the auth flow.
Three Common Patterns You Can Use
Pattern 1: Simple Authorization (Just Verify, No Data)
function simpleAction(AuthData calldata authData)
external requireAuth(authData)
{
// No data decoding needed
// Just verify the caller is authorized and execute
someState = true;
}Pattern 2: Multi-Source Data Pipeline
function complexAction(AuthData calldata authData)
external requireAuth(authData)
{
// Decode the aggregated result from your execution chain
FinalResult memory result = abi.decode(authData.result, (FinalResult));
// Validate it
require(result.confidence >= MIN_CONFIDENCE, "Low confidence");
// Use it
processResult(result);
}Pattern 3: Authorization + Business Parameters
function businessAction(
AuthData calldata authData,
uint256 amount,
address recipient
) external requireAuth(authData) {
// Combine authorization with your own parameters
// Auth is verified, now validate your business logic
require(amount > 0, "Amount must be positive");
require(recipient != address(0), "Invalid recipient");
// Do your thing
transfer(recipient, amount);
}When to use each:
Pattern 1: Permission changes, state toggles, simple actions
Pattern 2: External data validation, multi-API aggregation, complex analysis
Pattern 3: Most common! Authorized actions with user-provided parameters
Last updated

