Open source

Three contracts, 250 lines, all of them here.

This is every line of code that can move money in Vesta. It is published under the MIT licence, verified on independent block explorers, and short enough to read over a coffee.

How to check that this is the real code

Do not take this page's word for it. A block explorer compiles the source it is given and compares the result with the bytecode actually deployed on the blockchain; it only shows a “verified” mark when they match. Each address below links to three explorers that do that check independently: Basescan, Blockscout and Sourcify. The source they show is the source printed on this page. (The deposit factory is verified on Sourcify and Blockscout; its Basescan entry is in progress.)

Things worth noticing while you read: there is no owner, no pause, no upgrade and no withdraw in any of them. The fee and the fee wallet are immutable, which means they were fixed when the contract was deployed and cannot be changed by anyone, including us. Neither contract ever holds a balance between transactions.

Addresses

VestaPayments — wallet payments

VestaDepositFactory — exchange payments

The factory has the same address on both networks on purpose (it is deployed through a deterministic deployer), so a payment sent on the wrong network can still be forwarded to the merchant. VestaDeposit has no fixed address: one is created per invoice, lives for a single transaction, and is verified as part of the factory.

Compiler: Solidity 0.8.24, optimizer on (200 runs). VestaPayments was compiled for the paris EVM target, the deposit contracts for cancun. Licence: MIT. Each file can be downloaded as plain text from the link next to its name.

VestaPayments.sol (79 lines · download)

Wallet payments: one transaction moves USDC from the customer straight to the merchant and the fee wallet.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @dev Minimal ERC-20 surface Vesta needs. USDC returns `true` on success.
interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

