Getting started

Documentation

BVGS is a decentralized platform for managing Ethereum assets through smart contract powered BVGS NFTs that act as a tokenized safe deposit box and can store ETH, ERC20 tokens, and NFTs. Our documentation provides comprehensive technical details about our smart contracts, key fraction technology, and business model.

Independent business model

BVGS operates on a platform fee model for use of our key management technology to securely manage your assets. Our contracts are open and free to use — we do not have any tokens or tokenomics. This model allows us to focus on providing the best possible service without creating artificial scarcity or enforing arbitrary limits on your assets.

Quick start

Get started with BVGS by following these simple steps to connect your wallet and create your first bag.

Step 1: Connect Your Wallet

Click on the "Connect wallet" button in the top navigation bar and select your preferred wallet provider. Please accept any wallet connection popup requests.

Supported wallets:
MetaMaskCoinbase WalletBrave Wallet

Step 2: Authenticate session

Once your wallet is connected, you will be asked to create an active session by authenticating your wallet. This action will not incur any gas fees.

For security purposes, all active sessions expire automatically after 3 hours. You will need to reauthenticate to refresh your session.

Step 3: Select Assets to Bag

In your portfolio, click "Create bag" to enter selection mode or select "Bag assets" on an existing BVGS NFT to initiate a deposit. Then, click and select the tokens and NFTs you want to bag. Note that we recommend bagging no more than 3 different assets at a time to minimize risks of gas failures during minting. As blockchains improve in efficiency and scalability, more assets can be bundled within one transaction.

Step 4: Complete the Bagging Process

Follow the guided workflow to select your key management option, confirm your assets, and complete payment to redeem your BVGS NFT.

The bagging process includes:

Choosing your key management option

  • Self custody keys ($4.99 platform fee) - Full control with self custody keys and 2FA protection, requiring you to manage your own private keys.
  • BVGS secured keys ($9.99 platform fee) - Access to our patent-pending encrypted key fraction technology that combines your wallet signature with an encrypted key fraction to trustlessly derive key pairs and sign expiring authorization requests for your BVGS NFT.
  • BVGS secured keys include enterprise-grade encryption with industry-standard backups and HSM stored keys with FIPS level 3 compliance for enterprise-grade security.

Confirming your selected assets

  • The BVGS smart contract offers native support for all ERC20 cryptocurrencies, ERC721 NFTs, and ETH native tokens.
  • Our system enables different combinations of supported assets to be bundled in one transaction.

Key management selection

  • Choose between BVGS secured key fraction technology or self custody options.
  • We provide a free EIP-712 message builder for self custody options.

Stripe payment processing

  • After finalizing your bagging details, you will be redirected to Stripe to complete your platform fee payment.

Step 5: Mint your BVGS NFT

After successful payment, you'll be redirected back to our site where you can begin our secure bagging process to trustlessly derive your secondary key pair and mint your BVGS NFT to your wallet address. This soulbound BVGS NFT will serve as your secure safe deposit box for all bagged assets. Deposit and withdraw any time without any additional fees, limits, or restrictions.

Smart Contracts

Smart contracts

Contract overview

The BVGS platform is built on a minimalist suite of just four Solidity smart contracts with a clear separation of concerns. This intentionally simple architecture focuses solely on securely depositing and withdrawing assets from NFTs.

BVGS Contract

The main NFT contract that implements ERC-721 functionality and soulbound token properties (EIP-5192). This is the only concrete contract users interact with directly for minting.

Core NFT & mintingView details

Deposits Contract

Abstract contract that handles all deposit operations for ETH, ERC20 tokens, and ERC721 NFTs. It maintains the storage structure and provides deposit functionalities.

Asset deposit logicView details

Withdrawals Contract

Abstract contract that inherits from Deposits and adds signature-gated withdrawal functionality. All withdrawal operations require cryptographic authorization.

Asset withdrawal logicView details

Signature Verification

The security foundation that implements EIP-712 typed signature verification. All withdrawal operations rely on this to securely authorize asset movements.

Authorization layerView details

Intentional simplicity

BVGS employs a deliberately simple architecture with just four contracts. Each contract has a single, well-defined purpose: BVGS handles NFT functionality, Deposits manages asset storage, Withdrawals handles asset retrieval, and SignatureVerification provides secure authorization. This minimalist approach makes the code easier to audit and reduces attack surfaces.

Contract inheritance structure

BVGS
  ├── ERC721
  │   └── IERC721
  ├── Ownable
  ├── Withdrawals (abstract)
  │   └── Deposits (abstract)
  │       ├── SignatureVerification
  │       │   └── EIP712
  │       ├── IERC721Receiver
  │       └── ReentrancyGuard
  └── IERC5192 (Soulbound interface)

The inheritance diagram shows how the four contracts work together in a linear hierarchy. Deposits inherits from SignatureVerification, IERC721Receiver, and ReentrancyGuard. Withdrawals inherits from Deposits, and BVGS inherits from ERC721, Ownable, Withdrawals, and IERC5192. This creates a clean separation of concerns while maintaining a simple inheritance path.

BVGS Contract

The BVGS contract is the main entry point for users. It extends ERC721 functionality and implements the "bagging" system, allowing users to create safe deposit NFTs that can hold digital assets.

Contract declaration

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.30;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Withdrawals.sol";
import "./SignatureVerification.sol";

/* ───────────────────────────── ERC-5192 / Soulbound standard ──────────────────────────────── */
interface IERC5192 {
    /// Emitted exactly once when a token becomes locked (non-transferable).
    event Locked(uint256 tokenId);

    /// Emitted if a token ever becomes unlocked (not used, but declared for ERC-5192 compliance).
    event Unlocked(uint256 tokenId);

    /// MUST always return true for every existing BVGS NFT.
    function locked(uint256 tokenId) external view returns (bool);
}

/**
 * @title BVGS
 * @dev Core soul-bound ERC-721 contract for BVGS NFT creation and bagging flows 
 *      Implements ERC-5192 (soulbound standard) for non-transferability.
 *      Inherits the Withdrawals smart contract which inherits Deposits and SignatureVerification contracts.
 */
contract BVGS is
    ERC721,
    Ownable,
    Withdrawals,
    IERC5192
{
    /// @dev Next token ID to mint (auto-incremented per mint.
    uint256 private _nextId;

    /* ─────────── Custom errors ───────── */
    error ZeroTokenAddress();
    error ArrayLengthMismatch();
    error EthValueMismatch();
    error DefaultURIAlreadySet();
    error NoURI();
    error TransfersDisabled();
    error UseDepositETH();
    error FallbackNotAllowed();
    
    // ... additional contract code ...
}

Key features

  • Soulbound standard: Implements the EIP-5192 standard for non-transferrable NFTs, making all BVGS NFTs permanently bound to their owners
  • NFT creation: Mints unique NFTs that function as containers for other assets
  • Combined minting and bagging: Allows users to mint a new BVGS NFT and deposit assets in a single transaction
  • Metadata management: Includes both default and token-specific metadata URI functionality
  • ERC721 compatibility: Extends industry-standard NFT functionality while adding custom soulbound properties

Minting and bagging operations

The BVGS contract provides several functions for minting new BVGS NFTs and simultaneously depositing assets:

/**
 * @notice Mint a new BVGS NFT and deposit ETH.
 * @param to The address that will receive the newly minted BVGS NFT.
 * @param bvgsPublicKey The public key used for off-chain signature verification.
 * @param referenceId An external reference ID for off-chain tracking of this deposit.
 * 
 * Requirements:
 * - `to` must not be the zero address.
 * - `bvgsPublicKey` must not be the zero address.
 * - `msg.value` > 0 to deposit ETH.
 */
function bagETH(
    address to,
    address bvgsPublicKey,
    bytes32 referenceId
) external payable nonReentrant {
    if (to == address(0))            revert ZeroAddress();
    if (bvgsPublicKey == address(0)) revert ZeroKey();

    uint256 tokenId = _nextId++; 

    if (to.code.length == 0) {       
        _mint(to, tokenId);
    } else {                          
        _safeMint(to, tokenId);
    }

    emit Locked(tokenId);

    initialize(tokenId, bvgsPublicKey);
    _depositETH(tokenId, msg.value);

    emit Bagged(tokenId, referenceId);
}

The bagETH function showcases the streamlined minting and deposit process. It creates a new BVGS NFT, marks it as soulbound (locked), initializes its signing key, and deposits ETH in a single transaction. The contract includes similar functions for ERC20 tokens (bagERC20), ERC721 NFTs (bagERC721), and batch deposits (bagBatch).

Gas optimization for minting

The contract uses a gas-efficient approach for minting by checking the recipient's type:

  • For regular wallet addresses (EOAs): Uses _mint which skips unnecessary checks, saving gas
  • For contract addresses: Uses _safeMint to ensure the contract can receive NFTs by checking for ERC721Receiver implementation

This approach optimizes gas costs while maintaining compatibility with different types of recipients.

Soulbound standard implementation (EIP-5192)

Every BVGS NFT is compliant with the EIP-5192 soulbound standard, ensuring that tokens are permanently bound to their owners and cannot be transferred.

What is a soulbound token? A soulbound token (SBT) is a non-fungible token that cannot be transferred once minted to an address. BVGS NFTs implement this standard to enhance security by ensuring your safe deposit box remains in your control at all times.Read more about the EIP-5192 standard here

/**
 * @notice Always returns true for existing BVGS NFTs (soul‐bound).
 * @param tokenId The ID of the BVGS NFT.
 * @return Always true.
 * @dev Reverts if token does not exist.
 */
function locked(uint256 tokenId) external view override returns (bool) {
    if (!_exists(tokenId)) revert NonexistentToken();
    return true;
}

/// Disable any transfer—soul‐bound enforcement.
function _transfer(address, address, uint256) internal pure override {
    revert TransfersDisabled();
}

function supportsInterface(bytes4 interfaceId)
    public view override(ERC721)
    returns (bool)
{
    if (interfaceId == 0xb45a3c0e) return true; // IERC5192
    return super.supportsInterface(interfaceId);
}

The implementation disables all token transfers by overriding the _transfer function to always revert. It also implements the EIP-5192 locked function to indicate that tokens are permanently bound to their owners.

Metadata management

The contract provides functionality for managing token metadata:

Default metadata

The contract allows setting a default metadata URI that applies to all tokens without a custom URI. This is a one-time operation that can only be performed by the contract owner.

