Skip to main content

What are Resolvers?

Resolvers are smart contracts that define custom validation rules for attestations. When you attach a resolver to a schema, every attestation must pass through the resolver’s validation logic before being created.
Schemas and attestations are permissionless by default. Resolvers are how you add access control, fees, or custom rules when you need them.

Why Use Resolvers?

By default, anyone can attest to any schema. This permissionless model is intentional — it enables open, composable attestations. But sometimes you need control: Resolvers let you:
  • Control access — Only allow specific addresses to attest
  • Collect fees — Require payment before attestation creation
  • Distribute rewards — Incentivize attestation with token rewards
  • Enforce rules — Implement custom business logic
┌────────────────────────────────────────────────────────────────┐
│              WITHOUT RESOLVER (Permissionless)                 │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   Anyone ──► Attest to Any Schema ──► ✅ Attestation Created   │
│                                                                │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│               WITH RESOLVER (Access Control)                   │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   1. Attestation Request                                       │
│          │                                                     │
│          ▼                                                     │
│   2. Resolver.onattest()  ──── Validation ────┐                │
│          │                                    │                │
│          ▼ (if approved)                      ▼ (if rejected)  │
│   3. Store Attestation                   ❌ Error              │
│          │                                                     │
│          ▼                                                     │
│   4. Resolver.onresolve()  ──── Post-processing                │
│          │                                                     │
│          ▼                                                     │
│   ✅ Attestation Created                                       │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Resolver Interface

