# Same-block guard bypass via transferable positions

Guards keyed per address or one path; check every path and transferable position that moves the same balances

_Area: DeFi oracles, liquidations and flash loans. Seen in 2 disclosed reports; latest 2024-06-17. Updated 2026-10-24._

## How it works
A protocol tries to stop same-block manipulation (deposit then withdraw, or a rate change) by recording a block number and rejecting a second action in that block. The assumption that breaks is that the guarded action is tied to one identity or one code path. If the position, share token, or collateral balance can move to another address or another internal path without updating the recorded block, the guard no longer covers the funds. The attacker then performs the two halves of the manipulation through different identities or paths in one block.

## Where it shows up
- A `lastDepositBlock` / `lastTransactionBlock` mapping keyed on `msg.sender` or an account id, while the share or LP token is ERC20-transferable.
- Guard checks present in `deposit`/`withdraw` but absent in `liquidate`, `move`, `transfer`, `migrate`, or internal balance-shifting helpers.
- Collateral or debt moved between NFTs/accounts without writing the receiving account's block marker.
- Oracle or rate checks that reject when a timestamp equals `block.timestamp` rather than measuring staleness.

## How to review
Identify the invariant: every path that changes a guarded balance or position must update the same block marker that the guard reads, and the marker must follow the position, not the caller. Enumerate all functions that move the balance (deposit, withdraw, liquidate, move, transfer, migrate) and confirm each writes the marker on the receiving side. For transferable share tokens, confirm the guard is keyed on the position/token, not `msg.sender`. For freshness checks, confirm they compare against a staleness window, not equality with the current block.

## Variants seen
- Liquidation path `vault.move()` shifts collateral without setting the receiving vault's last-deposit block, enabling same-block withdraw [E1](https://bugbountylibrary.com/r/c4%3A2024-04-dyad%3AH-10.md).
- Guard keyed on `msg.sender` while LP tokens are transferable; a second address withdraws in the same block [E2](https://bugbountylibrary.com/r/cantina%3Aopal_feb2024%3A3.1.1.md).
- Oracle rejects any pair whose `blockTimestampLast` equals `block.timestamp`, so a permissionless `sync()` griefs victims per block [E3](https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AM-02.md).

## Impact and severity
E1 let attackers mint at ~100% CR and leave bad debt; E2 enabled exchange-rate manipulation; E3 caused per-block griefing of valid transactions. Severity is high where bad debt or rate manipulation results; griefing is lower but repeatable. Worse when the bypass is cheap and permissionless.

## Why fixes fail
Guards added to one path (deposit/withdraw) leave sibling paths (liquidation, move) unguarded [E1](https://bugbountylibrary.com/r/c4%3A2024-04-dyad%3AH-10.md); per-address guards fail once positions are transferable [E2](https://bugbountylibrary.com/r/cantina%3Aopal_feb2024%3A3.1.1.md).

## Evidence
- E1: https://bugbountylibrary.com/r/c4%3A2024-04-dyad%3AH-10.md
- E2: https://bugbountylibrary.com/r/cantina%3Aopal_feb2024%3A3.1.1.md
- E3: https://bugbountylibrary.com/r/c4%3A2026-01-olas%3AM-02.md
