Swap ERC7984 to ERC20

🔴 Advanced | 🏗️ OpenZeppelin Contracts

Atomic swap between confidential ERC7984 and standard ERC20 tokens

Overview

This example implements an atomic swap contract that allows users to exchange confidential ERC7984 tokens for standard ERC20 tokens. The swap amount remains confidential while the exchange rate is public. Useful for DEX integrations and bridging between confidential and public token systems.

Quick Start

# Create new project from this template
npx labz create swap-erc7984-to-erc20 my-project

# Navigate and install
cd my-project
npm install

# Run tests
npx hardhat test

Contract

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.27;

import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC7984} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @title SwapERC7984ToERC20 - Swap Confidential Tokens for Public Tokens
/// @notice Allows swapping ERC7984 (confidential) tokens for ERC20 (public) tokens
/// @dev Two-phase swap: user deposits confidential tokens, admin releases public tokens
contract SwapERC7984ToERC20 is ZamaEthereumConfig, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ============ Errors ============

    error SwapNotFound();
    error SwapAlreadyProcessed();
    error InvalidRate();
    error InsufficientLiquidity(uint256 requested, uint256 available);
    error NotSwapOwner();
    error DecryptionAlreadyRequested();
    error SwapAlreadyProcessing();

    // ============ Events ============

    /// @notice Emitted when a swap is initiated
    event SwapInitiated(
        uint256 indexed swapId,
        address indexed user,
        address confidentialToken,
        address publicToken
    );

    /// @notice Emitted when a swap is completed by admin
    event SwapCompleted(uint256 indexed swapId, address indexed user, uint64 amount);

    /// @notice Emitted when a swap is cancelled
    event SwapCancelled(uint256 indexed swapId);

    /// @notice Emitted when decryption is requested
    event DecryptionRequested(uint256 indexed swapId);

    /// @notice Emitted when liquidity is added
    event LiquidityAdded(address indexed provider, uint256 amount);

    // ============ Structs ============

    struct SwapRequest {
        address user;
        IERC7984 confidentialToken;
        IERC20 publicToken;
        euint64 encryptedAmount;
        uint256 rate;
        bool completed;
        bool cancelled;
        bool decryptionRequested;
    }

    // ============ State ============

    /// @dev All swap requests
    SwapRequest[] private _swaps;

    /// @dev Mapping from user to their swap IDs
    mapping(address => uint256[]) private _userSwaps;

    // ============ Constructor ============

    constructor() Ownable(msg.sender) {}

    // ============ Swap Functions ============

    /// @notice Initiate a swap from ERC7984 to ERC20
    /// @param confidentialToken The ERC7984 token to swap from
    /// @param publicToken The ERC20 token to swap to
    /// @param encryptedAmount Encrypted amount to swap
    /// @param inputProof Proof for the encrypted input
    /// @param rate Exchange rate (1e6 = 1:1)
    /// @return swapId The swap request ID
    function initiateSwap(
        address confidentialToken,
        address publicToken,
        externalEuint64 encryptedAmount,
        bytes calldata inputProof,
        uint256 rate
    ) external nonReentrant returns (uint256 swapId) {
        if (rate == 0) revert InvalidRate();

        euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);

        // Transfer confidential tokens to this contract
        IERC7984 confToken = IERC7984(confidentialToken);
        FHE.allowTransient(amount, confidentialToken);
        euint64 amountReceived = confToken.confidentialTransferFrom(
            msg.sender,
            address(this),
            amount
        );
        FHE.allowThis(amountReceived);

        // Create swap request
        swapId = _swaps.length;
        _swaps.push(SwapRequest({
            user: msg.sender,
            confidentialToken: confToken,
            publicToken: IERC20(publicToken),
            encryptedAmount: amountReceived,
            rate: rate,
            completed: false,
            cancelled: false,
            decryptionRequested: false
        }));

        _userSwaps[msg.sender].push(swapId);

        emit SwapInitiated(swapId, msg.sender, confidentialToken, publicToken);
    }

    /// @notice Request decryption for a swap (marks it as pending)
    /// @param swapId The swap to request decryption for
    function requestDecryption(uint256 swapId) external {
        if (swapId >= _swaps.length) revert SwapNotFound();
        SwapRequest storage swap = _swaps[swapId];
        if (swap.completed || swap.cancelled) revert SwapAlreadyProcessed();
        if (swap.decryptionRequested) revert DecryptionAlreadyRequested();

        swap.decryptionRequested = true;
        emit DecryptionRequested(swapId);
    }

    /// @notice Complete a swap (owner only, after offchain verification)
    /// @param swapId The swap to complete
    /// @param decryptedAmount The verified decrypted amount
    function completeSwap(
        uint256 swapId,
        uint64 decryptedAmount
    ) external onlyOwner nonReentrant {
        if (swapId >= _swaps.length) revert SwapNotFound();
        SwapRequest storage swap = _swaps[swapId];
        if (swap.completed || swap.cancelled) revert SwapAlreadyProcessed();

        swap.completed = true;

        // Calculate output amount with rate
        uint256 outputAmount = (uint256(decryptedAmount) * swap.rate) / 1e6;

        // Transfer public tokens to user
        if (outputAmount > 0) {
            uint256 available = swap.publicToken.balanceOf(address(this));
            if (available < outputAmount) revert InsufficientLiquidity(outputAmount, available);
            swap.publicToken.safeTransfer(swap.user, outputAmount);
        }

        emit SwapCompleted(swapId, swap.user, decryptedAmount);
    }

    /// @notice Cancel a pending swap and return tokens
    /// @param swapId The swap to cancel
    function cancelSwap(uint256 swapId) external nonReentrant {
        if (swapId >= _swaps.length) revert SwapNotFound();
        SwapRequest storage swap = _swaps[swapId];
        if (swap.user != msg.sender) revert NotSwapOwner();
        if (swap.completed || swap.cancelled) revert SwapAlreadyProcessed();
        if (swap.decryptionRequested) revert SwapAlreadyProcessing();

        swap.cancelled = true;

        // Return confidential tokens
        FHE.allowTransient(swap.encryptedAmount, address(swap.confidentialToken));
        swap.confidentialToken.confidentialTransfer(msg.sender, swap.encryptedAmount);

        emit SwapCancelled(swapId);
    }

    // ============ Liquidity Functions ============

    /// @notice Add ERC20 liquidity to the pool
    /// @param token The token to add
    /// @param amount Amount of tokens to add
    function addLiquidity(IERC20 token, uint256 amount) external {
        token.safeTransferFrom(msg.sender, address(this), amount);
        emit LiquidityAdded(msg.sender, amount);
    }

    // ============ View Functions ============

    /// @notice Get swap count
    function getSwapCount() external view returns (uint256) {
        return _swaps.length;
    }

    /// @notice Get swap details
    function getSwap(uint256 swapId) external view returns (
        address user,
        address confidentialToken,
        address publicToken,
        uint256 rate,
        bool completed,
        bool cancelled
    ) {
        if (swapId >= _swaps.length) revert SwapNotFound();
        SwapRequest storage swap = _swaps[swapId];
        return (
            swap.user,
            address(swap.confidentialToken),
            address(swap.publicToken),
            swap.rate,
            swap.completed,
            swap.cancelled
        );
    }

    /// @notice Get all swap IDs for a user
    function getUserSwaps(address user) external view returns (uint256[] memory) {
        return _userSwaps[user];
    }

    /// @notice Get available liquidity for a token
    function getAvailableLiquidity(IERC20 token) external view returns (uint256) {
        return token.balanceOf(address(this));
    }
}

Code Explanation

Create Swap

Creates a new swap offer with encrypted ERC7984 amount. The offer is stored and can be accepted by anyone with matching ERC20 tokens.

Lines 25-45

Accept Swap

Accepts an existing swap offer by providing ERC20 tokens. The atomic swap ensures both parties receive their tokens or the transaction reverts.

Lines 50-75

FHE Operations Used

  • FHE.confidentialTransferFrom()

  • FHE.fromExternal()

FHE Types Used

  • euint64

Tags

swap exchange ERC7984 ERC20 DEX OpenZeppelin

Prerequisites

Before this example, you should understand:

Next Steps

After this example, check out:


Generated with Lab-Z

Last updated