Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/examples/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Deployer private key
PRIVATE_KEY=YOUR PRIVATE KEY

# RPC Endpoints
INFURA_KEY=INFURA PROJECT ID

# Additional keys
ETHERSCAN_KEY=ETHERSCAN API KEY
BSCSCAN_KEY=BSCSCAN API KEY
POLYGONSCAN_KEY=POLYGONSCAN API KEY
AVALANCHE_KEY=AVALANCHE API KEY
COINMARKETCAP_KEY=COINMARKETCAP API KEY
16 changes: 16 additions & 0 deletions src/examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
.env
.DS_Store

# Hardhat files
cache
artifacts
coverage.json
coverage
abi

# Typechain generated files
generated-types

# Hardhat migrate
.storage.json
16 changes: 16 additions & 0 deletions src/examples/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
.env
.DS_Store
package-lock.json

# Hardhat files
cache
artifacts
coverage.json
coverage

# Typechain generated files
generated-types

# Hardhat migrate
.storage.json
4 changes: 4 additions & 0 deletions src/examples/.solcover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
skipFiles: ["mock/", "interfaces/"],
configureYulOptimizer: true,
};
17 changes: 17 additions & 0 deletions src/examples/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"reentrancy": "error",
"prettier/prettier": "warn",
"modifier-name-mixedcase": "off",
"func-name-mixedcase": "off",
"no-empty-blocks": "warn",
"func-visibility": ["warn", { "ignoreConstructors": true }],
"max-states-count": "warn",
"not-rely-on-time": "off",
"var-name-mixedcase": "off",
"compiler-version": "off",
"use-natspec": "off"
}
}
2 changes: 2 additions & 0 deletions src/examples/.solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
./contracts/verifiers
# ./contracts/mock
21 changes: 21 additions & 0 deletions src/examples/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Distributed Lab

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
49 changes: 49 additions & 0 deletions src/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SPV Gateway Examples

This directory contains example integrations with the SPV Gateway V2 contract, demonstrating how to leverage SPV (Simple Payment Verification) proofs for Bitcoin blockchain verification on Ethereum.

## Overview

The examples showcase practical use cases that utilize the SPV Gateway V2 contract to verify Bitcoin transactions and blocks on-chain. These contracts demonstrate key patterns and best practices for integrating SPV verification into Ethereum-based applications.

## Examples

### 1. BTC Whitelist

A contract that manages a whitelist of Ethereum addresses based on Bitcoin transactions. Any user can join the whitelist by providing an SPV proof of a Bitcoin transaction containing at least one output with a value exceeding the contract's configured minimum threshold.

**Key Features:**
- Whitelisting based on Bitcoin transaction verification
- Configurable minimum transaction amount
- Confirmation count requirements for security
- Paginated retrieval of whitelisted accounts

**How it works:**
1. A user submits a Bitcoin transaction alongside an SPV proof
2. The contract verifies the transaction was included in the Bitcoin blockchain
3. If the transaction meets the whitelist rules (minimum amount), the user's Ethereum address is added to the whitelist
4. The contract enforces a minimum confirmation count before considering a transaction valid

## Getting Started

### Installation

```bash
npm install
```

### Compiling

```bash
npm run compile
```

### Testing

```bash
npm run test
```

## Contract Architecture

Each example contract integrates with `ISPVGatewayV2`, which provides the core functionality for verifying Bitcoin blockchain proofs. The examples implement specific business logic on top of this base verification layer.
170 changes: 170 additions & 0 deletions src/examples/contracts/BTCWhitelist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {TxParser} from "@solarity/solidity-lib/libs/bitcoin/TxParser.sol";
import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol";

import {ISPVGatewayV2} from "./interfaces/ISPVGatewayV2.sol";
import {IBTCWhitelist} from "./interfaces/IBTCWhitelist.sol";