function setDefaultMetadataURI(string memory newDefaultURI)
    external onlyOwner

Token-specific metadata

Token owners can set custom metadata URIs for their BVGS NFTs. This operation requires a valid EIP-712 signature from the BVGS key associated with the token.

function setTokenMetadataURI(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    string memory newMetadataURI,
    // ... additional params
)
/**
 * @notice Returns the metadata URI for a BVGS NFT.
 * @param tokenId The ID of the token to query.
 * @return The custom URI if set; otherwise the default URI.
 * @dev Reverts if neither custom nor default URI is available.
 */
function tokenURI(uint256 tokenId)
    public view override(ERC721)
    returns (string memory)
{
    if (!_exists(tokenId)) revert NonexistentToken();
    string memory custom = _tokenMetadataURIs[tokenId];
    if (bytes(custom).length > 0) return custom;
    if (bytes(_defaultMetadataURI).length > 0) return _defaultMetadataURI;
    revert NoURI();
}

Security features

  • Soulbound property: NFTs cannot be transferred to another wallet, protecting against theft or accidental transfers
  • Reentrancy protection: All functions that transfer assets use the nonReentrant modifier
  • Input validation: Thorough validation of all function parameters to prevent errors and exploits
  • Custom errors: Gas-efficient custom error types for better developer experience and cheaper reverts
  • Limited fallback behavior: Explicit error handling for unexpected ETH transfers to prevent unintended deposits
  • Signature verification: All sensitive operations require valid EIP-712 signatures
/// Clears custom metadata on burn.
function _burn(uint256 tokenId)
    internal override(ERC721)
{
    super._burn(tokenId);
    delete _tokenMetadataURIs[tokenId];
}

Deposits Contract

The Deposits contract provides core functionality for storing assets inside BVGS NFTs. It handles the deposit operations for ETH, ERC20 tokens, and ERC721 NFTs, maintaining precise accounting of all stored assets.

Contract declaration

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.30;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./SignatureVerification.sol";            

/**
 * @title Deposits
 * @dev Internal ETH/ERC20/ERC721 deposit and bookkeeping logic.
 *      Inherits SignatureVerification for key access and ReentrancyGuard for safety.
 */
abstract contract Deposits is SignatureVerification, IERC721Receiver, ReentrancyGuard {
    using SafeERC20 for IERC20;

    /* ───────── Events ───────── */
    event Deposited (uint256 indexed tokenId, bytes32 indexed referenceId);

    /* ───────── Errors ───────── */
    error NonexistentToken();
    error ZeroAddress();
    error ZeroAmount();
    error MismatchedInputs();
    error ETHMismatch();

    /* ───────── Storage ───────── */
    mapping(uint256 => uint256) internal _baggedETH;

    // ERC-20
    mapping(uint256 => mapping(address => uint256))   internal _erc20Balances;
    mapping(uint256 => address[])                     internal _erc20TokenAddresses;
    mapping(uint256 => mapping(address => bool))      internal _erc20Known;

    // ERC-721
    struct BaggedNFT { address nftContract; uint256 nftTokenId; }
    mapping(uint256 => bytes32[])                     internal _nftKeys;
    mapping(uint256 => mapping(bytes32 => BaggedNFT)) internal _nftData;
    mapping(uint256 => mapping(bytes32 => bool))      internal _nftKnown;
    
    // ... additional code ...
}

Key features

  • Modular architecture: An abstract contract that is inherited by the main BVGS contract, separating concerns for better code organization
  • Multi-asset deposit: Handles ETH, ERC20 token, and ERC721 NFT deposits with precise accounting
  • Gas optimization: Uses efficient storage patterns and batch deposit capabilities
  • Security: Implements reentrancy protection via OpenZeppelin's ReentrancyGuard

Deposit functions

The Deposits contract provides several user-facing functions to deposit assets into bags:

/*
* @notice Deposit ETH into a BVGS NFT.
* @param tokenId    The ID of the BVGS NFT.
* @param referenceId External reference ID for off-chain tracking.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `msg.value` must be > 0.
*/
function depositETH(uint256 tokenId, bytes32 referenceId)
    external payable nonReentrant
{
    _requireOwnsBag(tokenId);
    if (msg.value == 0) revert ZeroAmount();

    _depositETH(tokenId, msg.value);
    emit Deposited(tokenId, referenceId);
}

The depositETH function allows users to add ETH to their bag. It verifies that the caller owns the bag and then delegates to an internal function to handle the deposit. The function is protected against reentrancy attacks using the nonReentrant modifier.

/// @notice Deposit ERC-20 tokens into a bag.
/// @param tokenId      The ID of the BVGS NFT.
/// @param tokenAddress The ERC-20 token contract address.
/// @param amount       The amount of tokens to deposit.
/// @param referenceId  External reference ID for off-chain tracking.
///
/// Requirements:
/// - `tokenId` must exist and caller must be its owner.
/// - `tokenAddress` must not be the zero address.
/// - `amount` must be greater than zero.
function depositERC20(
    uint256 tokenId,
    address tokenAddress,
    uint256 amount,
    bytes32 referenceId
) external nonReentrant {
    _requireOwnsBag(tokenId);
    if (tokenAddress == address(0)) revert ZeroAddress();
    if (amount == 0)           revert ZeroAmount();
    _depositERC20(tokenId, tokenAddress, amount, referenceId);
}

The depositERC20 function handles ERC20 token deposits. It performs additional validations to ensure the token address is not zero and the deposit amount is greater than zero.

/// @notice Batch-deposit ETH, multiple ERC-20s, and multiple ERC-721s in one call.
/// @param tokenId         The ID of the BVGS NFT.
/// @param amountETH       The ETH amount to deposit.
/// @param tokenAddresses  The list of ERC-20 token addresses.
/// @param tokenAmounts    The corresponding ERC-20 token amounts.
/// @param nftContracts    The list of ERC-721 contract addresses.
/// @param nftTokenIds     The corresponding ERC-721 token IDs.
/// @param referenceId     External reference ID for off-chain tracking.
///
/// Requirements:
/// - `tokenId` must exist and caller must be its owner.
/// - `tokenAddresses.length` must equal `tokenAmounts.length`.
/// - `nftContracts.length` must equal `nftTokenIds.length`.
/// - `msg.value` must equal `amountETH`.
function batchDeposit(
    uint256 tokenId,
    uint256 amountETH,
    address[] calldata tokenAddresses,
    uint256[] calldata tokenAmounts,
    address[] calldata nftContracts,
    uint256[] calldata nftTokenIds,
    bytes32 referenceId
) external payable nonReentrant {
    _requireOwnsBag(tokenId);
    if (msg.value != amountETH) revert ETHMismatch();
    _batchDeposit(tokenId, amountETH, tokenAddresses, tokenAmounts, nftContracts, nftTokenIds, referenceId);
}

The batchDeposit function provides gas-efficient batch operations, allowing users to deposit multiple asset types (ETH, ERC20 tokens, and ERC721 NFTs) in a single transaction. This significantly reduces overall gas costs compared to individual deposits.

Internal deposit implementations

The contract uses internal helper functions to implement the deposit logic:

function _depositETH(uint256 tokenId, uint256 amountETH) internal {
    if (amountETH == 0) return;
    _baggedETH[tokenId] += amountETH;
}
function _depositERC20(
    uint256 tokenId,
    address tokenAddress,
    uint256 amount
) internal {
    IERC20 t = IERC20(tokenAddress);

    // Register token if new
    if (!_erc20Known[tokenId][tokenAddress]) {
        _erc20TokenAddresses[tokenId].push(tokenAddress);
        _erc20Known[tokenId][tokenAddress] = true;
    }

    // Pull tokens
    uint256 beforeBal = t.balanceOf(address(this));
    t.safeTransferFrom(msg.sender, address(this), amount);
    uint256 afterBal  = t.balanceOf(address(this));

    // Book the delta
    // Ensures assets booked match assets received regardless of input
    uint256 delta = afterBal > beforeBal ? afterBal - beforeBal : 0;
    
    if (delta == 0) revert ZeroAmount();
    
    _erc20Balances[tokenId][tokenAddress] += delta;
}

The internal _depositERC20 function includes an additional safety feature: it calculates the actual token balance difference before and after the transfer. This ensures the contract only accounts for the actual tokens received, protecting against potential token transfer issues or fee-on-transfer tokens. The updated version now also explicitly checks that the delta is non-zero and reverts if no tokens were actually transferred.

Internal bookkeeping utilities

The contract includes helper functions for efficient removal of assets from storage:

/*
* @dev Remove an ERC20 token address from the bag's tracking array using swap & pop.
* @param tokenId The BVGS NFT ID.
* @param tokenAddress The ERC20 token address to remove.
*/
function _removeERC20Token(uint256 tokenId, address tokenAddress) internal {
    address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
    uint256 len = tokenAddresses.length;
    for (uint256 i; i < len; ) {
        if (tokenAddresses[i] == tokenAddress) {
            tokenAddresses[i] = tokenAddresses[len - 1];
            tokenAddresses.pop();
            break;
        }
        unchecked { ++i; } 
    }
}

/*
* @dev Remove an ERC721 key from the bag's tracking array using swap & pop.
* @param tokenId The BVGS NFT ID.
* @param key The keccak256(nftContract, tokenId) to remove.
*/
function _removeNFTKey(uint256 tokenId, bytes32 key) internal {
    bytes32[] storage keys = _nftKeys[tokenId];
    uint256 len = keys.length;
    for (uint256 i = 0; i < len; ) {
        if (keys[i] == key) {
            keys[i] = keys[len - 1];
            keys.pop();
            break;
        }
        unchecked { ++i; }
    }
}

These utility functions use a gas-efficient "swap and pop" pattern to remove items from storage arrays. When an item needs to be removed, it swaps the item with the last element and then removes the last element using pop(). This maintains O(1) deletion complexity instead of the O(n) complexity that would be required to shift all elements.

Security features

  • Reentrancy protection: All user-facing functions use OpenZeppelin's nonReentrant modifier to prevent reentrancy attacks
  • Ownership verification: All deposit functions verify that the caller is the owner of the target bag
  • Safe token transfers: Uses OpenZeppelin's SafeERC20 library to handle ERC20 tokens safely, protecting against transfer edge cases
  • Input validation: Thoroughly validates all function inputs to prevent errors and exploits
  • Precise accounting: For ERC20 tokens, calculates the actual token balance difference to ensure accurate accounting and explicitly reverts if no tokens were received
  • Gas-efficient storage: Implements "swap and pop" pattern for efficient array operations when removing elements
  • Simplified ETH storage: Uses direct uint256 mapping for ETH balances instead of a struct for improved gas efficiency
  • Unchecked increments: Uses unchecked blocks for counter increments where overflow is impossible, reducing gas costs

