aere-contracts/contracts/mocks/MockReentrantBountyToken.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

78 lines
3.0 KiB
Solidity

// 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);
}
}