fix: refactor: extract red-team prompt to red-team-program.md (#819)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55cfbeb291
commit
012b31056e
2 changed files with 373 additions and 351 deletions
348
scripts/harb-evaluator/red-team-program.md
Normal file
348
scripts/harb-evaluator/red-team-program.md
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
# Red-team mission: extract ETH from the KRAIKEN LiquidityManager
|
||||
|
||||
## Goal
|
||||
**Extract ETH from the LiquidityManager.** Make the LM end up with less total ETH than it started with.
|
||||
|
||||
Total LM ETH before: {{LM_ETH_BEFORE}} wei
|
||||
(free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
|
||||
|
||||
The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions,
|
||||
but it should NEVER be extractable by an adversary. Your job is to prove otherwise.
|
||||
|
||||
The metric is simple: if LM total ETH goes down, you win.
|
||||
|
||||
---
|
||||
|
||||
## Current Attack Target
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Candidate | {{CANDIDATE_NAME}} |
|
||||
| Optimizer Profile | {{OPTIMIZER_PROFILE}} |
|
||||
|
||||
Use the optimizer profile to reason about this candidate's behavior:
|
||||
- **CI** (concentration index %): higher → optimizer recenters more aggressively → more KRK minting opportunities
|
||||
- **AW** (anchorWidth ticks): wider → liquidity spread over larger price range → less ETH per tick
|
||||
- **AS** (anchorShare %): higher → more ETH locked in anchor position → different rebalancing behavior
|
||||
- **DD** (discoveryDepth %): higher → more ETH in discovery position (above-price) → price-sensitive exposure
|
||||
|
||||
---
|
||||
|
||||
## Contract addresses (local Anvil)
|
||||
|
||||
| Contract | Address |
|
||||
|--------------------|---------|
|
||||
| Kraiken (KRK) | {{KRK}} |
|
||||
| Stake | {{STAKE}} |
|
||||
| LiquidityManager | {{LM}} |
|
||||
| OptimizerProxy | {{OPT}} |
|
||||
| Pool (WETH/KRK 1%) | {{POOL}} |
|
||||
| NonfungiblePosManager (NPM) | {{NPM}} |
|
||||
| WETH | {{WETH}} |
|
||||
| SwapRouter02 | {{SWAP_ROUTER}} |
|
||||
|
||||
RPC: http://localhost:8545
|
||||
CAST binary: /home/debian/.foundry/bin/cast
|
||||
|
||||
---
|
||||
|
||||
## Your accounts
|
||||
|
||||
### Adversary — Anvil account 8 (your main account)
|
||||
- Address: {{ADV_ADDR}}
|
||||
- Private key: {{ADV_PK}}
|
||||
- Balance: ~9000 ETH (10k minus 1000 ETH used to fund LM), 0 KRK
|
||||
|
||||
### Recenter caller — Anvil account 2
|
||||
- Address: {{RECENTER_ADDR}}
|
||||
- Private key: {{RECENTER_PK}}
|
||||
- Can call recenter() (public, TWAP-enforced)
|
||||
|
||||
---
|
||||
|
||||
## Protocol mechanics
|
||||
|
||||
### ethPerToken (the floor)
|
||||
```
|
||||
ethPerToken = (LM_native_ETH + LM_WETH) * 1e18 / adjusted_supply
|
||||
adjusted_supply = KRK.outstandingSupply() - KRK_at_Stake
|
||||
```
|
||||
To DECREASE the floor you must either:
|
||||
- Reduce LM's ETH/WETH holdings, OR
|
||||
- Increase the adjusted outstanding supply of KRK
|
||||
|
||||
### Three LM positions
|
||||
The LiquidityManager maintains three Uniswap V3 positions:
|
||||
1. **ANCHOR** — straddles the current price; provides two-sided liquidity
|
||||
2. **DISCOVERY** — above current price; captures upside momentum
|
||||
3. **FLOOR** — a floor bid: ETH in, KRK out. Backing the floor price.
|
||||
|
||||
### recenter()
|
||||
Calling `LiquidityManager.recenter()` removes all three positions, mints or burns KRK
|
||||
to rebalance, then re-deploys positions at the current price. It:
|
||||
- Can mint NEW KRK (increasing supply → decreasing floor)
|
||||
- Can burn KRK (decreasing supply → increasing floor)
|
||||
- Moves ETH between positions
|
||||
Any account can call it (public). TWAP oracle enforces safety.
|
||||
|
||||
### Staking
|
||||
`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)`
|
||||
- taxRateIndex: 0–29 (index into the 30-element TAX_RATES array — not a raw percentage)
|
||||
- KRK staked is held by the Stake contract (excluded from adjusted_supply)
|
||||
- KRK in Stake does NOT count against the floor denominator
|
||||
|
||||
### outstandingSupply() vs totalSupply()
|
||||
`KRK.outstandingSupply() = totalSupply() - balanceOf(liquidityManager)`
|
||||
LM-held KRK (in pool positions) is excluded from outstandingSupply.
|
||||
The floor formula then additionally subtracts KRK at Stake to get adjusted_supply.
|
||||
feeDestination is set to LM itself, so its KRK is already excluded by outstandingSupply().
|
||||
|
||||
---
|
||||
|
||||
## Source Code (read-only reference)
|
||||
|
||||
Use the source code below to reason about internal state transitions, edge cases in tick math,
|
||||
exact mint/burn logic, optimizer parameter effects, and floor formula details.
|
||||
Do NOT attempt to deploy or modify contracts — these are for reference only.
|
||||
|
||||
### LiquidityManager.sol
|
||||
```solidity
|
||||
{{SOL_LM}}
|
||||
```
|
||||
|
||||
### ThreePositionStrategy.sol
|
||||
```solidity
|
||||
{{SOL_THREE_POS}}
|
||||
```
|
||||
|
||||
### Optimizer.sol (base)
|
||||
```solidity
|
||||
{{SOL_OPTIMIZER}}
|
||||
```
|
||||
|
||||
### OptimizerV3.sol (current candidate — reflects inject.sh output)
|
||||
```solidity
|
||||
{{SOL_OPTIMIZERV3}}
|
||||
```
|
||||
|
||||
### VWAPTracker.sol
|
||||
```solidity
|
||||
{{SOL_VWAP}}
|
||||
```
|
||||
|
||||
### PriceOracle.sol
|
||||
```solidity
|
||||
{{SOL_PRICE_ORACLE}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cast command patterns
|
||||
|
||||
### Check total LM ETH (run after each strategy)
|
||||
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
|
||||
```bash
|
||||
CAST=/home/debian/.foundry/bin/cast
|
||||
LM_ETH=$($CAST balance {{LM}} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
LM_WETH=$($CAST call {{WETH}} "balanceOf(address)(uint256)" {{LM}} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
SLOT0=$($CAST call {{POOL}} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
|
||||
CUR_TICK=$(echo "$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TOKEN0_IS_WETH=$(python3 -c "print(1 if '{{WETH}}'.lower() < '{{KRK}}'.lower() else 0)")
|
||||
POS_ETH=0
|
||||
for STAGE in 0 1 2; do
|
||||
POS=$($CAST call {{LM}} "positions(uint8)(uint128,int24,int24)" $STAGE --rpc-url http://localhost:8545)
|
||||
LIQ=$(echo "$POS" | sed -n '1p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TL=$(echo "$POS" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TU=$(echo "$POS" | sed -n '3p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
POS_ETH=$(python3 -c "
|
||||
import math
|
||||
L,tl,tu,tc,t0w=int('$LIQ'),int('$TL'),int('$TU'),int('$CUR_TICK'),bool($TOKEN0_IS_WETH)
|
||||
prev=int('$POS_ETH')
|
||||
if L==0: print(prev); exit()
|
||||
sa=math.sqrt(1.0001**tl); sb=math.sqrt(1.0001**tu); sc=math.sqrt(1.0001**tc)
|
||||
if t0w:
|
||||
e=L*(1/sa-1/sb) if tc<tl else (0 if tc>=tu else L*(1/sc-1/sb))
|
||||
else:
|
||||
e=L*(sb-sa) if tc>=tu else (0 if tc<tl else L*(sc-sa))
|
||||
print(prev+int(e))
|
||||
")
|
||||
done
|
||||
TOTAL=$(python3 -c "print(int('$LM_ETH')+int('$LM_WETH')+int('$POS_ETH'))")
|
||||
echo "Total LM ETH: $TOTAL wei (free: $LM_ETH + $LM_WETH, positions: $POS_ETH)"
|
||||
echo "Started with: {{LM_ETH_BEFORE}} wei"
|
||||
python3 -c "b={{LM_ETH_BEFORE}}; a=int('$TOTAL'); d=b-a; print(f'Delta: {d} wei ({d*100//b if b else 0}% extracted)' if d>0 else f'Delta: {d} wei (LM gained ETH)')"
|
||||
```
|
||||
|
||||
### Wrap ETH to WETH
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast send {{WETH}} "deposit()" --value 100ether \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Approve token spend
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast send <TOKEN> "approve(address,uint256)" <SPENDER> \
|
||||
115792089237316195423570985008687907853269984665640564039457584007913129639935 \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Buy KRK (WETH → KRK via SwapRouter)
|
||||
```bash
|
||||
# Must wrap ETH and approve WETH first
|
||||
/home/debian/.foundry/bin/cast send {{SWAP_ROUTER}} \
|
||||
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
||||
"({{WETH}},{{KRK}},{{POOL_FEE}},{{ADV_ADDR}},<WETH_AMOUNT>,0,0)" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Sell KRK (KRK → WETH via SwapRouter)
|
||||
```bash
|
||||
# Must approve KRK first
|
||||
/home/debian/.foundry/bin/cast send {{SWAP_ROUTER}} \
|
||||
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
||||
"({{KRK}},{{WETH}},{{POOL_FEE}},{{ADV_ADDR}},<KRK_AMOUNT>,0,0)" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Stake KRK (snatch with no snatching)
|
||||
```bash
|
||||
# Approve KRK to Stake first
|
||||
/home/debian/.foundry/bin/cast send {{STAKE}} \
|
||||
"snatch(uint256,address,uint32,uint256[])" \
|
||||
<KRK_AMOUNT> {{ADV_ADDR}} 0 "[]" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Unstake KRK
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast send {{STAKE}} \
|
||||
"exitPosition(uint256)" <POSITION_ID> \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Advance time (REQUIRED before each recenter call)
|
||||
recenter() has a 60-second cooldown AND requires 300s of TWAP oracle history.
|
||||
You MUST advance time before calling recenter:
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast rpc evm_increaseTime 600 --rpc-url http://localhost:8545
|
||||
for i in $(seq 1 10); do /home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545; done
|
||||
```
|
||||
|
||||
### Trigger recenter (account 2 only)
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast send {{LM}} "recenter()" \
|
||||
--private-key {{RECENTER_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Read KRK balance
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast call {{KRK}} "balanceOf(address)(uint256)" {{ADV_ADDR}} \
|
||||
--rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Read ETH balance
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast balance {{ADV_ADDR}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Add LP position via NPM (mint)
|
||||
```bash
|
||||
# Must approve both tokens to NPM first. tickLower/tickUpper must be multiples of 200 (pool tickSpacing).
|
||||
/home/debian/.foundry/bin/cast send {{NPM}} \
|
||||
"mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \
|
||||
"({{WETH}},{{KRK}},{{POOL_FEE}},<TICK_LOWER>,<TICK_UPPER>,<AMOUNT0>,<AMOUNT1>,0,0,{{ADV_ADDR}},<DEADLINE>)" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Remove LP position via NPM (decreaseLiquidity then collect)
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast send {{NPM}} \
|
||||
"decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))" \
|
||||
"(<TOKEN_ID>,<LIQUIDITY>,0,0,<DEADLINE>)" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
|
||||
/home/debian/.foundry/bin/cast send {{NPM}} \
|
||||
"collect((uint256,address,uint128,uint128))" \
|
||||
"(<TOKEN_ID>,{{ADV_ADDR}},340282366920938463463374607431768211455,340282366920938463463374607431768211455)" \
|
||||
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Mine a block
|
||||
```bash
|
||||
/home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
### Snapshot and revert (for resetting between strategies)
|
||||
```bash
|
||||
# Take snapshot (returns ID — save it):
|
||||
SNAP=$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://localhost:8545 | tr -d '"')
|
||||
# Revert to snapshot (one-shot — take a new snapshot immediately after):
|
||||
/home/debian/.foundry/bin/cast rpc anvil_revert $SNAP --rpc-url http://localhost:8545
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- **feeDestination = LM itself** — fees are NOT extracted, they accrue as LM liquidity.
|
||||
When computing ethPerToken, do NOT subtract KRK at feeDestination (it's the same as LM,
|
||||
and outstandingSupply() already excludes LM-held KRK).
|
||||
- **LM has ~1000 ETH reserve** — proportional to your 10,000 ETH (10:1 ratio). This is a
|
||||
realistic attack scenario, not an empty vault.
|
||||
- **You MUST NOT call anvil_reset, anvil_setCode, or anvil_setStorageAt.** These are infra
|
||||
cheats that invalidate the test. Use only swap/stake/LP/recenter protocol operations.
|
||||
|
||||
## Rules
|
||||
|
||||
1. You have ~9000 ETH (after funding LM with 1000 ETH). Start by wrapping some if you need WETH for swaps.
|
||||
2. Your goal is to make the LM's total ETH DECREASE vs the starting value ({{LM_ETH_BEFORE}} wei).
|
||||
3. Try at least 3 distinct strategies. After each attempt:
|
||||
a. Run the total LM ETH check command above.
|
||||
b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
|
||||
c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
|
||||
Remember: `anvil_revert` is one-shot. Take a new snapshot immediately after reverting.
|
||||
4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
|
||||
5. Be methodical. Report every strategy tried even if it failed.
|
||||
6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
|
||||
7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
|
||||
8. Start executing immediately. No lengthy planning — act, measure, iterate.
|
||||
9. For EVERY strategy attempted, record:
|
||||
- **Pattern**: abstract op sequence (e.g., "buy → stake_all → recenter_multi → unstake → sell")
|
||||
- **Insight**: WHY this worked or failed, referencing the optimizer profile ({{OPTIMIZER_PROFILE}}).
|
||||
For HELD/INCREASED: which mechanism defended the floor? How did CI/AW/AS/DD cause it?
|
||||
For DECREASED: which parameter combination created the vulnerability? Is it universal or optimizer-specific?
|
||||
|
||||
---
|
||||
|
||||
{{CROSS_CANDIDATE_SECTION}}
|
||||
|
||||
{{MEMORY_SECTION}}
|
||||
|
||||
## Final report format
|
||||
|
||||
After trying all strategies, output a clearly structured report:
|
||||
|
||||
```
|
||||
=== RED-TEAM REPORT ===
|
||||
|
||||
Candidate: {{CANDIDATE_NAME}}
|
||||
Optimizer Profile: {{OPTIMIZER_PROFILE}}
|
||||
lm_eth_before: <value> wei (total: free + positions)
|
||||
|
||||
STRATEGY 1: <name>
|
||||
Pattern: <abstract op sequence e.g. "buy → recenter → sell">
|
||||
Steps: <what you did>
|
||||
lm_eth_after: <value> wei
|
||||
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
|
||||
Insight: <WHY this worked/failed given the optimizer profile>
|
||||
|
||||
STRATEGY 2: ...
|
||||
...
|
||||
|
||||
=== CONCLUSION ===
|
||||
ETH extracted: YES / NO
|
||||
Winning strategy: <describe if YES, else "None">
|
||||
Universal pattern: <would this likely work on other candidates? Why or why not?>
|
||||
lm_eth_before: {{LM_ETH_BEFORE}} wei
|
||||
lm_eth_after: <final value> wei
|
||||
```
|
||||
|
|
@ -551,357 +551,31 @@ PYEOF
|
|||
)
|
||||
fi
|
||||
|
||||
PROMPT=$(cat <<PROMPT_EOF
|
||||
# Red-team mission: extract ETH from the KRAIKEN LiquidityManager
|
||||
|
||||
## Goal
|
||||
**Extract ETH from the LiquidityManager.** Make the LM end up with less total ETH than it started with.
|
||||
|
||||
Total LM ETH before: ${LM_ETH_BEFORE} wei
|
||||
(free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
|
||||
|
||||
The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions,
|
||||
but it should NEVER be extractable by an adversary. Your job is to prove otherwise.
|
||||
|
||||
The metric is simple: if LM total ETH goes down, you win.
|
||||
|
||||
---
|
||||
|
||||
## Current Attack Target
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Candidate | ${CANDIDATE_NAME} |
|
||||
| Optimizer Profile | ${OPTIMIZER_PROFILE} |
|
||||
|
||||
Use the optimizer profile to reason about this candidate's behavior:
|
||||
- **CI** (concentration index %): higher → optimizer recenters more aggressively → more KRK minting opportunities
|
||||
- **AW** (anchorWidth ticks): wider → liquidity spread over larger price range → less ETH per tick
|
||||
- **AS** (anchorShare %): higher → more ETH locked in anchor position → different rebalancing behavior
|
||||
- **DD** (discoveryDepth %): higher → more ETH in discovery position (above-price) → price-sensitive exposure
|
||||
|
||||
---
|
||||
|
||||
## Contract addresses (local Anvil)
|
||||
|
||||
| Contract | Address |
|
||||
|--------------------|---------|
|
||||
| Kraiken (KRK) | ${KRK} |
|
||||
| Stake | ${STAKE} |
|
||||
| LiquidityManager | ${LM} |
|
||||
| OptimizerProxy | ${OPT} |
|
||||
| Pool (WETH/KRK 1%) | ${POOL} |
|
||||
| NonfungiblePosManager (NPM) | ${NPM} |
|
||||
| WETH | ${WETH} |
|
||||
| SwapRouter02 | ${SWAP_ROUTER} |
|
||||
|
||||
RPC: http://localhost:8545
|
||||
CAST binary: /home/debian/.foundry/bin/cast
|
||||
|
||||
---
|
||||
|
||||
## Your accounts
|
||||
|
||||
### Adversary — Anvil account 8 (your main account)
|
||||
- Address: ${ADV_ADDR}
|
||||
- Private key: ${ADV_PK}
|
||||
- Balance: ~9000 ETH (10k minus 1000 ETH used to fund LM), 0 KRK
|
||||
|
||||
### Recenter caller — Anvil account 2
|
||||
- Address: ${RECENTER_ADDR}
|
||||
- Private key: ${RECENTER_PK}
|
||||
- Can call recenter() (public, TWAP-enforced)
|
||||
|
||||
---
|
||||
|
||||
## Protocol mechanics
|
||||
|
||||
### ethPerToken (the floor)
|
||||
\`\`\`
|
||||
ethPerToken = (LM_native_ETH + LM_WETH) * 1e18 / adjusted_supply
|
||||
adjusted_supply = KRK.outstandingSupply() - KRK_at_Stake
|
||||
\`\`\`
|
||||
To DECREASE the floor you must either:
|
||||
- Reduce LM's ETH/WETH holdings, OR
|
||||
- Increase the adjusted outstanding supply of KRK
|
||||
|
||||
### Three LM positions
|
||||
The LiquidityManager maintains three Uniswap V3 positions:
|
||||
1. **ANCHOR** — straddles the current price; provides two-sided liquidity
|
||||
2. **DISCOVERY** — above current price; captures upside momentum
|
||||
3. **FLOOR** — a floor bid: ETH in, KRK out. Backing the floor price.
|
||||
|
||||
### recenter()
|
||||
Calling \`LiquidityManager.recenter()\` removes all three positions, mints or burns KRK
|
||||
to rebalance, then re-deploys positions at the current price. It:
|
||||
- Can mint NEW KRK (increasing supply → decreasing floor)
|
||||
- Can burn KRK (decreasing supply → increasing floor)
|
||||
- Moves ETH between positions
|
||||
Any account can call it (public). TWAP oracle enforces safety.
|
||||
|
||||
### Staking
|
||||
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`
|
||||
- taxRateIndex: 0–29 (index into the 30-element TAX_RATES array — not a raw percentage)
|
||||
- KRK staked is held by the Stake contract (excluded from adjusted_supply)
|
||||
- KRK in Stake does NOT count against the floor denominator
|
||||
|
||||
### outstandingSupply() vs totalSupply()
|
||||
\`KRK.outstandingSupply() = totalSupply() - balanceOf(liquidityManager)\`
|
||||
LM-held KRK (in pool positions) is excluded from outstandingSupply.
|
||||
The floor formula then additionally subtracts KRK at Stake to get adjusted_supply.
|
||||
feeDestination is set to LM itself, so its KRK is already excluded by outstandingSupply().
|
||||
|
||||
---
|
||||
|
||||
## Source Code (read-only reference)
|
||||
|
||||
Use the source code below to reason about internal state transitions, edge cases in tick math,
|
||||
exact mint/burn logic, optimizer parameter effects, and floor formula details.
|
||||
Do NOT attempt to deploy or modify contracts — these are for reference only.
|
||||
|
||||
### LiquidityManager.sol
|
||||
\`\`\`solidity
|
||||
${SOL_LM}
|
||||
\`\`\`
|
||||
|
||||
### ThreePositionStrategy.sol
|
||||
\`\`\`solidity
|
||||
${SOL_THREE_POS}
|
||||
\`\`\`
|
||||
|
||||
### Optimizer.sol (base)
|
||||
\`\`\`solidity
|
||||
${SOL_OPTIMIZER}
|
||||
\`\`\`
|
||||
|
||||
### OptimizerV3.sol (current candidate — reflects inject.sh output)
|
||||
\`\`\`solidity
|
||||
${SOL_OPTIMIZERV3}
|
||||
\`\`\`
|
||||
|
||||
### VWAPTracker.sol
|
||||
\`\`\`solidity
|
||||
${SOL_VWAP}
|
||||
\`\`\`
|
||||
|
||||
### PriceOracle.sol
|
||||
\`\`\`solidity
|
||||
${SOL_PRICE_ORACLE}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Cast command patterns
|
||||
|
||||
### Check total LM ETH (run after each strategy)
|
||||
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
|
||||
\`\`\`bash
|
||||
CAST=/home/debian/.foundry/bin/cast
|
||||
LM_ETH=\$(\$CAST balance ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
LM_WETH=\$(\$CAST call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
SLOT0=\$(\$CAST call ${POOL} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
|
||||
CUR_TICK=\$(echo "\$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TOKEN0_IS_WETH=\$(python3 -c "print(1 if '${WETH}'.lower() < '${KRK}'.lower() else 0)")
|
||||
POS_ETH=0
|
||||
for STAGE in 0 1 2; do
|
||||
POS=\$(\$CAST call ${LM} "positions(uint8)(uint128,int24,int24)" \$STAGE --rpc-url http://localhost:8545)
|
||||
LIQ=\$(echo "\$POS" | sed -n '1p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TL=\$(echo "\$POS" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TU=\$(echo "\$POS" | sed -n '3p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
POS_ETH=\$(python3 -c "
|
||||
import math
|
||||
L,tl,tu,tc,t0w=int('\$LIQ'),int('\$TL'),int('\$TU'),int('\$CUR_TICK'),bool(\$TOKEN0_IS_WETH)
|
||||
prev=int('\$POS_ETH')
|
||||
if L==0: print(prev); exit()
|
||||
sa=math.sqrt(1.0001**tl); sb=math.sqrt(1.0001**tu); sc=math.sqrt(1.0001**tc)
|
||||
if t0w:
|
||||
e=L*(1/sa-1/sb) if tc<tl else (0 if tc>=tu else L*(1/sc-1/sb))
|
||||
else:
|
||||
e=L*(sb-sa) if tc>=tu else (0 if tc<tl else L*(sc-sa))
|
||||
print(prev+int(e))
|
||||
")
|
||||
done
|
||||
TOTAL=\$(python3 -c "print(int('\$LM_ETH')+int('\$LM_WETH')+int('\$POS_ETH'))")
|
||||
echo "Total LM ETH: \$TOTAL wei (free: \$LM_ETH + \$LM_WETH, positions: \$POS_ETH)"
|
||||
echo "Started with: ${LM_ETH_BEFORE} wei"
|
||||
python3 -c "b=${LM_ETH_BEFORE:-0}; a=int('\$TOTAL'); d=b-a; print(f'Delta: {d} wei ({d*100//b if b else 0}% extracted)' if d>0 else f'Delta: {d} wei (LM gained ETH)')"
|
||||
\`\`\`
|
||||
|
||||
### Wrap ETH to WETH
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast send ${WETH} "deposit()" --value 100ether \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Approve token spend
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast send <TOKEN> "approve(address,uint256)" <SPENDER> \
|
||||
115792089237316195423570985008687907853269984665640564039457584007913129639935 \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Buy KRK (WETH → KRK via SwapRouter)
|
||||
\`\`\`bash
|
||||
# Must wrap ETH and approve WETH first
|
||||
/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \
|
||||
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
||||
"(${WETH},${KRK},${POOL_FEE},${ADV_ADDR},<WETH_AMOUNT>,0,0)" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Sell KRK (KRK → WETH via SwapRouter)
|
||||
\`\`\`bash
|
||||
# Must approve KRK first
|
||||
/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \
|
||||
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
||||
"(${KRK},${WETH},${POOL_FEE},${ADV_ADDR},<KRK_AMOUNT>,0,0)" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Stake KRK (snatch with no snatching)
|
||||
\`\`\`bash
|
||||
# Approve KRK to Stake first
|
||||
/home/debian/.foundry/bin/cast send ${STAKE} \
|
||||
"snatch(uint256,address,uint32,uint256[])" \
|
||||
<KRK_AMOUNT> ${ADV_ADDR} 0 "[]" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Unstake KRK
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast send ${STAKE} \
|
||||
"exitPosition(uint256)" <POSITION_ID> \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Advance time (REQUIRED before each recenter call)
|
||||
recenter() has a 60-second cooldown AND requires 300s of TWAP oracle history.
|
||||
You MUST advance time before calling recenter:
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast rpc evm_increaseTime 600 --rpc-url http://localhost:8545
|
||||
for i in \$(seq 1 10); do /home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545; done
|
||||
\`\`\`
|
||||
|
||||
### Trigger recenter (account 2 only)
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast send ${LM} "recenter()" \
|
||||
--private-key ${RECENTER_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Read KRK balance
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${ADV_ADDR} \
|
||||
--rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Read ETH balance
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast balance ${ADV_ADDR} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Add LP position via NPM (mint)
|
||||
\`\`\`bash
|
||||
# Must approve both tokens to NPM first. tickLower/tickUpper must be multiples of 200 (pool tickSpacing).
|
||||
/home/debian/.foundry/bin/cast send ${NPM} \
|
||||
"mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \
|
||||
"(${WETH},${KRK},${POOL_FEE},<TICK_LOWER>,<TICK_UPPER>,<AMOUNT0>,<AMOUNT1>,0,0,${ADV_ADDR},<DEADLINE>)" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Remove LP position via NPM (decreaseLiquidity then collect)
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast send ${NPM} \
|
||||
"decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))" \
|
||||
"(<TOKEN_ID>,<LIQUIDITY>,0,0,<DEADLINE>)" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
|
||||
/home/debian/.foundry/bin/cast send ${NPM} \
|
||||
"collect((uint256,address,uint128,uint128))" \
|
||||
"(<TOKEN_ID>,${ADV_ADDR},340282366920938463463374607431768211455,340282366920938463463374607431768211455)" \
|
||||
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Mine a block
|
||||
\`\`\`bash
|
||||
/home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
### Snapshot and revert (for resetting between strategies)
|
||||
\`\`\`bash
|
||||
# Take snapshot (returns ID — save it):
|
||||
SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://localhost:8545 | tr -d '"')
|
||||
# Revert to snapshot (one-shot — take a new snapshot immediately after):
|
||||
/home/debian/.foundry/bin/cast rpc anvil_revert \$SNAP --rpc-url http://localhost:8545
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- **feeDestination = LM itself** — fees are NOT extracted, they accrue as LM liquidity.
|
||||
When computing ethPerToken, do NOT subtract KRK at feeDestination (it's the same as LM,
|
||||
and outstandingSupply() already excludes LM-held KRK).
|
||||
- **LM has ~1000 ETH reserve** — proportional to your 10,000 ETH (10:1 ratio). This is a
|
||||
realistic attack scenario, not an empty vault.
|
||||
- **You MUST NOT call anvil_reset, anvil_setCode, or anvil_setStorageAt.** These are infra
|
||||
cheats that invalidate the test. Use only swap/stake/LP/recenter protocol operations.
|
||||
|
||||
## Rules
|
||||
|
||||
1. You have ~9000 ETH (after funding LM with 1000 ETH). Start by wrapping some if you need WETH for swaps.
|
||||
2. Your goal is to make the LM's total ETH DECREASE vs the starting value (${LM_ETH_BEFORE} wei).
|
||||
3. Try at least 3 distinct strategies. After each attempt:
|
||||
a. Run the total LM ETH check command above.
|
||||
b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
|
||||
c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
|
||||
Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting.
|
||||
4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
|
||||
5. Be methodical. Report every strategy tried even if it failed.
|
||||
6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
|
||||
7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
|
||||
8. Start executing immediately. No lengthy planning — act, measure, iterate.
|
||||
9. For EVERY strategy attempted, record:
|
||||
- **Pattern**: abstract op sequence (e.g., "buy → stake_all → recenter_multi → unstake → sell")
|
||||
- **Insight**: WHY this worked or failed, referencing the optimizer profile (${OPTIMIZER_PROFILE}).
|
||||
For HELD/INCREASED: which mechanism defended the floor? How did CI/AW/AS/DD cause it?
|
||||
For DECREASED: which parameter combination created the vulnerability? Is it universal or optimizer-specific?
|
||||
|
||||
---
|
||||
|
||||
${CROSS_CANDIDATE_SECTION}
|
||||
|
||||
${MEMORY_SECTION}
|
||||
|
||||
## Final report format
|
||||
|
||||
After trying all strategies, output a clearly structured report:
|
||||
|
||||
\`\`\`
|
||||
=== RED-TEAM REPORT ===
|
||||
|
||||
Candidate: ${CANDIDATE_NAME}
|
||||
Optimizer Profile: ${OPTIMIZER_PROFILE}
|
||||
lm_eth_before: <value> wei (total: free + positions)
|
||||
|
||||
STRATEGY 1: <name>
|
||||
Pattern: <abstract op sequence e.g. "buy → recenter → sell">
|
||||
Steps: <what you did>
|
||||
lm_eth_after: <value> wei
|
||||
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
|
||||
Insight: <WHY this worked/failed given the optimizer profile>
|
||||
|
||||
STRATEGY 2: ...
|
||||
...
|
||||
|
||||
=== CONCLUSION ===
|
||||
ETH extracted: YES / NO
|
||||
Winning strategy: <describe if YES, else "None">
|
||||
Universal pattern: <would this likely work on other candidates? Why or why not?>
|
||||
lm_eth_before: ${LM_ETH_BEFORE} wei
|
||||
lm_eth_after: <final value> wei
|
||||
\`\`\`
|
||||
PROMPT_EOF
|
||||
)
|
||||
PROMPT=$(cat "$SCRIPT_DIR/red-team-program.md")
|
||||
PROMPT=${PROMPT//\{\{LM_ETH_BEFORE\}\}/$LM_ETH_BEFORE}
|
||||
PROMPT=${PROMPT//\{\{CANDIDATE_NAME\}\}/$CANDIDATE_NAME}
|
||||
PROMPT=${PROMPT//\{\{OPTIMIZER_PROFILE\}\}/$OPTIMIZER_PROFILE}
|
||||
PROMPT=${PROMPT//\{\{KRK\}\}/$KRK}
|
||||
PROMPT=${PROMPT//\{\{STAKE\}\}/$STAKE}
|
||||
PROMPT=${PROMPT//\{\{LM\}\}/$LM}
|
||||
PROMPT=${PROMPT//\{\{OPT\}\}/$OPT}
|
||||
PROMPT=${PROMPT//\{\{POOL\}\}/$POOL}
|
||||
PROMPT=${PROMPT//\{\{NPM\}\}/$NPM}
|
||||
PROMPT=${PROMPT//\{\{WETH\}\}/$WETH}
|
||||
PROMPT=${PROMPT//\{\{SWAP_ROUTER\}\}/$SWAP_ROUTER}
|
||||
PROMPT=${PROMPT//\{\{ADV_ADDR\}\}/$ADV_ADDR}
|
||||
PROMPT=${PROMPT//\{\{ADV_PK\}\}/$ADV_PK}
|
||||
PROMPT=${PROMPT//\{\{RECENTER_ADDR\}\}/$RECENTER_ADDR}
|
||||
PROMPT=${PROMPT//\{\{RECENTER_PK\}\}/$RECENTER_PK}
|
||||
PROMPT=${PROMPT//\{\{POOL_FEE\}\}/$POOL_FEE}
|
||||
PROMPT=${PROMPT//\{\{SOL_LM\}\}/$SOL_LM}
|
||||
PROMPT=${PROMPT//\{\{SOL_THREE_POS\}\}/$SOL_THREE_POS}
|
||||
PROMPT=${PROMPT//\{\{SOL_OPTIMIZER\}\}/$SOL_OPTIMIZER}
|
||||
PROMPT=${PROMPT//\{\{SOL_OPTIMIZERV3\}\}/$SOL_OPTIMIZERV3}
|
||||
PROMPT=${PROMPT//\{\{SOL_VWAP\}\}/$SOL_VWAP}
|
||||
PROMPT=${PROMPT//\{\{SOL_PRICE_ORACLE\}\}/$SOL_PRICE_ORACLE}
|
||||
PROMPT=${PROMPT//\{\{CROSS_CANDIDATE_SECTION\}\}/$CROSS_CANDIDATE_SECTION}
|
||||
PROMPT=${PROMPT//\{\{MEMORY_SECTION\}\}/$MEMORY_SECTION}
|
||||
|
||||
# ── 7. Create output directory and run the agent ───────────────────────────────
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue