Skip to content

Fix/executor liquidatable errors#37

Open
djaciel wants to merge 4 commits intomainfrom
fix/executor-liquidatable-errors
Open

Fix/executor liquidatable errors#37
djaciel wants to merge 4 commits intomainfrom
fix/executor-liquidatable-errors

Conversation

@djaciel
Copy link
Contributor

@djaciel djaciel commented Aug 27, 2025

No description provided.

…handling for delayed retries. Introduce new properties in Position interface for execution timing and liquidatability status. Refactor Redis operations for position management.
@TomasCImach
Copy link
Member

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines +75 to +91
export async function checkIfPositionStillLiquidatable(position: Position): Promise<boolean> {
try {
const { marketAddress, network, positionId, owner } = position

const provider = new ethers.providers.JsonRpcProvider(networksConfig[network].rpc_url)

// Use the same market state contract that liquidation-checker uses
const ovlMarketStateContract = new ethers.Contract(
'0x10575a9C8F36F9F42D7DB71Ef179eD9BEf8Df238', // ovl_state_address from config
[
'function liquidatable(address market, address owner, uint256 positionId) view returns (bool)'
],
provider
)

// Check if position is still liquidatable
const isLiquidatable = await ovlMarketStateContract.liquidatable(marketAddress, owner, parseInt(positionId))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid parsing positionId into a JavaScript number

The new liquidatability check converts position.positionId with parseInt before calling the contract. parseInt returns a JavaScript number, which loses precision once the value exceeds Number.MAX_SAFE_INTEGER. For large position IDs the contract is called with a truncated ID, so valid positions may be reported as non‑liquidatable and removed from Redis while still being liquidatable on chain. The call can take the string or a BigNumber directly—no conversion is needed.

Useful? React with 👍 / 👎.

Comment on lines +281 to +378
describe('liquidablePositionsListener - Real Flow Tests', () => {
it('should process delayed positions first, then check for new positions', async () => {
// Mock delayed positions ready to retry
vi.mocked(redis.zrangebyscore).mockResolvedValue([JSON.stringify(mockPosition)])
vi.mocked(redis.zrem).mockResolvedValue(1)

// Mock no new positions available
vi.mocked(redis.brpop).mockResolvedValue(null)

// Mock successful liquidation for delayed position
const mockReceipt = { status: 1, transactionHash: '0xhash' }
const mockTransaction = {
wait: vi.fn().mockResolvedValue(mockReceipt),
}
mockMarketContract.liquidate.mockResolvedValue(mockTransaction)

vi.mocked(redis.hdel).mockResolvedValue(1)
vi.mocked(redis.incr).mockResolvedValue(1)

// Start the listener (it will run one iteration)
const listenerPromise = liquidablePositionsListener()

// Wait a bit for the first iteration
await new Promise(resolve => setTimeout(resolve, 100))

// Verify both queues were processed
expect(redis.zrangebyscore).toHaveBeenCalledWith('delayed_positions', 0, expect.any(Number), 'LIMIT', 0, 1)
expect(redis.brpop).toHaveBeenCalledWith('liquidatable_positions', 1)

// Clean up
vi.mocked(redis.brpop).mockRejectedValue(new Error('Stop listener'))
await new Promise(resolve => setTimeout(resolve, 100))
})

it('should process new positions when available', async () => {
// Mock no delayed positions ready
vi.mocked(redis.zrangebyscore).mockResolvedValue([])

// Mock new position available
vi.mocked(redis.brpop).mockResolvedValue(['liquidatable_positions', JSON.stringify(mockPosition)])

// Mock successful liquidation
const mockReceipt = { status: 1, transactionHash: '0xhash' }
const mockTransaction = {
wait: vi.fn().mockResolvedValue(mockReceipt),
}
mockMarketContract.liquidate.mockResolvedValue(mockTransaction)

vi.mocked(redis.hdel).mockResolvedValue(1)
vi.mocked(redis.incr).mockResolvedValue(1)

// Start the listener (it will run one iteration)
const listenerPromise = liquidablePositionsListener()

// Wait a bit for the first iteration
await new Promise(resolve => setTimeout(resolve, 100))

// Verify new position was processed
expect(mockMarketContract.liquidate).toHaveBeenCalledWith(
mockPosition.owner,
mockPosition.positionId
)

// Clean up
vi.mocked(redis.brpop).mockRejectedValue(new Error('Stop listener'))
await new Promise(resolve => setTimeout(resolve, 100))
})

it('should handle both queues independently every second', async () => {
// Mock delayed positions ready
vi.mocked(redis.zrangebyscore).mockResolvedValue([JSON.stringify(mockPosition)])
vi.mocked(redis.zrem).mockResolvedValue(1)

// Mock no new positions available
vi.mocked(redis.brpop).mockResolvedValue(null)

// Mock successful liquidation for delayed position
const mockReceipt = { status: 1, transactionHash: '0xhash' }
const mockTransaction = {
wait: vi.fn().mockResolvedValue(mockReceipt),
}
mockMarketContract.liquidate.mockResolvedValue(mockTransaction)

vi.mocked(redis.hdel).mockResolvedValue(1)
vi.mocked(redis.incr).mockResolvedValue(1)

// Start the listener (it will run one iteration)
const listenerPromise = liquidablePositionsListener()

// Wait a bit for the first iteration
await new Promise(resolve => setTimeout(resolve, 100))

// Verify both queues were processed independently
expect(redis.zrangebyscore).toHaveBeenCalledWith('delayed_positions', 0, expect.any(Number), 'LIMIT', 0, 1)
expect(redis.brpop).toHaveBeenCalledWith('liquidatable_positions', 1)

// Clean up
vi.mocked(redis.brpop).mockRejectedValue(new Error('Stop listener'))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Tests never stop the infinite listener

The tests invoke liquidablePositionsListener() but do not provide any mechanism to cancel the while (true) loop. After the expectations run, the listener continues scheduling setTimeout callbacks forever, so the Vitest process will not exit and the test suite hangs. Consider adding a way to signal the listener to stop or refactor the tests to avoid spawning a non‑terminating async loop.

Useful? React with 👍 / 👎.

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

Successfully merging this pull request may close these issues.

2 participants