Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mcintyre94 opened this issue Sep 11, 2022 · 5 comments

Comments

@mcintyre94
Copy link
Collaborator

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:

  • They're using Solana Pay TX requests to do gasless NFT minting, by making a signer of theirs the payer
  • It's supposed to be for a SOAP
  • Somebody created a bunch of wallets and used their API route to get the transaction and executed it themselves, draining the payer wallet and emptying the candy machine

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:

  • Another coordination challenge to get these keypairs generated and published, and wallets updated to sign requests
  • APIs would only support the wallets that they verified against. Could probably mitigate against this by publishing a library that checks them all similar to wallet-adapter, but still doesn't seem great
  • Would expose which wallet was used to the API, which currently won't happen unless the wallet chooses to identify itself in its request

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
Copy link
Collaborator

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
Copy link

sevazhidkov commented Sep 12, 2022

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:

  • Give unique Solana Pay links for each cart/payment attempt (Stripe calls it "session"). Implement checks when giving out these links, Solana Pay POST request should just check whether this particular link exists and wasn't used before.
  • Allow ~5 requests per device id using a tool like Fingerprint.js
  • Allow ~5 requests per IP address per day
  • The checks above make it very unprofitable to try draining your fees at scale.
  • If someone is very determined to burn your money and doesn't mind spending much more than you, limit the impact. Separate transaction fee payer keypair from payment receiver keypair. Automatically top-up the payer keypair every few hours with maximum amount of SOL you can afford to spend on transaction fees.

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
Copy link
Collaborator

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
Copy link

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
Copy link
Collaborator

jordaaash commented Sep 15, 2022

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants