lx: fix data race in LendingPool.Borrow/Repay

Borrow and Repay used a per-AssetPool mutex (pool.mu) while Supply,
Withdraw and the read accessors all serialize via the LendingPool-wide
lock (lp.mu). The shared LendingPool maps (TotalBorrowed, UtilizationRates,
AccruedFees, ReserveFunds) and per-pool fields written by updateInterestRates
(UtilizationRate, BorrowRate, SupplyRate) were therefore protected by two
different locks, so concurrent Supply + Borrow races on the same
asset — reliably reproduced by TestLendingPool_ConcurrentAccess under
-race.

Use lp.mu in Borrow and Repay so every mutator on a given LendingPool
serializes through the same mutex, and drop the now-dead AssetPool.mu
field (single way to do everything).

Verified with: go test -race -count=50 -run TestLendingPool_ConcurrentAccess
This commit is contained in:
2026-05-16 19:21:24 -07:00
parent c9e3b72466
commit fedb77c1da
+6 -7
View File
@@ -50,7 +50,6 @@ type AssetPool struct {
Suppliers map[string]*SupplyPosition
Borrowers map[string]*BorrowPosition
Paused bool
mu sync.RWMutex
}
// LendingAccount represents a supplier's account
@@ -322,14 +321,14 @@ func (lp *LendingPool) Withdraw(userID, asset string, amount *big.Int) error {
// Borrow allows users to borrow against collateral
func (lp *LendingPool) Borrow(asset string, amount *big.Int) error {
// Implementation handled by MarginEngine
lp.mu.Lock()
defer lp.mu.Unlock()
pool, exists := lp.Pools[asset]
if !exists {
return fmt.Errorf("pool not found for %s", asset)
}
pool.mu.Lock()
defer pool.mu.Unlock()
// Check available liquidity
available := new(big.Int).Sub(pool.TotalSupply, pool.TotalBorrow)
if amount.Cmp(available) > 0 {
@@ -349,14 +348,14 @@ func (lp *LendingPool) Borrow(asset string, amount *big.Int) error {
// Repay repays borrowed amount with interest
func (lp *LendingPool) Repay(asset string, principal, interest *big.Int) error {
lp.mu.Lock()
defer lp.mu.Unlock()
pool, exists := lp.Pools[asset]
if !exists {
return fmt.Errorf("pool not found for %s", asset)
}
pool.mu.Lock()
defer pool.mu.Unlock()
// Update pool
pool.TotalBorrow.Sub(pool.TotalBorrow, principal)
lp.TotalBorrowed[asset].Sub(lp.TotalBorrowed[asset], principal)