Building a Decentralized Credit Card System Part 2: Solidity Smart Contract Implementation

In the first part of this series, we explored the conceptual architecture of a blockchain-based credit card system using multi-signature keys and encrypted spending limits. Now, let’s dive into the technical implementation with concrete Solidity examples.

This post will give you production-ready smart contract code that demonstrates how to build a secure, multi-signature credit card system on Ethereum or any EVM-compatible blockchain.

Smart Contract Architecture Overview

Our decentralized credit card system consists of five interconnected smart contracts:

  1. CreditFacility Contract: Manages the master account and credit line
  2. CardManager Contract: Handles individual card issuance and lifecycle
  3. SpendingLimits Contract: Enforces encrypted spending rules
  4. PaymentProcessor Contract: Executes and settles transactions
  5. MultiSigGovernance Contract: Handles high-value transaction approvals

Each contract has a specific responsibility, following the principle of separation of concerns. This modular approach makes the system more maintainable, upgradeable, and secure.

Note: These contracts are designed for educational and proof-of-concept purposes. Production deployment would require extensive security audits, gas optimization, and integration with off-chain systems.

1. The Credit Facility Contract

This contract represents the bank account or credit line, the source of funds controlled by the master key. It implements multi-signature controls to ensure that no single party can unilaterally make critical decisions.

Key Features

  • Multi-signature authorization for sensitive operations
  • Credit limit management with approval workflows
  • Real-time tracking of available credit and outstanding balance
  • Support for multiple authorized signers per account
  • Emergency account suspension capabilities

The Complete Contract

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

/**
 * @title CreditFacility
 * @dev Manages the master credit account with multi-sig controls
 */
contract CreditFacility {

    struct CreditAccount {
        uint256 creditLimit;
        uint256 availableCredit;
        uint256 outstandingBalance;
        bool isActive;
        address[] authorizedSigners;
        uint256 requiredSignatures;
    }

    mapping(address => CreditAccount) public accounts;
    mapping(address => mapping(bytes32 => uint256)) public transactionApprovals;

    event CreditAccountCreated(address indexed account, uint256 creditLimit);
    event CreditLimitUpdated(address indexed account, uint256 newLimit);
    event CreditUsed(address indexed account, uint256 amount);
    event CreditRepaid(address indexed account, uint256 amount);
    event TransactionApproved(address indexed signer, bytes32 transactionHash);

    modifier onlyAccountOwner(address account) {
        require(isAuthorizedSigner(account, msg.sender), "Not authorized");
        _;
    }

    modifier accountActive(address account) {
        require(accounts[account].isActive, "Account not active");
        _;
    }

    function createCreditAccount(
        address accountAddress,
        uint256 creditLimit,
        address[] memory signers,
        uint256 requiredSigs
    ) external {
        require(signers.length >= requiredSigs, "Invalid signer configuration");
        require(!accounts[accountAddress].isActive, "Account already exists");

        accounts[accountAddress] = CreditAccount({
            creditLimit: creditLimit,
            availableCredit: creditLimit,
            outstandingBalance: 0,
            isActive: true,
            authorizedSigners: signers,
            requiredSignatures: requiredSigs
        });

        emit CreditAccountCreated(accountAddress, creditLimit);
    }

    function updateCreditLimit(
        address account,
        uint256 newLimit,
        bytes32 approvalHash
    ) external onlyAccountOwner(account) accountActive(account) {
        require(hasRequiredApprovals(account, approvalHash), "Insufficient approvals");

        CreditAccount storage creditAccount = accounts[account];
        uint256 difference = newLimit > creditAccount.creditLimit 
            ? newLimit - creditAccount.creditLimit 
            : 0;

        creditAccount.creditLimit = newLimit;
        creditAccount.availableCredit += difference;

        emit CreditLimitUpdated(account, newLimit);
        clearApprovals(account, approvalHash);
    }

    function useCredit(
        address account,
        uint256 amount
    ) external accountActive(account) returns (bool) {
        CreditAccount storage creditAccount = accounts[account];
        require(creditAccount.availableCredit >= amount, "Insufficient credit");

        creditAccount.availableCredit -= amount;
        creditAccount.outstandingBalance += amount;

        emit CreditUsed(account, amount);
        return true;
    }

    function repayCredit(
        address account,
        uint256 amount
    ) external payable accountActive(account) {
        require(msg.value >= amount, "Insufficient payment");

        CreditAccount storage creditAccount = accounts[account];
        require(creditAccount.outstandingBalance >= amount, "Overpayment");

        creditAccount.outstandingBalance -= amount;
        creditAccount.availableCredit += amount;

        emit CreditRepaid(account, amount);
    }

    function approveTransaction(
        address account,
        bytes32 transactionHash
    ) external onlyAccountOwner(account) {
        transactionApprovals[account][transactionHash]++;
        emit TransactionApproved(msg.sender, transactionHash);
    }

    function hasRequiredApprovals(
        address account,
        bytes32 transactionHash
    ) public view returns (bool) {
        return transactionApprovals[account][transactionHash] >= 
               accounts[account].requiredSignatures;
    }

    function isAuthorizedSigner(
        address account,
        address signer
    ) public view returns (bool) {
        address[] memory signers = accounts[account].authorizedSigners;
        for (uint i = 0; i < signers.length; i++) {
            if (signers[i] == signer) return true;
        }
        return false;
    }

    function clearApprovals(address account, bytes32 transactionHash) internal {
        delete transactionApprovals[account][transactionHash];
    }

    function getAccountDetails(address account) external view returns (
        uint256 creditLimit,
        uint256 availableCredit,
        uint256 outstandingBalance,
        bool isActive
    ) {
        CreditAccount storage acc = accounts[account];
        return (
            acc.creditLimit,
            acc.availableCredit,
            acc.outstandingBalance,
            acc.isActive
        );
    }
}Code language: JavaScript (javascript)