/// @title VestaPayments — non-custodial USDC invoice payments (testnet MVP)
/// @notice A customer pays an invoice; USDC moves in ONE transaction directly from the
///         customer to the merchant (gross − fee) and to the Vesta fee wallet (fee).
///         This contract never holds a USDC balance and has no withdraw function.
///
///         Trust model (approved in docs/architecture-usdc-flow.md):
///         - The caller supplies `merchant` and `amount`. A tampered call can only
///           misroute the caller's own USDC; the off-chain indexer only marks an
///           invoice CONFIRMED when the emitted event matches the invoice record.
///         - Expiry is enforced off-chain (soft expiry).
///         - Fee and fee wallet are fixed at deployment; there is no owner/admin.
contract VestaPayments {
    /// @notice The only token this contract will ever move.
    IERC20 public immutable usdc;
    /// @notice Receives the Vesta fee on every payment.
    address public immutable feeWallet;
    /// @notice Fee in basis points (300 = 3%).
    uint256 public immutable feeBps;
    uint256 public constant BPS_DENOMINATOR = 10_000;

    /// @notice invoiceId => paid. Set before any transfer; blocks double payment.
    mapping(bytes32 => bool) public paid;

    event PaymentReceived(
        bytes32 indexed invoiceId,
        address indexed merchant,
        address indexed payer,
        uint256 grossAmount,
        uint256 feeAmount,
        uint256 merchantAmount
    );

    error ZeroAddress();
    error FeeTooHigh();
    error ZeroAmount();
    error AlreadyPaid(bytes32 invoiceId);
    error TransferFailed();

    constructor(address usdc_, address feeWallet_, uint256 feeBps_) {
        if (usdc_ == address(0) || feeWallet_ == address(0)) revert ZeroAddress();
        if (feeBps_ >= BPS_DENOMINATOR) revert FeeTooHigh();
        usdc = IERC20(usdc_);
        feeWallet = feeWallet_;
        feeBps = feeBps_;
    }

    /// @notice Pure fee split, mirrored in web/lib/fees.ts. Fee rounds down; dust stays with merchant.
    function split(uint256 grossAmount) public view returns (uint256 feeAmount, uint256 merchantAmount) {
        feeAmount = (grossAmount * feeBps) / BPS_DENOMINATOR;
        merchantAmount = grossAmount - feeAmount;
    }

    /// @notice Pay `amount` USDC (6-decimal units) for `invoiceId` to `merchant`.
    /// @dev Caller must have approved this contract for at least `amount` on USDC.
    ///      Reverts (and moves nothing) on insufficient balance/allowance or duplicate invoiceId.
    function pay(bytes32 invoiceId, address merchant, uint256 amount) external {
        if (merchant == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (paid[invoiceId]) revert AlreadyPaid(invoiceId);
        paid[invoiceId] = true; // effects before interactions

        (uint256 feeAmount, uint256 merchantAmount) = split(amount);

        if (!usdc.transferFrom(msg.sender, merchant, merchantAmount)) revert TransferFailed();
        if (feeAmount > 0) {
            if (!usdc.transferFrom(msg.sender, feeWallet, feeAmount)) revert TransferFailed();
        }

        emit PaymentReceived(invoiceId, merchant, msg.sender, amount, feeAmount, merchantAmount);
    }
}

VestaDepositFactory.sol (120 lines · download)

Exchange payments: gives every invoice its own deposit address and forwards what arrives there. Anyone can trigger the forward.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {VestaDeposit, IERC20} from "./VestaDeposit.sol";

/// @title VestaDepositFactory
/// @notice Gives every invoice its own deposit address, and forwards the USDC
///         that arrives there to the merchant (97%) and the Vesta fee wallet (3%).
/// @dev    Holds no funds and has no owner, no pause and no withdraw function.
///         `sweep` is open to anyone: the money can only ever move to the
///         merchant and the fee wallet, so an open sweep means funds can never
///         be stuck waiting for Vesta to act.
///
///         Chain-independence: the factory is deployed through the standard
///         deterministic CREATE2 deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C)
///         with a fixed salt, and neither the factory's constructor nor the
///         deposit init code contains a token address. So the factory, and every
///         deposit address, is the same on every chain where the same fee wallet
///         and fee are used. If a customer sends USDC on the wrong network, the
///         factory can be deployed there at the same address and the payment
///         swept with that chain's USDC. The token to sweep is a parameter of
///         `sweep`, which also means a wrong token is forwardable, not stranded.
contract VestaDepositFactory {
    address public immutable feeWallet;
    uint16 public immutable feeBps;

    /// @dev Set only for the duration of one sweep; the deposit contract reads it
    ///      from its constructor. Zero at all other times.
    address private _sweepToken;

    event DepositSwept(
        bytes32 indexed invoiceId,
        address indexed merchant,
        address indexed token,
        address depositAddress,
        uint256 gross,
        uint256 fee,
        uint256 merchantAmount
    );

    error ZeroAddress();
    error FeeTooHigh();
    error NothingToSweep();
    error AddressMismatch();
    error SweepIncomplete();

    constructor(address _feeWallet, uint16 _feeBps) {
        if (_feeWallet == address(0)) revert ZeroAddress();
        if (_feeBps > 1_000) revert FeeTooHigh(); // hard ceiling of 10%
        feeWallet = _feeWallet;
        feeBps = _feeBps;
    }

    /// @notice The token being swept right now (zero outside a sweep).
    function sweepToken() external view returns (address) {
        return _sweepToken;
    }

    /// @notice The address a customer should send USDC to for this invoice.
    /// @dev    Pure arithmetic: the same inputs always give the same address,
    ///         on-chain and off-chain, whether or not anything is deployed yet.
    ///         Independent of the token.
    function depositAddress(bytes32 invoiceId, address merchant) public view returns (address) {
        return address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            bytes1(0xff),
                            address(this),
                            invoiceId,
                            initCodeHash(merchant)
                        )
                    )
                )
            )
        );
    }

    /// @notice Hash of the init code for a merchant's deposit contracts.
    /// @dev    Published so the backend can derive addresses without an RPC call.
    function initCodeHash(address merchant) public view returns (bytes32) {
        return keccak256(_initCode(merchant));
    }

    /// @notice Forward everything of `token` sitting at an invoice's deposit address.
    /// @dev    Callable by anyone. Reverts if there is nothing there. `token` is
    ///         USDC in normal operation; any other ERC-20 sent by mistake can be
    ///         forwarded the same way (it goes to the merchant and the fee wallet
    ///         in the same 97/3 split).
    function sweep(bytes32 invoiceId, address merchant, address token)
        external
        returns (address deposit, uint256 gross, uint256 fee, uint256 merchantAmount)
    {
        if (token == address(0) || merchant == address(0)) revert ZeroAddress();
        deposit = depositAddress(invoiceId, merchant);

        gross = IERC20(token).balanceOf(deposit);
        if (gross == 0) revert NothingToSweep();

        fee = (gross * feeBps) / 10_000;
        merchantAmount = gross - fee;

        _sweepToken = token;
        VestaDeposit child = new VestaDeposit{salt: invoiceId}(merchant, feeWallet, feeBps);
        _sweepToken = address(0);

        if (address(child) != deposit) revert AddressMismatch();
        if (IERC20(token).balanceOf(deposit) != 0) revert SweepIncomplete();

        emit DepositSwept(invoiceId, merchant, token, deposit, gross, fee, merchantAmount);
    }

    function _initCode(address merchant) private view returns (bytes memory) {
        return abi.encodePacked(
            type(VestaDeposit).creationCode,
            abi.encode(merchant, feeWallet, feeBps)
        );
    }
}