Withdrawals Contract

The Withdrawals contract provides signature-gated withdrawal functionality for assets stored in BVGS NFTs. It inherits from the Deposits contract and adds withdrawal operations for ETH, ERC20 tokens, and ERC721 NFTs, all secured through signature verification.

Contract declaration

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.30;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

import "./Deposits.sol";


/**
 * @title Withdrawals
 * @dev Signature-gated unbag, burn, key-rotation, plus view helpers.
 *      Inherits Deposits for storage & deposit helpers, and
 *      SignatureVerification for EIP-712 auth.
 */
abstract contract Withdrawals is Deposits {
    using SafeERC20 for IERC20;

    /* ───────── Events ───────── */
    event Unbagged  (uint256 indexed tokenId, bytes32 indexed referenceId);
    event BagBurned (uint256 indexed tokenId, bytes32 indexed referenceId);
    event KeyRotated(uint256 indexed tokenId, bytes32 indexed referenceId);

    /* ───────── Errors ───────── */
    error SignatureExpired();
    error NoETHBagged();
    error InsufficientTokenBalance();
    error NFTNotFound();
    error EthTransferFailed();
    
    // ... unbagging functions and other functionality ...
}

Key features

  • Signature-gated authorization: All withdraw operations require valid EIP-712 signatures from the associated BVGS key
  • Multi-asset withdrawal: Supports withdrawing ETH, ERC20 tokens, and ERC721 NFTs individually or in batch
  • Key rotation: Secure mechanism for rotating the signing key associated with a bag
  • Time-bounded signatures: All signatures include expiry timestamps to prevent replay attacks
  • Secure burn function: Allows destroying a bag and clearing all associated storage

Withdrawal functions

The Withdrawals contract implements several signature-gated functions for securely withdrawing assets:

/*
* @notice Withdraw ETH from a BVGS NFT, authorized via EIP-712 signature.
* @param tokenId         The ID of the BVGS NFT.
* @param messageHash     The EIP-712 digest that was signed.
* @param signature       The EIP-712 signature by the active BVGS key.
* @param amountETH       The amount of ETH to withdraw.
* @param recipient       The address receiving the ETH.
* @param referenceId     External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - BVGS NFT must have ≥ `amountETH` ETH
*/
function unbagETH(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    uint256 amountETH,
    address recipient,
    bytes32 referenceId,
    uint256 signatureExpiry
) external nonReentrant {
    _requireOwnsBag(tokenId);
    if (recipient == address(0)) revert ZeroAddress();
    if (block.timestamp > signatureExpiry)  revert SignatureExpired();

    // 1) Verify
    bytes memory data = abi.encode(tokenId, amountETH, recipient, referenceId, msg.sender, signatureExpiry);
    verifySignature(tokenId, messageHash, signature, address(0), OperationType.UNBAG_ETH, data);

    // 2) Effects
    uint256 currentBal = _baggedETH[tokenId];
    if (currentBal < amountETH) revert NoETHBagged();
    _baggedETH[tokenId] = currentBal - amountETH;

    // 3) Interaction
    (bool success, ) = payable(recipient).call{value: amountETH}("");
    if (!success) revert EthTransferFailed();

    emit Unbagged(tokenId, referenceId);
}

The unbagETH function showcases the Withdrawals pattern: ownership verification, signature expiry check, EIP-712 signature verification, state updates, and then asset transfer. The function follows the Checks-Effects-Interactions pattern to ensure security.

ERC20 Token Withdrawals

/*
* @notice Withdraw an ERC-20 token from a BVGS NFT, authorized via EIP-712 signature.
* @param tokenId         The ID of the BVGS NFT.
* @param messageHash     The EIP-712 digest that was signed.
* @param signature       The EIP-712 signature by the active BVGS key.
* @param tokenAddress    The ERC-20 token address to withdraw.
* @param amount          The amount of tokens to withdraw.
* @param recipient       The address receiving the tokens.
* @param referenceId     External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - BVGS NFT must have ≥ `amount` balance of `tokenAddress`.
*/
function unbagERC20(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    address tokenAddress,
    uint256 amount,
    address recipient,
    bytes32 referenceId,
    uint256 signatureExpiry
) external nonReentrant {
    _requireOwnsBag(tokenId);
    if (recipient == address(0)) revert ZeroAddress();
    if (block.timestamp > signatureExpiry)  revert SignatureExpired();

    // 1) Verify
    bytes memory data = abi.encode(tokenId, tokenAddress, amount, recipient, referenceId, msg.sender, signatureExpiry);
    verifySignature(tokenId, messageHash, signature, address(0), OperationType.UNBAG_ERC20, data);

    // 2) Effects
    mapping(address => uint256) storage balMap = _erc20Balances[tokenId]; 
    uint256 bal = balMap[tokenAddress];

    if (bal < amount) revert InsufficientTokenBalance();
    unchecked {
        balMap[tokenAddress] = bal - amount;
    }

    if (balMap[tokenAddress] == 0) {
        // full storage refund for setting slot from non-zero → zero
        delete balMap[tokenAddress];
        delete _erc20Known[tokenId][tokenAddress];
        _removeERC20Token(tokenId, tokenAddress);
    }

    // 3) Interaction
    IERC20(tokenAddress).safeTransfer(recipient, amount);

    emit Unbagged(tokenId, referenceId);
}

The unbagERC20 function now includes gas optimizations with complete storage cleanup when tokens are fully withdrawn. When a token's balance reaches zero, the function:

  • Deletes the balance mapping entry to get a gas refund
  • Removes the token from the known tokens mapping
  • Uses the _removeERC20Token helper to remove it from the token address array

This implementation is more gas-efficient and completely cleans up storage to minimize blockchain bloat.

Burn implementation

/**
* @dev Internal helper called by `burnBag`.
*      - Wipes all ETH / ERC20 / ERC721 bookkeeping for the vault.
*      - Delegates the actual ERC-721 burn to `_burnBagNFT` (implemented in BVGS).
*/
function _finalizeBurn(uint256 tokenId) internal {
    /* ---- ETH ---- */
    delete _baggedETH[tokenId];

    /* ---- ERC-20 balances ---- */
    address[] storage toks = _erc20TokenAddresses[tokenId];
    for (uint256 i; i < toks.length; ) {
        address t = toks[i];
        delete _erc20Balances[tokenId][t];
        delete _erc20Known[tokenId][t];
        unchecked { ++i; }
    }
    delete _erc20TokenAddresses[tokenId];

    /* ---- ERC-721 bookkeeping ---- */
    bytes32[] storage keys = _nftKeys[tokenId];
    for (uint256 i; i < keys.length;) {
        bytes32 k = keys[i];
        delete _nftData[tokenId][k];
        delete _nftKnown[tokenId][k];
        unchecked { ++i; }
    }
    delete _nftKeys[tokenId];

    /* ---- finally burn the NFT itself ---- */
    _burnBagNFT(tokenId);
}

The _finalizeBurn function completely wipes all storage associated with a bag before burning the NFT itself. The implementation now:

  • Uses gas-optimized storage deletion
  • Uses unchecked increments for gas efficiency in loops
  • Works with the simplified ETH storage structure
  • Properly cleans up all mappings and arrays to minimize storage costs

Batch operations

The contract supports batch withdrawals through batchUnbag, allowing multiple assets (ETH, ERC20 tokens, and NFTs) to be withdrawn with a single signature verification. This provides gas efficiency for complex withdrawals.

Key rotation

The rotateBvgsKey function enables secure rotation of the signing key used for withdrawal authorizations. This is a critical security feature that allows users to change their keys in case of compromise or as a regular security practice.

Viewing bag contents

The contract provides a view function to retrieve the full contents of a bag:

/*
* @notice Returns the full contents of a BVGS NFT: ETH, ERC-20 balances, and ERC-721s.
* @param tokenId The ID of the BVGS NFT.
* @return bagETH      The ETH amount held.
* @return erc20Tokens Array of (tokenAddress, balance) for each ERC-20.
* @return nfts        Array of BaggedNFT structs representing each ERC-721.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
*/
struct BaggedERC20 { address tokenAddress; uint256 balance; }

function getFullBag(uint256 tokenId)
    external view
    returns (
        uint256 bagETH,
        BaggedERC20[] memory erc20Tokens,
        BaggedNFT[]  memory nftContracts
    )
{
    _requireExists(tokenId);
    if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();

    bagETH = _baggedETH[tokenId];

    // ERC-20s
    address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
    erc20Tokens = new BaggedERC20[](tokenAddresses.length);
    for (uint256 i; i < tokenAddresses.length; ) {
        erc20Tokens[i] = BaggedERC20({
            tokenAddress: tokenAddresses[i],
            balance: _erc20Balances[tokenId][tokenAddresses[i]]
        });
        unchecked { ++i; }
    }

    // ERC-721s
    // ... counts and populates nftContracts array ...
}

The getFullBag function allows owners to view a complete inventory of all assets stored in their bag. This provides transparency and facilitates integration with frontend applications. The updated version now uses the simplified ETH storage structure and includes unchecked increments for gas efficiency.

Security features

  • Reentrancy protection: All withdrawal functions use the nonReentrant modifier to prevent reentrancy attacks
  • Checks-Effects-Interactions pattern: State changes are applied before external interactions to prevent reentrancy vulnerabilities
  • Time-bounded signatures: All signatures include an expiry timestamp to limit the window of validity
  • Ownership verification: All functions verify the caller is the owner of the BVGS NFT
  • Cryptographic authorization: EIP-712 signatures provide secure, off-chain authorization for withdrawals
  • Safe transfers: Uses OpenZeppelin's SafeERC20 for token transfers and checks ETH transfer success
  • Gas optimizations: Uses unchecked math operations where overflow is impossible and simplified storage structures
  • Complete storage cleanup: Removes empty entries from storage to minimize blockchain bloat and earn gas refunds
  • Optimal array management: Uses the swap-and-pop pattern for efficient removal from arrays

Signature verification

The SignatureVerification contract implements secure transaction authorization using EIP-712 typed signatures. This system ensures that only authorized users with the correct signature keys can perform sensitive operations like withdrawals.

Core functionality

// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 BVGS. All Rights Reserved.
// You may use, modify, and share this code for NON-COMMERCIAL purposes only.
// Commercial use requires written permission from BVGS.
pragma solidity ^0.8.30;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

/**
 * @title SignatureVerification
 * @notice Provides signature-based authorization for Bagging contract operations using EIP‑712.
 * @dev Each BVGS NFT references an active BVGS public key that must sign operations.
 *      The contract stores a nonce to prevent replay attacks.
 */
contract SignatureVerification is EIP712 {
    using ECDSA for bytes32;

    /// @notice Enumerates the possible operations that require BVGS key authorization.
    enum OperationType {
        ROTATE_KEY,
        UNBAG_ETH,
        UNBAG_ERC20,
        UNBAG_NFT,
        BURN_BAG,
        SET_TOKEN_URI,
        BATCH_UNBAG
    }

    /// @dev Gas-cheap pointer to the BVGS ERC-721 (set once in constructor).
    ERC721 immutable _erc721;  

    /**
     * @dev Stores authorization data for each BVGS NFT.
     * @param nonce Monotonically increasing value to prevent signature replay.
     * @param activeBvgsPublicKey The public key currently authorized to sign operations for this BVGS NFT.
     */
    struct TokenAuth {
        address activeBvgsPublicKey;      
        uint96  nonce;                  
    }

    /// @dev Mapping from BVGS NFT token ID to its TokenAuth.
    mapping(uint256 => TokenAuth) private _tokenAuth;


    /* ─────────────────── Errors ────────────────────── */
    error NotOwner();
    error InvalidMessageHash();
    error InvalidSignature();
    error AlreadyInitialized();
    error ZeroKey();
    
    // Type hash for the Operation struct used in EIP-712 signing
    bytes32 private constant OPERATION_TYPEHASH =
        keccak256("Operation(uint256 tokenId,uint256 nonce,uint8 opType,bytes32 dataHash)");

    // ... contract implementation continues ...
}

Signature verification process

The core of the SignatureVerification contract is the verifySignature function that validates EIP-712 signatures:

/**
 * @notice Verifies an EIP‑712 signature for a specific operation.
 * @param tokenId The ID of the BVGS NFT.
 * @param messageHash The EIP‑712 digest that was signed.
 * @param signature The BVGS NFT private key signature to verify.
 * @param newBvgsPublicKey The new BVGS NFT public key (if rotating the key).
 * @param opType The operation being authorized.
 * @param data Encoded parameters for the specific operation.
 *
 * Requirements:
 * - The EIP-712 message digest must match `messageHash`.
 * - The signature must be valid for the current, active BVGS NFT public key.
 * - On successful verification, the nonce increments.
 * - If `opType` is `ROTATE_KEY`, the BVGS NFT public key is updated to `newBvgsPublicKey`.
 */
function verifySignature(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    address newBvgsPublicKey,
    OperationType opType,
    bytes memory data
) internal {
    TokenAuth storage tokenAuth = _tokenAuth[tokenId];

    // Compute the hash of the operation data.
    bytes32 dataHash = keccak256(data);
    bytes32 structHash = keccak256(
        abi.encode(
            OPERATION_TYPEHASH,
            tokenId,
            tokenAuth.nonce,
            uint8(opType),
            dataHash
        )
    );
    bytes32 expectedHash = _hashTypedDataV4(structHash);

    if (messageHash != expectedHash) {
        revert InvalidMessageHash();
    }

    address signer = expectedHash.recover(signature);
    if (signer != tokenAuth.activeBvgsPublicKey) {
        revert InvalidSignature();
    }

    // Increment nonce after successful verification.
    tokenAuth.nonce++;

    // If rotating the key, update the active BVGS public key.
    if (opType == OperationType.ROTATE_KEY && newBvgsPublicKey != address(0)) {
        if (newBvgsPublicKey == address(0)) revert ZeroKey();
        tokenAuth.activeBvgsPublicKey = newBvgsPublicKey;
    }
}

This verification process ensures that only the correct BVGS key can authorize operations for a specific token. The nonce is incremented after each successful verification to prevent replay attacks.

Token initialization

When a new BVGS NFT is created, it must be initialized with a BVGS public key:

/**
 * @notice Initializes the BVGS NFT data with a public key and nonce.
 * @dev Intended to be called once upon minting a new BVGS NFT.
 * @param tokenId The ID of the BVGS NFT being initialized.
 * @param bvgsPublicKey The public key that will sign operations for this BVGS NFT.
 */
function initialize(uint256 tokenId, address bvgsPublicKey) internal {
    if (_tokenAuth[tokenId].activeBvgsPublicKey != address(0)) {
        revert AlreadyInitialized();
    }   

    _tokenAuth[tokenId].activeBvgsPublicKey = bvgsPublicKey; 
    _tokenAuth[tokenId].nonce               = 1;            
}

This associates a public key with the token and sets the initial nonce to 1. The function also checks that the token hasn't already been initialized, providing an extra safety check.

View functions

The contract provides view functions to retrieve the current BVGS public key and nonce for a token:

/**
 * @notice Retrieves the current BVGS public key for the given BVGS NFT.
 * @param tokenId The ID of the BVGS NFT.
 * @return The currently active BVGS public key.
 *
 * Requirements:
 * - Caller must be the owner of `tokenId`.
 */
function getActiveBvgsPublicKeyForToken(uint256 tokenId)
    external
    view
    onlyTokenOwner(tokenId)
    returns (address)
{
    return _tokenAuth[tokenId].activeBvgsPublicKey;
}

/**
 * @notice Retrieves the current nonce for the given BVGS NFT.
 * @param tokenId The ID of the BVGS NFT.
 * @return The current nonce used for signature verification.
 *
 * Requirements:
 * - Caller must be the owner of `tokenId`.
 */
function getNonce(uint256 tokenId)
    external
    view
    onlyTokenOwner(tokenId)
    returns (uint256)
{
    return uint256(_tokenAuth[tokenId].nonce); 
}

These functions allow the token owner to check the current state of their token's authorization data. Note that the nonce is converted from uint96 to uint256 when returned.

Key features

  • EIP-712 signatures: Implements the EIP-712 standard for typed structured data signing, providing human-readable transaction data during signing
  • Nonce tracking: Each token maintains a nonce counter that increments with every signature verification to prevent replay attacks
  • Operation typing: Differentiates between various operations (withdrawals, key rotation, etc.) to ensure signatures are operation-specific
  • Time-bound signatures: Accepts expiration timestamps for signatures to limit the window of opportunity for potential attacks
  • Key rotation support: Allows changing the signing key with proper authorization from the current key

EIP-712 typing system

The contract uses EIP-712 for structured data signing, which provides users with human-readable data during signing.

What is EIP-712? EIP-712 is a standard for typed structured data hashing and signing. It improves the user experience when signing transactions by displaying data in a readable format, rather than showing a confusing hexadecimal string.Read more about EIP-712 here

// Example of EIP-712 domain and types
const domain = {
    name: "BVGS",
    version: "1",
    chainId: 1, // Ethereum mainnet
    verifyingContract: "0x123...789" // BVGS contract address
};

const types = {
    Operation: [
        { name: "tokenId", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "opType", type: "uint8" },
        { name: "dataHash", type: "bytes32" }
    ]
};

const message = {
    tokenId: 123,
    nonce: 42,
    opType: 1, // UNBAG_ETH (note: now at index 1)
    dataHash: "0x123...abc"
};

When users need to sign a withdrawal or other sensitive operation, they'll see a structured message with clear information about what they're authorizing, rather than an opaque hexadecimal string.

Key rotation

The contract supports rotating the BVGS public key, which enhances security by allowing periodic key updates:

/*
* @notice Rotate the off-chain authorization key for a BVGS NFT.
* @param tokenId         The ID of the BVGS NFT.
* @param messageHash     The EIP-712 digest that was signed.
* @param signature       The EIP-712 signature by the active BVGS key.
* @param newPublicKey    The new authorized BVGS public key.
* @param referenceId     External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `block.timestamp` must be ≤ `signatureExpiry`.
*/
function rotateBvgsKey(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    address newPublicKey,
    bytes32 referenceId,
    uint256 signatureExpiry
) external nonReentrant {
    _requireOwnsBag(tokenId);
    if (block.timestamp > signatureExpiry) revert SignatureExpired();

    bytes memory data = abi.encode(
        tokenId, newPublicKey, referenceId, msg.sender, signatureExpiry
    );
    verifySignature(
        tokenId, messageHash, signature,
        newPublicKey, OperationType.ROTATE_KEY, data
    );
    emit KeyRotated(tokenId, referenceId);
}

This function allows the token owner to change the public key used for authorizing operations. It requires a signature from the current key to authorize the change, ensuring that only the current key holder can approve a key rotation. Once the key is rotated, all future operations will require signatures from the new key.

Security considerations

  • Replay protection: Nonces ensure each signature can only be used once
  • Ownership verification: Operations require both token ownership and a valid signature
  • Time-bound signatures: Signatures can be created with expiration timestamps
  • Structured signing: EIP-712 ensures users can see exactly what they're signing
  • Operation-specific signing: Signatures are tied to specific operation types
  • Key rotation: Keys can be updated if compromised or as a security practice
  • Initialization protection: Tokens can only be initialized once with a public key
  • Hash verification: Explicit verification that the message hash matches the expected hash
  • Non-zero key validation: Prevents setting a zero address as the public key
  • Gas optimization: Uses uint96 for nonce to pack structs efficiently

BVGS NFT

Secure Asset Storage

BVGS NFTs function as secure digital safe deposit boxes in your wallet, with direct 1:1 mapping of deposited assets to the specific BVGS NFT. Each asset is precisely mapped to its owner's NFT through a secure token ID system, with exact precision maintained for all asset types.

Direct Asset Mapping

Every BVGS NFT maintains precise mappings between the tokenId and deposited assets using secure on-chain storage structures:

// ETH balance mapping - simplified for gas efficiency
mapping(uint256 => uint256) internal _baggedETH;

// ERC20 token mappings
mapping(uint256 => mapping(address => uint256)) internal _erc20Balances;
mapping(uint256 => address[]) internal _erc20TokenAddresses;
mapping(uint256 => mapping(address => bool)) internal _erc20Known;

// ERC721 NFT mappings
struct BaggedNFT { address nftContract; uint256 nftTokenId; }
mapping(uint256 => bytes32[]) internal _nftKeys;
mapping(uint256 => mapping(bytes32 => BaggedNFT)) internal _nftData;
mapping(uint256 => mapping(bytes32 => bool)) internal _nftKnown;

