How to Handle Race Conditions in ICP Canisters

I’m building a finance application on the Internet Computer (ICP) where users can borrow tokens. I’m facing a race condition issue where multiple users can borrow tokens at the same time, leading to an incorrect state.

Example Scenario:

  • The system has 500 tokens available for borrowing.
  • Two users simultaneously request to borrow 500 tokens each.
  • If not handled properly, both requests might succeed, resulting in a negative balance (-500 tokens).

Issue:

  • ICP canisters process requests asynchronously, meaning two transactions can start at the same time, making state updates inconsistent.
  • Using a mutex (locking mechanism) didn’t work because if both users start their borrow request at the same time but one completes before the other, the second user’s transaction still operates on outdated data, leading to incorrect results.
  • How can we prevent double borrowing while ensuring transactions remain efficient and responsive?

This issue i am facing with all kind of transactions like supply, withdraw and repay as well.

Hi @jyotirmaygithub,
ICP has an asynchronous model for messaging between canisters, but each canister handles one update at a time in a single threaded manner. So if you take a lock out on the 500 tokens when handling the first request (before making any async calls), then the second request won’t be able to grab them (even if the first request hasn’t been replied to yet). Some further guidance can be found here and here.

The case is my entire function is aync by nature, because inside of it numerous async calls are there to proceed.

That’s fine. It still sounds like you want to take a lock out on the tokens as soon as a request comes in and before you make calls to other canisters. That chunk of code will only be run once at a time, so whichever message is handled first will get the lock.

Issue (in more descriptive way)

Specifically, when two users attempt to borrow from a shared data asset simultaneously, both transactions read the initial state (e.g., borrow and reserve balances as zero) and proceed independently. This results in an incorrect final state (e.g., both users borrowing 30 units, but the reserve only reflecting 30 instead of the expected 60). This issue is prevalent across various transaction types like supply, withdraw, and repay.

I have considered several approaches to mitigate this, such as atomic transactions, mutexes, optimistic concurrency control, and queue-based processing but nothing works, so can you suggest me something else which can be useful. And i considered your links as well.

On ICP, updates to a canister happen sequentially so it’s impossible for two transactions to “borrow a shared data asset simultaneously”. If two messages come in, one will get handled first (up until its first await point) before the second starts. So you just need to update the state before that first await point occurs. Maybe it would also help to look at this example of a canister which keeps a counter: examples/rust/counter at master · dfinity/examples · GitHub. It has an inc method which increments the counter and there are no race conditions between multiple calls to that endpoint.

1 Like