Venmo/Garanti Contracts

Smart Contracts

Ramp

The Ramp contract contains the business logic for the protocol. All user interactions will go through the Ramp contract.

State

This section will focus more on the state that is updated as part of users using the protocol. There are four main pieces of state that store operational information:

  • globalAccount: This is a mapping of a Venmo ID Hash to a struct that stores a user's last on ramp timestamp, current intent hash (if they have an open intent), and deny list. One Global Account can have many Ethereum accounts operating under it. We do not specifically track each Ethereum address tied to a Global Account. Global Accounts are used to enforce restrictions across all of user's Ethereum addresses they register with the protocol.

  • accounts: This mapping ties specfic Ethereum addresses to a Venmo ID Hash and also tracks all open deposits made by an Ethereum address. Accounts have restrictions on the amount of deposits they are able to have open at one time.

  • deposits: This mapping ties a deposit ID, represented by a unique uint256 identifier, to a Deposit.

  • intents: This mapping ties an intent hash, represented by a unique bytes32 identifier, to an Intent.

A Deposit is defined by the following struct:

    struct Deposit {
        address depositor;
        uint256[3] packedVenmoId;
        uint256 depositAmount;              // Amount of USDC deposited
        uint256 remainingDeposits;          // Amount of remaining deposited liquidity
        uint256 outstandingIntentAmount;    // Amount of outstanding intents (may include expired intents)
        uint256 conversionRate;             // Conversion required by off-ramper between USDC/USD
        bytes32[] intentHashes;             // Array of hashes of all open intents (may include some expired if not pruned)
    }

while an Intent is defined by the following struct:

    struct Intent {
        address onRamper;                   // On-ramper's address
        address to;                         // Address to forward funds to (can be same as onRamper)
        uint256 deposit;                    // ID of the deposit the intent is signaling on
        uint256 amount;                     // Amount of USDC the on-ramper signals intent for on-chain
        uint256 intentTimestamp;            // Timestamp of when the intent was signaled
    }

Deposits can have multiple open intents on it at any one time however the value of the open intents cannot exceed the amount of available liquidity in the deposit. This means that any new intents must be for less than the remainingDeposits (plus any prunable intents, see Other Concepts section on pruning) on a Deposit. When an intent is successfully completed the amount it was completed for is subtracted from the outstandingIntentAmount of a Deposit and the intent hash is removed from the Deposit's intentHashes array. When intents are fulfilled, cancelled, or pruned they are deleted from the intents mapping.

User Functions

register

To use zkp2p you must first register which links an Ethereum address to a hashed venmoId. A user may use multiple Ethereum addresses per venmoId but may not update the venmoId associated with an Ethereum address.

Error States:

  • The caller must not have previously registered

  • A valid proof must be provided that contains the following:

    • The correct DKIM signature must have been used to sign the e-mail

    • The email must be from the correct from address

offRamp

In order to offRamp users deposit funds into the Ramp contract and specify the amount of USD they wish to receive. A deposit is created and assigned an ID.

Error States:

  • Caller must not exceed the max amount of deposits allowed

  • Caller must provide the correct packed version of their venmoId so that the onRamper knows where to send funds

  • Caller must deposit more than the minDeposit amount

  • Caller must be receiving more than $0 in USD (this check is to prevent accidentally swapping their USDC for $0)

signalIntent

In order to put deposited assets into escrow the on ramper must signal their intent to send a Venmo transaction to the off ramper. The on ramper specifies a deposit they are fulfilling, an amount they wish to put into escrow, and an address to send the escrowed funds to when unlocked.

Error States:

  • Caller's venmoIdHash must not be on depositor's / off ramper's deny list

  • The caller's cool down period must have elapsed from last completed on ramp transaction, this is enforced based on the user's venmoIdHash not Ethereum address

  • Caller must not have an outstanding intent, this is enforced based on the user's venmoIdHash not Ethereum address

  • Caller cannot be signaling intent on their own deposit, this is enforced based on the user's venmoIdHash not Ethereum address

  • Passed deposit ID must exist

  • Amount signaled must be greater than 0 but less than max on ramp amount

  • To address must not be the zero address

cancelIntent