These storage structures ensure that each asset is linked directly to a specific BVGS NFT token ID, creating a secure 1:1 relationship between your NFT and the assets it contains.

Your Assets, Your Control

BVGS contracts have no administrator backdoors, privileged roles, or built-in methods to access user funds. The contract has no "admin functions" that can drain liquidity, pause withdrawals, or modify balances. Your assets remain securely mapped to your NFT with no external control mechanisms.

Zero Fee Structure

There are no built-in smart contract fees or costs for minting, deposits, or withdrawals, with no interaction fees, arbitrary deposit or withdrawal limits, tokenomics, upgradeability, or liquidity-draining functions. Your BVGS NFT—and the assets it holds—always remains mapped to the wallet it is bound to, giving you continuous access to your funds even if the BVGS interface or web app becomes unavailable.

Maximum Precision Guarantees

ETH Precision

ETH balances are stored in full wei precision (1018 units per ETH), ensuring exact accounting down to the smallest unit of ether:

// Add ETH with exact precision
function _depositETH(uint256 tokenId, uint256 amountETH) internal {
    if (amountETH == 0) return;
    _baggedETH[tokenId] += amountETH;
}

ERC20 Token Precision with Delta Tracking

ERC20 token balances maintain the token's native decimal precision with an advanced delta-based tracking system that guarantees the exact amount received is what gets recorded:

// Add ERC20 tokens with exact token precision using delta tracking
function _depositERC20(
    uint256 tokenId,
    address tokenAddress,
    uint256 amount
) internal {
    IERC20 t = IERC20(tokenAddress);

    // Register token if new
    if (!_erc20Known[tokenId][tokenAddress]) {
        _erc20TokenAddresses[tokenId].push(tokenAddress);
        _erc20Known[tokenId][tokenAddress] = true;
    }

    // Pull tokens
    uint256 beforeBal = t.balanceOf(address(this));
    t.safeTransferFrom(msg.sender, address(this), amount);
    uint256 afterBal  = t.balanceOf(address(this));

    // Book the delta
    // Ensures assets booked match assets received regardless of input
    uint256 delta = afterBal > beforeBal ? afterBal - beforeBal : 0;
    
    if (delta == 0) revert ZeroAmount();
    
    _erc20Balances[tokenId][tokenAddress] += delta;
}

Enhanced precision: Instead of directly adding the requested amount, our contracts measure the actual token balance change (delta) before and after the transfer. This guarantees that the exact amount received is what gets recorded, providing protection against tokens with transfer fees, rebasing mechanisms, or other non-standard behaviors. The contract now also explicitly verifies that tokens were actually received by checking for a non-zero delta.

Viewing Your Bag Contents

The contract provides a comprehensive view function that allows only you, the valid BVGS NFT owner, to see all assets contained in your BVGS NFT:

function getFullBag(uint256 tokenId)
    external
    view
    returns (
        uint256 bagETH,
        BaggedERC20[] memory erc20Tokens,
        BaggedNFT[] memory nfts
    )
{
    _requireExists(tokenId);
    if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();
    
    // Return ETH amount
    bagETH = _baggedETH[tokenId];
    
    // Return all ERC20 tokens with balances
    address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
    erc20Tokens = new BaggedERC20[](tokenAddresses.length);
    
    for (uint256 i; i < tokenAddresses.length; ) {
        address tokenAddr = tokenAddresses[i];
        uint256 bal = _erc20Balances[tokenId][tokenAddr];
        erc20Tokens[i] = BaggedERC20(tokenAddr, bal);
        unchecked { ++i; }
    }
    
    // Return all contained NFTs
    // [...NFT enumeration logic...]
}

Gas optimizations

The contract includes numerous gas optimizations to minimize transaction costs:

  • Simplified ETH storage: Using a direct uint256 mapping for ETH balances instead of a struct reduces gas costs
  • Unchecked increments: Loop counters use unchecked blocks where overflow is impossible, saving gas on increment operations
  • Complete storage cleanup: When asset balances reach zero, all associated storage is deleted to earn gas refunds
  • Swap and pop pattern: Efficient array item removal that maintains O(1) complexity instead of shifting elements
  • Conditional safe minting: Uses regular _mint for EOA addresses and _safeMint only for contract recipients
  • Minimal storage: Storage is carefully structured to minimize blockchain storage costs
// Example of swap-and-pop pattern for efficient array management
function _removeERC20Token(uint256 tokenId, address tokenAddress) internal {
    address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
    uint256 len = tokenAddresses.length;
    for (uint256 i; i < len; ) {
        if (tokenAddresses[i] == tokenAddress) {
            // Swap with last element
            tokenAddresses[i] = tokenAddresses[len - 1];
            // Remove last element
            tokenAddresses.pop();
            break;
        }
        unchecked { ++i; } 
    }
}

Soulbound Nature

BVGS NFTs are "soulbound" to the wallet they are minted to, meaning they cannot be transferred to other wallets. This crucial security feature ensures your assets remain safely within your ownership and cannot be stolen or transferred away.

EIP-5192 Standard Implementation

What is EIP-5192? EIP-5192 is the Ethereum Improvement Proposal that standardizes "soulbound" or non-transferable tokens. This standard allows applications to easily identify which NFTs are permanently bound to their current owner.Read the EIP-5192 specification

The BVGS contract implements the EIP-5192 standard through the IERC5192 interface:

// ERC-5192 Soulbound standard interface
interface IERC5192 {
    /// Emitted exactly once when a token becomes locked (non-transferable).
    event Locked(uint256 tokenId);

    /// Emitted if a token ever becomes unlocked (not used, but must be declared for compliance).
    event Unlocked(uint256 tokenId);

    /// MUST always return true for every existing BVGS NFT.
    function locked(uint256 tokenId) external view returns (bool);
}

When a BVGS NFT is minted, it's immediately marked as locked and an EIP-5192 Locked event is emitted:

// During minting, the NFT is marked as locked
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();

_safeMint(to, tokenId);
emit Locked(tokenId); // EIP-5192 Locked event
initialize(tokenId, bvgsPublicKey);

Transfer Prevention

The core of the soulbound mechanism is the overridden _transfer function, which prevents any transfers of BVGS NFTs:

/// Disable any transfer—soul‐bound enforcement.
function _transfer(address, address, uint256) internal pure override {
    revert TransfersDisabled();
}

This simple but powerful override ensures that no standard transfer function can move a BVGS NFT from its original owner's wallet. All transfer attempts will immediately revert with a TransfersDisabled error.

The contract also implements the required locked function, which confirms that a token is non-transferrable:

/**
 * @notice Always returns true for existing BVGS NFTs (soul‐bound).
 * @param tokenId The ID of the BVGS NFT.
 * @return Always true.
 * @dev Reverts if token does not exist.
 */
function locked(uint256 tokenId) external view override returns (bool) {
    if (!_exists(tokenId)) revert NonexistentToken();
    return true;
}

Interface Detection

To ensure applications can detect that BVGS NFTs implement the EIP-5192 soulbound standard, the contract includes the interface detection method:

function supportsInterface(bytes4 interfaceId)
    public view override(ERC721, ERC721Enumerable)
    returns (bool)
{
    if (interfaceId == 0xb45a3c0e) return true; // IERC5192
    return super.supportsInterface(interfaceId);
}

This enables wallets, marketplaces, and other applications to detect and appropriately handle the soulbound nature of BVGS NFTs.

Marketplace Protection

The soulbound nature prevents BVGS NFTs from being listed or sold on NFT marketplaces. Unlike regular NFTs, your BVGS NFT cannot be accidentally listed for sale or transferred to another wallet, ensuring your assets remain securely in your control.

Theft Prevention

Even if your wallet's private key is compromised, an attacker cannot transfer your BVGS NFT to another wallet. This provides an additional layer of security beyond standard NFTs, as the attacker would also need your BVGS signing key to access the assets within your bag.

Why Soulbound Matters

The soulbound property creates a fundamental security improvement by:

  • Preventing social engineering attacks that trick users into transferring NFTs
  • Eliminating the risk of approvals that could allow third parties to move your NFT
  • Creating a permanent bond between your wallet and your BVGS NFT assets
  • Requiring both wallet access AND signature verification for any asset withdrawals
  • Ensuring your assets remain accessible even if marketplaces or frontends go offline

Multi-Asset Support

BVGS NFTs are designed to securely hold multiple types of digital assets simultaneously within a single NFT. This creates a unified "digital safe deposit box" for your assets, all represented in your wallet as one NFT.

Supported Asset Types

ETH

Native ETH with full wei precision (10^18)

ERC20 Tokens

Any ERC20-compatible token with native precision

NFTs (ERC721)

Individual NFTs can be stored inside your BVGS NFT

All these asset types can be stored within a single BVGS NFT, creating a consolidated view of your digital assets in one place.

Batch Management

BVGS allows for efficient batch operations, enabling you to deposit or withdraw multiple assets in a single transaction. This significantly reduces gas costs and simplifies asset management.

Batch deposit operations

The batch deposit function allows simultaneous deposit of ETH, ERC20 tokens, and NFTs with a single transaction. Here's how the contract handles different asset types:

// Batch deposit function with support for ETH, ERC20 tokens, and NFTs
function batchDeposit(
    uint256 tokenId,
    uint256 amountETH,
    address[] calldata tokenAddresses,
    uint256[] calldata tokenAmounts,
    address[] calldata nftContracts,
    uint256[] calldata nftTokenIds,
    bytes32 referenceId
) external payable nonReentrant {
    _requireOwnsBag(tokenId);
    if (msg.value != amountETH) revert ETHMismatch();
    _batchDeposit(tokenId, amountETH, tokenAddresses, tokenAmounts, nftContracts, nftTokenIds, referenceId);
}

// Internal batch deposit implementation
function _batchDeposit(
    uint256 tokenId,
    uint256 amountETH,
    address[] calldata tokenAddresses,
    uint256[] calldata tokenAmounts,
    address[] calldata nftContracts,
    uint256[] calldata nftTokenIds,
    bytes32 referenceId
) internal {
    if (tokenAddresses.length != tokenAmounts.length || nftContracts.length != nftTokenIds.length) revert MismatchedInputs();
    if (amountETH > 0) _baggedETHs[tokenId].ethAmount += amountETH;

    // Process all ERC20 token deposits
    for (uint256 i; i < tokenAddresses.length; ) {
        _depositERC20(tokenId, tokenAddresses[i], tokenAmounts[i], referenceId);
        unchecked { ++i; }
}

    // Process all NFT deposits
    for (uint256 i; i < nftContracts.length; ) {
        _depositERC721(tokenId, nftContracts[i], nftTokenIds[i], referenceId);
        unchecked { ++i; }
    }
}