Understanding the Code

Let’s break down the key components of this contract.

CreditAccount Structure

The CreditAccount struct stores all essential information about a credit account. It tracks the credit limit, available credit, outstanding balance, activation status, and the multi-signature configuration. This structure ensures that all account data is organized and easily accessible.

Multi-Signature Security

The contract implements a flexible multi-signature system. When creating an account, you specify both the authorized signers and how many signatures are required for critical operations. For example, a business account might have five authorized signers but only require three signatures to approve a credit limit increase.

The approveTransaction function allows authorized signers to vote on proposed actions. Once enough approvals are collected, the action can be executed. This prevents any single compromised key from causing damage to the system.

Credit Management

The useCredit and repayCredit functions handle the core financial operations. When a card makes a purchase (which we’ll see in later contracts), it calls useCredit to deduct from the available balance. When a payment is made, repayCredit restores the available credit.

Security Feature: Notice how credit operations include checks for account status and available balance. These guards prevent overdrafts and ensure the account is active before any transaction proceeds.

2. The Card Manager Contract

While the Credit Facility manages the master account, individual cards need their own management layer. The Card Manager contract handles card issuance, activation, deactivation, and the assignment of spending limits to individual cards.

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

interface ICreditFacility {
    function useCredit(address account, uint256 amount) external returns (bool);
    function accounts(address) external view returns (
        uint256 creditLimit,
        uint256 availableCredit,
        uint256 outstandingBalance,
        bool isActive
    );
}

contract CardManager {

    struct Card {
        address cardAddress;
        address linkedAccount;
        bool isActive;
        uint256 dailyLimit;
        uint256 monthlyLimit;
        uint256 perTransactionLimit;
        uint256 dailySpent;
        uint256 monthlySpent;
        uint256 lastResetDay;
        uint256 lastResetMonth;
        string cardholderName;
        bytes32 cardType;
    }

    ICreditFacility public creditFacility;

    mapping(address => Card) public cards;
    mapping(address => address[]) public accountCards;
    mapping(address => bool) public cardExists;

    event CardIssued(
        address indexed cardAddress,
        address indexed account,
        string cardholderName
    );
    event CardActivated(address indexed cardAddress);
    event CardDeactivated(address indexed cardAddress);
    event CardLimitsUpdated(address indexed cardAddress);
    event SpendingRecorded(address indexed cardAddress, uint256 amount);

    modifier onlyActiveCard(address cardAddress) {
        require(cards[cardAddress].isActive, "Card not active");
        _;
    }

    modifier cardOwner(address cardAddress) {
        require(msg.sender == cardAddress, "Not card owner");
        _;
    }

    constructor(address _creditFacility) {
        creditFacility = ICreditFacility(_creditFacility);
    }

    function issueCard(
        address cardAddress,
        address account,
        string memory cardholderName,
        uint256 dailyLimit,
        uint256 monthlyLimit,
        uint256 perTransactionLimit,
        bytes32 cardType
    ) external {
        require(!cardExists[cardAddress], "Card already exists");

        cards[cardAddress] = Card({
            cardAddress: cardAddress,
            linkedAccount: account,
            isActive: true,
            dailyLimit: dailyLimit,
            monthlyLimit: monthlyLimit,
            perTransactionLimit: perTransactionLimit,
            dailySpent: 0,
            monthlySpent: 0,
            lastResetDay: block.timestamp / 1 days,
            lastResetMonth: getMonthFromTimestamp(block.timestamp),
            cardholderName: cardholderName,
            cardType: cardType
        });

        accountCards[account].push(cardAddress);
        cardExists[cardAddress] = true;

        emit CardIssued(cardAddress, account, cardholderName);
    }

    function activateCard(address cardAddress) external {
        require(cardExists[cardAddress], "Card does not exist");
        cards[cardAddress].isActive = true;
        emit CardActivated(cardAddress);
    }

    function deactivateCard(address cardAddress) external {
        require(cardExists[cardAddress], "Card does not exist");
        cards[cardAddress].isActive = false;
        emit CardDeactivated(cardAddress);
    }

    function updateCardLimits(
        address cardAddress,
        uint256 dailyLimit,
        uint256 monthlyLimit,
        uint256 perTransactionLimit
    ) external {
        require(cardExists[cardAddress], "Card does not exist");

        Card storage card = cards[cardAddress];
        card.dailyLimit = dailyLimit;
        card.monthlyLimit = monthlyLimit;
        card.perTransactionLimit = perTransactionLimit;

        emit CardLimitsUpdated(cardAddress);
    }

    function checkAndRecordSpending(
        address cardAddress,
        uint256 amount
    ) external onlyActiveCard(cardAddress) returns (bool) {
        Card storage card = cards[cardAddress];

        resetSpendingIfNeeded(cardAddress);

        require(amount <= card.perTransactionLimit, "Exceeds per-transaction limit");
        require(card.dailySpent + amount <= card.dailyLimit, "Exceeds daily limit");
        require(card.monthlySpent + amount <= card.monthlyLimit, "Exceeds monthly limit");

        card.dailySpent += amount;
        card.monthlySpent += amount;

        emit SpendingRecorded(cardAddress, amount);
        return true;
    }

    function resetSpendingIfNeeded(address cardAddress) internal {
        Card storage card = cards[cardAddress];
        uint256 currentDay = block.timestamp / 1 days;
        uint256 currentMonth = getMonthFromTimestamp(block.timestamp);

        if (currentDay > card.lastResetDay) {
            card.dailySpent = 0;
            card.lastResetDay = currentDay;
        }

        if (currentMonth > card.lastResetMonth) {
            card.monthlySpent = 0;
            card.lastResetMonth = currentMonth;
        }
    }

    function getMonthFromTimestamp(uint256 timestamp) internal pure returns (uint256) {
        return timestamp / 30 days;
    }

    function getCardDetails(address cardAddress) external view returns (
        address linkedAccount,
        bool isActive,
        uint256 dailyLimit,
        uint256 monthlyLimit,
        uint256 perTransactionLimit,
        uint256 dailySpent,
        uint256 monthlySpent,
        string memory cardholderName
    ) {
        Card storage card = cards[cardAddress];
        return (
            card.linkedAccount,
            card.isActive,
            card.dailyLimit,
            card.monthlyLimit,
            card.perTransactionLimit,
            card.dailySpent,
            card.monthlySpent,
            card.cardholderName
        );
    }

    function getAccountCards(address account) external view returns (address[] memory) {
        return accountCards[account];
    }

    function getRemainingDailyLimit(address cardAddress) external view returns (uint256) {
        Card storage card = cards[cardAddress];
        if (card.dailySpent >= card.dailyLimit) return 0;
        return card.dailyLimit - card.dailySpent;
    }

    function getRemainingMonthlyLimit(address cardAddress) external view returns (uint256) {
        Card storage card = cards[cardAddress];
        if (card.monthlySpent >= card.monthlyLimit) return 0;
        return card.monthlyLimit - card.monthlySpent;
    }
}Code language: PHP (php)

