Back to Insights

In Fintech, Trust is an Engineering Specification

Blockchain & Security9 minDecember 15, 2023
BlockchainSecurityFintech

In Fintech, Trust is an Engineering Specification

Category: Blockchain & Security
Reading Time: 9 minutes
Tags: Blockchain, Security, Fintech


The $31 Million Bug

In June 2016, a hacker drained $31 million from The DAO (Decentralized Autonomous Organization) by exploiting a reentrancy vulnerability in their Ethereum smart contract.

The code looked fine. It compiled. It deployed. It even passed basic tests.

But it had a subtle bug:

// Vulnerable code
function withdraw(uint amount) public {
    if (balances[msg.sender] >= amount) {
        // DANGER: External call before state update
        msg.sender.call.value(amount)();

        // State update happens AFTER external call
        balances[msg.sender] -= amount;
    }
}

The hacker called withdraw(), which triggered a fallback function that called withdraw() again before the balance was updated. Rinse and repeat until all funds were drained.

The lesson I learned building blockchain systems at ICBC: In fintech, trust is not a feeling. It's an engineering specification, not an aspiration.


Code is Law (Whether You Like It Or Not)

In traditional finance:

  • Banks can reverse transactions
  • Courts can freeze accounts
  • Humans can intervene when things go wrong
  • "Trust" is enforced by institutions

In blockchain:

  • Transactions are immutable
  • There are no account freezes
  • No one can "undo" a smart contract execution
  • Trust is enforced by mathematics

When you deploy a smart contract, you're not just shipping code. You're deploying immutable financial logic that will execute exactly as written, forever, with no possibility of intervention.

There is no "oops" button.


Building Trust: The Blockchain Way

When I joined ICBC in 2017, we were building a blockchain-based payment gateway for cross-border remittances. The stakes were high:

  • Volume: $50M+/day in transactions
  • Jurisdictions: 12 countries with different regulations
  • Finality: Once confirmed, transactions are irreversible
  • Accountability: Smart contracts had to be auditable by regulators

We couldn't afford bugs. Not "we prefer to avoid bugs." We literally could not afford bugs.

Here's how we built systems where trust was provable, not promised.


Principle 1: Immutability by Design

The Problem with Mutable State

Traditional systems trust databases to maintain state:

# Traditional database update
def transfer_money(from_account, to_account, amount):
    # Check balance
    if accounts[from_account] >= amount:
        # Update balances
        accounts[from_account] -= amount
        accounts[to_account] += amount

        # Log transaction
        db.insert("transactions", {
            "from": from_account,
            "to": to_account,
            "amount": amount,
            "timestamp": now()
        })

Problems:

  • Database can be modified after the fact
  • No cryptographic proof of transaction history
  • Trust requires trusting the database administrator
  • Audit trail can be tampered with

The Blockchain Solution

// Immutable smart contract
contract PaymentGateway {
    // State stored on blockchain (immutable)
    mapping(address => uint256) public balances;

    // Events are permanent log records
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 amount,
        uint256 timestamp
    );

    function transfer(address to, uint256 amount) public {
        // Check balance
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Update balances (atomic)
        balances[msg.sender] -= amount;
        balances[to] += amount;

        // Emit event (permanent record)
        emit Transfer(msg.sender, to, amount, block.timestamp);
    }
}

What changed:

  • ✅ Transaction history is cryptographically secured
  • ✅ No administrator can modify past transactions
  • ✅ Anyone can verify the entire history
  • ✅ Trust is mathematical, not institutional

Principle 2: Verification, Not Trust

Don't Trust. Verify.

The Bitcoin whitepaper's key innovation wasn't just decentralization—it was making trust verifiable.

Bad approach (Trust-based):

# User sends money
user_balance = api.get_balance(user_id)
if user_balance >= amount:
    api.transfer(user_id, recipient_id, amount)