The batch deposit process intelligently verifies all inputs and then processes each asset type using the appropriate deposit method. Notice the use of unchecked blocks for gas optimization in the loops.

Batch withdrawal operations

Similarly, the batch unbag function enables withdrawing multiple assets in a single transaction, with just one signature verification. This is especially valuable for efficient portfolio management:

// Single signature verification covers all assets
function batchUnbag(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    uint256 amountETH,
    address[] calldata tokenAddresses,
    uint256[] calldata tokenAmounts,
    address[] calldata nftContracts,
    uint256[] calldata nftTokenIds,
    address recipient,
    bytes32 referenceId,
    uint256 signatureExpiry
) external nonReentrant {
    // Validate key parameters
    _requireOwnsBag(tokenId);
    if (block.timestamp > signatureExpiry) revert SignatureExpired();
    if (recipient == address(0)) revert ZeroAddress();
    
    // Verify signature with all batch data
    bytes memory data = abi.encode(
    tokenId, amountETH, tokenAddresses, tokenAmounts, 
    nftContracts, nftTokenIds, recipient, referenceId, 
    msg.sender, signatureExpiry
);
verifySignature(tokenId, messageHash, signature, address(0), OperationType.BATCH_UNBAG, data);

// Process ETH withdrawal
if (amountETH > 0) {
        if (_baggedETHs[tokenId].ethAmount < amountETH) revert InsufficientETH();
    _baggedETHs[tokenId].ethAmount -= amountETH;
    (bool success, ) = payable(recipient).call{value: amountETH}("");
        if (!success) revert ETHTransferFailed();
}

    // Process all ERC20 token withdrawals
    // [...ERC20 withdrawal logic...]
    
    // Process all NFT withdrawals
    // [...NFT withdrawal logic...]
    
    emit Unbagged(tokenId, referenceId);
}

Key benefits of batch operations

The multi-asset batch operations provide several important advantages:

  • Gas efficiency: Batch operations significantly reduce total gas costs compared to individual transactions
  • Simplified management: Deposit or withdraw multiple assets with a single transaction
  • Unified security: All operations are protected by the same signature verification process
  • Asset diversity: Handle ETH, any ERC20 token, and any ERC721 NFT simultaneously
  • Portfolio rebalancing: Easily adjust your asset allocation in a single transaction
  • Optimized loops: Uses unchecked blocks for gas optimization in loops with array bounds guarantees

Gas optimization note

Due to current Ethereum gas transaction limits, we recommend limiting batch operations to no more than 3 different assets in a single transaction. Exceeding this number may approach block gas limits, especially when transferring multiple NFTs which require more complex storage operations.

Wallet Representation

Your BVGS NFT appears directly in your wallet, representing all contained assets. This provides a seamless user experience where all your stored assets are visible as a single NFT in your wallet, while still maintaining the highest security standards.

Security Benefits

Wallet Attack Protection

BVGS NFTs provide significant security benefits by using a separate signing key that's distinct from your wallet's private key. This architecture creates an additional security layer that protects your assets even if your wallet becomes compromised.

Defense Against Common Wallet Attacks

Attack Scenario: Wallet Private Key Compromise

Traditional Vulnerability: If an attacker obtains your wallet's private key, they typically gain complete control over all assets in that wallet.

Without BVGS Protection:
  • Attacker can transfer all ETH and tokens
  • Attacker can approve malicious contracts to spend tokens
  • All NFTs can be immediately transferred away
With BVGS Protection:
  • Attacker sees the BVGS NFT but cannot transfer it (soulbound)
  • Cannot withdraw assets without the separate BVGS signing key
  • All assets inside the bag remain secure

Security Mechanism: BVGS requires a separate signature from the BVGS key linked to your NFT for any asset withdrawal. Even with your wallet's private key, an attacker cannot generate valid signatures from your BVGS key.

Attack Scenario: Signature Phishing

Traditional Vulnerability: Attackers trick users into signing malicious transactions or permissions that drain their wallets or grant access to their assets.

Without BVGS Protection:
  • One malicious signature can drain entire wallet
  • Users often can't tell what they're signing
  • Approvals may enable ongoing asset theft
With BVGS Protection:
  • Assets in BVGS bags require specific EIP-712 signatures
  • Phished wallet signatures cannot withdraw from bags
  • Separate BVGS key is needed for withdrawals

Security Mechanism: The BVGS contract only accepts properly formatted EIP-712 signatures from the specific BVGS key associated with your NFT. Signatures from your wallet key alone are insufficient.

Attack Scenario: Seed Phrase Compromise

Traditional Vulnerability: If attackers obtain your wallet's seed phrase, they can recreate your wallet and gain full access to all assets.

Without BVGS Protection:
  • Complete wallet takeover on any device
  • All assets can be transferred immediately
  • No secondary verification required
With BVGS Protection:
  • Seed phrase doesn't reveal BVGS signing key
  • Assets within bags remain secured
  • Attacker would need access to both your wallet and BVGS key

Security Mechanism: BVGS keys are generated separately from your wallet and can be stored securely through our key fraction technology or Self custody options. A compromised seed phrase doesn't expose these keys.

Attack Scenario: Blind Signature Attacks

Traditional Vulnerability: Attackers trick users into signing messages with obscured or misleading contents, which are actually authorizations for asset transfers.

Without BVGS Protection:
  • Users may sign messages without understanding implications
  • Signatures might authorize unexpected operations
  • Unclear what action is being authorized
With BVGS Protection:
  • EIP-712 provides structured, human-readable data
  • Operations specify exact amounts and recipients
  • Two-key architecture requires both wallet and BVGS key signatures

Security Mechanism: BVGS uses EIP-712 typed data signatures that clearly display the operation being performed, including specific assets, amounts, and recipients. Additionally, the separation of wallet and BVGS keys provides protection even if one signature is compromised.

Attack Scenario: Malware and RAT Trojan Attacks

Traditional Vulnerability: Sophisticated trojans like StilachiRAT target cryptocurrency wallets to steal private keys and credentials from wallet extensions such as MetaMask.

Without BVGS Protection:
  • Malware can extract keys from memory
  • Private keys stored in browser extensions can be compromised
  • Once extracted, all wallet assets are immediately at risk
With BVGS Protection:
  • BVGS keys are ephemeral - never persistently stored
  • Keys generated on-demand and immediately destroyed after use
  • Compromised wallet doesn't expose assets inside soulbound NFT

Security Mechanism: BVGS protects against advanced malware by using ephemeral key technology. Even if trojans compromise your main wallet's private key, they cannot access assets inside your soulbound BVGS NFT since withdrawals require the secondary ephemeral key that's generated on-demand and immediately destroyed after use.

Attack Scenario: Wallet Drainers and Scam Sites

Traditional Vulnerability: Wallet drainer scams trick users into connecting their wallets to malicious websites that rapidly drain all assets through token approvals or direct transfers.

Without BVGS Protection:
  • Entire wallet can be emptied instantly
  • All approvals can be exploited
  • Drainer scripts operate automatically
With BVGS Protection:
  • BVGS NFTs are soulbound and cannot be transferred
  • All withdrawals require separate EIP-712 signatures
  • Contract interactions with BVGS NFTs are strictly limited

Security Mechanism: BVGS NFTs are soulbound and cannot be drained or transferred from the wallet they are bound to. All content inside requires a separate interaction with the BVGS smart contract, which requires a separate EIP-712 typed signature. All bagged assets remain secure inside the BVGS NFT.

Attack Scenario: Malicious Airdrops

Traditional Vulnerability: Attackers use malicious airdrops to send tokens with exploitative code that can drain wallets when users attempt to interact with the tokens.

Without BVGS Protection:
  • Unexpected tokens appear in wallet
  • Interacting with tokens triggers malicious code
  • Can lead to approval exploits or wallet draining
With BVGS Protection:
  • BVGS NFTs only accept deposits from the token owner
  • No automatic token acceptance mechanism
  • Malicious tokens remain outside the secure bag

Security Mechanism: BVGS NFTs only accept deposits from the token owner, so the contents cannot be compromised by malicious airdrops or random tokens. This provides an extra layer of security against increasingly common airdrop-based attack vectors.

Attack Scenario: Proprietary Wallet Infrastructure Risks

Traditional Vulnerability: Reliance on proprietary wallet infrastructure creates single points of failure, exposes users to vendor-specific risks, and may limit interoperability.

Without BVGS Protection:
  • Vendor-specific security risks
  • Service outages may restrict access to assets
  • Limited wallet compatibility and migration options
With BVGS Protection:
  • Works with any ERC-721 compatible wallet
  • Simple dual-key multisignature system
  • Open and accessible smart contract architecture

Security Mechanism: Unlike complex systems that rely on proprietary wallet infrastructure, BVGS assets are held inside a soulbound NFT adhering to the ERC-721 standard that any Ethereum Self custody wallet can own. The dual-key multisignature system is simple to interact with, and the smart contract is public and always accessible.

Contract Attack Protection

The BVGS smart contracts implement numerous security measures to protect against common attack vectors that target smart contracts. These protections ensure the integrity and security of the contract's operations.

Smart Contract Security Measures

Attack Scenario: Reentrancy Attacks

Vulnerability: In a reentrancy attack, a malicious contract calls back into the victim contract before the first invocation is complete, potentially manipulating state and draining assets.

// Protection against reentrancy attacks
function unbagETH(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    uint256 amount,
    address recipient,
    bool burnToken,
    bytes32 referenceId
) public nonReentrant {
    // ... function logic ...
}

Security Mechanism: All deposit and withdrawal functions in the BVGS contract use OpenZeppelin's nonReentrant modifier, which prevents reentrancy attacks by using a mutex pattern. This ensures that a function cannot be re-entered before its first invocation completes.

Attack Scenario: Signature Replay Attacks

Vulnerability: In a replay attack, a valid signature is reused to execute the same operation multiple times, potentially draining assets repeatedly.

