019c0ea9-07b9-70a2-acc1-0b6c38a3ac98
Open this run to answer one question: is this real attacker-relevant behavior with real impact, or just normal contract plumbing?
| ID | Type | Status | Attempts | Heartbeat |
|---|---|---|---|---|
| 41662 | artifact.fetch | complete | 1 / 3 | 2 months ago |
| 41663 | proxy.resolve | complete | 1 / 3 | 2 months ago |
| 41664 | tool.cast_disasm | complete | 1 / 3 | 2 months ago |
| 41665 | analysis.bundle | complete | 1 / 3 | 2 months ago |
| 41666 | capability.graph | complete | 1 / 3 | 2 months ago |
| 41667 | detector.run | complete | 1 / 3 | 2 months ago |
| 41668 | validation.fork | complete | 1 / 3 | 2 months ago |
| 42255 | report.consolidate | complete | 1 / 6 | 2 months ago |
| 42256 | tool.slither | failed | 2 / 2 | 2 months ago |
{
"method": "etherscan+onchain",
"status": "confirmed",
"etherscan_hint": {
"source": "etherscan",
"etherscan": {
"proxy_flag": true,
"implementation": "0x965983977c29158ec53a296a6f47be145ddecc36"
}
},
"implementation": "0x965983977c29158ec53a296a6f47be145ddecc36",
"onchain_method": "eip1967"
}
Traceback (most recent call last):
File "/var/www/tripwire/worker/worker.py", line 198, in main
payload = dispatch_job(job_type, job_id, job_meta, run_data, config, laravel_client)
File "/var/www/tripwire/worker/worker.py", line 114, in dispatch_job
return slither_run.run(job_id, job_meta, run_data, config, log, laravel_client)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/worker/jobs/slither_run.py", line 1084, in run
stdout_text, stderr_text, result_json = _run_slither(
~~~~~~~~~~~~^
workspace,
^^^^^^^^^^
...<4 lines>...
target_path=target_path,
^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/var/www/tripwire/worker/jobs/slither_run.py", line 958, in _run_slither
raise RuntimeError(f"slither failed (exit {result.returncode}): {stderr_text or stdout_text}")
RuntimeError: slither failed (exit 1): 'forge clean' running (wd: /tmp/slither-srvlyk_e)
'forge config --json' running
'forge build --build-info --skip ./test/** ./script/** --force' running (wd: /tmp/slither-srvlyk_e)
Traceback (most recent call last):
File "/var/www/tripwire/.venv/bin/slither", line 10, in <module>
sys.exit(main())
~~~~^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 776, in main
main_impl(all_detector_classes=detectors, all_printer_classes=printers)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 882, in main_impl
) = process_all(filename, args, detector_classes, printer_classes)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 96, in process_all
compilations = compile_all(target, **vars(args))
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 722, in compile_all
compilations.append(CryticCompile(target, **kwargs))
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 211, in __init__
self._compile(**kwargs)
~~~~~~~~~~~~~^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 633, in _compile
self._platform.compile(self, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/foundry.py", line 102, in compile
hardhat_like_parsing(
~~~~~~~~~~~~~~~~~~~~^
crytic_compile, str(self._target), build_directory, str(self._project_root)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/hardhat.py", line 52, in hardhat_like_parsing
raise InvalidCompilation(txt)
crytic_compile.platform.exceptions.InvalidCompilation: Compilation failed. Can you run build command?
/tmp/slither-srvlyk_e/out/build-info is not a directory.
{"no_findings":false,"summary":"Found a high‑risk reentrancy in the relayer refund flow plus two low‑risk issues around unchecked ERC20 returns and oracle sanity checks used for relayer fee math.","findings":[{"severity":"high","category":"reentrancy","title":"Relayer refund can be reentered before balance reset, enabling repeated refunds","exploit_scenario":"A registered relayer calls `relayerLift`/`relayerLower` with `triggerRefund=true`. `_attemptRelayerRefund` calls `__refundRelayer`, which swaps and then sends ETH to the relayer via `.call`. The relayer’s fallback reenters `relayerLift`/`relayerLower` (e.g., with `gasCost=0` and a 0‑amount permit for itself) and triggers another refund while `relayerBalance` is still the pre‑refund value. This allows multiple swaps/refunds against the same balance and can drain the contract’s USDC reserves.","recommendation":"Add reentrancy protection to `relayerLift`/`relayerLower` and/or set `relayerBalance` to the trace value before performing the external refund. Alternatively, implement a pull‑based refund where the relayer claims in a separate non‑reentrant call, or add an “in‑refund” flag checked in relayer entrypoints.","evidence":"`relayerLift`/`relayerLower` call `_attemptRelayerRefund(balance)` before persisting `relayerBalance`, and `_attemptRelayerRefund` performs an external call that ends with `relayer.call{value: ethAmount}(\"\")`.","locations":["contracts/TruthBridge.sol:296","contracts/TruthBridge.sol:311","contracts/TruthBridge.sol:320","contracts/TruthBridge.sol:338","contracts/TruthBridge.sol:484","contracts/TruthBridge.sol:565","contracts/TruthBridge.sol:574"]},{"severity":"low","category":"other","title":"Unchecked ERC20 transfer/transferFrom return values in relayer flows","exploit_scenario":"If the USDC token returns `false` instead of reverting, `relayerLift` and `relayerLower` proceed as if funds moved, leading to incorrect accounting or incomplete payments. The Uniswap callback likewise ignores a failed transfer, which can revert later in the swap or leave state inconsistent.","recommendation":"Use `SafeERC20.safeTransfer`/`safeTransferFrom` (or check boolean return values) for all ERC20 interactions in relayer paths and the Uniswap callback.","evidence":"USDC transfers use raw `IERC20.transfer`/`transferFrom` without checking return values.","locations":["contracts/TruthBridge.sol:289","contracts/TruthBridge.sol:304","contracts/TruthBridge.sol:336","contracts/TruthBridge.sol:358"]},{"severity":"low","category":"oracle","title":"Oracle price used without sanity/staleness checks can break fee math","exploit_scenario":"`usdcEth()` uses `latestAnswer()` without validating positivity or freshness. If the feed is stale, paused, or returns 0/negative, relayer fee math can under/overcharge users or revert due to division by zero in relayer paths and refund checks.","recommendation":"Use `latestRoundData()` and verify `answer > 0` with `updatedAt` freshness bounds; handle invalid answers with a revert or fallback mechanism.","evidence":"`usdcEth()` directly casts `latestAnswer()` and is used in fee calculations and refund slippage checks.","locations":["contracts/TruthBridge.sol:347","contracts/TruthBridge.sol:300","contracts/TruthBridge.sol:326","contracts/TruthBridge.sol:572"]}]}
OpenAI Codex v0.77.0 (research preview)
--------
workdir: /tmp/codex-4j2tkmv2
model: gpt-5.2-codex
provider: openai
approval: never
sandbox: read-only
reasoning effort: high
reasoning summaries: auto
session id: 019c0ee8-bd8e-7910-91f8-aa8bfe5b0fb4
--------
user
Context: The user requested the proxy at 0x50c02710b06d6addb864d6b038010ef6fa1bcd92.
Audit the implementation at 0x965983977c29158ec53a296a6f47be145ddecc36.
Findings should refer to the implementation unless explicitly about proxy mechanics.
You are a security auditor. Audit this Solidity codebase for exploitable vulnerabilities.
Focus on: reentrancy, access control, upgradeability/proxy issues, signature validation, oracle manipulation,
accounting/invariant breaks, external call patterns, unchecked return values, and dangerous delegatecall.
Scope rules:
- You MUST fully read `AUDIT_BUNDLE.txt` before concluding. It contains the complete contents of all first-party Solidity sources in this workspace.
- Treat `lib/` and `@openzeppelin/` as third-party dependencies; skip them unless there is a concrete misuse angle in first-party code.
- If there are only dependency files and no first-party sources, set no_findings=true and explain in summary.
Output MUST be valid JSON matching the provided schema.
- If you find nothing meaningful: set no_findings=true and findings=[].
- Otherwise: set no_findings=false and include findings with severity/category/title and file/line locations when possible.
Output constraints (IMPORTANT):
- severity MUST be exactly one of: info, low, medium, high, critical (lowercase).
- category MUST be exactly one of: access_control, reentrancy, math, oracle, upgradeability, auth, other.
Keep summary short and concrete.
mcp startup: no servers
exec
/bin/bash -lc ls in /tmp/codex-4j2tkmv2 succeeded in 6ms:
AUDIT_BUNDLE.txt
codex-output-schema.json
contracts
@openzeppelin
exec
/bin/bash -lc 'cat AUDIT_BUNDLE.txt' in /tmp/codex-4j2tkmv2 succeeded in 10ms:
This file concatenates the full contents of first-party Solidity sources for auditing.
Do not treat dependency code under lib/ or @openzeppelin/ as first-party.
================================================================================
FILE: contracts/TruthBridge.sol
================================================================================
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
/**
* @dev Bridging contract between Truth Network and Ethereum.
* Enables Author nodes to periodically publish T2 transactional state.
* Allows Authors to be added and removed from participation in consensus.
* "lifts" tokens from Ethereum addresses to Truth Network accounts.
* "lowers" tokens from Truth Network accounts to Ethereum addresses.
* Enables gasless lifting of USDC funds via relayers.
* Accepts optional ERC-2612 permits for lifting.
* Proxy upgradeable implementation utilising EIP-1822.
*/
exec
/bin/bash -lc 'rg -n "relayerLift|relayerLower|_attemptRelayerRefund|__refundRelayer" contracts/TruthBridge.sol' in /tmp/codex-4j2tkmv2 succeeded in 19ms:
296: function relayerLift(uint256 gasCost, uint256 amount, address user, uint8 v, bytes32 r, bytes32 s, bool triggerRefund) external checkAddress(user) {
311: if (triggerRefund) _attemptRelayerRefund(balance);
320: function relayerLower(uint256 gasCost, bytes calldata proof, bool triggerRefund) external {
338: if (triggerRefund) _attemptRelayerRefund(balance);
484: function _attemptRelayerRefund(int256 balance) private {
485: try this.__refundRelayer(msg.sender, balance - 1) {
565: function __refundRelayer(address relayer, int256 balance) external {
exec
/bin/bash -lc "nl -ba contracts/TruthBridge.sol | sed -n '280,360p'" in /tmp/codex-4j2tkmv2 succeeded in 6ms:
280 }
281
282 /**
283 * @dev Deregisters an existing relayer
284 */
285 function deregisterRelayer(address relayer) external onlyOwner {
286 int256 balance = relayerBalance[relayer];
287 if (balance == 0) revert(); // no such relayer
288 relayerBalance[relayer] = 0;
289 if (balance > 1) IERC20(usdc).transfer(relayer, uint256(balance - 1)); // transfer any unclaimed USDC
290 emit LogRelayerDeregistered(relayer);
291 }
292
293 /**
294 * @dev Enables a relayer to lift USDC to the prediciton market on behalf of a user and extract the tx cost from the USDC
295 */
296 function relayerLift(uint256 gasCost, uint256 amount, address user, uint8 v, bytes32 r, bytes32 s, bool triggerRefund) external checkAddress(user) {
297 int256 balance = relayerBalance[msg.sender];
298 if (balance < 1) revert RelayerOnly();
299
exec
/bin/bash -lc "nl -ba contracts/TruthBridge.sol | sed -n '470,590p'" in /tmp/codex-4j2tkmv2 succeeded in 8ms:
470 }
471
472 function _addNewAuthor(address t1Address, bytes32 t2PubKey) private returns (uint256 id) {
473 unchecked {
474 id = nextAuthorId++;
475 }
476 if (t2PubKeyToId[t2PubKey] != 0) revert T2KeyInUse(t2PubKey);
477 idToT1Address[id] = t1Address;
478 idToT2PubKey[id] = t2PubKey;
479 t1AddressToId[t1Address] = id;
480 t2PubKeyToId[t2PubKey] = id;
481 isAuthor[id] = true;
482 }
483
484 function _attemptRelayerRefund(int256 balance) private {
485 try this.__refundRelayer(msg.sender, balance - 1) {
486 relayerBalance[msg.sender] = 1; // reset to trace balance on success
487 } catch {
488 emit LogRefundFailed(msg.sender, balance);
489 }
codex
{"no_findings":false,"summary":"Found a high‑risk reentrancy in the relayer refund flow plus two low‑risk issues around unchecked ERC20 returns and oracle sanity checks used for relayer fee math.","findings":[{"severity":"high","category":"reentrancy","title":"Relayer refund can be reentered before balance reset, enabling repeated refunds","exploit_scenario":"A registered relayer calls `relayerLift`/`relayerLower` with `triggerRefund=true`. `_attemptRelayerRefund` calls `__refundRelayer`, which swaps and then sends ETH to the relayer via `.call`. The relayer’s fallback reenters `relayerLift`/`relayerLower` (e.g., with `gasCost=0` and a 0‑amount permit for itself) and triggers another refund while `relayerBalance` is still the pre‑refund value. This allows multiple swaps/refunds against the same balance and can drain the contract’s USDC reserves.","recommendation":"Add reentrancy protection to `relayerLift`/`relayerLower` and/or set `relayerBalance` to the trace value before performing the external refund. Alternatively, implement a pull‑based refund where the relayer claims in a separate non‑reentrant call, or add an “in‑refund” flag checked in relayer entrypoints.","evidence":"`relayerLift`/`relayerLower` call `_attemptRelayerRefund(balance)` before persisting `relayerBalance`, and `_attemptRelayerRefund` performs an external call that ends with `relayer.call{value: ethAmount}(\"\")`.","locations":["contracts/TruthBridge.sol:296","contracts/TruthBridge.sol:311","contracts/TruthBridge.sol:320","contracts/TruthBridge.sol:338","contracts/TruthBridge.sol:484","contracts/TruthBridge.sol:565","contracts/TruthBridge.sol:574"]},{"severity":"low","category":"other","title":"Unchecked ERC20 transfer/transferFrom return values in relayer flows","exploit_scenario":"If the USDC token returns `false` instead of reverting, `relayerLift` and `relayerLower` proceed as if funds moved, leading to incorrect accounting or incomplete payments. The Uniswap callback likewise ignores a failed transfer, which can revert later in the swap or leave state inconsistent.","recommendation":"Use `SafeERC20.safeTransfer`/`safeTransferFrom` (or check boolean return values) for all ERC20 interactions in relayer paths and the Uniswap callback.","evidence":"USDC transfers use raw `IERC20.transfer`/`transferFrom` without checking return values.","locations":["contracts/TruthBridge.sol:289","contracts/TruthBridge.sol:304","contracts/TruthBridge.sol:336","contracts/TruthBridge.sol:358"]},{"severity":"low","category":"oracle","title":"Oracle price used without sanity/staleness checks can break fee math","exploit_scenario":"`usdcEth()` uses `latestAnswer()` without validating positivity or freshness. If the feed is stale, paused, or returns 0/negative, relayer fee math can under/overcharge users or revert due to division by zero in relayer paths and refund checks.","recommendation":"Use `latestRoundData()` and verify `answer > 0` with `updatedAt` freshness bounds; handle invalid answers with a revert or fallback mechanism.","evidence":"`usdcEth()` directly casts `latestAnswer()` and is used in fee calculations and refund slippage checks.","locations":["contracts/TruthBridge.sol:347","contracts/TruthBridge.sol:300","contracts/TruthBridge.sol:326","contracts/TruthBridge.sol:572"]}]}
tokens used
57,161
{
"codex": {
"ran_at": "2026-01-30T12:41:38.395776+00:00",
"returncode": 0
},
"project": {
"mode": "source",
"file_count": 28,
"import_count": 31,
"first_party_contracts_count": 7
},
"summary": "Found a high\u2011risk reentrancy in the relayer refund flow plus two low\u2011risk issues around unchecked ERC20 returns and oracle sanity checks used for relayer fee math.",
"raw_final": {
"summary": "Found a high\u2011risk reentrancy in the relayer refund flow plus two low\u2011risk issues around unchecked ERC20 returns and oracle sanity checks used for relayer fee math.",
"findings": [
{
"title": "Relayer refund can be reentered before balance reset, enabling repeated refunds",
"category": "reentrancy",
"evidence": "`relayerLift`/`relayerLower` call `_attemptRelayerRefund(balance)` before persisting `relayerBalance`, and `_attemptRelayerRefund` performs an external call that ends with `relayer.call{value: ethAmount}(\"\")`.",
"severity": "high",
"locations": [
"contracts/TruthBridge.sol:296",
"contracts/TruthBridge.sol:311",
"contracts/TruthBridge.sol:320",
"contracts/TruthBridge.sol:338",
"contracts/TruthBridge.sol:484",
"contracts/TruthBridge.sol:565",
"contracts/TruthBridge.sol:574"
],
"recommendation": "Add reentrancy protection to `relayerLift`/`relayerLower` and/or set `relayerBalance` to the trace value before performing the external refund. Alternatively, implement a pull\u2011based refund where the relayer claims in a separate non\u2011reentrant call, or add an \u201cin\u2011refund\u201d flag checked in relayer entrypoints.",
"exploit_scenario": "A registered relayer calls `relayerLift`/`relayerLower` with `triggerRefund=true`. `_attemptRelayerRefund` calls `__refundRelayer`, which swaps and then sends ETH to the relayer via `.call`. The relayer\u2019s fallback reenters `relayerLift`/`relayerLower` (e.g., with `gasCost=0` and a 0\u2011amount permit for itself) and triggers another refund while `relayerBalance` is still the pre\u2011refund value. This allows multiple swaps/refunds against the same balance and can drain the contract\u2019s USDC reserves."
},
{
"title": "Unchecked ERC20 transfer/transferFrom return values in relayer flows",
"category": "other",
"evidence": "USDC transfers use raw `IERC20.transfer`/`transferFrom` without checking return values.",
"severity": "low",
"locations": [
"contracts/TruthBridge.sol:289",
"contracts/TruthBridge.sol:304",
"contracts/TruthBridge.sol:336",
"contracts/TruthBridge.sol:358"
],
"recommendation": "Use `SafeERC20.safeTransfer`/`safeTransferFrom` (or check boolean return values) for all ERC20 interactions in relayer paths and the Uniswap callback.",
"exploit_scenario": "If the USDC token returns `false` instead of reverting, `relayerLift` and `relayerLower` proceed as if funds moved, leading to incorrect accounting or incomplete payments. The Uniswap callback likewise ignores a failed transfer, which can revert later in the swap or leave state inconsistent."
},
{
"title": "Oracle price used without sanity/staleness checks can break fee math",
"category": "oracle",
"evidence": "`usdcEth()` directly casts `latestAnswer()` and is used in fee calculations and refund slippage checks.",
"severity": "low",
"locations": [
"contracts/TruthBridge.sol:347",
"contracts/TruthBridge.sol:300",
"contracts/TruthBridge.sol:326",
"contracts/TruthBridge.sol:572"
],
"recommendation": "Use `latestRoundData()` and verify `answer > 0` with `updatedAt` freshness bounds; handle invalid answers with a revert or fallback mechanism.",
"exploit_scenario": "`usdcEth()` uses `latestAnswer()` without validating positivity or freshness. If the feed is stale, paused, or returns 0/negative, relayer fee math can under/overcharge users or revert due to division by zero in relayer paths and refund checks."
}
],
"no_findings": false
},
"no_findings": false,
"proxy_context": {
"proxy_run_id": "019c0ea9-07b9-70a2-acc1-0b6c38a3ac98",
"proxy_address": "0x50c02710b06d6addb864d6b038010ef6fa1bcd92",
"implementation_run_id": "019c0ea9-e35a-710a-b93e-e2ba664180fb",
"implementation_address": "0x965983977c29158ec53a296a6f47be145ddecc36"
},
"schema_version": 1
}
00000000: PUSH1 0x80 00000002: PUSH1 0x40 00000004: MSTORE 00000005: PUSH1 0x0a 00000007: PUSH1 0x0c 00000009: JUMP 0000000a: JUMPDEST 0000000b: STOP 0000000c: JUMPDEST 0000000d: PUSH1 0x18 0000000f: PUSH1 0x14 00000011: PUSH1 0x1a 00000013: JUMP 00000014: JUMPDEST 00000015: PUSH1 0x51 00000017: JUMP 00000018: JUMPDEST 00000019: JUMP 0000001a: JUMPDEST 0000001b: PUSH1 0x00 0000001d: PUSH1 0x4c 0000001f: PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 00000040: SLOAD 00000041: PUSH1 0x01 00000043: PUSH1 0x01 00000045: PUSH1 0xa0 00000047: SHL 00000048: SUB 00000049: AND 0000004a: SWAP1 0000004b: JUMP 0000004c: JUMPDEST 0000004d: SWAP1 0000004e: POP 0000004f: SWAP1 00000050: JUMP 00000051: JUMPDEST 00000052: CALLDATASIZE 00000053: PUSH1 0x00 00000055: DUP1 00000056: CALLDATACOPY 00000057: PUSH1 0x00 00000059: DUP1 0000005a: CALLDATASIZE 0000005b: PUSH1 0x00 0000005d: DUP5 0000005e: GAS 0000005f: DELEGATECALL 00000060: RETURNDATASIZE 00000061: PUSH1 0x00 00000063: DUP1 00000064: RETURNDATACOPY 00000065: DUP1 00000066: DUP1 00000067: ISZERO 00000068: PUSH1 0x6f 0000006a: JUMPI 0000006b: RETURNDATASIZE 0000006c: PUSH1 0x00 0000006e: RETURN 0000006f: JUMPDEST 00000070: RETURNDATASIZE 00000071: PUSH1 0x00 00000073: REVERT 00000074: INVALID 00000075: LOG2 00000076: PUSH5 0x6970667358 0000007c: UNKNOWN(0x22) 0000007d: SLT 0000007e: KECCAK256 0000007f: UNKNOWN(0xD0) 00000080: UNKNOWN(0x23) 00000081: UNKNOWN(0x2C) 00000082: STATICCALL 00000083: DUP2 00000084: UNKNOWN(0x21) 00000085: PUSH13 0x3e4973e570f043b57ccb69ae4a 00000093: DUP2 00000094: UNKNOWN(0xB8) 00000095: UNKNOWN(0xBC) 00000096: MOD 00000097: NUMBER 00000098: CODESIZE 00000099: PUSH18
| Severity | Category | Tool | Title | Confidence | Validated | Confirmed | Details |
|---|---|---|---|---|---|---|---|
| critical | upgradeability | detector | Untrusted DELEGATECALL target reachable | 70 | no | no |
viewtrace_id: 0xd7659e895cf248b56edffa8bd12f22b3dcd401e559a1beb2850b4913f29138c1
call_targets:
0x965983977c29158ec53a296a6f47be145ddecc36
target_varies:
no
classification:
constant_target
validation_json{
"sink": "DELEGATECALL",
"errors": 0,
"status": "sink_reached",
"attempts": 1,
"trace_id": "0xd7659e895cf248b56edffa8bd12f22b3dcd401e559a1beb2850b4913f29138c1",
"confirmed": false,
"trace_mode": "callTracer",
"call_targets": [
"0x965983977c29158ec53a296a6f47be145ddecc36"
],
"matched_probe": null,
"target_varies": false,
"classification": "constant_target"
}
witness_json{
"notes": "heuristic witness; entrypoints are best-effort selectors with zeroed calldata",
"constraints": [],
"entrypoints": [
{
"calldata": "0x",
"selector": "fallback",
"calldata_variants": [
"0x"
]
}
]
}
evidence_json{
"examples": [
{
"pc": 95,
"opcode": "DELEGATECALL",
"returndata_checked": null,
"value_expression_category": "n/a",
"target_expression_category": "computed"
}
],
"guards_detected": [],
"delegatecall_count": 1,
"untrusted_delegatecall_count": 1
}
|
| high | reentrancy | codex | Relayer refund can be reentered before balance reset, enabling repeated refunds | 85 | no | — |
viewevidence_json{
"evidence": "`relayerLift`/`relayerLower` call `_attemptRelayerRefund(balance)` before persisting `relayerBalance`, and `_attemptRelayerRefund` performs an external call that ends with `relayer.call{value: ethAmount}(\"\")`.",
"locations": [
"contracts/TruthBridge.sol:296",
"contracts/TruthBridge.sol:311",
"contracts/TruthBridge.sol:320",
"contracts/TruthBridge.sol:338",
"contracts/TruthBridge.sol:484",
"contracts/TruthBridge.sol:565",
"contracts/TruthBridge.sol:574"
],
"recommendation": "Add reentrancy protection to `relayerLift`/`relayerLower` and/or set `relayerBalance` to the trace value before performing the external refund. Alternatively, implement a pull\u2011based refund where the relayer claims in a separate non\u2011reentrant call, or add an \u201cin\u2011refund\u201d flag checked in relayer entrypoints.",
"exploit_scenario": "A registered relayer calls `relayerLift`/`relayerLower` with `triggerRefund=true`. `_attemptRelayerRefund` calls `__refundRelayer`, which swaps and then sends ETH to the relayer via `.call`. The relayer\u2019s fallback reenters `relayerLift`/`relayerLower` (e.g., with `gasCost=0` and a 0\u2011amount permit for itself) and triggers another refund while `relayerBalance` is still the pre\u2011refund value. This allows multiple swaps/refunds against the same balance and can drain the contract\u2019s USDC reserves."
}
|
| medium | upgradeability | cast | DELEGATECALL present | 80 | no | — |
viewevidence_json{
"notes": "delegatecall enables execution in caller context; common in proxies/upgradeable patterns",
"delegatecall_count": 1
}
|
| low | other | codex | Unchecked ERC20 transfer/transferFrom return values in relayer flows | 65 | no | — |
viewevidence_json{
"evidence": "USDC transfers use raw `IERC20.transfer`/`transferFrom` without checking return values.",
"locations": [
"contracts/TruthBridge.sol:289",
"contracts/TruthBridge.sol:304",
"contracts/TruthBridge.sol:336",
"contracts/TruthBridge.sol:358"
],
"recommendation": "Use `SafeERC20.safeTransfer`/`safeTransferFrom` (or check boolean return values) for all ERC20 interactions in relayer paths and the Uniswap callback.",
"exploit_scenario": "If the USDC token returns `false` instead of reverting, `relayerLift` and `relayerLower` proceed as if funds moved, leading to incorrect accounting or incomplete payments. The Uniswap callback likewise ignores a failed transfer, which can revert later in the swap or leave state inconsistent."
}
|
| low | oracle | codex | Oracle price used without sanity/staleness checks can break fee math | 65 | no | — |
viewevidence_json{
"evidence": "`usdcEth()` directly casts `latestAnswer()` and is used in fee calculations and refund slippage checks.",
"locations": [
"contracts/TruthBridge.sol:347",
"contracts/TruthBridge.sol:300",
"contracts/TruthBridge.sol:326",
"contracts/TruthBridge.sol:572"
],
"recommendation": "Use `latestRoundData()` and verify `answer > 0` with `updatedAt` freshness bounds; handle invalid answers with a revert or fallback mechanism.",
"exploit_scenario": "`usdcEth()` uses `latestAnswer()` without validating positivity or freshness. If the feed is stale, paused, or returns 0/negative, relayer fee math can under/overcharge users or revert due to division by zero in relayer paths and refund checks."
}
|
| info | upgradeability | cast | Likely proxy-like runtime | 60 | no | — |
viewevidence_json{
"notes": "short runtime with SLOAD and DELEGATECALL resembles minimal proxy/upgradeable pattern",
"sload_count": 1,
"total_opcodes": 84,
"delegatecall_count": 1
}
|