DEV Community

rayQu
rayQu

Posted on

Building a Confidential Cross-Chain Identity Protocol with Oasis Sapphire, ENS & Base

The biggest paradox in Web3 identity is that while we demand privacy in the real world, our on-chain infrastructure defaults to absolute transparency. If you attach a KYC credential, credit score, or real name to your ENS domain, it is immediately indexable by anyone.

But what if you could store your sensitive Personally Identifiable Information (PII) inside a secure enclave, and only expose cryptographic proofs of that data to smart contracts on other chains?

In this advanced tutorial, we are building a Confidential Cross-Chain Identity Layer. We will use Oasis Sapphire (the first confidential EVM) as our secure vault, Base (Ethereum L2) as our execution environment, and the Oasis Privacy Layer (OPL) via Celer IM to bridge the two.


The Real-World Use Case: Compliant DeFi on Base

Imagine a DeFi lending protocol on Base that requires users to be non-US citizens and over 18 to access leverage.

  • The Problem: Storing "Country" and "Date of Birth" directly on Base violates privacy and data protection laws (like GDPR).
  • The Solution: The user mints an ENS name (alice.base.eth) on Base. They store their private KYC data in a smart contract on Oasis Sapphire. When the Base DeFi protocol queries the ENS resolver, the resolver triggers a cross-chain message via OPL to Sapphire. Sapphire computes the logic inside a Trusted Execution Environment (TEE), checking if age > 18 && country != US, and returns a simple true or false back to Base.

No PII is ever leaked, the public RPCs see nothing but ciphertext, and the DeFi protocol remains fully compliant.


Architecture & Interaction Flow

We will utilize EIP-3668 (CCIP-Read) for off-chain data resolution and Celer Inter-Chain Messaging (IM) for state synchronization between Base and Sapphire.

sequenceDiagram
    participant User
    participant DeFi as Base DeFi Protocol
    participant Resolver as Base ENS Resolver
    participant Relayer as Celer IM / OPL
    participant Sapphire as Oasis Sapphire (TEE)

    User->>Sapphire: 1. Encrypt & Store KYC (Age, Country)
    Note over Sapphire: State is encrypted end-to-end
    User->>DeFi: 2. Request Undercollateralized Loan
    DeFi->>Resolver: 3. Verify compliance for `alice.base.eth`
    Resolver->>Relayer: 4. Dispatch Cross-Chain Request
    Relayer->>Sapphire: 5. Execute query inside TEE
    Sapphire-->>Sapphire: 6. Decrypt -> Check logic -> Re-encrypt
    Sapphire->>Relayer: 7. Emit boolean compliance proof
    Relayer->>Resolver: 8. CCIP-Read Callback with Proof
    Resolver-->>DeFi: 9. Returns `true` (Compliant)
    DeFi-->>User: 10. Loan Approved
Enter fullscreen mode Exit fullscreen mode

Repository Structure

For this advanced stack, we will use a monorepo built with Hardhat/Foundry to manage multi-chain deployments.

cross-chain-identity/
├── contracts/
│   ├── base/
│   │   ├── ENSIdentityResolver.sol     # Custom ENS Resolver on Base
│   │   └── IDeFiProtocol.sol           # Mock DeFi contract
│   ├── sapphire/
│   │   ├── ConfidentialVault.sol       # TEE-encrypted storage contract
│   │   └── VerificationEngine.sol      # Logic computation 
│   └── interfaces/
│       ├── ICelerMessageBus.sol        # Celer IM interface
│       └── IOasisPrivacyLayer.sol
├── scripts/
│   ├── deploy-base.js
│   ├── deploy-sapphire.js
│   └── bridge-setup.js
├── hardhat.config.js                   # Configured for Base & Sapphire networks
└── package.json
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Implementation

Step 1: The Confidential Vault on Oasis Sapphire
Oasis Sapphire is EVM-compatible but runs inside secure enclaves. State variables are encrypted by default. We will write ConfidentialVault.sol to store user data securely.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";