# We trust the API response
# We trust the transfer succeeded
# We have no way to verify

Good approach (Verification-based):

from web3 import Web3

# Connect to Ethereum node
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io'))

# User sends transaction
tx_hash = contract.functions.transfer(
    recipient_address,
    amount
).transact({'from': user_address})

# Wait for confirmation
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

# Verify the transaction was mined
assert receipt['status'] == 1, "Transaction failed"

# Verify the event was emitted
transfer_event = contract.events.Transfer().process_receipt(receipt)
assert len(transfer_event) == 1, "Transfer event not found"

# Verify the balances updated correctly
new_balance = contract.functions.balances(user_address).call()
assert new_balance == old_balance - amount, "Balance mismatch"

# Anyone can verify this independently
print(f"Transaction verified: {tx_hash.hex()}")

Key insight: Every claim can be independently verified by anyone with access to the blockchain.


Principle 3: Security Through Simplicity

Complex Code = Attack Surface

The more complex your smart contract, the more likely it has vulnerabilities.

Bad (Complex):

// 500+ lines of complex logic
contract ComplexPayment {
    struct Payment {
        address sender;
        address receiver;
        uint256 amount;
        uint256 timestamp;
        bool executed;
        mapping(address => bool) confirmations;
        uint256 confirmationCount;
    }

    Payment[] public payments;
    mapping(address => bool) public isOwner;

    function submitPayment(...) public { /* complex logic */ }
    function confirmPayment(...) public { /* complex logic */ }
    function executePayment(...) public { /* complex logic */ }
    function revokeConfirmation(...) public { /* complex logic */ }

    // 20+ functions, 500+ lines
    // Attack surface: LARGE
}

Good (Simple):

// Minimal, auditable logic
contract SimplePayment {
    mapping(address => uint256) public balances;

    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);

    function deposit() public payable {
        require(msg.value > 0, "Must deposit something");
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Check-Effects-Interaction pattern
        balances[msg.sender] -= amount;  // Update state FIRST

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        emit Withdrawal(msg.sender, amount);
    }
}

Principles:

  • Check-Effects-Interaction: Update state before external calls
  • Minimal logic: Do one thing well
  • No complex dependencies: Reduces attack surface
  • Easy to audit: 50 lines vs 500 lines

Principle 4: Defense in Depth

Never Rely on a Single Protection

contract SecurePayment {
    // Layer 1: Access control
    address public owner;
    mapping(address => bool) public authorized;

    modifier onlyAuthorized() {
        require(authorized[msg.sender], "Not authorized");
        _;
    }

    // Layer 2: Rate limiting
    mapping(address => uint256) public lastTransactionTime;
    uint256 public constant RATE_LIMIT = 1 minutes;

    modifier rateLimit() {
        require(
            block.timestamp >= lastTransactionTime[msg.sender] + RATE_LIMIT,
            "Rate limit exceeded"
        );
        _;
        lastTransactionTime[msg.sender] = block.timestamp;
    }

    // Layer 3: Amount limits
    uint256 public constant MAX_TRANSFER = 100 ether;

    modifier validateAmount(uint256 amount) {
        require(amount > 0, "Amount must be positive");
        require(amount <= MAX_TRANSFER, "Amount exceeds limit");
        _;
    }

    // Layer 4: Circuit breaker
    bool public paused;

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    // Protected transfer function
    function transfer(address to, uint256 amount)
        public
        onlyAuthorized
        rateLimit
        validateAmount(amount)
        whenNotPaused
    {
        // Transfer logic
    }

    // Emergency stop
    function pause() public {
        require(msg.sender == owner, "Only owner can pause");
        paused = true;
    }
}

Layers of protection:

  1. Access control: Who can call this?
  2. Rate limiting: How often can they call it?
  3. Amount validation: What are the bounds?
  4. Circuit breaker: Can we stop everything if needed?

Principle 5: Auditability is Non-Negotiable