If the on ramper wants to cancel their intent for any reason they can do so with this function. This is in effect a force “pruning” of the intent (can take place before or after expiry) and has all the same ramifications as pruning (see Other Concepts section for more on pruning).

Error States:

  • Specified intent does not exist

  • Caller is not the on ramper specified in intent

onRamp

This function is called by on rampers who provide a proof that they sent payment to the off ramper. Upon successful completion, funds are released to the on ramper. Additionally, the deposit state is updated (or deleted if no more intents outstanding or funds remaining) and the intent is deleted from the intents mapping AND from the on ramper’s outstanding intent mapping (so they can signal another intent).

Error States:

  • A valid proof must be provided that contains the following:

    • The specified intent being fulfilled must exist

    • The amount of the transaction must be greater than intent amount divided by the conversion rate

    • ID of the venmo transaction receiver must match the off ramper’s venmoId

    • ID of the venmo transaction sender must match the on ramper’s venmoId

    • The e-mail must not have been used in a previous transaction (been nullified)

    • The correct DKIM signature must have been used to sign the e-mail

    • The email must be from the correct from address

    • The timestamp of the e-mail must be after the intent’s timestamp

withdrawDeposit

This function must be called by the owner of all deposits passed into the contract. For each deposit we cycle through and sum the remaining amount plus any prunable intents. We then delete the prunable intents, update the deposit (delete if no outstanding intents) and transfer the summed funds to the caller.

Error States:

  • The caller must be the depositor for all passed in deposits

addAccountToDenylist

Adds the passed venmoIdHash to the caller’s denylist. It’s important to note that the denylist exists on a per venmoId basis so denying a user from one Ethereum address propagates to all other Ethereum addresses registered to the caller’s venmoId.

Error States:

  • The denied user must not already be on the caller’s denylist

removeAccountToDenylist

Removed the passed venmoIdHash from the caller’s denylist. It’s important to note that the deny list exists on a per venmoId basis so approving a user from one Ethereum address propagates to all other Ethereum addresses registered to the caller’s venmoId.

Error States:

  • The approved user must be on the caller’s denylist

Administrative Functions

There are a couple administrative functions that users should be aware of, in order to understand risks and also understand why they are necessary.

setSendProcessor This function allows an admin to change the send processor used to validate zero-knowledge proofs. This function is necessary in order to allow updates to the circuit verifiers in case updates need to be made to the circuit. Circuits may need to be updated in case the e-mail template is changed by one of the payment networks enable by ZKP2P.

setRegistrationProcessor This function allows an admin to change the registration processor used to validate zero-knowledge proofs. See above for reasoning.

setMinDepositAmount This function allows an admin to change the minimum deposit amount that user's are able to off-ramp. We do this to avoid cluttering the protocol with dust off-ramp amounts.

setSustainabilityFee This function allows an admin to change the sustainability fee charged to on rampers on a successful on ramp transaction. Note that the sustainability fee can never exceed the MAX_SUSTAINABILITY_FEE hard-coded into the contract. See the Other Concepts section for our reasoning behind charging a sustainability fee.

setMaxOnRampAmount This function allows an admin to change the max amount that a user can on ramp in one transaction. Given ZKP2P is still new technology we feel it's important to put reasonable bounds on usage at the beginning. Together with the setOnRampCooldownPeriod this defines a de facto "daily limit".

setOnRampCooldownPeriod This function allows an admin to change the required amount of time between completing an on ramp and signaling intent for a new transaction. Given ZKP2P is still new technology we feel it's important to put reasonable bounds on usage at the beginning. Together with the maxOnRampAmount this defines a de facto "daily limit".

setIntentExpirationPeriod This function allows an admin to change the amount of time before an intent is considered "expired" and thus can be pruned on the next signalIntent or withdrawDeposit transaction. Any intents that have expired are risky to complete because they may be pruned before funds are released to the on ramper on-chain.

Other Concepts

Pruning Intents

Outside of an intent being resolved during onRamp, intents can be pruned in the signalIntent, cancelIntent, and withdrawDeposit function. Intent pruning occurs when there are outstanding intents that have been outstanding for longer than the specified intent expiration period (or it has been forced via cancelIntent). We do this to prevent spammers from locking up depositors funds for long periods of time. The implication is that on rampers have less than the intent expiration period to provide their proof of payment to unlock their funds, otherwise their intent can be pruned and thus their escrowed funds released back to the deposit pool.