Key Features of Card Manager

The Card Manager introduces several important concepts:

Individual Card Limits

Each card has three types of limits:

  • Per-transaction limit: Maximum amount for a single purchase
  • Daily limit: Maximum spending in a 24-hour period
  • Monthly limit: Maximum spending in a 30-day period

This multi-tiered approach provides granular control over spending patterns and helps prevent fraud.

Automatic Limit Resets

The contract automatically resets daily and monthly spending counters. The resetSpendingIfNeeded function checks if a new day or month has begun and resets the appropriate counters. This happens transparently during transaction validation.

Card Lifecycle Management

Cards can be issued, activated, and deactivated. A deactivated card cannot make purchases, but the card data remains on-chain for historical records. This is crucial for fraud prevention, if a card is compromised, it can be immediately deactivated without affecting other cards linked to the same account.

3. The Spending Limits Contract with Encryption

Now we get to the interesting part: encrypted spending limits. This contract demonstrates how to store and validate spending rules while keeping certain parameters private using commitment schemes.

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

contract SpendingLimits {

    struct EncryptedLimit {
        bytes32 limitCommitment;
        bytes32 categoryCommitment;
        bool isActive;
        uint256 validUntil;
        bytes32[] allowedMerchantCategories;
        string[] restrictedCountries;
    }

    mapping(address => mapping(uint256 => EncryptedLimit)) public cardLimits;
    mapping(address => uint256) public limitCount;

    event LimitCreated(
        address indexed cardAddress,
        uint256 limitId,
        bytes32 limitCommitment
    );
    event LimitValidated(address indexed cardAddress, uint256 limitId, bool success);
    event LimitRevoked(address indexed cardAddress, uint256 limitId);

    function createEncryptedLimit(
        address cardAddress,
        bytes32 limitCommitment,
        bytes32 categoryCommitment,
        uint256 validUntil,
        bytes32[] memory allowedCategories,
        string[] memory restrictedCountries
    ) external returns (uint256) {
        uint256 limitId = limitCount[cardAddress];

        cardLimits[cardAddress][limitId] = EncryptedLimit({
            limitCommitment: limitCommitment,
            categoryCommitment: categoryCommitment,
            isActive: true,
            validUntil: validUntil,
            allowedMerchantCategories: allowedCategories,
            restrictedCountries: restrictedCountries
        });

        limitCount[cardAddress]++;

        emit LimitCreated(cardAddress, limitId, limitCommitment);
        return limitId;
    }

    function validateTransaction(
        address cardAddress,
        uint256 limitId,
        uint256 amount,
        bytes32 merchantCategory,
        string memory country,
        bytes32 proof
    ) external view returns (bool) {
        EncryptedLimit storage limit = cardLimits[cardAddress][limitId];

        require(limit.isActive, "Limit not active");
        require(block.timestamp <= limit.validUntil, "Limit expired");

        bytes32 computedCommitment = keccak256(
            abi.encodePacked(amount, merchantCategory, country, proof)
        );

        if (computedCommitment != limit.limitCommitment) {
            return false;
        }

        if (!isCategoryAllowed(limit.allowedMerchantCategories, merchantCategory)) {
            return false;
        }

        if (isCountryRestricted(limit.restrictedCountries, country)) {
            return false;
        }

        return true;
    }

    function isCategoryAllowed(
        bytes32[] memory allowedCategories,
        bytes32 category
    ) internal pure returns (bool) {
        if (allowedCategories.length == 0) return true;

        for (uint i = 0; i < allowedCategories.length; i++) {
            if (allowedCategories[i] == category) return true;
        }
        return false;
    }

    function isCountryRestricted(
        string[] memory restrictedCountries,
        string memory country
    ) internal pure returns (bool) {
        for (uint i = 0; i < restrictedCountries.length; i++) {
            if (keccak256(bytes(restrictedCountries[i])) == keccak256(bytes(country))) {
                return true;
            }
        }
        return false;
    }

    function revokeLimit(address cardAddress, uint256 limitId) external {
        require(cardLimits[cardAddress][limitId].isActive, "Limit already inactive");
        cardLimits[cardAddress][limitId].isActive = false;
        emit LimitRevoked(cardAddress, limitId);
    }

    function getLimitDetails(
        address cardAddress,
        uint256 limitId
    ) external view returns (
        bytes32 limitCommitment,
        bool isActive,
        uint256 validUntil,
        bytes32[] memory allowedCategories,
        string[] memory restrictedCountries
    ) {
        EncryptedLimit storage limit = cardLimits[cardAddress][limitId];
        return (
            limit.limitCommitment,
            limit.isActive,
            limit.validUntil,
            limit.allowedMerchantCategories,
            limit.restrictedCountries
        );
    }
}Code language: JavaScript (javascript)

How Encrypted Limits Work

The encryption here uses a commitment scheme, a cryptographic technique where you commit to a value without revealing it.

Creating a Commitment

When you create a spending limit, instead of storing the actual amount on-chain (which would be publicly visible), you store a hash commitment:

limitCommitment = keccak256(amount + merchantCategory + country + secret)

The actual limit amount remains off-chain or encrypted. Only someone with the correct values can prove they’re within the limit.

Validating Transactions

During a transaction, the card provides:

  • The transaction amount
  • The merchant category
  • The country
  • A proof (the secret used in the original commitment)

The contract recomputes the commitment and checks if it matches. If it does, the transaction is within the encrypted limit. The beauty is that observers can see the transaction was validated, but they cannot see what the actual spending limit is.

Merchant Categories and Geographic Restrictions

Beyond amount limits, the contract supports:

  • Merchant category codes: Restrict cards to specific types of merchants (e.g., only gas stations and groceries)
  • Geographic restrictions: Block transactions from certain countries (useful for fraud prevention)

These restrictions are stored openly because they don’t reveal sensitive financial information about the cardholder.

4. The Payment Processor Contract

This contract orchestrates the entire payment flow, bringing together all the previous components.

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

interface ICardManager {
    function checkAndRecordSpending(address cardAddress, uint256 amount) external returns (bool);
    function cards(address) external view returns (
        address cardAddress,
        address linkedAccount,
        bool isActive,
        uint256 dailyLimit,
        uint256 monthlyLimit,
        uint256 perTransactionLimit,
        uint256 dailySpent,
        uint256 monthlySpent,
        uint256 lastResetDay,
        uint256 lastResetMonth,
        string memory cardholderName,
        bytes32 cardType
    );
}

interface ISpendingLimits {
    function validateTransaction(
        address cardAddress,
        uint256 limitId,
        uint256 amount,
        bytes32 merchantCategory,
        string memory country,
        bytes32 proof
    ) external view returns (bool);
}