// Nonce management in the SignatureVerification contract
function verifySignature(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    address newBvgsPublicKey,
    OperationType opType,
    bytes memory data
) internal {
    // ... verification logic ...
    
    // Increment nonce after successful verification
    tokenData.nonce++;
    
    // ... additional logic ...
}

Security Mechanism: BVGS incorporates a nonce-based signature verification system. Each signature includes the current nonce value, and after verification, the nonce is incremented. This prevents the same signature from being used twice, as the nonce value would no longer match.

Attack Scenario: Transaction Ordering Manipulation

Vulnerability: Attackers can observe pending transactions and insert their own transactions with higher gas prices to manipulate execution order for profit (front-running).

Traditional Risks:
  • Front-running valuable transactions
  • Sandwich attacks on swap transactions
  • Racing to claim limited opportunities
BVGS Protection:
  • All operations require unique signatures
  • Operations are tied to specific recipients
  • No shared pools that can be manipulated

Security Mechanism: BVGS uses a direct asset mapping approach where each operation is explicitly authorized for a specific recipient with an exact amount. Transaction ordering cannot be exploited because each transaction's outcome is deterministic and not dependent on the state of a shared pool or global price.

Attack Scenario: Unauthorized Function Access

Vulnerability: Without proper access controls, attackers might call sensitive functions to manipulate contract state or steal assets.

// Access control in contract functions
function unbagETH(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    uint256 amount,
    address recipient,
    bool burnToken,
    bytes32 referenceId
) public nonReentrant {
    if (ownerOf(tokenId) != msg.sender) revert NotOwner();
    
    // Verify the signature from the BVGS key
    bytes memory data = abi.encode(amount, recipient, burnToken, referenceId);
    verifySignature(tokenId, messageHash, signature, address(0), OperationType.UNBAG_ETH, data);
    
    // ... additional logic ...
}

Security Mechanism: BVGS implements strict access controls on all functions. Deposit functions verify the caller owns the NFT. Withdrawal functions require both ownership verification and a valid signature from the BVGS key associated with the NFT, creating a dual-authorization requirement.

Key Management

Key Fraction Technology

BVGS implements a unique key fraction technology that enhances security through a multi-party computation approach to key generation and management. This technology ensures private keys are never stored in their entirety—instead, they're deterministically derived when needed through a secure process.

Zero Trust Architecture

Our key fraction technology implements zero trust principles. BVGS never stores complete private keys—not even temporarily. Keys are derived on-demand and immediately deleted from memory after use.

How Key Fraction Technology Works

  1. User Authentication: You sign a unique message that includes a random "message key fraction" generated for your session.

  2. Server-Side Key Fraction: BVGS generates a random key fraction that's encrypted and stored securely in Google Cloud KMS (Key Management Service). This is protected by Hardware Security Modules (HSM) with FIPS level 3 compliance for enterprise-grade security.

  3. Ephemeral Key Derivation: When needed, your signature is combined with the server-side key fraction to deterministically derive the public/private key pair through a cryptographic hash function. This creates a unique, one-time-use signing key that exists only for the duration of the specific operation.

  4. Expiring Signatures: All generated signatures have a short lifespan and include operation-specific data that prevents them from being reused for other purposes. This time-bound approach provides enhanced security by limiting the window of opportunity for potential attacks.

  5. Immediate Cleanup: After the operation is completed, the derived private key is immediately removed from memory. Keys must be rederived for every operation, ensuring no persistent key material exists that could be compromised.

Security Benefits

  • No complete private key is ever stored anywhere
  • Both user input (signature) and server-side fraction are required to derive the key
  • Server-side key fractions are encrypted at rest
  • Advanced plan users benefit from HSM protection for their key fractions
  • Key rotation supported for enhanced security

Key Export & Recovery

BVGS provides you with the encrypted key fraction during setup, which you can securely store as a backup. This fraction is essential for key recovery and export:

Key Export Process

  1. When you need to export your private key, you'll need to provide:
    • Your wallet signature
    • The encrypted key fraction you received during setup
    • Valid 2FA code as proof of ownership
  2. Once verified, BVGS temporarily reconstructs your private key for export
  3. The key is securely delivered to you and immediately purged from our systems

Key Ownership

Even though BVGS manages the key fraction technology, you always maintain ultimate ownership of your keys. You can export your complete key details whenever needed by proving ownership through our verification process.

2FA Protection

BVGS implements robust 2FA (Two-Factor Authentication) protection using the same trustless key derivation approach:

Trustless TOTP Management

  • BVGS creates TOTP (Time-based One-Time Password) secrets using the same trustless key derivation process used for BVGS key fractions
  • Like key fractions, TOTP secrets are never stored in their complete form anywhere
  • The 2FA code is required to decrypt associated BVGS key fractions and export them
  • Unlike traditional web2 systems, BVGS doesn't store any TOTP secrets - they are trustlessly derived on-demand
  • We verify the code against your authentication application to guarantee security

Compatible Applications

BVGS 2FA is compatible with industry-standard authenticator applications:

Google Authenticator
Twilio Authy
Microsoft Authenticator
And other TOTP-compatible apps

Self custody signing

While BVGS provides a secure key fraction service, users can alternatively opt for complete Self custody of their keys. This option gives you full control and responsibility over the key management process.

Self custody vs. BVGS key fractions

FeatureBVGS key fractionSelf custody
Key storageEncrypted key fraction in KMSUser's responsibility
Security levelMulti-party computation with 2FADependent on user's security measures
RecoverySupported through recovery processUser's responsibility
Two-factor authenticationIncludedIncluded
Site/web app usageUnlimitedUnlimited

Using Self custody mode

To use Self custody mode for signing transactions, follow these steps:

  1. Copy the JSON provided to you during your unbagging authentication process into Remix
  2. Verify the parameters are correct
  3. Sign into Remix with your secondary signer
  4. Right click the file holding the JSON and click sign
  5. Copy the signature provided in the terminal to the frontend modal
  6. Click submit signature

Interactive EIP-712 message builder

DOMAIN:
  name: "BVGS"
  version: "1"
  chainId: 1
  verifyingContract: 0x1234...5678

TYPE: Operation
  tokenId: uint256
  nonce: uint256
  opType: uint8
  dataHash: bytes32

VALUE:
  tokenId: 123
  nonce: 45
  opType: 1
  dataHash: [automatically generated from the parameters below]

OPERATION DATA (1):
  signatureExpiry: 1688654321 (expires: in 15 minutes)
  amount: 1000000000000000000 wei
  recipient: 0xabcd...ef01
Wallet: Not connected
Note: Your signature will be valid for 15 minutes from the time it was generated.Generated during signature request
No signature generated yet. Click "Sign with Wallet" above to generate a signature.

Important Security Notice

With Self custody, you are fully responsible for securing your private keys. Loss of your private key will result in permanent loss of access to your bagged assets. Make sure to implement proper backup procedures.

Key Rotation

BVGS implements a key rotation mechanism that allows users to update the BVGS public key associated with their bag. This enhances security by allowing periodic updates of authorization keys.

Rotation Process

/**
 * @dev Rotates the BVGS NFT public key associated with a BVGS NFT, after signature verification.
 * @param tokenId The ID of the BVGS NFT for which to rotate the key.
 * @param messageHash The message hash that the BVGS NFT private key signed.
 * @param signature The BVGS NFT private key signature for verification.
 * @param newPublicKey The new BVGS NFT public key.
 * @param referenceId An external reference for off-chain tracking.
 * @param signatureExpiry UNIX timestamp until which the signature is valid.
 */
function rotateBvgsKey(
    uint256 tokenId,
    bytes32 messageHash,
    bytes memory signature,
    address newPublicKey,
    bytes32 referenceId,
    uint256 signatureExpiry
) external {
    _requireOwnsBag(tokenId);
    if (block.timestamp > signatureExpiry) revert SignatureExpired();

    bytes memory data = abi.encode(tokenId, newPublicKey, referenceId, msg.sender, signatureExpiry);
    verifySignature(tokenId, messageHash, signature, newPublicKey, OperationType.ROTATE_KEY, data);
    emit KeyRotated(tokenId, referenceId);
}

Key rotation requires a valid signature from the current BVGS public key, ensuring that only authorized parties can update keys.

Parameters & Flow

Key Parameters

  • tokenId: Identifies the specific BVGS NFT whose key is being rotated
  • messageHash: A cryptographic hash of the message that was signed by the current BVGS private key
  • signature: The cryptographic signature generated by the current BVGS private key
  • newPublicKey: The address of the new public key that will replace the current one
  • referenceId: An external identifier for tracking and audit purposes
  • signatureExpiry: A timestamp that defines when the signature becomes invalid, adding time-bound security

Rotation Security Flow

  1. Ownership verification: Ensures only the owner of the BVGS NFT can initiate key rotation
  2. Signature expiry check: Verifies the rotation request hasn't expired, preventing replay attacks
  3. Data bundling: Combines all critical parameters into a single data structure for verification
  4. Signature verification: Confirms the operation is authorized by the current BVGS key
  5. Key update: Replaces the old BVGS public key with the new one once verification succeeds
  6. Event emission: Records the key rotation for transparency and audit purposes

Security Best Practices

  • Rotate keys periodically (e.g., every 90 days)
  • Immediately rotate keys if you suspect any compromise
  • Use hardware wallets or secure key management solutions for BVGS keys
  • Maintain secure backups of your keys

Security & Authorization

EIP-712 Signatures

BVGS uses EIP-712 signatures for secure transaction authorization. This standard provides a better user experience by showing users a structured and readable message when signing transactions.

What is EIP-712?

EIP-712 is an Ethereum Improvement Proposal that defines a standard for signing typed structured data. It improves upon simple message signing by:

  • Providing human-readable data in signing interfaces
  • Enabling structured data with types
  • Preventing signature replay across different domains
  • Ensuring consistent hashing across implementations

Implementation in BVGS

In our contracts, EIP-712 is implemented through OpenZeppelin's EIP712 contract:

// Type hash for operations
bytes32 private constant OPERATION_TYPEHASH =
    keccak256("Operation(uint256 tokenId,uint256 nonce,uint8 opType,bytes32 dataHash)");

constructor(address erc721Address)
    EIP712("BVGS", "1")
{
    _erc721 = ERC721(erc721Address);
}

When a user initiates an operation like withdrawing assets, the frontend will:

  1. Construct the typed data according to EIP-712 specification
  2. Request the user to sign this data with their wallet
  3. Submit the signature along with the operation parameters to the contract
  4. The contract will verify the signature before executing the operation