Every Action Must Leave a Trail

contract AuditablePayment {
    // Events are permanent, indexed logs
    event Deposit(
        address indexed user,
        uint256 amount,
        uint256 timestamp,
        bytes32 indexed transactionId
    );

    event Withdrawal(
        address indexed user,
        uint256 amount,
        uint256 timestamp,
        bytes32 indexed transactionId
    );

    event AuthorizationGranted(
        address indexed user,
        address indexed grantedBy,
        uint256 timestamp
    );

    event AuthorizationRevoked(
        address indexed user,
        address indexed revokedBy,
        uint256 timestamp
    );

    // Detailed transaction records
    struct Transaction {
        address from;
        address to;
        uint256 amount;
        uint256 timestamp;
        bytes32 transactionId;
        string memo;
    }

    Transaction[] public transactions;

    function withdraw(uint256 amount, string memory memo) public {
        // ... validation ...

        // Generate unique transaction ID
        bytes32 txId = keccak256(abi.encodePacked(
            msg.sender,
            amount,
            block.timestamp,
            block.number
        ));

        // Record transaction
        transactions.push(Transaction({
            from: address(this),
            to: msg.sender,
            amount: amount,
            timestamp: block.timestamp,
            transactionId: txId,
            memo: memo
        }));

        // Emit event for off-chain indexing
        emit Withdrawal(msg.sender, amount, block.timestamp, txId);

        // ... transfer logic ...
    }

    // Anyone can verify transaction history
    function getTransactionCount() public view returns (uint256) {
        return transactions.length;
    }

    function getTransaction(uint256 index)
        public
        view
        returns (Transaction memory)
    {
        require(index < transactions.length, "Invalid index");
        return transactions[index];
    }
}

Regulatory compliance:

# Off-chain service for regulators
class BlockchainAuditor:
    def __init__(self, contract_address):
        self.contract = w3.eth.contract(
            address=contract_address,
            abi=CONTRACT_ABI
        )

    def generate_compliance_report(self, start_block, end_block):
        """Generate transaction report for regulators"""

        # Get all withdrawal events
        events = self.contract.events.Withdrawal.get_logs(
            fromBlock=start_block,
            toBlock=end_block
        )

        report = []
        for event in events:
            # Extract transaction details
            tx_hash = event['transactionHash'].hex()
            user = event['args']['user']
            amount = event['args']['amount']
            timestamp = event['args']['timestamp']

            # Verify on blockchain
            tx_receipt = w3.eth.get_transaction_receipt(tx_hash)
            block = w3.eth.get_block(tx_receipt['blockNumber'])

            report.append({
                'transaction_hash': tx_hash,
                'user_address': user,
                'amount_wei': amount,
                'amount_usd': self.wei_to_usd(amount),
                'timestamp': datetime.fromtimestamp(timestamp),
                'block_number': tx_receipt['blockNumber'],
                'block_hash': block['hash'].hex(),
                'verified': True  # On blockchain = verified
            })

        return pd.DataFrame(report)

    def verify_user_balance(self, user_address):
        """Verify user balance matches blockchain state"""

        # Get all deposits
        deposits = self.contract.events.Deposit.get_logs(
            argument_filters={'user': user_address}
        )

        # Get all withdrawals
        withdrawals = self.contract.events.Withdrawal.get_logs(
            argument_filters={'user': user_address}
        )

        # Calculate expected balance
        total_deposits = sum(d['args']['amount'] for d in deposits)
        total_withdrawals = sum(w['args']['amount'] for w in withdrawals)
        expected_balance = total_deposits - total_withdrawals

        # Get actual balance from contract
        actual_balance = self.contract.functions.balances(user_address).call()

        # Verify they match
        assert expected_balance == actual_balance, \
            f"Balance mismatch: expected {expected_balance}, got {actual_balance}"

        return {
            'user': user_address,
            'balance': actual_balance,
            'total_deposits': total_deposits,
            'total_withdrawals': total_withdrawals,
            'verified': True
        }