contract PaymentProcessor {

    struct Transaction {
        address cardAddress;
        address merchant;
        uint256 amount;
        bytes32 merchantCategory;
        string country;
        uint256 timestamp;
        TransactionStatus status;
        bytes32 transactionHash;
    }

    enum TransactionStatus {
        Pending,
        Approved,
        Declined,
        Settled,
        Refunded
    }

    ICreditFacility public creditFacility;
    ICardManager public cardManager;
    ISpendingLimits public spendingLimits;

    mapping(bytes32 => Transaction) public transactions;
    mapping(address => bytes32[]) public cardTransactions;
    mapping(address => bytes32[]) public merchantTransactions;

    uint256 public transactionCount;

    event TransactionInitiated(
        bytes32 indexed transactionHash,
        address indexed cardAddress,
        address indexed merchant,
        uint256 amount
    );
    event TransactionApproved(bytes32 indexed transactionHash);
    event TransactionDeclined(bytes32 indexed transactionHash, string reason);
    event TransactionSettled(bytes32 indexed transactionHash);
    event TransactionRefunded(bytes32 indexed transactionHash);

    constructor(
        address _creditFacility,
        address _cardManager,
        address _spendingLimits
    ) {
        creditFacility = ICreditFacility(_creditFacility);
        cardManager = ICardManager(_cardManager);
        spendingLimits = ISpendingLimits(_spendingLimits);
    }

    function initiateTransaction(
        address cardAddress,
        address merchant,
        uint256 amount,
        bytes32 merchantCategory,
        string memory country,
        uint256 limitId,
        bytes32 proof
    ) external returns (bytes32) {
        bytes32 txHash = keccak256(
            abi.encodePacked(
                cardAddress,
                merchant,
                amount,
                merchantCategory,
                country,
                block.timestamp,
                transactionCount++
            )
        );

        transactions[txHash] = Transaction({
            cardAddress: cardAddress,
            merchant: merchant,
            amount: amount,
            merchantCategory: merchantCategory,
            country: country,
            timestamp: block.timestamp,
            status: TransactionStatus.Pending,
            transactionHash: txHash
        });

        cardTransactions[cardAddress].push(txHash);
        merchantTransactions[merchant].push(txHash);

        emit TransactionInitiated(txHash, cardAddress, merchant, amount);

        bool approved = processTransaction(txHash, limitId, proof);

        if (approved) {
            emit TransactionApproved(txHash);
        }

        return txHash;
    }

    function processTransaction(
        bytes32 txHash,
        uint256 limitId,
        bytes32 proof
    ) internal returns (bool) {
        Transaction storage txn = transactions[txHash];

        (, address linkedAccount, bool isActive, , , , , , , , , ) = 
            cardManager.cards(txn.cardAddress);

        if (!isActive) {
            txn.status = TransactionStatus.Declined;
            emit TransactionDeclined(txHash, "Card not active");
            return false;
        }

        bool cardLimitCheck = cardManager.checkAndRecordSpending(
            txn.cardAddress,
            txn.amount
        );

        if (!cardLimitCheck) {
            txn.status = TransactionStatus.Declined;
            emit TransactionDeclined(txHash, "Card limit exceeded");
            return false;
        }

        bool encryptedLimitCheck = spendingLimits.validateTransaction(
            txn.cardAddress,
            limitId,
            txn.amount,
            txn.merchantCategory,
            txn.country,
            proof
        );

        if (!encryptedLimitCheck) {
            txn.status = TransactionStatus.Declined;
            emit TransactionDeclined(txHash, "Encrypted limit validation failed");
            return false;
        }

        bool creditUsed = creditFacility.useCredit(linkedAccount, txn.amount);

        if (!creditUsed) {
            txn.status = TransactionStatus.Declined;
            emit TransactionDeclined(txHash, "Insufficient credit");
            return false;
        }

        txn.status = TransactionStatus.Approved;
        return true;
    }

    function settleTransaction(bytes32 txHash) external {
        Transaction storage txn = transactions[txHash];
        require(txn.status == TransactionStatus.Approved, "Transaction not approved");

        txn.status = TransactionStatus.Settled;

        // In a real system, this would transfer funds to the merchant
        // payable(txn.merchant).transfer(txn.amount);

        emit TransactionSettled(txHash);
    }

    function refundTransaction(bytes32 txHash) external {
        Transaction storage txn = transactions[txHash];
        require(
            txn.status == TransactionStatus.Settled,
            "Transaction not settled"
        );

        (, address linkedAccount, , , , , , , , , , ) = 
            cardManager.cards(txn.cardAddress);

        // Return credit to account
        // creditFacility.repayCredit{value: txn.amount}(linkedAccount, txn.amount);

        txn.status = TransactionStatus.Refunded;
        emit TransactionRefunded(txHash);
    }

    function getTransaction(bytes32 txHash) external view returns (
        address cardAddress,
        address merchant,
        uint256 amount,
        bytes32 merchantCategory,
        string memory country,
        uint256 timestamp,
        TransactionStatus status
    ) {
        Transaction storage txn = transactions[txHash];
        return (
            txn.cardAddress,
            txn.merchant,
            txn.amount,
            txn.merchantCategory,
            txn.country,
            txn.timestamp,
            txn.status
        );
    }

    function getCardTransactions(address cardAddress) external view returns (bytes32[] memory) {
        return cardTransactions[cardAddress];
    }

    function getMerchantTransactions(address merchant) external view returns (bytes32[] memory) {
        return merchantTransactions[merchant];
    }
}Code language: PHP (php)

Payment Flow Explained

The Payment Processor orchestrates a complex multi-step validation process:

  1. Transaction Initiation: A transaction is created with all necessary details (card, merchant, amount, category, country)
  2. Card Validation: Check if the card is active and in good standing
  3. Card Limit Checks: Validate against daily, monthly, and per-transaction limits
  4. Encrypted Limit Validation: Verify the transaction against encrypted spending rules using the commitment proof
  5. Credit Availability: Ensure the linked account has sufficient credit
  6. Approval/Decline: If all checks pass, approve the transaction; otherwise, decline with a specific reason
  7. Settlement: After approval, the transaction is marked as settled and funds are transferred (in a production system)
  8. Refund Capability: Transactions can be refunded, returning credit to the account

Transaction Status Lifecycle

Transactions move through distinct states:

  • Pending: Initial state when created
  • Approved: All validations passed
  • Declined: Failed one or more validations
  • Settled: Funds transferred to merchant
  • Refunded: Transaction reversed

This status tracking provides transparency and enables proper accounting.

5. Multi-Signature Governance Contract

For high-value transactions or critical system changes, we need additional oversight beyond individual card limits.

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

