{"record":{"id":"806f47205ef9899d","repo":"affaan-m/ECC","slug":"simulation-actual-out-expected-min-out","errorCode":null,"errorMessage":"Simulation: {actual_out} < {expected_min_out}","messagePattern":"Simulation: (.+?) < (.+?)","errorType":"exception","errorClass":"SlippageError","httpStatus":null,"severity":"error","filePath":"skills/llm-trading-agent-security/SKILL.md","lineNumber":86,"sourceCode":"\n        self._record_spend(usd_amount)\n```\n\n### Simulate before sending\n\n```python\nclass SlippageError(Exception):\n    pass\n\nasync def safe_execute(self, tx: dict, expected_min_out: int | None = None) -> str:\n    sim_result = await self.w3.eth.call(tx)\n\n    if expected_min_out is None:\n        raise ValueError(\"min_amount_out is required before send\")\n\n    actual_out = decode_uint256(sim_result)\n    if actual_out < expected_min_out:\n        raise SlippageError(f\"Simulation: {actual_out} < {expected_min_out}\")\n\n    signed = self.account.sign_transaction(tx)\n    return await self.w3.eth.send_raw_transaction(signed.raw_transaction)\n```\n\n### Circuit breaker\n\n```python\nclass TradingCircuitBreaker:\n    MAX_CONSECUTIVE_LOSSES = 3\n    MAX_HOURLY_LOSS_PCT = 0.05\n\n    def check(self, portfolio_value: float) -> None:\n        if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:\n            self.halt(\"Too many consecutive losses\")\n\n        if self.hour_start_value <= 0:\n            self.halt(\"Invalid hour_start_value\")","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/llm-trading-agent-security/SKILL.md#L68-L104","documentation":"`safe_execute` simulates the call (`eth_call`) and decodes the returned uint256. If `actual_out < expected_min_out`, it raises `SlippageError(f\"Simulation: {actual_out} < {expected_min_out}\")` and never signs. This is the slippage protection that 594 guards.","triggerScenarios":"On-chain liquidity moved between quote and execution; the simulated return is below the caller's minimum. MEV/front-run reduces the realistic output. Pool fee or route changed.","commonSituations":"Stale quote used as `expected_min_out`; slippage tolerance set too tight (e.g. 0.1% on a volatile pool); router path is no longer optimal; fee bump from the pool.","solutions":["Re-fetch a fresh quote and recompute `expected_min_out = quote * (1 - tolerance)`.","Loosen slippage tolerance for volatile pairs (but stay within the spend-limit guard).","Retry with a higher deadline / different route; consider splitting the trade.","If simulations consistently underperform, investigate the router and pool fee tier."],"exampleFix":"# before\nactual_out = decode_uint256(sim_result)\nif actual_out < expected_min_out:\n    raise SlippageError(f\"Simulation: {actual_out} < {expected_min_out}\")\n\n# after — expose tolerance in the message; suggest a retry bound\nslippage_bps = (expected_min_out - actual_out) * 10_000 // expected_min_out\nraise SlippageError(\n    f\"Simulation: got {actual_out}, min {expected_min_out} \"\n    f\"(negative {slippage_bps} bps); re-quote or raise tolerance\"\n)","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"for attempt in range(3):\n    try:\n        return await agent.safe_execute(tx, expected_min_out=min_out)\n    except SlippageError as e:\n        quote = await refetch_quote()\n        min_out = int(quote * (1 - SLIPPAGE_TOLERANCE))\n        continue\nraise SlippageError('slippage exceeded after retries')","preventionTips":["Re-quote immediately before execution; do not reuse stale quotes.","Set slippage tolerance appropriate to the pool volatility.","Split large trades so per-trade slippage stays bounded."],"tags":["web3","trading","slippage","security","agents"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}