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! */functiondirectdebit( uint256[8] calldata proof, bytes32[2] calldata hashes, address payee, uint256[4] calldata debit ) externalnonReentrantwhenNotPaused {_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 */functioncancelPaymentIntent( 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 balanceThis is useful when the account balance is calculated from external wallet's balance! */functiongetAccount( bytes32 commitment) externalviewvirtualreturns (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 ) internalvirtual;/** Process the eth withdraw and decrease the account balance */function_processEthWithdraw( bytes32 commitment, address payee, uint256 payment ) internalvirtual;
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! */functiondepositEth( bytes32 _commitment, uint256 balance, string calldata encryptedNote ) externalpayablenonReentrant;
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 */functiondepositToken( bytes32 _commitment, uint256 balance, address token, string calldata encryptedNote ) externalnonReentrant {
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. */functiontopUpETH( bytes32 _commitment, uint256 balance ) externalpayablenonReentrant
/** Top up your account balance with tokens@param _commitment is the identifier of the account@param balance is the amount of top up balance */functiontopUpTokens( bytes32 _commitment, uint256 balance ) externalnonReentrant
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 creationfunctionwithdraw(bytes32 commitment) externalnonReentrant {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 */functionconnectWallet( bytes32 _commitment, address token, string calldata encryptedNote) externalnonReentrant
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 */functiondisconnectWallet(bytes32 commitment) externalnonReentrant {if (!accounts[commitment].active) revert InactiveAccount();if (msg.sender != accounts[commitment].creator) revert OnlyAccountOwner();