All resolvers implement the same interface, making them pluggable and interchangeable:
pub trait ResolverInterface {
    // Validate before attestation creation
    fn onattest(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError>;

    // Validate before attestation revocation
    fn onrevoke(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError>;

    // Post-processing callback (after creation/revocation)
    fn onresolve(env: Env, attestation_uid: BytesN<32>, attester: Address) -> Result<(), ResolverError>;

    // Resolver metadata for discovery
    fn metadata(env: Env) -> ResolverMetadata;
}

Hook Functions

FunctionWhen CalledPurposeCritical?
onattestBefore attestation creationValidate and gate accessYes
onrevokeBefore attestation revocationValidate revocation permissionYes
onresolveAfter creation/revocationPost-processing (rewards, cleanup)No
metadataOn queryReturn resolver informationNo
Critical vs Non-Critical: If onattest or onrevoke fails, the operation is aborted. If onresolve fails, the attestation still succeeds — it’s for side effects only.

Attestation Data

Resolvers receive complete attestation data for validation:
pub struct ResolverAttestationData {
    pub uid: BytesN<32>,           // Unique attestation ID
    pub schema_uid: BytesN<32>,    // Associated schema
    pub recipient: Address,        // Who is being attested about
    pub attester: Address,         // Who is attesting
    pub time: u64,                 // Creation timestamp
    pub expiration_time: u64,      // When it expires (0 = never)
    pub revocation_time: u64,      // When revoked (0 = not revoked)
    pub revocable: bool,           // Can be revoked?
    pub ref_uid: Bytes,            // Reference to another attestation
    pub data: Bytes,               // Encoded attestation data
    pub value: i128,               // Optional payment value
}

Resolver Types

Default Resolver

The simplest resolver — validates basic rules without economic requirements.
impl ResolverInterface for DefaultResolver {
    fn onattest(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        // Require attester authorization
        attestation.attester.require_auth();

        // Prevent self-attestation
        if attestation.attester == attestation.recipient {
            return Err(ResolverError::ValidationFailed);
        }

        // Validate expiration
        if attestation.expiration_time > 0
            && attestation.expiration_time < env.ledger().timestamp() {
            return Err(ResolverError::InvalidAttestation);
        }

        Ok(true)
    }
}
Use cases:
  • Development and testing
  • Schemas without special requirements
  • Base implementation for custom resolvers

Fee Collection Resolver

Requires payment before attestation creation.
impl ResolverInterface for FeeCollectionResolver {
    fn onattest(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        attestation.attester.require_auth();

        // Check payment amount
        let required_fee = get_attestation_fee(&env);
        if attestation.value < required_fee {
            return Err(ResolverError::InsufficientFunds);
        }

        // Collect the fee
        collect_fee(&env, &attestation.attester, attestation.value)?;

        Ok(true)
    }
}
Use cases:
  • Monetizing attestation services
  • Spam prevention through economic cost
  • Revenue generation for attestation providers

Token Reward Resolver

Distributes token rewards for attestation creation.
impl ResolverInterface for TokenRewardResolver {
    fn onattest(_env: Env, _attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        // Permissionless - gas cost provides spam resistance
        Ok(true)
    }

    fn onresolve(env: Env, _attestation_uid: BytesN<32>, attester: Address) -> Result<(), ResolverError> {
        // Distribute reward after attestation
        let reward_amount = get_reward_amount(&env);
        distribute_reward(&env, &attester, reward_amount)?;
        Ok(())
    }
}
Use cases:
  • Incentivizing attestation creation
  • Community engagement programs
  • Protocol growth mechanics

Authority Resolver

Permission-based access control with verification.
impl ResolverInterface for AuthorityResolver {
    fn onattest(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        attestation.attester.require_auth();

        // Check if attester is a registered authority
        if !is_registered_authority(&env, &attestation.attester) {
            return Err(ResolverError::NotAuthorized);
        }

        Ok(true)
    }
}
Use cases:
  • Restricted attestation environments
  • Verified issuer programs
  • Credentialed attestation systems

Creating a Custom Resolver

Step 1: Implement the Interface

use soroban_sdk::{contract, contractimpl, Env, Address, BytesN, String};
use resolvers::{ResolverInterface, ResolverAttestationData, ResolverError, ResolverMetadata, ResolverType};

#[contract]
pub struct MyCustomResolver;

#[contractimpl]
impl ResolverInterface for MyCustomResolver {
    fn onattest(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        // Your validation logic here
        attestation.attester.require_auth();

        // Example: Only allow attestations on weekdays
        // (This is just an example - implement your own logic)

        Ok(true)
    }

    fn onrevoke(env: Env, attestation: ResolverAttestationData) -> Result<bool, ResolverError> {
        attestation.attester.require_auth();

        if !attestation.revocable {
            return Err(ResolverError::ValidationFailed);
        }

        Ok(true)
    }

    fn onresolve(_env: Env, _attestation_uid: BytesN<32>, _attester: Address) -> Result<(), ResolverError> {
        // Optional post-processing
        Ok(())
    }

    fn metadata(env: Env) -> ResolverMetadata {
        ResolverMetadata {
            name: String::from_str(&env, "My Custom Resolver"),
            version: String::from_str(&env, "1.0.0"),
            description: String::from_str(&env, "Custom validation for my use case"),
            resolver_type: ResolverType::Custom,
        }
    }
}

Step 2: Build the Contract

cargo build --target wasm32v1-none --release

Step 3: Deploy to Stellar

stellar contract deploy \
  --wasm target/wasm32v1-none/release/my_resolver.wasm \
  --source YOUR_IDENTITY \
  --network testnet

Step 4: Attach to a Schema

const schemaUid = await client.createSchema({
  definition: 'string credential, uint64 issuedAt',
  resolver: 'CRESOLVER...', // Your deployed resolver address
  revocable: true
});

Binding Resolvers to Schemas

When you create a schema, you can optionally attach a resolver:
import { StellarAttestationClient } from '@attestprotocol/stellar-sdk';

const client = new StellarAttestationClient({ network: 'testnet' });

// Schema with a resolver
const schemaWithResolver = await client.createSchema({
  definition: 'string credential, bool verified',
  resolver: 'CRESOLVER...', // Resolver contract address
  revocable: true
});

// Schema without a resolver (permissionless - anyone can attest)
const schemaWithoutResolver = await client.createSchema({
  definition: 'string note',
  resolver: null, // No resolver = anyone can attest
  revocable: false
});
Once a schema is created, its resolver cannot be changed. Choose your resolver carefully before deploying to production.

Error Handling

Resolvers use a standard error enum:
#[contracterror]
pub enum ResolverError {
    NotAuthorized = 1,      // Caller lacks permission
    InvalidAttestation = 2, // Attestation data is invalid
    InvalidSchema = 3,      // Schema mismatch
    InsufficientFunds = 4,  // Payment too low
    TokenTransferFailed = 5,// Token operation failed
    StakeRequired = 6,      // Staking requirement not met
    ValidationFailed = 7,   // Generic validation failure
    CustomError = 8,        // Custom error condition
}

Security Considerations

Resolver bugs can lead to unauthorized attestations or denial of service. Test thoroughly before production deployment.

Best Practices

  1. Always require auth — Call require_auth() on the attester
  2. Validate all inputs — Don’t trust data from the protocol
  3. Handle failures gracefully — Return proper error codes
  4. Avoid state dependencies — Don’t rely on external state that can be manipulated
  5. Test edge cases — Expired attestations, revocations, etc.

Common Pitfalls

PitfallIssueSolution
Missing auth checkAnyone can attestAlways call require_auth()
Self-attestationUsers attest to themselvesCheck attester != recipient
Expired dataUsing outdated timestampsValidate against current time
ReentrancyToken callbacksUse checks-effects-interactions pattern

Testing Resolvers

#[test]
fn test_reject_self_attestation() {
    let (env, resolver) = setup();
    let user = Address::generate(&env);

    let attestation = build_attestation(&env, &user, &user, 0);

    let result = resolver.try_onattest(&attestation);
    assert!(matches!(result.err(), Some(Ok(ResolverError::ValidationFailed))));
}

#[test]
fn test_accept_valid_attestation() {
    let (env, resolver) = setup();
    let attester = Address::generate(&env);
    let recipient = Address::generate(&env);

    let attestation = build_attestation(&env, &attester, &recipient, 0);

    assert!(resolver.onattest(&attestation));
}

Built-in Resolvers

The protocol provides pre-built resolvers you can use:
ResolverPurposeBuild Command
DefaultBasic validation--features export-default-resolver
Token RewardDistribute rewards--features export-token-reward-resolver
Fee CollectionCollect fees--features export-fee-collection-resolver
# Build the default resolver
cargo build --target wasm32v1-none --release --features export-default-resolver

# Deploy
stellar contract deploy \
  --wasm target/wasm32v1-none/release/resolvers.wasm \
  --source YOUR_IDENTITY \
  --network testnet

Next Steps