contract MultiSigGovernance {

    struct Proposal {
        uint256 proposalId;
        ProposalType proposalType;
        address targetContract;
        bytes callData;
        uint256 value;
        string description;
        uint256 createdAt;
        uint256 executionTime;
        bool executed;
        bool cancelled;
        mapping(address => bool) hasVoted;
        uint256 votesFor;
        uint256 votesAgainst;
    }

    enum ProposalType {
        CreditLimitIncrease,
        CardIssuance,
        SystemUpgrade,
        EmergencyAction,
        ParameterChange
    }

    address[] public governors;
    mapping(address => bool) public isGovernor;

    uint256 public quorumPercentage;
    uint256 public proposalCount;
    uint256 public votingPeriod;
    uint256 public timelockPeriod;

    mapping(uint256 => Proposal) public proposals;

    event ProposalCreated(
        uint256 indexed proposalId,
        ProposalType proposalType,
        address indexed proposer,
        string description
    );
    event VoteCast(
        uint256 indexed proposalId,
        address indexed voter,
        bool support
    );
    event ProposalExecuted(uint256 indexed proposalId);
    event ProposalCancelled(uint256 indexed proposalId);
    event GovernorAdded(address indexed governor);
    event GovernorRemoved(address indexed governor);

    modifier onlyGovernor() {
        require(isGovernor[msg.sender], "Not a governor");
        _;
    }

    constructor(
        address[] memory _governors,
        uint256 _quorumPercentage,
        uint256 _votingPeriod,
        uint256 _timelockPeriod
    ) {
        require(_governors.length > 0, "Must have at least one governor");
        require(_quorumPercentage > 0 && _quorumPercentage <= 100, "Invalid quorum");

        for (uint i = 0; i < _governors.length; i++) {
            governors.push(_governors[i]);
            isGovernor[_governors[i]] = true;
        }

        quorumPercentage = _quorumPercentage;
        votingPeriod = _votingPeriod;
        timelockPeriod = _timelockPeriod;
    }

    function createProposal(
        ProposalType proposalType,
        address targetContract,
        bytes memory callData,
        uint256 value,
        string memory description
    ) external onlyGovernor returns (uint256) {
        uint256 proposalId = proposalCount++;

        Proposal storage proposal = proposals[proposalId];
        proposal.proposalId = proposalId;
        proposal.proposalType = proposalType;
        proposal.targetContract = targetContract;
        proposal.callData = callData;
        proposal.value = value;
        proposal.description = description;
        proposal.createdAt = block.timestamp;
        proposal.executed = false;
        proposal.cancelled = false;

        emit ProposalCreated(proposalId, proposalType, msg.sender, description);
        return proposalId;
    }

    function vote(uint256 proposalId, bool support) external onlyGovernor {
        Proposal storage proposal = proposals[proposalId];

        require(!proposal.executed, "Proposal already executed");
        require(!proposal.cancelled, "Proposal cancelled");
        require(!proposal.hasVoted[msg.sender], "Already voted");
        require(
            block.timestamp <= proposal.createdAt + votingPeriod,
            "Voting period ended"
        );

        proposal.hasVoted[msg.sender] = true;

        if (support) {
            proposal.votesFor++;
        } else {
            proposal.votesAgainst++;
        }

        emit VoteCast(proposalId, msg.sender, support);

        if (hasReachedQuorum(proposalId)) {
            proposal.executionTime = block.timestamp + timelockPeriod;
        }
    }

    function executeProposal(uint256 proposalId) external onlyGovernor {
        Proposal storage proposal = proposals[proposalId];

        require(!proposal.executed, "Already executed");
        require(!proposal.cancelled, "Proposal cancelled");
        require(hasReachedQuorum(proposalId), "Quorum not reached");
        require(
            block.timestamp >= proposal.executionTime,
            "Timelock not expired"
        );
        require(proposal.executionTime > 0, "Execution time not set");

        proposal.executed = true;

        (bool success, ) = proposal.targetContract.call{value: proposal.value}(
            proposal.callData
        );
        require(success, "Execution failed");

        emit ProposalExecuted(proposalId);
    }

    function cancelProposal(uint256 proposalId) external onlyGovernor {
        Proposal storage proposal = proposals[proposalId];

        require(!proposal.executed, "Already executed");
        require(!proposal.cancelled, "Already cancelled");

        proposal.cancelled = true;
        emit ProposalCancelled(proposalId);
    }

    function hasReachedQuorum(uint256 proposalId) public view returns (bool) {
        Proposal storage proposal = proposals[proposalId];
        uint256 totalVotes = proposal.votesFor + proposal.votesAgainst;
        uint256 requiredVotes = (governors.length * quorumPercentage) / 100;

        return totalVotes >= requiredVotes && proposal.votesFor > proposal.votesAgainst;
    }

    function addGovernor(address newGovernor) external {
        require(!isGovernor[newGovernor], "Already a governor");

        governors.push(newGovernor);
        isGovernor[newGovernor] = true;

        emit GovernorAdded(newGovernor);
    }

    function removeGovernor(address governor) external {
        require(isGovernor[governor], "Not a governor");
        require(governors.length > 1, "Cannot remove last governor");

        isGovernor[governor] = false;

        for (uint i = 0; i < governors.length; i++) {
            if (governors[i] == governor) {
                governors[i] = governors[governors.length - 1];
                governors.pop();
                break;
            }
        }

        emit GovernorRemoved(governor);
    }

    function getProposalDetails(uint256 proposalId) external view returns (
        ProposalType proposalType,
        address targetContract,
        string memory description,
        uint256 votesFor,
        uint256 votesAgainst,
        bool executed,
        bool cancelled
    ) {
        Proposal storage proposal = proposals[proposalId];
        return (
            proposal.proposalType,
            proposal.targetContract,
            proposal.description,
            proposal.votesFor,
            proposal.votesAgainst,
            proposal.executed,
            proposal.cancelled
        );
    }

    function getGovernors() external view returns (address[] memory) {
        return governors;
    }
}Code language: PHP (php)

Governance Features

This contract implements a sophisticated governance system with several key protections:

Proposal System

Any governor can create a proposal for:

  • Increasing credit limits beyond normal thresholds
  • Issuing special cards with elevated privileges
  • Upgrading system contracts
  • Emergency actions (like freezing all cards)
  • Changing system parameters

Voting Mechanism

Governors vote on proposals during a voting period. The system supports:

  • Quorum requirements: A minimum percentage of governors must participate
  • Simple majority: More votes for than against
  • Timelock: Even after approval, there’s a delay before execution