Real-World Battle Scars: What We Learned at ICBC

Lesson 1: Gas Price Volatility Breaks User Experience

Problem: During high network congestion, transaction fees spiked from $0.50 to $50+.

Solution: Implement gas price oracle with fallback:

contract GasOptimized {
    uint256 public maxGasPrice = 100 gwei;

    function setMaxGasPrice(uint256 newMax) public onlyOwner {
        maxGasPrice = newMax;
    }

    function transfer(address to, uint256 amount) public {
        // Check current gas price
        require(tx.gasprice <= maxGasPrice, "Gas price too high");

        // ... transfer logic ...
    }
}

Lesson 2: Oracle Data Can Be Manipulated

Problem: Using exchange rates from a single source created manipulation risk.

Solution: Aggregate multiple oracles with median calculation:

contract SecureOracle {
    address[] public oracleSources;

    function getExchangeRate() public view returns (uint256) {
        require(oracleSources.length >= 3, "Need at least 3 oracles");

        uint256[] memory rates = new uint256[](oracleSources.length);

        // Fetch from all sources
        for (uint i = 0; i < oracleSources.length; i++) {
            rates[i] = IOracle(oracleSources[i]).getRate();
        }

        // Return median (resistant to outliers)
        return _median(rates);
    }

    function _median(uint256[] memory arr) private pure returns (uint256) {
        // Sort array
        _quickSort(arr, 0, arr.length - 1);

        // Return middle value
        return arr[arr.length / 2];
    }
}

Lesson 3: Front-Running is Real

Problem: Attackers monitored mempool and front-ran profitable transactions.

Solution: Commit-reveal pattern:

contract ProtectedTrading {
    mapping(bytes32 => bool) public commitments;

    // Step 1: User commits hash of their trade
    function commitTrade(bytes32 commitment) public {
        commitments[commitment] = true;
    }

    // Step 2: After 1 block, reveal actual trade
    function revealTrade(
        address token,
        uint256 amount,
        bytes32 salt
    ) public {
        // Verify commitment
        bytes32 commitment = keccak256(abi.encodePacked(
            msg.sender,
            token,
            amount,
            salt
        ));

        require(commitments[commitment], "No commitment found");
        delete commitments[commitment];

        // Execute trade
        _executeTrade(token, amount);
    }
}

The Ultimate Test: Could You Defend It in Court?

Every smart contract we deployed had to pass this test:

"If this contract loses user funds, can you prove in court that:

  1. The code did what it was supposed to do?
  2. Users were warned of the risks?
  3. You followed industry best practices?
  4. The failure wasn't due to negligence?"

This meant:

  • Formal verification of critical functions
  • Multiple external audits (minimum 2)
  • Comprehensive test coverage (>95%)
  • Clear user documentation and warnings
  • Gradual rollout with monitoring
  • Bug bounty program before mainnet launch

Key Takeaways

  1. Code is law: Smart contracts execute exactly as written, forever
  2. Trust must be verifiable: Mathematical proof > institutional trust
  3. Simplicity is security: Complex code = large attack surface
  4. Defense in depth: Multiple layers of protection
  5. Auditability is non-negotiable: Every action leaves an immutable trail

The hard truth: In traditional finance, you can fix mistakes. In blockchain, you can't. The code you deploy today will execute exactly as written for years, with billions of dollars flowing through it.

Trust isn't a feeling. It's an engineering specification.


About the Author
Devesh Kumar is a Staff Software Engineer who built blockchain payment systems at ICBC, processing $50M+/day in cross-border transactions. He's now focused on GenAI platforms and cloud infrastructure at StartupManch.

Want to discuss blockchain architecture?

Want to discuss this topic?

I work with 3-4 high-stakes organizations per year on platform architecture and infrastructure strategy.

Schedule Strategy Session