From e872f0c9e21f698abb840e94c1325f973091862b Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:41:44 +0100 Subject: [PATCH 1/8] ContagionTrap example Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/ContagionTrap.sol | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 contagion-trap/ContagionTrap.sol diff --git a/contagion-trap/ContagionTrap.sol b/contagion-trap/ContagionTrap.sol new file mode 100644 index 0000000..3fdc222 --- /dev/null +++ b/contagion-trap/ContagionTrap.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./interfaces/ITrap.sol"; + +interface IUniswapV2Pair { + function getReserves() external view returns ( + uint112 reserve0, + uint112 reserve1, + uint32 blockTimestampLast + ); +} + +/** + * ContagionTrap + * - data[0] is newest snapshot (Drosera convention) + * - shouldRespond returns (true, "") when we want to call pause() + * + * - compute naive liquidity = reserve0 + reserve1 + * - compute average of previous snapshots (exclude newest index 0) + * - require avgPrev > MIN_LIQUIDITY_FLOOR to avoid tiny pools + * - trigger when newest < THRESHOLD * avgPrev (threshold = 0.5) + */ +contract ContagionTrap is ITrap { + address public immutable NEIGHBOR_DEX_POOL; + address public immutable LENDING_PROTOCOL; + + struct PoolSnapshot { + uint256 liquidity; + uint256 blockNumber; + } + + uint256 public constant THRESH_NUM = 1; // (THRESH_NUM / THRESH_DEN) = 0.5 + uint256 public constant THRESH_DEN = 2; + uint256 public constant MIN_LIQUIDITY_FLOOR = 1_000_000; // adjust if your pool units differ + + constructor(address _neighborDex, address _lendingProtocol) { + NEIGHBOR_DEX_POOL = _neighborDex; + LENDING_PROTOCOL = _lendingProtocol; + } + + /// collect: called by operators to snapshot neighbor pool + function collect() external view override returns (bytes memory) { + (uint112 r0, uint112 r1, ) = IUniswapV2Pair(NEIGHBOR_DEX_POOL).getReserves(); + uint256 liq = uint256(r0) + uint256(r1); + return abi.encode(PoolSnapshot({ liquidity: liq, blockNumber: block.number })); + } + + /// shouldRespond: Drosera passes an array of snapshots (data[0] newest) + function shouldRespond(bytes[] calldata data) + external + pure + override + returns (bool shouldTrigger, bytes memory responseData) + { + // Need at least newest + one previous snapshot + if (data.length < 2) return (false, bytes("insufficient_history")); + + PoolSnapshot memory newest = abi.decode(data[0], (PoolSnapshot)); + + // compute average of previous snapshots (indices 1..data.length-1) + uint256 sum = 0; + uint256 count = 0; + for (uint256 i = 1; i < data.length; ++i) { + PoolSnapshot memory s = abi.decode(data[i], (PoolSnapshot)); + sum += s.liquidity; + unchecked { ++count; } + } + if (count == 0) return (false, bytes("no_baseline")); + + uint256 avgPrev = sum / count; + + // Avoid triggering on tiny pools or tiny baselines + if (avgPrev < MIN_LIQUIDITY_FLOOR) return (false, bytes("baseline_too_small")); + + // Trigger if newest < THRESH * avgPrev (THRESH = 0.5 here) + // Cross multiply to avoid precision issues + if (newest.liquidity * THRESH_DEN < avgPrev * THRESH_NUM) { + // pause() has no args — return empty bytes + return (true, bytes("")); + } + + return (false, bytes("")); + } +} From 65e43d92119a4396acc4639a069a2dc710d8f400 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:43:48 +0100 Subject: [PATCH 2/8] ITrap.sol Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/interfaces/ITrap.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contagion-trap/interfaces/ITrap.sol diff --git a/contagion-trap/interfaces/ITrap.sol b/contagion-trap/interfaces/ITrap.sol new file mode 100644 index 0000000..cf82437 --- /dev/null +++ b/contagion-trap/interfaces/ITrap.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface ITrap { + /// @notice Collects state data to analyze later. + function collect() external view returns (bytes memory); + + /// @notice Decides whether a trap should respond, based on collected data. + /// @param data Collected snapshots (data[0] is newest). + /// @return shouldTrigger Whether the trap should trigger. + /// @return responseData Optional data for responder actions. + function shouldRespond(bytes[] calldata data) + external + pure + returns (bool shouldTrigger, bytes memory responseData); +} From 008a384b84b0c249a667dd4f018e24ea3040f5f4 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:06:13 +0100 Subject: [PATCH 3/8] Create ContagionTrap.sol Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/src/ContagionTrap.sol | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 contagion-trap/src/ContagionTrap.sol diff --git a/contagion-trap/src/ContagionTrap.sol b/contagion-trap/src/ContagionTrap.sol new file mode 100644 index 0000000..a767e28 --- /dev/null +++ b/contagion-trap/src/ContagionTrap.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITrap} from "../interfaces/ITrap.sol"; + +interface IUniswapV2Pair { + function getReserves() external view returns ( + uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast + ); +} + +/** + * @title ContagionTrap + * @notice Monitors a neighbor DEX pool; triggers if newest liquidity < 50% of + * the average of previous snapshots in the window. + */ +contract ContagionTrap is ITrap { + address public constant NEIGHBOR_DEX_POOL = 0x0000000000000000000000000000000000000000; + + struct PoolSnapshot { + uint256 liquidity; + uint256 blockNumber; + } + + function collect() external view override returns (bytes memory) { + (uint112 r0, uint112 r1, ) = IUniswapV2Pair(NEIGHBOR_DEX_POOL).getReserves(); + uint256 liq = uint256(r0) + uint256(r1); + return abi.encode(PoolSnapshot({ liquidity: liq, blockNumber: block.number })); + } + + function shouldRespond(bytes[] calldata data) + external + pure + override + returns (bool shouldTrigger, bytes memory responseData) + { + if (data.length < 2) return (false, bytes("insufficient_history")); + + PoolSnapshot memory newest = abi.decode(data[0], (PoolSnapshot)); + + uint256 sum; + for (uint256 i = 1; i < data.length; i++) { + PoolSnapshot memory s = abi.decode(data[i], (PoolSnapshot)); + sum += s.liquidity; + } + + uint256 avgPrev = sum / (data.length - 1); + if (newest.liquidity * 2 < avgPrev) { + return (true, bytes("")); + } + + return (false, bytes("")); + } +} From c10fab83f6f637d1fa3826b270e4127c43d137de Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:09:28 +0100 Subject: [PATCH 4/8] ContagionTrap.t.sol Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/test/ContagionTrap.t.sol | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 contagion-trap/test/ContagionTrap.t.sol diff --git a/contagion-trap/test/ContagionTrap.t.sol b/contagion-trap/test/ContagionTrap.t.sol new file mode 100644 index 0000000..7d57a0d --- /dev/null +++ b/contagion-trap/test/ContagionTrap.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../src/ContagionTrap.sol"; + +contract ContagionTrapTest is Test { + ContagionTrap trap; + + function setUp() public { + trap = new ContagionTrap(); + } + + function testTriggerWhenLiquidityDrops() public { + // Simulate older high liquidity snapshots + bytes ; + data[0] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 100, blockNumber: 100 })); + data[1] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 400, blockNumber: 90 })); + data[2] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 500, blockNumber: 80 })); + + // Since newest (100) < 50% of avg (450), should trigger + (bool shouldTrigger, ) = trap.shouldRespond(data); + assertTrue(shouldTrigger, "Expected trigger when liquidity drops by >50%"); + } + + function testNoTriggerWhenStableLiquidity() public { + bytes ; + data[0] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 450, blockNumber: 100 })); + data[1] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 500, blockNumber: 90 })); + data[2] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 480, blockNumber: 80 })); + + (bool shouldTrigger, ) = trap.shouldRespond(data); + assertFalse(shouldTrigger, "Should not trigger when liquidity stable"); + } + + function testInsufficientHistory() public { + bytes ; + data[0] = abi.encode(ContagionTrap.PoolSnapshot({ liquidity: 500, blockNumber: 100 })); + + (bool shouldTrigger, bytes memory info) = trap.shouldRespond(data); + assertFalse(shouldTrigger, "Should not trigger with insufficient history"); + assertEq(string(info), "insufficient_history"); + } +} From b57704c4b9c11892a4757dfe5b289e6816375af6 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:25:29 +0100 Subject: [PATCH 5/8] README.md Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 contagion-trap/README.md diff --git a/contagion-trap/README.md b/contagion-trap/README.md new file mode 100644 index 0000000..50d57aa --- /dev/null +++ b/contagion-trap/README.md @@ -0,0 +1,34 @@ +# ContagionTrap + +**Trap Name:** ContagionTrap +**Author:** Bindoskii (MUJI) +**License:** MIT + +## Description +ContagionTrap monitors liquidity levels of a neighboring DEX pool to detect sharp liquidity drains that may indicate contagion or instability. +It acts as an early warning mechanism that triggers a response when the newest liquidity snapshot drops below 50% of the recent average. + +## Trigger Logic +- Collects reserve data (`reserve0 + reserve1`) from a UniswapV2 compatible pool. +- Maintains a history of liquidity snapshots. +- Compares the latest liquidity value to the average of previous snapshots. +- **Triggers** if: + `newest_liquidity * 2 < average_previous_liquidity` + +## Action on Trigger +When triggered, the trap emits a response intended for Drosera responders typically to **pause** the affected protocol or activate a containment mechanism. + +## Deployment Details +- **Network:** Hoodi Testnet (or simulated Foundry localnet) +- **Parameters:** + Replace `NEIGHBOR_DEX_POOL` with the target DEX pool address you want to monitor. +- **Setup:** + 1. Deploy the contract using Foundry or Drosera’s CLI. + 2. Register the trap on Drosera. + 3. Define response logic for containment. + +## Testing +You can simulate falling liquidity by adjusting mock pool reserves in your Foundry tests. + +## License +MIT From 162ddf1995e079c1950612bcf09fa62cb4501716 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:28:42 +0100 Subject: [PATCH 6/8] foundry.toml Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/foundry.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 contagion-trap/foundry.toml diff --git a/contagion-trap/foundry.toml b/contagion-trap/foundry.toml new file mode 100644 index 0000000..c84a9ef --- /dev/null +++ b/contagion-trap/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +remappings = [ + "contracts/=src/", +] +compiler_version = "0.8.19" From d0d08b99dcbc34529f2814e92a0c5ef6b7815eb9 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:31:50 +0100 Subject: [PATCH 7/8] remappings.txt Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/remappings.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 contagion-trap/remappings.txt diff --git a/contagion-trap/remappings.txt b/contagion-trap/remappings.txt new file mode 100644 index 0000000..8555209 --- /dev/null +++ b/contagion-trap/remappings.txt @@ -0,0 +1,2 @@ +contracts/=src/ +forge-std/=lib/forge-std/src/ From 82f3b02f5737a773598ed80763a8277f008e2d78 Mon Sep 17 00:00:00 2001 From: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:34:49 +0000 Subject: [PATCH 8/8] Update README.md Signed-off-by: Bindoskii098 <119932003+Bindoskii098@users.noreply.github.com> --- contagion-trap/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contagion-trap/README.md b/contagion-trap/README.md index 50d57aa..ff243c0 100644 --- a/contagion-trap/README.md +++ b/contagion-trap/README.md @@ -9,9 +9,9 @@ ContagionTrap monitors liquidity levels of a neighboring DEX pool to detect shar It acts as an early warning mechanism that triggers a response when the newest liquidity snapshot drops below 50% of the recent average. ## Trigger Logic -- Collects reserve data (`reserve0 + reserve1`) from a UniswapV2 compatible pool. -- Maintains a history of liquidity snapshots. -- Compares the latest liquidity value to the average of previous snapshots. +Collects reserve data (`reserve0 + reserve1`) from a UniswapV2 compatible pool. +Maintains a history of liquidity snapshots. +Compares the latest liquidity value to the average of previous snapshots. - **Triggers** if: `newest_liquidity * 2 < average_previous_liquidity`