anza-xyz / solana-pay

A new standard for decentralized payments.
https://solanapay.com
Apache License 2.0
1.31k stars 460 forks source link

Is there anything we can do to let an API verify the request comes from a wallet? #157

Open mcintyre94 opened 2 years ago

mcintyre94 commented 2 years ago

Opening this after seeing this SE question: https://solana.stackexchange.com/questions/2953/how-to-stop-bots-from-spamming-solana-pay-apis

Summary of that:

Basically there's a use case here where the transaction shouldn't be entirely public, and they only want it to work in Solana Pay.

Currently I don't think we can really do this, the wallet just sends the public key and if you know the API you can trivially do the same. There's no obvious way to secure this, the wallet signing something with the user's private key wouldn't stop someone who owns the public key they're passing in.

The only idea I can really think of is for wallets to each generate a keypair and publish the public key. They could then sign requests and APIs could verify that it came from them using the public key. I don't like this solution though:

My guess is that there's not a realistic solution here, you just need to keep the API link secret and if it's leaked then anyone can perform the transaction it produces. For most cases this is fine because the transaction has whatever costs it wants in it. But there is a limitation around things like a SOAP.

Figured I'd open an issue for discussion in case anyone has any better ideas!

jordaaash commented 2 years ago

It sounds like this may be related to #156. Have you checked out #152 yet? The specific use case for POAP is probably best served by not using transactions at all.

In general, I think using a static QR code for transaction requests is going to lead to problems, especially when signing to pay fees. When there's a client app, it can help make sure requests are unique, associate them with a user account beforehand, perform captcha if needed.

Having wallets sign requests is interesting but has problems (some of which you already pointed out). I would think key distribution and security is a challenge. How does each installed instance of a given wallet app obtain the same key to sign with? Is there a way for an attacker to obtain this from the binary? I assume there must be. How do you rotate keys if compromised?

cc @SevaZhidkov who has been thinking about some of these things with regard to https://github.com/solana-labs/octane

sevazhidkov commented 2 years ago

Re: other ways to prevent spam other than verifying that it's a request from a wallet.

I'd decompose this issue into two. Incentives for attackers and solutions for merchants for each problem are quite different.

  1. NFT distribution with Solana Pay

    Jordan has already pointed out that proof of attendance is better served by message signing. There is a bunch of other things you can do using unique links (one link - one mint allowed): for example, provide attendees with a dynamic QR code with different links generated on the backend, changing every 10 seconds and physically protect it or require unique proof-of-humanity / Twitter auth / event badge scan before giving out a unique link. In general, fair minting of NFTs without some sort of proof-of-humanity (and, hence, giving out "unique tokens") seems to be an unsolvable problem.

One thing to point out, adding Captcha + IP rate limits isn't the answer for NFT mints. There are APIs that solve 1k reCaptchas for $1 and a non-residential IP address costs ~$0.005 per hour. If NFT might have value in the future or there is value in not letting anyone get NFTs, an attacker can cover that.

  1. Transaction fee draining

This is a bit out of scope for the original issue, since the problem here is that more people were allowed to mint than desired. If all of them were legitimate, unique Hacker House participants, CandyPay probably wouldn't have a problem with paying all of their transaction fees.

However, let's say it was a transaction designed to be "unlimited in volume": for example, transfer of an SPL token in exchange for merchant doing something off-chain (giving out content on an e-commerce website or giving out the order in a coffee shop) with transaction fees covered by merchant. Hypothetically, bots can get a pre-signed transaction from merchant, hold on to it, change the bank state to make it fail (e.g. withdraw all of their SPL tokens), then submit the pre-signed transaction to the network with skip-preflight flag. Transaction fails, but merchant's transaction fee is spent. This is a nasty thing to do: bot doesn't gain anything but makes merchant waste transaction fee.

When it's offline, the solution is easy: unique links for each request given by PoS system. You need 30 transaction to spend a cent for merchant — that seems impossible in a human-involved environment.

When it's online, here is how I'd combine checks in a UX friendly, but not too-hard-to-implement way:

Captcha might be an overkill here (ironically, Captcha is underkill for NFTs and overkill for transaction fees). It affects UX significantly, but for transaction fee draining device+IP address limitting works fine, as measured by cost-to-attack to fee payer spend.

jordaaash commented 2 years ago

Good points above. Merchants can also store wallet addresses that have made requests, and require wallets to have some balance and rate limit them based on address.

If an attacker has to spend SOL on transactions to create lots of token accounts, and also has to spend SOL on transactions to move the tokens to make the merchant's transaction fail, this becomes a very uneconomical attack.

Attacker pays for ~2 transactions with 1 signature each, merchant pays for 1 transaction with 2 signatures at ~ the same cost, plus the attacker needs to have SOL to make the token accounts rent-exempt.

I think you could go a step further and perform some checks against the history of the account balance, which would make it very expensive to create lots of accounts for this.

umangveerma commented 2 years ago

Hey! Umang from CandyPay here, thanks to Callum for raising this issue, these perspectives from @jordansexton and @sevazhidkov have helped us a lot to redefine and work on a new user flow

For every Gasless NFTs and SOAP minting to date, we directly used the solana: URL pointing to our server, which lets the wallet sign and perform the txn. But recently, someone passed the account directly into the body of API, got the partially signed transaction, and continued the process automatically till our payer wallet reached the limit to pay for minting

Keeping this QR Code link static caused problems for both our payer wallet getting drained and the UX being not so seamless as they need to have a wallet created beforehand and the current wallet providers don't support redirects (more on redirects in a new feature request issue)

That's why from the suggestions here and some feedback we gathered there, we are planning to shift it from a static mint URL to a static web page unique for each gasless collection, where users can social login, and we present them an expirable dynamic mint link which ones successfully minted will mark them true and new link won't be generated.

It will preferably be a better UX for new users and attackers won't have an easy way to create multiple accounts and mint again and again. Storing a pub key post mint is a way too, but looping through all addresses in dB and processing the txn may not be good for performance, as wallet providers sometimes timeout the req

jordaaash commented 2 years ago

@Vampo7152 yeah, sounds like a good approach. Static QR codes will lead to problems when used with transaction requests, they should probably only be used with transfer requests. Even then, it's better to have them be dynamic so you can use unique reference keys and manage the UX.

looping through all addresses in dB and processing the txn may not be good for performance

If you're persisting them, you should just be looking them up against an indexed column. You could also do this caching with Redis or something. You are less interested in persisting the cache forever than just having it be fresh enough to prevent DoS.

Caching in-memory is also perfectly fine, you can see how this is done in Octane by just keying an object with addresses.