contract ConfidentialVault {
    // This state is encrypted inside the Sapphire TEE
    struct IdentityRecord {
        uint256 dateOfBirth;
        string countryCode;
        bool isVerified;
    }

    mapping(address => IdentityRecord) private privateIdentities;

    // Address of the Celer Message Bus on Sapphire
    address public messageBus;

    constructor(address _messageBus) {
        messageBus = _messageBus;
    }

    /**
     * @dev User updates their private data. Transactions are end-to-end encrypted.
     */
    function updateIdentity(uint256 _dob, string calldata _countryCode) external {
        privateIdentities[msg.sender] = IdentityRecord({
            dateOfBirth: _dob,
            countryCode: _countryCode,
            isVerified: true
        });
    }

    /**
     * @dev Executed via cross-chain message from Base.
     * Computes compliance without revealing raw data.
     */
    function verifyComplianceCrossChain(address _user, address _baseCallbackContract) external {
        require(msg.sender == messageBus, "Only Relayer");

        IdentityRecord memory record = privateIdentities[_user];

        // Confidential computation: Is over 18 and not US?
        bool isCompliant = false;
        if (record.isVerified) {
            uint256 age = (block.timestamp - record.dateOfBirth) / 31556926; // Approx years
            if (age >= 18 && keccak256(bytes(record.countryCode)) != keccak256(bytes("US"))) {
                isCompliant = true;
            }
        }

        // Construct payload to send back to Base
        bytes memory message = abi.encode(_user, isCompliant);

        // Send via Celer IM (Oasis Privacy Layer) back to Base
        // (Pseudocode for Celer IM integration)
        ICelerMessageBus(messageBus).sendMessage(
            _baseCallbackContract,
            8453, // Base Mainnet Chain ID
            message
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: Because Sapphire encrypts calldata, an observer on block explorers will only see gibberish bytes, completely protecting the user's DoB and Country.

Step 2: The Cross-Chain ENS Resolver on Base
On Base, our custom ENS Resolver implements executeWithCCIPRead to pause execution, request data from Sapphire, and resume via a callback once Celer IM delivers the result.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol";

contract ENSIdentityResolver {
    address public messageBus;
    address public sapphireVaultAddress;

    mapping(address => bool) public complianceCache;

    error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData);

    constructor(address _messageBus, address _sapphireVaultAddress) {
        messageBus = _messageBus;
        sapphireVaultAddress = _sapphireVaultAddress;
    }

    /**
     * @dev Standard check called by DeFi protocols on Base.
     */
    function isUserCompliant(address _user) external view returns (bool) {
        // If not in cache, trigger CCIP-Read / Cross-chain flow
        if (!complianceCache[_user]) {
            string[] memory urls = new string[](1);
            urls[0] = "[https://our-relayer-gateway.io/verify/](https://our-relayer-gateway.io/verify/){sender}/{data}";

            revert OffchainLookup(
                address(this),
                urls,
                abi.encodePacked(_user),
                this.celfCallback.selector,
                abi.encode(_user)
            );
        }
        return complianceCache[_user];
    }

    /**
     * @dev Callback executed by Celer Executor once Sapphire computes the result.
     */
    function executeMessage(
        address _srcContract,
        uint64 _srcChainId,
        bytes calldata _message,
        address _executor
    ) external payable returns (uint256) {
        require(msg.sender == messageBus, "Only Celer Message Bus");
        require(_srcChainId == 23294, "Only Sapphire Mainnet"); // Sapphire Chain ID
        require(_srcContract == sapphireVaultAddress, "Invalid source contract");

        (address user, bool isCompliant) = abi.decode(_message, (address, bool));

        // Cache the confidential state on Base
        complianceCache[user] = isCompliant;

        return 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Hardhat Deployment & Orchestration
To deploy this, your hardhat.config.js needs RPC endpoints for both networks. Oasis Sapphire provides standard Web3 RPC endpoints, making the developer experience seamless.

module.exports = {
  solidity: "0.8.19",
  networks: {
    base: {
      url: "[https://mainnet.base.org](https://mainnet.base.org)",
      accounts: [process.env.PRIVATE_KEY]
    },
    sapphire: {
      url: "[https://sapphire.oasis.io](https://sapphire.oasis.io)",
      chainId: 23294,
      accounts: [process.env.PRIVATE_KEY]
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

When integrating the frontend, use the @oasisprotocol/sapphire-paratime wrapper for Ethers.js. This automatically encrypts your transactions on the client side before sending them to the Sapphire network.

import { ethers } from "ethers";
import * as sapphire from "@oasisprotocol/sapphire-paratime";

// Wrap your standard signer to enable end-to-end encryption
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = sapphire.wrap(provider.getSigner());

// Now call the vault; the calldata will be automatically encrypted!
const vaultContract = new ethers.Contract(VAULT_ADDR, VAULT_ABI, signer);
await vaultContract.updateIdentity(946684800, "UK");
Enter fullscreen mode Exit fullscreen mode

Step 4: Building the CCIP-Read Off-Chain Gateway (Node.js)
When the Base contract reverts with OffchainLookup, the user's wallet (or the dApp frontend) will automatically send an HTTP GET request to the URL specified in the revert. We need an Express server to catch this, trigger the Celer IM message to Sapphire, and manage the asynchronous state.

// server.js (Gateway)
const express = require('express');
const { ethers } = require('ethers');

const app = express();
app.use(express.json());

app.get('/verify/:sender/:data', async (req, res) => {
    const { sender, data } = req.params;

    try {
        // Decode the user address from the original call data
        const decodedUser = ethers.utils.defaultAbiCoder.decode(['address'], data)[0];

        // 1. Trigger the cross-chain message on the Relayer/Sapphire side
        // In a production environment, this triggers a relayer bot to pay gas on Sapphire
        await dispatchToSapphire(decodedUser);

        // 2. We return a pending state or an acknowledgment 
        // Note: Because Celer IM is asynchronous, the Base contract will receive 
        // the actual boolean value from the Celer Executor in a subsequent block.
        res.status(200).json({
            status: "pending_cross_chain_execution",
            message: "Celer IM dispatched to Oasis Sapphire. Await callback on Base."
        });

    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.listen(3000, () => console.log('CCIP-Read Gateway running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Step 5: Gas Abstraction (Zero-Gas UX on Sapphire)
A major UX hurdle in cross-chain architectures is forcing the user to hold the native gas token (ROSE) on Oasis Sapphire just to update their identity.

To solve this, we can implement the Oasis Gas Station Network (GSN) or a custom Paymaster. Because Sapphire supports EIP-4337 (Account Abstraction) and standard meta-transactions, you can set up a Gas Relayer. The user signs an EIP-712 message containing their encrypted KYC data, and your backend submits it to Sapphire, paying the ROSE gas fee on their behalf. The user only ever needs ETH on Base.

Step 6: Security & Threat Model
When dealing with cross-chain PII, the threat model extends beyond standard smart contract bugs:

  • State Desyncs: If a user updates their age on Sapphire but the Base cache isn't refreshed, the Base DeFi protocol might operate on stale data. Always implement a Time-to-Live (TTL) or an expiration timestamp on the complianceCache mapping on Base.

  • Replay Attacks: Ensure the Celer IM callback (executeMessage) strictly verifies _srcChainId and _srcContract. If an attacker can spoof the message origin, they can arbitrarily set their complianceCache to true on Base.

  • TEE Side-Channels: While Sapphire's Secure Enclaves (Intel SGX) protect data in use, developers must ensure they do not leak data via access patterns. Avoid writing logic that significantly changes gas consumption based on the value of a private variable.

Conclusion

By combining ENS on Base with the Oasis Privacy Layer, we've built a robust architecture that solves Web3's biggest compliance hurdle. Smart contracts on L2s can now leverage confidential logic, request attestations, and maintain composability without doxxing their users.

The future of Web3 identity isn't fully transparent, nor is it siloed in isolated private chains. The future is modular, cross-chain, and selectively private.

If you found this advanced tutorial helpful, leave a comment below or check out the Oasis Privacy Layer Documentation to dive deeper into confidential smart contracts!

Top comments (0)