The timelock is critical, it gives governors time to react if a malicious proposal passes, potentially vetoing it before execution.

Governor Management

Governors can be added or removed through the governance process itself. This creates a self-governing system that can adapt over time without external intervention.

Real-World Usage Example

Let’s walk through a complete transaction flow using all these contracts:

Step 1: Setup

// Deploy contracts
const creditFacility = await CreditFacility.deploy();
const cardManager = await CardManager.deploy(creditFacility.address);
const spendingLimits = await SpendingLimits.deploy();
const paymentProcessor = await PaymentProcessor.deploy(
    creditFacility.address,
    cardManager.address,
    spendingLimits.address
);

// Create credit account with 3-of-5 multisig
await creditFacility.createCreditAccount(
    accountAddress,
    ethers.utils.parseEther("10000"), // $10,000 credit limit
    [signer1, signer2, signer3, signer4, signer5],
    3 // requires 3 signatures
);Code language: JavaScript (javascript)

Step 2: Issue a Card

// Issue card with spending limits
await cardManager.issueCard(
    cardAddress,
    accountAddress,
    "John Doe",
    ethers.utils.parseEther("500"),  // $500 daily limit
    ethers.utils.parseEther("5000"), // $5000 monthly limit
    ethers.utils.parseEther("200"),  // $200 per transaction
    ethers.utils.formatBytes32String("STANDARD")
);Code language: JavaScript (javascript)

Step 3: Create Encrypted Spending Limit

// Create commitment for encrypted limit
const secretAmount = ethers.utils.parseEther("100");
const category = ethers.utils.formatBytes32String("GROCERY");
const country = "US";
const secret = ethers.utils.randomBytes(32);

const commitment = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
        ["uint256", "bytes32", "string", "bytes32"],
        [secretAmount, category, country, secret]
    )
);

// Store encrypted limit on-chain
await spendingLimits.createEncryptedLimit(
    cardAddress,
    commitment,
    category,
    Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, // valid for 1 year
    [ethers.utils.formatBytes32String("GROCERY")],
    ["RU", "KP"] // restricted countries
);Code language: JavaScript (javascript)

Step 4: Process a Transaction

// Initiate payment at grocery store
const txHash = await paymentProcessor.initiateTransaction(
    cardAddress,
    merchantAddress,
    ethers.utils.parseEther("75"), // $75 purchase
    ethers.utils.formatBytes32String("GROCERY"),
    "US",
    0, // limitId
    secret // proof for encrypted limit
);

// Transaction automatically validated against:
// 1. Card active status
// 2. Daily/monthly/per-transaction limits
// 3. Encrypted spending rules
// 4. Available creditCode language: JavaScript (javascript)

Step 5: Settlement

// After validation, settle the transaction
await paymentProcessor.settleTransaction(txHash);

// Funds are now transferred to merchant
// Credit account balance is updatedCode language: JavaScript (javascript)

Advanced Features and Optimizations

Gas Optimization Techniques

These contracts can be further optimized for production:

Batch Operations: Instead of processing transactions one-by-one, implement batch processing to reduce gas costs.

function batchProcessTransactions(
    bytes32[] memory txHashes
) external {
    for (uint i = 0; i < txHashes.length; i++) {
        settleTransaction(txHashes[i]);
    }
}Code language: JavaScript (javascript)

Storage Packing: Use smaller data types where possible and pack related variables.

struct PackedCard {
    address cardAddress;        // 20 bytes
    address linkedAccount;      // 20 bytes
    uint128 dailyLimit;        // 16 bytes
    uint128 monthlyLimit;      // 16 bytes
    bool isActive;             // 1 byte
    // Total: 73 bytes (can fit in 3 storage slots)
}Code language: JavaScript (javascript)

Event Indexing: Properly index events for efficient off-chain querying.

Security Considerations

Reentrancy Protection

Always use the Checks-Effects-Interactions pattern:

function useCredit(address account, uint256 amount) external {
    // Checks
    require(accounts[account].availableCredit >= amount);

    // Effects
    accounts[account].availableCredit -= amount;
    accounts[account].outstandingBalance += amount;

    // Interactions (external calls)
    emit CreditUsed(account, amount);
}Code language: JavaScript (javascript)

Access Control

Implement role-based access control using OpenZeppelin:

import "@openzeppelin/contracts/access/AccessControl.sol";

contract SecuredCardManager is AccessControl {
    bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    function issueCard(...) external onlyRole(ISSUER_ROLE) {
        // Card issuance logic
    }
}Code language: PHP (php)

Oracle Integration

For real-world data (exchange rates, merchant verification), integrate Chainlink oracles:

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceOracle {
    AggregatorV3Interface internal priceFeed;

    function getLatestPrice() public view returns (int) {
        (, int price, , ,) = priceFeed.latestRoundData();
        return price;
    }
}Code language: JavaScript (javascript)

Privacy Enhancements

For production systems, implement zero-knowledge proofs using libraries like ZoKrates or Circom:

// Pseudocode for ZK proof
circuit SpendingLimit {
    private input actualLimit;
    private input transactionAmount;
    public input commitment;

    assert(hash(actualLimit) == commitment);
    assert(transactionAmount <= actualLimit);
}Code language: PHP (php)

This allows truly private spending limits where even the contract cannot see the actual values.

Integration with Off-Chain Systems

Real-world credit card systems require integration with:

Payment Networks

Connect to Visa/Mastercard networks through payment gateway APIs:

