5 contracts deployed on X Layer (chainId 196). Solidity 0.8.24+, Foundry toolchain, OpenZeppelin base contracts.
USDC on X Layer: 0x74b7f16337b8972027f6196a17a631ac6de26d22 (6 decimals)
Deployment Order:
- AgentRegistry
- TrustScorer(agentRegistry)
- TrustGate(agentRegistry, trustScorer)
- SmartonTreasury(usdc)
- ServiceRegistry(agentRegistry, trustGate, treasury, usdc)
- Wire:
agentRegistry.setTrustScorer(address(trustScorer)) - Wire:
treasury.setServiceRegistry(address(serviceRegistry))
ERC-721 based soulbound agent identity registry.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
struct AgentInfo {
address owner; // Human owner (accountability chain)
uint8 tier; // 0=unregistered, 1=registered, 2=proven, 3=trusted
uint64 registeredAt; // Block timestamp at registration
uint64 lastActive; // Last interaction timestamp
string agentURI; // Metadata URI (capabilities, endpoints, description)
}
interface IAgentRegistry {
// Errors
error NotAgentOwner();
error AgentAlreadyRegistered();
error InvalidAgentURI();
error AgentNotFound();
error NotAuthorizedScorer();
error SoulboundTransferBlocked();
// Events
event AgentRegistered(uint256 indexed agentId, address indexed owner, string agentURI);
event AgentTierUpdated(uint256 indexed agentId, uint8 oldTier, uint8 newTier);
// Write
function registerAgent(string calldata agentURI) external returns (uint256 agentId);
function updateTier(uint256 agentId, uint8 newTier) external; // only TrustScorer
function updateLastActive(uint256 agentId) external; // only agent owner
function setTrustScorer(address scorer) external; // only contract owner
// Read
function getAgentInfo(uint256 agentId) external view returns (AgentInfo memory);
function getAgentTier(uint256 agentId) external view returns (uint8);
function getAgentByAddress(address wallet) external view returns (uint256);
function isRegistered(address wallet) external view returns (bool);
function totalAgents() external view returns (uint256);
}Storage:
uint256 private _nextAgentId = 1; // auto-increment, starts at 1
mapping(uint256 => AgentInfo) private _agents; // agentId → info
mapping(address => uint256) private _walletToAgent; // wallet → agentId (reverse lookup)
address public trustScorer; // authorized tier updaterKey Behaviors:
-
registerAgent(agentURI):- Revert if
agentURIis empty bytes →InvalidAgentURI() - Revert if
_walletToAgent[msg.sender] != 0→AgentAlreadyRegistered() - Mint ERC-721 token with
_nextAgentIdtomsg.sender - Store
AgentInfo{owner: msg.sender, tier: 1, registeredAt: uint64(block.timestamp), lastActive: uint64(block.timestamp), agentURI: agentURI} - Set
_walletToAgent[msg.sender] = _nextAgentId - Increment
_nextAgentId - Emit
AgentRegistered
- Revert if
-
updateTier(agentId, newTier):- Revert if
msg.sender != trustScorer→NotAuthorizedScorer() - Store old tier, set new tier
- Emit
AgentTierUpdated(agentId, oldTier, newTier)
- Revert if
-
Soulbound mechanism — override ERC-721
_update(to, tokenId, auth):function _update(address to, uint256 tokenId, address auth) internal override returns (address) { address from = _ownerOf(tokenId); if (from != address(0) && to != address(0)) { revert SoulboundTransferBlocked(); // block transfer, allow mint (from=0) and burn (to=0) } return super._update(to, tokenId, auth); }
-
getAgentByAddress(wallet): Returns 0 if not registered (agentIds start at 1, so 0 = not found)
On-chain trust score storage with automatic tier management.
struct TrustScore {
uint16 overall; // 0-10000 (basis points, display as 0-100.00)
uint16 tradePerformance; // 0-10000, weight 30%
uint16 securityHygiene; // 0-10000, weight 25%
uint16 peerRating; // 0-10000, weight 25%
uint16 uptime; // 0-10000, weight 20%
uint64 lastUpdated; // timestamp of last score update
uint32 totalInteractions; // lifetime interaction count (incremented each update)
}
interface ITrustScorer {
// Errors
error AgentNotRegistered();
error ScoreOutOfRange();
error CallerNotAuthorized();
error CooldownNotElapsed();
// Events
event ScoreUpdated(uint256 indexed agentId, uint16 oldScore, uint16 newScore, uint32 interactions);
event TierUpgrade(uint256 indexed agentId, uint8 oldTier, uint8 newTier);
event TierDowngrade(uint256 indexed agentId, uint8 oldTier, uint8 newTier);
// Write
function updateScore(uint256 agentId, uint16 tradePerformance, uint16 securityHygiene, uint16 peerRating, uint16 uptime) external;
function setOracle(address oracle) external;
// Read
function getScore(uint256 agentId) external view returns (TrustScore memory);
function getOverallScore(uint256 agentId) external view returns (uint16);
function getScoreHistory(uint256 agentId) external view returns (uint16[10] memory);
function computeComposite(uint16 trade, uint16 security, uint16 peer, uint16 up) external pure returns (uint16);
}Constants:
uint16 constant TIER_2_THRESHOLD = 6000; // 60.00 score
uint32 constant TIER_2_INTERACTIONS = 50;
uint16 constant TIER_3_THRESHOLD = 8500; // 85.00 score
uint32 constant TIER_3_INTERACTIONS = 500;
uint64 constant UPDATE_COOLDOWN = 300; // 5 minutes between updatesStorage:
IAgentRegistry public immutable agentRegistry;
address public oracle; // authorized score updater (backend)
mapping(uint256 => TrustScore) private _scores;
mapping(uint256 => uint16[10]) private _scoreHistory; // circular buffer
mapping(uint256 => uint8) private _historyIndex;Key Behaviors:
-
updateScore(agentId, trade, security, peer, up):- Revert if
msg.sender != oracle→CallerNotAuthorized() - Revert if any component > 10000 →
ScoreOutOfRange() - Revert if
!agentRegistry.isRegistered(ownerOf(agentId))→AgentNotRegistered() - Revert if
block.timestamp - _scores[agentId].lastUpdated < UPDATE_COOLDOWNand lastUpdated != 0 →CooldownNotElapsed() - Compute composite:
(trade * 3000 + security * 2500 + peer * 2500 + up * 2000) / 10000 - Store all components, overall, lastUpdated, increment totalInteractions
- Push overall to circular buffer:
_scoreHistory[agentId][_historyIndex[agentId] % 10] = overall - Call internal
_checkAndUpdateTier(agentId) - Emit
ScoreUpdated
- Revert if
-
_checkAndUpdateTier(agentId)(internal):- Get current tier from
agentRegistry.getAgentTier(agentId) - Determine target tier based on score + interactions:
overall >= 8500 && totalInteractions >= 500→ tier 3overall >= 6000 && totalInteractions >= 50→ tier 2- else → tier 1 (never demote below 1)
- If target != current: call
agentRegistry.updateTier(agentId, target) - Emit
TierUpgradeorTierDowngradeas appropriate
- Get current tier from
-
computeComposite: Pure function,(t*3000 + s*2500 + p*2500 + u*2000) / 10000
Lightweight read-only access control.
interface ITrustGate {
error AgentNotRegistered();
error TierTooLow(uint8 required, uint8 actual);
function checkTier(address agent, uint8 requiredTier) external view returns (bool);
function requireTier(address agent, uint8 requiredTier) external view; // reverts if not met
}Storage: IAgentRegistry and ITrustScorer references (immutable, set in constructor).
Key Behaviors:
-
checkTier(agent, requiredTier):- If
!agentRegistry.isRegistered(agent)→ return false - Get
agentId = agentRegistry.getAgentByAddress(agent) - Get
tier = agentRegistry.getAgentTier(agentId) - Return
tier >= requiredTier
- If
-
requireTier(agent, requiredTier):- Same logic but reverts with
AgentNotRegistered()orTierTooLow(required, actual)
- Same logic but reverts with
Pure reads — no state changes, minimal gas.
Marketplace — service listings, USDC escrow, peer ratings, dispute resolution.
enum ServiceStatus { Listed, Escrowed, Delivered, Completed, Disputed, Refunded }
struct ServiceListing {
uint256 serviceId;
uint256 sellerAgentId;
address sellerWallet;
string serviceType; // "signal", "analysis", "execution", "security-scan"
string description;
uint256 priceUSDC; // in USDC smallest unit (6 decimals)
uint8 minBuyerTier; // minimum tier required to purchase
bool active;
}
struct ServiceOrder {
uint256 orderId;
uint256 serviceId;
uint256 buyerAgentId;
address buyerWallet;
uint256 sellerAgentId;
address sellerWallet;
uint256 amount; // USDC escrowed
ServiceStatus status;
uint64 createdAt;
uint64 deliveredAt; // timestamp when delivered (for dispute window calc)
uint64 completedAt;
uint8 buyerRating; // 1-5 stars, 0 = not rated
string deliveryHash; // hash of delivered content
}
interface IServiceRegistry {
// Errors
error ServiceNotFound();
error InsufficientPayment();
error ServiceAlreadyCompleted();
error NotBuyer();
error NotSeller();
error BuyerCannotBeSeller();
error DisputeWindowExpired();
error DisputeAlreadyFiled();
error RatingOutOfRange();
error OrderNotInExpectedStatus(ServiceStatus expected, ServiceStatus actual);
// Events
event ServiceListed(uint256 indexed serviceId, uint256 indexed sellerAgentId, string serviceType, uint256 price);
event ServiceDelisted(uint256 indexed serviceId);
event OrderCreated(uint256 indexed orderId, uint256 indexed serviceId, uint256 indexed buyerAgentId, uint256 amount);
event ServiceDelivered(uint256 indexed orderId, string deliveryHash);
event OrderCompleted(uint256 indexed orderId, uint8 rating);
event DisputeFiled(uint256 indexed orderId, address indexed filer);
event DisputeResolved(uint256 indexed orderId, bool refunded);
// Seller functions
function listService(string calldata serviceType, string calldata description, uint256 priceUSDC, uint8 minBuyerTier) external returns (uint256 serviceId);
function delistService(uint256 serviceId) external;
function deliverService(uint256 orderId, string calldata deliveryHash) external;
// Buyer functions
function purchaseService(uint256 serviceId) external returns (uint256 orderId);
function confirmAndRate(uint256 orderId, uint8 rating) external;
function fileDispute(uint256 orderId) external;
// Admin
function resolveDispute(uint256 orderId, bool refund) external; // onlyOwner
function completeExpiredOrder(uint256 orderId) external; // anyone, after dispute window
// Read functions
function getService(uint256 serviceId) external view returns (ServiceListing memory);
function getOrder(uint256 orderId) external view returns (ServiceOrder memory);
function getActiveServices() external view returns (ServiceListing[] memory);
function getAgentServices(uint256 agentId) external view returns (ServiceListing[] memory);
function getAgentOrders(uint256 agentId) external view returns (ServiceOrder[] memory);
function getAverageRating(uint256 agentId) external view returns (uint16);
function totalServices() external view returns (uint256);
function totalOrders() external view returns (uint256);
}Constants:
uint256 constant PROTOCOL_FEE_BPS = 200; // 2%
uint256 constant RATING_INCENTIVE = 1000; // 0.001 USDC (6 decimals)
uint64 constant DISPUTE_WINDOW = 3600; // 1 hour after deliveryStorage:
IAgentRegistry public immutable agentRegistry;
ITrustGate public immutable trustGate;
SmartonTreasury public immutable treasury;
IERC20 public immutable usdc;
uint256 private _nextServiceId = 1;
uint256 private _nextOrderId = 1;
mapping(uint256 => ServiceListing) private _services;
mapping(uint256 => ServiceOrder) private _orders;
mapping(uint256 => uint256[]) private _agentServices; // agentId → serviceId[]
mapping(uint256 => uint256[]) private _agentOrders; // agentId → orderId[] (as buyer or seller)Key Behaviors:
-
listService(serviceType, description, priceUSDC, minBuyerTier):- Require
msg.senderis registered agent - Create
ServiceListing, store, emitServiceListed
- Require
-
purchaseService(serviceId)— nonReentrant:- Require service exists and active
- Require buyer is registered agent
trustGate.requireTier(msg.sender, service.minBuyerTier)- Require
msg.sender != service.sellerWallet→BuyerCannotBeSeller() usdc.transferFrom(msg.sender, address(this), service.priceUSDC)— escrow- Create
ServiceOrderwith statusEscrowed - Emit
OrderCreated
-
deliverService(orderId, deliveryHash):- Require
msg.sender == order.sellerWallet - Require status ==
Escrowed - Set status =
Delivered, store deliveryHash, setdeliveredAt = block.timestamp - Emit
ServiceDelivered
- Require
-
confirmAndRate(orderId, rating)— nonReentrant:- Require
msg.sender == order.buyerWallet - Require status ==
Delivered - Require
rating >= 1 && rating <= 5 - Calculate fee:
amount * PROTOCOL_FEE_BPS / 10000 - Transfer
(amount - fee)USDC to seller - Transfer
feeUSDC to treasury address - Treasury pays
RATING_INCENTIVEUSDC to buyer (if treasury has sufficient balance) - Set status =
Completed, store rating +completedAt - Emit
OrderCompleted
- Require
-
fileDispute(orderId):- Require
msg.sender == order.buyerWallet - Require status ==
Delivered - Require
block.timestamp - order.deliveredAt <= DISPUTE_WINDOW - Set status =
Disputed - Emit
DisputeFiled
- Require
-
resolveDispute(orderId, refund)— onlyOwner:- Require status ==
Disputed - If refund: transfer escrowed USDC back to buyer, status =
Refunded - If !refund: release to seller minus fee (same as confirmAndRate flow), status =
Completed
- Require status ==
-
completeExpiredOrder(orderId):- Require status ==
Delivered - Require
block.timestamp - order.deliveredAt > DISPUTE_WINDOW - Release to seller minus fee, status =
Completed, rating = 5 (default) - Anyone can call this (incentive: no gas cost on X Layer)
- Require status ==
-
getAverageRating(agentId): Iterate seller's completed orders, compute mean of non-zero ratings × 100 (returns 100-500 for 1.00-5.00)
Protocol fee collector + rating incentive distributor.
interface ISmartonTreasury {
error NotAuthorizedCaller();
error InsufficientBalance();
event FeeCollected(uint256 amount);
event IncentivePaid(address indexed rater, uint256 amount);
event FundsWithdrawn(address indexed to, uint256 amount);
function collectFee(uint256 amount) external; // called by ServiceRegistry
function payRatingIncentive(address rater) external; // called by ServiceRegistry
function withdraw(address to, uint256 amount) external; // only owner
function getBalance() external view returns (uint256);
}Storage:
IERC20 public immutable usdc;
address public serviceRegistry; // authorized caller, set via setServiceRegistry (onlyOwner)
uint256 constant RATING_INCENTIVE = 1000; // 0.001 USDCKey Behaviors:
collectFee(amount): Called by ServiceRegistry after order completion. The fee USDC is transferred directly from ServiceRegistry to treasury address (ServiceRegistry does theusdc.transfer). Treasury just emits event for tracking.payRatingIncentive(rater): TransferRATING_INCENTIVEUSDC from treasury to rater. Silently skip if insufficient balance (don't revert — incentive is optional).withdraw(to, amount): onlyOwner. Transfer USDC to recipient.getBalance():usdc.balanceOf(address(this))
Simplification for hackathon: The fee transfer flow is kept simple. ServiceRegistry transfers fee USDC directly to treasury address. Treasury's collectFee just emits the tracking event. This avoids complex approval chains.
| Contract | Test File | Test Count |
|---|---|---|
| AgentRegistry | AgentRegistry.t.sol | 8 |
| TrustScorer | TrustScorer.t.sol | 10 |
| TrustGate | TrustGate.t.sol | (included in AgentRegistry tests) |
| ServiceRegistry | ServiceRegistry.t.sol | 12 |
| Integration | Integration.t.sol | 1 |
| Total | ~31 |
- USDC address:
0x74b7f16337b8972027f6196a17a631ac6de26d22 - Use
IERC20interface for all USDC interactions - Gas costs are near-zero on X Layer (~0.001 OKB per tx)
- Block confirmations are fast (~3 seconds)
- For Foundry deployment:
--rpc-url https://rpc.xlayer.tech - X Layer explorer for verification:
https://www.okx.com/explorer/xlayer - Note:
forge verify-contractmay not work with X Layer explorer. Use--verifyflag in deploy script and if it fails, skip verification (manual verification via explorer UI).