// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; interface IPayVault { function pay(bytes32 findingId, address reporter, uint256 amount) external; function fund(uint256 amount) external; } /// TEST-ONLY mock (local hardhat, NEVER deploy to chain 2800): an ERC20 whose /// transfer() re-enters AereBugBountyVault.pay() (or fund()) exactly once, using /// try/catch so the outer legitimate call can still complete. Lets the fuzz suite /// prove the ReentrancyGuard on pay()/fund() blocks a token-callback re-entry /// drain. A plain ERC20 can never exercise this path. contract MockReentrantBountyToken is ERC20 { address public vault; // AereBugBountyVault under test bool public armed; // when true, transfer() re-enters once bool public reenterFund; // re-enter fund() instead of pay() when true bool public reentryAttempted; bool public reentryReverted; string public lastRevert; constructor() ERC20("Reentrant WAERE", "rWAERE") {} function mint(address to, uint256 amt) external { _mint(to, amt); } function setVault(address v) external { vault = v; } function arm(bool on, bool useFund) external { armed = on; reenterFund = useFund; } /// Outer entrypoints so this token can be set AS the vault's triager, letting /// the nested re-entry pass onlyTriager and actually reach the ReentrancyGuard. function triggerPay(bytes32 findingId, address reporter, uint256 amount) external { IPayVault(vault).pay(findingId, reporter, amount); } function triggerFund(uint256 amount) external { // vault.fund does transferFrom(msg.sender=this, ...); ensure allowance is set by test. IPayVault(vault).fund(amount); } function _maybeReenter() internal { if (!armed || vault == address(0)) return; armed = false; // one-shot so we do not recurse forever reentryAttempted = true; if (reenterFund) { try IPayVault(vault).fund(1) { reentryReverted = false; } catch Error(string memory reason) { reentryReverted = true; lastRevert = reason; } catch { reentryReverted = true; lastRevert = "unknown"; } } else { try IPayVault(vault).pay(bytes32("REENTRY"), address(0xBEEF), 1) { reentryReverted = false; } catch Error(string memory reason) { reentryReverted = true; lastRevert = reason; } catch { reentryReverted = true; lastRevert = "unknown"; } } } function transfer(address to, uint256 amount) public override returns (bool) { _maybeReenter(); return super.transfer(to, amount); } function transferFrom(address from, address to, uint256 amount) public override returns (bool) { _maybeReenter(); return super.transferFrom(from, to, amount); } }