// EIP-712 domain and types
const domain = {
    name: "BVGS",
    version: "1",
    chainId: chainId,
    verifyingContract: BVGS_CONTRACT_ADDRESS
};

const types = {
    Operation: [
        { name: "tokenId", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "opType", type: "uint8" },
        { name: "dataHash", type: "bytes32" }
    ]
};

Security Measures

BVGS contracts implement multiple security measures to protect user assets and ensure secure operations.

Reentrancy Protection

All functions that transfer assets implement OpenZeppelin's ReentrancyGuard to prevent reentrancy attacks.

EIP-712 Compliance

EIP-712 signatures with nonces prevent replay attacks and ensure only authorized operations.

Transfer Restrictions

Bags cannot be transferred, preventing unauthorized movement of assets.

EIP-5192 Compliance

All BVGS NFTs implement the EIP-5192 soulbound standard, ensuring they remain permanently locked to their owners.

Direct contract interaction

All BVGS contracts are verified on Etherscan, allowing for direct interaction with the contracts without requiring our frontend interface. This provides an additional layer of security and accessibility, ensuring your assets remain accessible even if our website becomes unavailable.

Benefits of verified contracts

  • Full transparency of contract code
  • Direct interaction with contract functions
  • Ability to verify transaction details
  • Independence from BVGS frontend
  • Permanent access to your assets

Interacting with BVGS contracts on Etherscan

Step 1: Find the verified contract

Navigate to Etherscan and search for the BVGS contract address:

0x6cC1ACE12eAafbBedB41560Cc48856f3f4fcd6b9

Alternatively, you can search by name "BVGS" in the Etherscan search bar, but be sure to verify you're interacting with the official contract.

Step 2: Navigate to the "Contract" tab

Once on the contract page, click on the "Contract" tab to see the verified contract code and interact with it.

Transactions
Contract
Events
Analytics

Step 3: Use the "Read Contract" section

The "Read Contract" section lets you query contract state without spending gas:

  • getFullBag(tokenId) - View all assets stored in your BVGS NFT
  • locked(tokenId) - Verify if a token is soulbound (always returns true)
  • ownerOf(tokenId) - Check the owner of a specific BVGS NFT
  • getActiveBvgsPublicKeyForToken(tokenId) - Get the current signing key for a bag
  • getNonce(tokenId) - Get the current nonce for a bag (needed for signatures)

Note: Some functions are restricted to the token owner. If you're not connected with the owner's wallet, you'll receive an error when calling these functions.

Step 4: Use the "Write Contract" section

The "Write Contract" section allows you to modify contract state by executing transactions:

Connect your wallet

Before interacting with write functions, connect your wallet by clicking "Connect to Web3" at the top of the Write Contract section.

Common write functions
  • depositETH(tokenId, referenceId) - Deposit ETH into your bag
  • depositERC20(tokenId, tokenAddress, amount, referenceId) - Deposit ERC20 tokens
  • depositERC721(tokenId, nftContract, nftTokenId, referenceId) - Deposit NFTs
  • unbagETH(...) - Withdraw ETH (requires signature)
  • unbagERC20(...) - Withdraw ERC20 tokens (requires signature)
  • unbagERC721(...) - Withdraw NFTs (requires signature)
  • rotateBvgsKey(...) - Change the signing key (requires signature)

Important: For functions requiring signatures (unbag operations), you'll need to generate EIP-712 signatures. This typically requires using our frontend or signing utilities like eth-sig-util. The process is complex and requires precise formatting of message data.

Example: Checking your bag contents

Let's walk through the process of checking what's in your BVGS NFT:

  1. Navigate to the "Read Contract" section
  2. Find the getFullBag function
  3. Enter your token ID in the input field
  4. Click the "Query" button
  5. The function will return three values:
    • bagETH: The amount of ETH stored (in wei)
    • erc20Tokens: Array of token addresses and balances
    • nfts: Array of NFT contracts and token IDs
Example Response
[bagETH]: 1000000000000000000
[erc20Tokens]: [
  [0]: {
    tokenAddress: 0x6B175474E89094C44Da98b954EedeAC495271d0F,
    balance: 50000000000000000000
  }
]
[nfts]: [
  [0]: {
    nftContract: 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D,
    nftTokenId: 1234
  }
]

Advanced tips for Etherscan interaction

Using the correct network

Make sure you're using Ethereum Mainnet when accessing the BVGS contract. The contract is deployed on Ethereum Mainnet only, and the verification and interaction features will only work with the correct network selected.

Reading events

Check the "Events" tab to see all emitted events from the contract, including deposits, withdrawals, and key rotations. This can be useful for tracking activity or verifying operations.

Decoding transaction input data

When viewing a transaction involving the BVGS contract, Etherscan will automatically decode the function call and parameters due to contract verification, making it easier to understand what each transaction does.

Verifying contract addresses

Always double-check contract addresses by comparing them with official sources. The verified contract will have a green checkmark and the "Contract" label next to the address on Etherscan.

FAQ & Support

Frequently Asked Questions

BVGS is a secure platform for managing digital assets (ETH, ERC20 tokens, and NFTs) through smart contract "bags" that provide enhanced security features through EIP-712 signatures.

BVGS is built to defend against many common Self custody wallet attacks:

  • Wallet private key or seed phrase compromise:
    If your wallet's private key or seed phrase is compromised, attackers could easily transfer or drain your funds. With BVGS, your assets are held inside a soulbound NFT, and withdrawals require a separate BVGS signature derived from an ephemeral key. Even if your main key is compromised, the BVGS NFT and its bagged assets remain secure. The attacker would need BOTH your compromised wallet keys AND the secondary BVGS NFT key. With 2FA protection, an attacker would also need access to your mobile device to recreate your secondary key. You can quickly transfer your assets from your BVGS NFT to a non-compromised wallet.
  • Signature phishing defense:
    BVGS only accepts structured EIP‑712 signatures generated by your BVGS key—not your wallet's key alone. EIP-712 authenticates individual details such as token amounts, addresses, recipients, chain IDs, and expirations, ensuring operation data remains transparent and providing additional security against phished signatures.
  • Wallet drainers and scam sites:
    BVGS NFTs are soulbound and cannot be drained or transferred from the wallet they are bound to. All content inside requires a separate interaction with the BVGS smart contract, which requires a separate EIP-712 typed signature. All bagged assets remain secure inside the BVGS NFT, allowing you to quickly transfer them to a non-compromised wallet if needed.
  • Malicious airdrops:
    BVGS NFTs only accept deposits from the token owner, so the contents cannot be compromised by malicious airdrops or random tokens.
  • Problems with proprietary wallet infrastructure:
    Unlike complex systems that rely on proprietary wallet infrastructure, BVGS assets are held inside a soulbound NFT adhering to the ERC-721 standard that any Ethereum Self custody wallet can own. The dual-key multisignature system is simple to interact with, and the smart contract is public and always accessible, ensuring no reliance on centralized infrastructure. It has no tokenomics and no dependence on off-chain signals, reducing friction even further.

Key fraction technology ensures that no complete private key is ever stored anywhere. The key is derived on-demand from a combination of your signature (which only you can produce) and an encrypted server-side key fraction. This means that an attacker would need both your wallet and access to our secure cloud infrastructure to compromise your keys. After use, the derived private key is immediately deleted from memory. Additionally, we hash resulting signatures with time-based expiry parameters to invalidate signatures after a set period, preventing long-term vulnerability. Two-factor authentication (2FA) is required to access the decrypted fractions during key recreation and the signing process, adding another critical layer of security.

With our key fraction technology, control is shared. You maintain control through your wallet's signature capability, and BVGS stores a necessary key fraction. Neither party alone can derive the private key. A valid 2FA code is also required for any key derivation process, and the 2FA system itself is built using key fraction technology, ensuring no single entity has access to complete keys. Alternatively, you can choose Self custody mode, where you manage your keys entirely without BVGS involvement.

The BVGS smart contracts are completely autonomous and operate independently on the blockchain. Even if BVGS services become unavailable, your assets remain safe and accessible. You can:

  • Interact directly with the smart contracts using blockchain tools
  • Generate your own EIP-712 signatures if you have your private key (either Self custody or exported)
  • Use alternative frontends or tools developed by the community

This contract independence is why we offer Self custody options and always provide the ability to export your keys if using our hosted key fraction technology, which eliminates any dependency on our servers for key generation and validation. We always recommend exporting and securely backing up your keys.

No, BVGS does not have its own token or tokenomics. We operate on a platform fee model, charging one-time fees for key management services. This approach allows us to provide consistent service without token price volatility.

BVGS uses EIP-712 signatures to authorize operations. Each BVGS NFT has an associated public key that must sign operations like withdrawals. This provides an additional layer of security beyond just owning the NFT. BVGS key fractions are convenient because they automate key derivation and signing technology with 2FA integration. With the BVGS secured keys option, a valid 2FA code combined with a valid wallet signature will recreate the original key used to sign for operations, streamlining the entire process. Alternatively, in Self custody mode, you can sign transactions yourself by receiving the EIP-712 JSON format to sign with external signing tools like Remix.

No, BVGS bags are non-transferable by design. This is a security feature that prevents unauthorized movement of the assets contained within the bag.

If using our hosted key fraction technology, we ensure all key fractions are encrypted and stored with all relevant backups. As long as you have paid the platform fee and can verify ownership of your wallet private key, encrypted fraction, and 2FA code, you will always be able to recover your keys. If using Self custody, you are responsible for managing your own keys and we unfortunately cannot help you recover your Self custody keys.

Troubleshooting

We're here to help if you encounter any issues. Here are some common problems and their solutions:

  • If you're having trouble connecting your wallet, make sure you're using the correct wallet provider and network. Try refreshing the page or using a different browser if issues persist.
  • If you're unable to sign transactions, check your wallet's transaction history to see if there are any pending transactions that need to be addressed first.
  • For any issues with the signature verification process, try refreshing your session and attempting again. If problems persist, please contact our support team for assistance.
  • For "Injected Provider" errors, ensure that your wallet extension is properly installed and updated to the latest version.
  • When interacting with dApps, make sure to grant the necessary permissions when prompted by your wallet.
  • If your wallet keeps disconnecting from the dApp, try enabling the "Always connect to this site" option in your wallet settings.
  • Ensure 2FA is setup and fully confirmed before accessing sensitive functions.

Contact Support

Our support team is ready to assist you with any questions or issues you may encounter.

[email protected]