Protecting Funds in Smart Signature Contract Accounts

Using smart signatures as escrow contract accounts seems to be generally discouraged, and for good reasons, but with the relatively new opcode (ed25519verify_bare), my article discusses an approach for going there! Feedback/comments welcome.

Be careful with this! It’s not a secure way of protecting funds. The problem with this design is that every transaction is public, even before it’s added to a block. This means that a malicious actor could watch the memory pool and perform a MITM attack with the same signature.

If you are generating a key-pair for the recipient, why not just generate them an Algorand account that is used in the logic of the smart contract?

1 Like

Really appreciate you leaning into this. To unpack this a bit …

The TEAL is written to distribute all funds to the recipient (and close out the account) when the recipient submits the transaction using the correct secret key (signature).

So, in order to break this, the attacker would have to observe this transaction in the mempool, then create and submit a new transaction (with same signature), which would then need to be processed AHEAD of the original transaction. If this scenario possible?

Yes, this scenario is possible if the adversary is better connected to relays than the sender (which itself is possible).

It is even easier if the block proposer is malicious or if the node used by the sender is malicious.
But these assumptions are not even needed to create an issue.

Thanks for these insights - not what I expected, and very edge case, but obviously needs to be respected.
My scenario involves only knowing who the receiver is AFTER funds are already in the contract account.

So, in an attempt to wrap this up, please clarify whether following is an accurate statement:

Assuming adherence to the usual smart contract best practices, the only way to secure funds in a smart signature contract account is to pre-define the receiver, close remainder, asset close to etc. etc. to prevent malicious overrides. Just looking to create a hard boundary around implementation details.

This is one option.
This is not the only option,
You can also have the smart signature takes as input a signature of the recipient address, signed under a public key stored in the contract.
You can also use the smart signature in conjunction with a smart contract that would contain, in its global storage, the recipient of the account.

However, as @joe-p pointed out, in most of these use cases, it is simpler to actually have the sender sign the transaction sending the assets to the recipient.
Since the sender would anyway need to sign either the address of the recipient or some transaction to update a smart contract.

I believe I am already doing this one " You can also have the smart signature takes as input a signature of the recipient address, signed under a public key stored in the contract." - see code section below.

The contract is setup such that the secret key is known only to the recipient, which is used to sign the recipient’s address → arg 1: signature. The secret key is the other side of the ${pubKey} hard-coded below. Therefore, even if a snooper knows the signature (from the mempool), it only passes ed25519verify_bare if AssetReceiver is what was used originally. To change the receiver successfully, they would have to know the secretKey. Seems like this solution should be secure ?

  txn AssetReceiver
  arg 1 // Signature
  pushbytes b64 ${pubKey}
  bnz handle_next_thing // whatever that is for you

  // If we reach here, impostor alert, the signature is no good!
  b handle_fail 

I’ve not checked in detail, but it should be secure at first glance.

1 Like

Yes apologies, didn’t catch that is what you are doing. In that case that is acceptable, with the only potential “attack” being an AssetTransfer with different fields (ie. less assetAmount) being sent, but I assume this is not of concern for your use case.

That being said, I still fail to see why generating a ed25519 keypair to sign an Algorand account (which is just another keypair) is useful here when you could instead just generate an Algorand account and hardcode the pubkey for the account in the contract.

Ok great, so just to clarify, everything pretty much stays the same except for how the keypairs get generated, i.e. no need to use nacltweet directly, just use the equivalent algorand account functionality, which includes wrappers to nacltweet.

So, the current approach seems OK, the above is more of an optimization in steps?