contract BTCWhitelist is IBTCWhitelist, Ownable {
using EnumerableSet for EnumerableSet.AddressSet;
using Paginator for EnumerableSet.AddressSet;
using TxParser for bytes;

ISPVGatewayV2 public immutable spvGatewayV2;

uint256 internal _whitelistMinAmount;
uint256 internal _minConfirmationsCount;

EnumerableSet.AddressSet internal _whitelist;

modifier onlyWhitelistedAccount() {
_onlyWhitelistedAccount(msg.sender);
_;
}

constructor(
address spvGatewayV2Addr_,
uint256 whitelistMinAmount_,
uint256 minConfirmationsCount_
) Ownable(msg.sender) {
spvGatewayV2 = ISPVGatewayV2(spvGatewayV2Addr_);

_updateWhitelistMinAmount(whitelistMinAmount_);
_updateMinConfirmationsCount(minConfirmationsCount_);
}

/// @inheritdoc IBTCWhitelist
function updateWhitelistMinAmount(uint256 whitelistMinAmount_) external onlyOwner {
_updateWhitelistMinAmount(whitelistMinAmount_);
}

/// @inheritdoc IBTCWhitelist
function updateMinConfirmationsCount(uint256 minConfirmationsCount_) external onlyOwner {
_updateMinConfirmationsCount(minConfirmationsCount_);
}

/// @inheritdoc IBTCWhitelist
function enterToWhitelist(bytes calldata txData_, bytes calldata txInclusionProof_) external {
require(!isAccountWhitelisted(msg.sender), AccountIsAlreadyWhitelisted(msg.sender));

bytes32 txId_ = txData_.calculateTxId();

_checkTxInclusionProof(txId_, txInclusionProof_);

(TxParser.Transaction memory tx_, ) = txData_.parseTransaction();

require(_verifyWhitelistEntryRules(tx_), WhitelistEntryRulesAreNotMatched());

_whitelistAccount(msg.sender);
}

/// @inheritdoc IBTCWhitelist
function getWhitelistMinAmount() external view returns (uint256) {
return _whitelistMinAmount;
}

/// @inheritdoc IBTCWhitelist
function getMinConfirmationsCount() external view returns (uint256) {
return _minConfirmationsCount;
}

/// @inheritdoc IBTCWhitelist
function getWhitelistedAccountsCount() external view returns (uint256) {
return _whitelist.length();
}

/// @inheritdoc IBTCWhitelist
function getAllWhitelistedAccounts() external view returns (address[] memory) {
return _whitelist.values();
}

/// @inheritdoc IBTCWhitelist
function getWhitelistedAccountsPart(
uint256 offset_,
uint256 limit_
) external view returns (address[] memory) {
return _whitelist.part(offset_, limit_);
}

/// @inheritdoc IBTCWhitelist
function isAccountWhitelisted(address account_) public view virtual returns (bool) {
return _whitelist.contains(account_);
}

function _updateWhitelistMinAmount(uint256 whitelistMinAmount_) internal {
_whitelistMinAmount = whitelistMinAmount_;

emit WhitelistMinAmountUpdated(whitelistMinAmount_);
}

function _updateMinConfirmationsCount(uint256 minConfirmationsCount_) internal {
_minConfirmationsCount = minConfirmationsCount_;

emit MinConfirmationsCountUpdated(minConfirmationsCount_);
}

function _whitelistAccount(address account_) internal virtual {
_whitelist.add(account_);

emit AccountWhitelisted(account_);
}

function _verifyWhitelistEntryRules(
TxParser.Transaction memory tx_
) internal view virtual returns (bool) {
TxParser.TransactionOutput[] memory outputs_ = tx_.outputs;

for (uint256 i = 0; i < outputs_.length; i++) {
if (outputs_[i].value >= _whitelistMinAmount) {
return true;
}
}

return false;
}

function _checkTxInclusionProof(
bytes32 txId_,
bytes calldata txInclusionProof_
) internal view virtual {
(
bytes memory blockHeaderRaw_,
uint256 txIndex_,
bytes32[] memory merkleProof_,
ISPVGatewayV2.HistoryBlockInclusionProofData memory blockProofData_
) = abi.decode(
txInclusionProof_,
(bytes, uint256, bytes32[], ISPVGatewayV2.HistoryBlockInclusionProofData)
);

require(
spvGatewayV2.checkTxInclusion(
merkleProof_,
blockHeaderRaw_,
txId_,
txIndex_,
blockProofData_
),
TxNotIncluded()
);
_checkBlockConfirmations(uint64(blockProofData_.blockHeight));
}

function _checkBlockConfirmations(uint64 blockHeight_) internal view virtual {
uint64 currentMainchainHeight_ = spvGatewayV2.getMainchainHeight();

require(
blockHeight_ + _minConfirmationsCount <= currentMainchainHeight_,
MinConfirmationsCountNotReached(blockHeight_, currentMainchainHeight_)
);
}

function _onlyWhitelistedAccount(address account_) internal view {
require(isAccountWhitelisted(account_), NotAWhitelistedAccount(account_));
}
}
Loading