Smart Contract Implementation details

This part of the documentation is more technical, it explains how the smart contracts work

Direct Debit

The base contract is defined as an Abstract contract that needs to be implemented by Accounts.

It provides the Interface to create directly debitable Smart Contract Accounts

   /**
      A function that allows direct debit with a reusable proof
      N times to M address with L max amount that can be withdrawn
      The proof and public inputs are the PaymentIntent
      @param proof contains the zkSnark
      @param hashes are [0] = paymentIntent [1] = commitment
      @param payee is the account recieving the payment
      @param debit[4] are [0] = max debit amount, [1] = debitTimes, [2] = debitInterval, [3] = payment amount. 
      Last param is not used in the circuit but it must be smaller than the max debit amount
      By using a separate max debit amount and a payment amount we can create dynamic subscriptions, where the final price varies 
      but can't be bigger than the allowed amount!
      This function can be paused by the owner to disable it!
    */
    function directdebit(
        uint256[8] calldata proof,
        bytes32[2] calldata hashes,
        address payee,
        uint256[4] calldata debit
    ) external nonReentrant whenNotPaused {
        _verifyPaymentIntent(proof, hashes, payee, debit);
        _processPaymentIntent(hashes, payee, debit);
    }

Account contracts that allow direct debit must have this direct debit function that accepts the zkSnark proof, and parameters for it.

Cancel Payment Intent


    /**
      Cancels the payment intent! The caller must be the creator of the account and must have the zksnark (paymentIntent)
      The zksnark is needed so the Payment Intent can be cancelled before it's used and for that we need proof that it exists!
      @param proof is the snark
      @param hashes are [0] = paymentIntent [1] = commitment
      @param payee is the account recieving the payment
      @param debit [4] are [0] = max debit amount, [1] = debitTimes, [2] = debitInterval, [3] = payment amount. 
      In case of cancellation the 4th value in the debit array can be arbitrary. It is kept here to keep the verifier function's interface
     */
    function cancelPaymentIntent(
        uint256[8] calldata proof,
        bytes32[2] calldata hashes,
        address payee,
        uint256[4] calldata debit
    ) external {
        if (!_verifyProof(proof, hashes, payee, debit)) revert InvalidProof();
        if (msg.sender != accounts[hashes[1]].creator && msg.sender != payee)
            revert OnlyRelatedPartiesCanCancel();
        paymentIntents[hashes[0]].isNullified = true;
        emit PaymentIntentCancelled(hashes[1], hashes[0], payee);
    }

There must be a cancelPaymentIntent function that allows nullifying a payment intent by the related parties. (Customer or Merchant)

The Direct Debit contract also contains an owner fee, it's ownable and the direct debit can be paused by the owner.

/**
A view function to get and display the account's balance
This is useful when the account balance is calculated from external wallet's balance!
 */
function getAccount(
    bytes32 commitment
) external view virtual returns (AccountData memory);

The account balances must be fetched using the getAccount function as the connected wallet's balance implementation differs from virtual accounts.

Child contracts implement these hooks to modify how the direct debit transfer withdraw is implemented:

    /*
       Process the token withdrawal and decrease the account balance
    */

    function _processTokenWithdraw(
        bytes32 commitment,
        address payee,
        uint256 payment
    ) internal virtual;

    /**
      Process the eth withdraw and decrease the account balance
    */

    function _processEthWithdraw(
        bytes32 commitment,
        address payee,
        uint256 payment
    ) internal virtual;

Virtual Accounts

One of the account types DebitLlama implements is Virtual Debit Accounts. They are inspired by Virtual Credit Cards. They are smart contract accounts that need to be manually topped up to contain balance and they support both native tokens ETH and ERC-20 tokens.

 /**
      @dev : Create a new Account by depositing ETH
      @param _commitment is the poseidon hash created for the note on client side
      @param balance that is the value of the note. 
      @param encryptedNote is the crypto note that is encrypted client side and stored in the contract. 
      Storing the note allows for meta transactions where the user needs to decrypt instead of sign a message, and compute an off-chain zkp.
      The zkp created is a "payment intent" and no gas fees are involved creating those!
    */

    function depositEth(
        bytes32 _commitment,
        uint256 balance,
        string calldata encryptedNote
    ) external payable nonReentrant; 

Deposit ETH or native tokens using this function.

The function takes the commitment and the encrypted note and the balance that is deposited must be transferred

Deposit Tokens:

    /**
   @dev : depositToken is for creating an account by depositing tokens, the wallet calling this function must approve ERC20 spend first
   @param _commitment is the poseidon hash of the note
   @param balance is the amount of token transferred to the contract that represents the note's value. balance does not contain the fee
   @param token is the ERC20 token that is used for this deposits  
   @param encryptedNote is the crypto note created for this account
    */
    function depositToken(
        bytes32 _commitment,
        uint256 balance,
        address token,
        string calldata encryptedNote
    ) external nonReentrant {

Top up balance

  /**
      Top up your balance with ETH or other gas tokens
      @param _commitment is the identifier of the account
      @param balance is the top up balance to add to the account

      It is allowed for a user to top up another user's account.
    */

    function topUpETH(
        bytes32 _commitment,
        uint256 balance
    ) external payable nonReentrant

   /**
      Top up your account balance with tokens
      @param _commitment is the identifier of the account
      @param balance is the amount of top up balance
     */

    function topUpTokens(
        bytes32 _commitment,
        uint256 balance
    ) external nonReentrant

Withdrawing balance

     // The account creator can withdraw the value deposited and close the account
    // This will set the active false but the creator address remains, hence the edge case we handled on account creation
    function withdraw(bytes32 commitment) external nonReentrant {
        if (!accounts[commitment].active) revert InactiveAccount();
        if (msg.sender != accounts[commitment].creator)
            revert OnlyAccountOwner();
.......

To withdraw the balance the Account creator EOA needs to initiate the transaction.

Connected Wallets

Connected wallet smart contract lets you directly connect an EOA. It is powered by ERC-20 allowances. The connected wallet debitable balance is the approved amount.

/**
  @dev  Connect an external wallet to create an account that allows directly debiting it!
  @param _commitment is the poseidon hash of the note
  @param token is the ERC20 token that is used for transactions, the connected wallet must approve allowance!
  @param encryptedNote is the crypto note created for this account
 */
function connectWallet(
    bytes32 _commitment,
    address token,
    string calldata encryptedNote
) external nonReentrant

Disconnect wallet

   /**
      @dev  disconnect the wallet from the account, the payment intents can't be used to debit it from now on!
      @param  commitment is the commitment of the account    
     */

    function disconnectWallet(bytes32 commitment) external nonReentrant {
        if (!accounts[commitment].active) revert InactiveAccount();
        if (msg.sender != accounts[commitment].creator)
            revert OnlyAccountOwner();

Last updated