We choose to passively prune intents as we go in order to not require the depositor to manually maintain their deposit state. The signalIntent function makes sense as a place to prune since this allows escrowed funds to be released into the deposit pool in order to satisfy any liquidity needs not met by unclaimed funds. We also prune in withdrawDeposits so that depositors don’t have to wait for someone to signal an intent on their deposit in order to get funds tied in expired escrows back.

Pruning vs Resolving Intents

Pruning an intent occurs when there is an outstanding intent that has elapsed past the expiration time (or it has been force canceled by the originator). Resolving an intent occurs when an on-ramping action has been completed with that intent. The main difference between pruning and resolving intents is that when an intent is pruned the amount of USDC associated with the intent is added back to the remainingDeposits field while for resolved intents it is solely deleted from the outstandingIntentAmount field. In both cases the intentHashes field is updated (intentHash removed).

Sustainability Fee

The protocol has the option to charge a sustainability fee on each on ramp transaction. Because ZKP2P is funded entirely from grants in the sustainability fee is included to be able to fund continued development of the protocol. Additionally, this gives us extra flexibility to explore ways to return value to our users.

DKIM Processors (Venmo, Garanti)

Processors are contracts that are responsible for verifying, parsing, and validating outputs of proofs. Some processors are also supported by an assortment of Adapters and Registries (see Adapters and Registries section for more information) that provide information to the Processor on things such as nullifier usage and e-mail server domain keys. Additionally, all Processors inherit from a contract called BaseProcessor that holds some common administrative logic common across Processors. The BaseProcessor has two main administrative functions it endows inheriting contracts with:

  • setMailServerKeyHashAdapter: This allows an admin to update the contract that fetches the hash of the mail server public key. This function is necessary because mail server keys are often rotated and we expect we will continue to evolve the ways in which the contracts fetch the current mail server key hash. It is our hope to culminate in a decentralized registry of mail server keys that Processors can pull from.

  • setEmailFromAddress: This allows an admin to update the from address expected to send e-mails that are being verified. It is important this parameter is changeable so that if the from address is ever changed we can quickly update it in order to continue operations of the protocol.

Registration Processors

The Registration Processor is responsible for validating proofs as part of Ramp's register flow. It only has one callable function, processProof.

processProof The processProof function receives a struct from (and only from) the Ramp contract containing a proof that states a protocol user has an account on the specified payment network. If all validations pass a userIdHash is returned which represents a Poseidon hash of the user's payment network ID. The following validations can be found in the processProof function:

  • A valid zero-knowledge proof was created from the Registration circuit

  • The signer of the e-mail submitted to the circuit was the Venmo e-mail server (via comparison of DKIM public key hash)

  • The sender of the e-mail submitted to the circuit was the known Venmo admin address (i.e. venmo@venmo.com)

Send Processors

The Send Processor is responsible for validating proofs as part of Ramp's onRamp flow. It only has one callable function, processProof.

processProof The processProof function receives a struct from (and only from) the Ramp contract containing a proof that states a userA sent some amount, x, to userB. Where userA is the on-ramper and userB is the off-ramper. If all validations pass the following data is returned:

  • Amount the transaction was for

  • Timestamp of the transaction

  • Hash of the off-ramper's payment network ID

  • Hash of the on-ramper's payment network ID

  • intentHash of the intent being resolved by the transaction

No further validations of the above returned data are done in the Processor contract, this data is validated in the Ramp contract. However, there are still the following validations done in the processProof function:

  • A valid zero-knowledge proof was created from the Send circuit

  • The signer of the e-mail submitted to the circuit was the Venmo e-mail server (via comparison of DKIM public key hash)

  • The sender of the e-mail submitted to the circuit was the known Venmo admin address (i.e. venmo@venmo.com)

  • The e-mail provided to the circuit was not used in any prior on ramp transaction. To do this we check that a has of the e-mail is not present in the NullifierRegistry.

All e-mails submitted to Processors are subject to nullifcation. This means that they cannot be reused in the future in order to prevent any potential replay attacks or false registrations.

Last updated