A simple idea to avoid test payments

Test payments are common in crypto. Send a little bit of funds. Wait until the other party has confirmed they received it, then send the whole amount. The reason for this little dance is the fear that the address was somehow miscommunicated, that it’s an old address, a misspelled address, or somebody else’s address. It takes time, and it can’t happen asynchronously.

Instead consider the following protocol. Alice generates a secret claim_code and sends tez to a smart contract, alongside a hash of the claim_code and the recipient’s address. At any point in time, Alice can call the contract to take back those funds, so even if she entered the wrong hash, the funds are not locked.

Bob who’s the recipient gets the claim_code from Alice off band, maybe via email. The code is secret, but it does not need to be super secret.

Bob can claim the funds by calling the contract with the claim code. The contract verifies that the hash of the claim call and his address match the hash of the deposit and releases the funds.

What if Alice used a bad address? No problem, she can claim back the fund. What if Alice used someone else’s address? They won’t know because they don’t know the claim code. Even if the claim code leaks, for a problem to occur, Alice would have had to use the address of someone who’s constantly scanning the chain to see if there are funds they can erroneously claim. Thus it is much better to keep claim_code secret.

I think this could improve payment UX in crypto quite a bit if well integrated in wallets. Here’s a short 100% untested, 100% unaudited implementation to illustrate what I mean. A full implementation would also support FA1.2 and FA2 which require a slightly different albeit very similar mechanism.

type fund_record = {
  sender: address;  
  amount: tez;
}

type storage = (bytes, fund_record) big_map

// Entry point for depositing funds
let deposit (storage: storage) (hash: bytes) : storage =  
    let record: fund_record = {sender = Tezos.get_sender (); amount = Tezos.get_amount ()} in
        if Big_map.mem hash storage then
            failwith "Record already exists"
        else
            Big_map.update hash (Some(record)) storage    

// Entry point for claiming funds
let claim (storage: storage) (claim_code: bytes) (destination: address) : (operation list) * storage =
    let destination_contract : unit contract = Tezos.get_contract destination in
    let hash = Crypto.blake2b (Bytes.pack (claim_code, Tezos.get_sender())) in
    match Big_map.find_opt hash storage with
    | Some record ->
        let op = Tezos.transaction () record.amount destination_contract in
        ([op], Big_map.remove hash storage)
    | _ -> failwith "Invalid record"

// Entry point for refunding funds
let refund (storage: storage) (hash: bytes) (destination: address): (operation list) * storage =
    let destination_contract : unit contract = Tezos.get_contract destination in
    match Big_map.find_opt hash storage with
    | Some record -> 
      if record.sender = Tezos.get_sender () then
        let op = Tezos.transaction () record.amount destination_contract in
        ([op], Big_map.remove hash storage)
      else
        failwith "Invalid caller"
    | _ -> failwith "Invalid record"


// Main entry point
type action =
  | Deposit of bytes
  | Claim of bytes * address
  | Refund of bytes * address
  | Default 

let main (param: action) (storage: storage) : (operation list) * storage =
  match param with
    Deposit (hash) -> ([], deposit storage hash)
  | Claim (claim_code, destination) -> claim storage claim_code destination
  | Refund (hash, destination) -> refund storage hash destination
  | Default -> failwith "Do not send funds to the default entry point"
8 Likes

This would certainly work perfectly. My only question is: it allows paying safely in a single pass, but does it really take less time than sending a test payment first and the rest after?

How is it different from tornado cash?

Yes because you can do it asynchronously.

1 Like

Tornado Cash provides anonymity, this doesn’t. This provides a UX benefit to speed up a common payment pattern, Tornado Cash doesn’t. They basically don’t have much in common and solve a different problem.

I like it! It’s quite annoying having to do “test payments” all the time. Though I only do it for large sums these days. Would it makes sense to add this to wallets? :thinking:

1 Like

This could be useful if the amount to send is big and the extra gas fees to spend on this protocol do not represent a blocker. Also if the recipient has minimum gas to do the claim, otherwise it cannot do a transaction

There is no mechanism today to check if a tz1xxxx “IBAN” exists =)
Can we have at least a protocol check that the public key from a tz1xxxx has been revealed onchain ? It would be a minimal secure check (imo, it does not exist today)

In Canada, we have a service which solve this case in tradfi : How to send money via email or text - INTERAC e-Transfer

Currently when I have time, creating a tutorial to develop this simple idea.
Contract code here : https://github.com/Laucans/Intezos-tutorial-2/blob/main/contracts_final/intezos.jsligo

Tutorial preview here :

GitHub

GitHub - Laucans/Intezos-tutorial-1

Contribute to Laucans/Intezos-tutorial-1 development by creating an account on GitHub.

GitHub

GitHub - Laucans/Intezos-tutorial-2: P1 here :…

P1 here : GitHub - Laucans/Intezos-tutorial-1 - GitHub - Laucans/Intezos-tutorial-2: P1 here : GitHub - Laucans/Intezos-tutorial-1

GitHub

GitHub - Laucans/Intezos-tutorial-3: Intezos-tutorial-3

Intezos-tutorial-3. Contribute to Laucans/Intezos-tutorial-3 development by creating an account on GitHub.

I have to rewrite Readme before to make it public.

To make it prod ready I have to :

  • Implement tests and fix the desired bug (which gonna be detected and resolved in part-4).
  • Redo of the UI design (Currently it is ugly because only used for tutorial purpose)
  • Audit

Tutorial will introduce tests in part-4.

I plan to introduce usage of tickets (as receipt).
Thinking about introducing FA2.1 too. I have to think how.

If people want to test the concept → https://github.com/Laucans/Intezos-tutorial-3/tree/main/ui-final

1 Like

New protocol feature : Delayed transaction

A delayed transaction is taking the money from a sender into a global vault for x hours. If the sender does not claim back this money , it is sent automatically to the receiver when the delay expires. This kind of operation can be tracked by a wallet and displayed as forecast amounts

I forgot to say that only the sender can cancel its own delayed transfers. It is quite obvious

My personal use of test payment is when i interact with custodial addresses… so it doesn’t help me =)

Whatever i think that’s a good solution for P2P payments and it can also be used to provide a proof of founds for C2C marketplaces

1 Like

This solves the problem of : sending to incorrect address (unrevealed or not)

  1. if an offchain communication between the sender and receiver confims that all is ok. Wait the the expiration and the transfer is done automatically
  2. if an offchain communication between the sender and receiver reveals a problem , the sender cancel the transfer
  3. if there is no communication between the sender and receiver during the period of check, delay expired and transaction is done sorry … you are both too bad