VestaDeposit.sol (51 lines · download)

The deposit address itself: born, pays out to the merchant and the fee wallet, and is gone — all in one transaction.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
}

/// @dev The one thing a deposit contract asks its factory: which token is being swept right now.
interface IVestaDepositFactory {
    function sweepToken() external view returns (address);
}

/// @title VestaDeposit
/// @notice One invoice, one address. This contract is deployed by
///         VestaDepositFactory at an address derived from the invoice id, and
///         its whole life happens inside the constructor: it splits whatever
///         token balance is sitting at that address between the merchant and
///         the Vesta fee wallet, then deletes itself so the same address can be
///         used again if a further payment arrives.
/// @dev    There is no private key for this address and no other code can ever
///         run at it: the destination addresses are baked into the CREATE2
///         init code, so the address itself commits to where the money goes.
///         The token is deliberately NOT part of the init code — it is read from
///         the factory at sweep time — so the same deposit address exists on
///         every chain (see VestaDepositFactory) and a payment sent on the wrong
///         network, or in the wrong token, can still be forwarded to the merchant.
///         Stray ETH is forwarded to the merchant as well.
contract VestaDeposit {
    constructor(address merchant, address feeWallet, uint16 feeBps) {
        address token = IVestaDepositFactory(msg.sender).sweepToken();

        uint256 gross = IERC20(token).balanceOf(address(this));
        uint256 fee = (gross * feeBps) / 10_000;
        uint256 merchantAmount = gross - fee;

        if (merchantAmount > 0) _send(token, merchant, merchantAmount);
        if (fee > 0) _send(token, feeWallet, fee);

        // Created and destroyed in the same transaction, so the account is
        // cleared (EIP-6780) and the address can be reused for a later payment.
        selfdestruct(payable(merchant));
    }

    function _send(address token, address to, uint256 value) private {
        (bool ok, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.transfer.selector, to, value)
        );
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "VestaDeposit: transfer failed");
    }
}

What is not on this page

The web application (dashboard, checkout page, the indexer that watches the chain) is not a contract and is not published here. It cannot move anyone's money: it only reads the blockchain and holds a single key that can trigger the forward of a deposit to its merchant and nothing else. What it stores about you is written down on the privacy architecture page, and what happens if it stops running is on what if Vesta disappears?.

An independent audit of these contracts has not been done yet. When it has, the report will be linked from here and from the security page — not before.