Problem Statement / Feature Objective
The settlement contract invokes external callbacks on the resource token contract during payment finalization, specifically calling post_settlement_hook() on each token address involved in the batch. If the resource token contract is malicious or contains exploitable logic that re-enters the settlement contract before the first invocation completes, it can manipulate intermediate settlement state—double-claiming payments or inflating balances. Soroban contracts execute atomically within a single ledger entry, but reentrancy can still occur at the cross-contract call boundary when control flow is yielded via env.invoke_contract(). The objective is to implement a reentrancy guard using a mutex pattern stored in temporary contract data with a TtlEntry that lives for the duration of the contract call.
Technical Invariants & Bounds
The reentrancy guard must be implemented as a DataKey::ReentrancyLock boolean stored in contract storage with a TTL of 1 ledger (approximately 5 seconds). The guard MUST be set BEFORE any env.invoke_contract() call and cleared AFTER the call returns, using a try-finally pattern via Rust's Drop implementation. Soroban panic will revert all storage writes, so the guard must be cleared in a Drop impl that runs even on unwind. The guard flag uses a single byte (DataEntry with key 0x01). Operation cost: 2 storage writes (set + clear) = ~20,000 instructions. The maximum callback depth is bounded to MAX_CALLBACK_DEPTH = 3 to limit potential damage from reentrant attacks. The settlement batch size is bounded to MAX_BATCH_SIZE = 50 tokens per call. The invariant is: at no point during the execution of finalize_settlement() should the reentrancy lock be unset when an external call is in flight.
Codebase Navigation Guide
Core settlement logic: contracts/settlement/src/lib.rs — find finalize_settlement() at line 142 and its internal process_token_settlement() helper at line 203. The external callback to resource tokens is invoked via env.invoke_contract() in contracts/settlement/src/callbacks.rs at line 56. Storage key definitions are in contracts/settlement/src/storage.rs where DataKey enum is defined. Existing tests are in contracts/settlement/src/test.rs. The resource token hook interface is defined in contracts/resource-token/src/lib.rs at pub fn post_settlement_hook() around line 310.
Implementation Blueprint
Step 1: In contracts/settlement/src/storage.rs, add ReentrancyLock variant to DataKey. Step 2: Create contracts/settlement/src/reentrancy.rs with a ReentrancyGuard struct holding an Env reference, implementing fn new(env: &Env) -> Self that checks lock not already set (panic if set), sets lock, returns guard. Implement Drop that clears the lock. Step 3: In lib.rs, wrap the body of finalize_settlement() and process_token_settlement() with let _guard = ReentrancyGuard::new(&env); at entry. Step 4: In callbacks.rs, wrap each env.invoke_contract() call with a depth counter check using DataKey::CallbackDepth that panics if depth > MAX_CALLBACK_DEPTH. Step 5: Add tests in test.rs simulating a malicious token contract that attempts reentry; assert the second entry panics. Step 6: Verify all test pass with cargo test --package settlement.
Problem Statement / Feature Objective
The settlement contract invokes external callbacks on the resource token contract during payment finalization, specifically calling post_settlement_hook() on each token address involved in the batch. If the resource token contract is malicious or contains exploitable logic that re-enters the settlement contract before the first invocation completes, it can manipulate intermediate settlement state—double-claiming payments or inflating balances. Soroban contracts execute atomically within a single ledger entry, but reentrancy can still occur at the cross-contract call boundary when control flow is yielded via env.invoke_contract(). The objective is to implement a reentrancy guard using a mutex pattern stored in temporary contract data with a TtlEntry that lives for the duration of the contract call.
Technical Invariants & Bounds
The reentrancy guard must be implemented as a DataKey::ReentrancyLock boolean stored in contract storage with a TTL of 1 ledger (approximately 5 seconds). The guard MUST be set BEFORE any env.invoke_contract() call and cleared AFTER the call returns, using a try-finally pattern via Rust's Drop implementation. Soroban panic will revert all storage writes, so the guard must be cleared in a Drop impl that runs even on unwind. The guard flag uses a single byte (DataEntry with key 0x01). Operation cost: 2 storage writes (set + clear) = ~20,000 instructions. The maximum callback depth is bounded to MAX_CALLBACK_DEPTH = 3 to limit potential damage from reentrant attacks. The settlement batch size is bounded to MAX_BATCH_SIZE = 50 tokens per call. The invariant is: at no point during the execution of finalize_settlement() should the reentrancy lock be unset when an external call is in flight.
Codebase Navigation Guide
Core settlement logic: contracts/settlement/src/lib.rs — find finalize_settlement() at line 142 and its internal process_token_settlement() helper at line 203. The external callback to resource tokens is invoked via env.invoke_contract() in contracts/settlement/src/callbacks.rs at line 56. Storage key definitions are in contracts/settlement/src/storage.rs where DataKey enum is defined. Existing tests are in contracts/settlement/src/test.rs. The resource token hook interface is defined in contracts/resource-token/src/lib.rs at pub fn post_settlement_hook() around line 310.
Implementation Blueprint
Step 1: In contracts/settlement/src/storage.rs, add ReentrancyLock variant to DataKey. Step 2: Create contracts/settlement/src/reentrancy.rs with a ReentrancyGuard struct holding an Env reference, implementing fn new(env: &Env) -> Self that checks lock not already set (panic if set), sets lock, returns guard. Implement Drop that clears the lock. Step 3: In lib.rs, wrap the body of finalize_settlement() and process_token_settlement() with let _guard = ReentrancyGuard::new(&env); at entry. Step 4: In callbacks.rs, wrap each env.invoke_contract() call with a depth counter check using DataKey::CallbackDepth that panics if depth > MAX_CALLBACK_DEPTH. Step 5: Add tests in test.rs simulating a malicious token contract that attempts reentry; assert the second entry panics. Step 6: Verify all test pass with cargo test --package settlement.