// Off-chain service
async function processCardPayment(transaction) {
    // Validate with smart contract
    const isValid = await paymentProcessor.initiateTransaction(...);

    if (isValid) {
        // Submit to payment network
        await visaGateway.authorizePayment({
            cardNumber: encryptedCard,
            amount: transaction.amount,
            merchant: transaction.merchant
        });
    }
}Code language: JavaScript (javascript)

KYC/AML Compliance

Implement identity verification before card issuance:

async function issueCardWithKYC(user) {
    // Verify identity off-chain
    const kycResult = await kycProvider.verify(user);

    if (kycResult.approved) {
        // Issue card on-chain
        await cardManager.issueCard(
            user.cardAddress,
            user.accountAddress,
            user.name,
            ...
        );
    }
}Code language: JavaScript (javascript)

Fraud Detection

Use machine learning models to detect suspicious patterns:

async function monitorTransactions() {
    const transactions = await getRecentTransactions();

    for (const tx of transactions) {
        const riskScore = await fraudModel.analyze(tx);

        if (riskScore > THRESHOLD) {
            // Automatically freeze card
            await cardManager.deactivateCard(tx.cardAddress);

            // Alert governance
            await notifyGovernors(tx);
        }
    }
}Code language: JavaScript (javascript)

Deployment Strategy

Testnet Deployment

// deployment script
async function main() {
    const [deployer] = await ethers.getSigners();

    console.log("Deploying contracts with account:", deployer.address);

    // Deploy core contracts
    const CreditFacility = await ethers.getContractFactory("CreditFacility");
    const creditFacility = await CreditFacility.deploy();
    await creditFacility.deployed();
    console.log("CreditFacility deployed to:", creditFacility.address);

    const CardManager = await ethers.getContractFactory("CardManager");
    const cardManager = await CardManager.deploy(creditFacility.address);
    await cardManager.deployed();
    console.log("CardManager deployed to:", cardManager.address);

    // Deploy remaining contracts...

    // Verify contracts on Etherscan
    await verify(creditFacility.address, []);
    await verify(cardManager.address, [creditFacility.address]);
}

async function verify(contractAddress, args) {
    await hre.run("verify:verify", {
        address: contractAddress,
        constructorArguments: args,
    });
}

main();Code language: JavaScript (javascript)

Mainnet Considerations

Before mainnet deployment:

  1. Complete Security Audit: Engage firms like Trail of Bits, ConsenSys Diligence, or OpenZeppelin
  2. Bug Bounty Program: Incentivize security researchers to find vulnerabilities
  3. Gradual Rollout: Start with limited users and transaction volumes
  4. Emergency Pause: Implement circuit breakers for crisis situations
  5. Upgrade Path: Use proxy patterns for upgradeable contracts
  6. Insurance: Consider DeFi insurance protocols like Nexus Mutual

Future Enhancements

Layer 2 Scaling

Deploy on L2 solutions for lower costs:

// Optimism/Arbitrum deployment
const l2Provider = new ethers.providers.JsonRpcProvider(L2_RPC_URL);
const l2Deployer = new ethers.Wallet(PRIVATE_KEY, l2Provider);

// Same deployment script, different networkCode language: JavaScript (javascript)

Cross-Chain Interoperability

Use bridges to enable cross-chain transactions:

import "@chainlink/contracts/src/v0.8/interfaces/CCIPRouter.sol";

contract CrossChainPayment {
    function sendCrossChainPayment(
        uint64 destinationChain,
        address receiver,
        uint256 amount
    ) external {
        // Use Chainlink CCIP for cross-chain messaging
    }
}Code language: JavaScript (javascript)

Account Abstraction Integration

Implement ERC-4337 for better user experience:

contract CardWallet is BaseAccount {
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external override returns (uint256) {
        // Validate card transaction as user operation
    }
}Code language: JavaScript (javascript)

Conclusion

We’ve built a complete decentralized credit card system with:

  • Multi-signature security for master accounts
  • Flexible card management with individual spending limits
  • Encrypted spending rules for privacy
  • Comprehensive payment processing with full validation
  • Governance system for high-value operations

This architecture demonstrates how blockchain technology can reimagine traditional financial infrastructure. The system is transparent yet private, decentralized yet secure, and programmable in ways traditional systems cannot match.

The smart contracts provided here are educational starting points. Production systems would require extensive hardening, optimization, regulatory compliance, and integration with existing financial infrastructure.

Key Takeaways

  1. Separation of Concerns: Each contract handles a specific domain, making the system modular and maintainable
  2. Security Layers: Multiple validation checkpoints ensure transactions are legitimate before processing
  3. Privacy Techniques: Commitment schemes enable encrypted rules while maintaining blockchain transparency
  4. Governance: Multi-signature and voting mechanisms distribute control and prevent single points of failure
  5. Extensibility: The modular design allows adding features without rewriting core logic

What’s Next?

In future posts, I’ll explore:

  • Advanced zero-knowledge proof implementations for complete privacy
  • Integration with hardware wallets and biometric authentication
  • Compliance frameworks for regulated financial products
  • Performance optimization and Layer 2 scaling strategies
  • Real-world case studies of blockchain payment systems

Have questions or suggestions? Drop a comment below or reach out on Twitter at @ithora. If you’re building something similar, I’d love to hear about your approach!

Disclaimer: This code is for educational purposes only. It has not been audited and should not be used in production without comprehensive security review and testing. Financial systems involve real money and require professional security expertis

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.