Settlement layer
The contract suite.
Two contracts and one deploy script. The registry holds attested scores and their evidence hashes; the pool prices credit straight off the registry, so nothing about your reputation depends on this website staying online.
Chain
Robinhood Chain · 4663
RPC
https://rpc.mainnet.chain.robinhood.com
Explorer
https://robinhoodchain.blockscout.com
Testnet
46630 · https://rpc.testnet.chain.robinhood.com
Attestations bound to a SIWE-verified subject wallet + credit limit, freeze/revocation, tier terms, single-call creditFor() for pools.
src/CreditRegistry.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
/// @title VouchX Credit Registry
/// @notice Non-transferable, revocable reputation attestations for X handles.
/// @dev Handles are hashed off-chain with keccak256(bytes(lowercaseHandle)) so no PII is stored on-chain.
/// Each attestation binds a handle to a single subject wallet (the SIWE-verified
/// owner) and stores its integrity-scaled credit limit, so a lending pool can
/// enforce BOTH "only the real owner can borrow" and "borrow up to the real line".
contract CreditRegistry {
struct Attestation {
uint16 score; // 0-1000 composite
uint8 tier; // derived band
uint64 updatedAt;
uint64 expiresAt;
bytes32 evidenceHash;
address attestor;
address subject; // wallet authorized to borrow against this handle
uint256 limit; // attested credit ceiling (reference denomination)
bool frozen;
}
uint8 public constant TIER_BLOCKED = 0;
uint8 public constant TIER_WATCHLIST = 1;
uint8 public constant TIER_BRONZE = 2;
uint8 public constant TIER_SILVER = 3;
uint8 public constant TIER_GOLD = 4;
uint8 public constant TIER_PRIME = 5;
address public owner;
mapping(address => bool) public isAttestor;
mapping(bytes32 => Attestation) internal _attestations;
mapping(uint8 => uint256) public tierLimit; // tier ceiling, reference denomination
mapping(uint8 => uint16) public tierAprBps; // basis points
event Attested(bytes32 indexed handleHash, uint16 score, uint8 tier, bytes32 evidenceHash, uint64 expiresAt, address indexed subject, address attestor);
event Frozen(bytes32 indexed handleHash, string reason);
event Unfrozen(bytes32 indexed handleHash);
event AttestorUpdated(address indexed attestor, bool allowed);
event TermsUpdated(uint8 indexed tier, uint256 limit, uint16 aprBps);
event OwnershipTransferred(address indexed from, address indexed to);
error Unauthorized();
error ScoreOutOfRange();
error InvalidTtl();
error ZeroAddress();
error ExceedsTier();
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized();
_;
}
modifier onlyAttestor() {
if (!isAttestor[msg.sender]) revert Unauthorized();
_;
}
constructor(address owner_) {
if (owner_ == address(0)) revert ZeroAddress();
owner = owner_;
isAttestor[owner_] = true;
_setTerms(TIER_BLOCKED, 0, 0);
_setTerms(TIER_WATCHLIST, 250, 2400);
_setTerms(TIER_BRONZE, 1500, 1800);
_setTerms(TIER_SILVER, 5000, 1250);
_setTerms(TIER_GOLD, 12000, 820);
_setTerms(TIER_PRIME, 25000, 540);
emit AttestorUpdated(owner_, true);
}
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert ZeroAddress();
emit OwnershipTransferred(owner, newOwner);
isAttestor[owner] = false; // strip the outgoing owner of attest power
owner = newOwner;
isAttestor[newOwner] = true;
}
function setAttestor(address attestor, bool allowed) external onlyOwner {
isAttestor[attestor] = allowed;
emit AttestorUpdated(attestor, allowed);
}
function setTerms(uint8 tier, uint256 limit, uint16 aprBps) external onlyOwner {
_setTerms(tier, limit, aprBps);
}
function _setTerms(uint8 tier, uint256 limit, uint16 aprBps) internal {
tierLimit[tier] = limit;
tierAprBps[tier] = aprBps;
emit TermsUpdated(tier, limit, aprBps);
}
/// @notice Publish a score and bind it to the SIWE-verified subject wallet and
/// the integrity-scaled limit the agent computed off-chain. Only allowlisted
/// agent signers may call this.
function attest(
bytes32 handleHash,
uint16 score,
bytes32 evidenceHash,
uint64 ttl,
address subject,
uint256 limit
) external onlyAttestor {
if (score > 1000) revert ScoreOutOfRange();
if (ttl == 0 || ttl > 365 days) revert InvalidTtl();
if (subject == address(0)) revert ZeroAddress();
uint8 tier = tierFor(score);
// The per-handle limit can never exceed the tier ceiling, so a rogue or
// compromised attestor cannot mint a larger line than the tier allows.
if (limit > tierLimit[tier]) revert ExceedsTier();
_attestations[handleHash] = Attestation({
score: score,
tier: tier,
updatedAt: uint64(block.timestamp),
expiresAt: uint64(block.timestamp) + ttl,
evidenceHash: evidenceHash,
attestor: msg.sender,
subject: subject,
limit: limit,
frozen: false
});
emit Attested(handleHash, score, tier, evidenceHash, uint64(block.timestamp) + ttl, subject, msg.sender);
}
function freeze(bytes32 handleHash, string calldata reason) external onlyAttestor {
_attestations[handleHash].frozen = true;
emit Frozen(handleHash, reason);
}
function unfreeze(bytes32 handleHash) external onlyAttestor {
_attestations[handleHash].frozen = false;
emit Unfrozen(handleHash);
}
function tierFor(uint16 score) public pure returns (uint8) {
if (score >= 850) return TIER_PRIME;
if (score >= 720) return TIER_GOLD;
if (score >= 600) return TIER_SILVER;
if (score >= 450) return TIER_BRONZE;
if (score >= 300) return TIER_WATCHLIST;
return TIER_BLOCKED;
}
function getAttestation(bytes32 handleHash) external view returns (Attestation memory) {
return _attestations[handleHash];
}
/// @notice The wallet authorized to draw credit against this handle.
function subjectOf(bytes32 handleHash) external view returns (address) {
return _attestations[handleHash].subject;
}
/// @notice Frozen or expired attestations price at zero credit.
function effectiveScore(bytes32 handleHash) public view returns (uint16) {
Attestation memory record = _attestations[handleHash];
if (record.frozen || record.expiresAt <= block.timestamp) return 0;
return record.score;
}
/// @notice Single call a lending pool needs: per-handle limit, rate, tier, usable.
/// @dev The pool uses tier + aprBps + usable to gate borrowing; the per-handle
/// limit is the attested reference capacity (the ETH pool enforces its own
/// per-tier ETH cap, since converting USD capacity to ETH needs an oracle).
function creditFor(bytes32 handleHash)
external
view
returns (uint256 limit, uint16 aprBps, uint8 tier, bool usable)
{
Attestation memory record = _attestations[handleHash];
uint16 score = effectiveScore(handleHash);
if (score == 0 || record.limit == 0) return (0, 0, record.tier, false);
uint8 derived = tierFor(score);
return (record.limit, tierAprBps[derived], derived, true);
}
}
Deploy runbook
The sources compile with solc 0.8.24. Foundry is the shortest path to bytecode and a broadcast transaction; the four files above are the whole project.
01Initialise the project
mkdir vouchx-contracts && cd vouchx-contracts forge init --force . && rm -f src/Counter.sol test/Counter.t.sol script/Counter.s.sol
02Add the four files below
# paste CreditRegistry.sol and CreditPool.sol into src/ # paste Deploy.s.sol into script/ and foundry.toml at the root
03Compile
forge build --sizes
04Deploy to chain 4663
export ATTESTOR_ADDRESS=0x<agent-signer> export DEPLOYER_PRIVATE_KEY=0x<key> export VOUCHX_INITIAL_SUPPLY=100000000000000000000000 forge script script/Deploy.s.sol:Deploy --rpc-url https://rpc.mainnet.chain.robinhood.com --broadcast -vv # then set the printed VouchXToken address as the app's vouchxToken
These contracts are unaudited reference implementations. Test on chain 46630 first, and have the pool reviewed before it holds user funds.