Seven findings identified and resolved. Zero open findings in the final release.
The NexumRise NBES token contract underwent a structured iterative audit across eight sequential versions (V2.9 → V2.16). Each round followed the same discipline: issues reported → developer fixes in isolation → new version submitted for re-audit and fix verification before the next pass began.
Seven findings were identified and resolved across the audit cycle. No findings remain open in the final V2.16 release. The contract is production-ready from a smart contract security standpoint:
• Critical findings: 0 • High findings: 0 • Medium findings: 3 (all resolved) • Low findings: 4 (all resolved) • Informational / documented design decisions: several (no action required)
The contract implements a sophisticated architecture — formula-driven bonding curve pricing, dual-reserve fund separation, five-layer liquidity trap protection, commit-reveal oracle governance, dual-signer emergency paths, and a 30-day holder rescue mechanism — and does so with consistent application of CEI (Checks-Effects-Interactions), return bomb protection, reentrancy guards, and on-chain governance timelocks. The security posture at V2.16 is strong.
V2.16 IS CLEARED FOR DEPLOYMENT ON ETHEREUM MAINNET.
NBES is a fee-on-transfer ERC-20 token with a fully on-chain bonding curve. Users buy and sell directly against the contract at a formula-derived price — there is no Uniswap pool or AMM. Price is determined by a volume-weighted formula over BTC, ETH, and SOL Chainlink oracle feeds.
NBES Price (Wei) =
(BTC_wei × 50% × VW_btc) + (ETH_wei × 30% × VW_eth) + (SOL_wei × 20% × VW_sol)
──────────────────────────────────────────────────────────────────────────────
totalSupply / 100
Where:
BTC_wei = (BTC_usd_1e8 × 1e18) / ETH_usd_1e8
ETH_wei = 1e18 (ETH is always 1 ETH = 1e18 Wei)
SOL_wei = (SOL_usd_1e8 × 1e18) / ETH_usd_1e8
VW_xxx = asset 24h volume / (BTC+ETH+SOL total volume) [1e18-scaled]
Price is entirely Wei-native. No USD arithmetic exists on-chain.
| Reserve | Source | Purpose |
|---|---|---|
| Main Reserve | Buyer ETH deposits | Backs sell redemptions at base formula price |
| Investment Reserve | Buy/sell spread (0.5% each side) | Funds reserve multiplier premium paid to sellers |
The two reserves share address(this).balance but are ring-fenced via the investmentReserve accounting variable. getMainReserve() = address(this).balance − investmentReserve.
| Mechanism | Description |
|---|---|
| Buy / Sell Spread | ±0.5% around formula price; all proceeds to Investment Reserve |
| Reserve Multiplier Premium | Sellers receive additional ETH from Investment Reserve × multiplier (1–20×), capped at investmentReserve / circulatingSupply |
| ETH Crash Protection | If ETH drops >5% in 24h, sell price adjusted by drop% (base price only; premium self-adjusts) |
| 5-Layer Liquidity Trap | Circuit breaker → stress fee → daily 10% exit window → 5% max single sell → health tiers |
| Phase 1 Burns | 1% of every sell and transfer burned; 500M supply floor enforced |
| Phase 2 Redirect | At 500M floor, 1% ETH equivalent redirected to Investment Reserve |
| Milestone Burns | 4 triggered burns (50M/100M/150M/200M NBES) at $0.001/$0.01/$0.10/$1.00 |
| Commit-Reveal Volumes | 24h timelock, dual-signer hash commitment prevents front-running |
| Holder Rescue | 30-day timelock; proportional main-reserve ETH claim; epoch-keyed snapshots; 7-day hold required |
| Version | Lines | Role |
|---|---|---|
| V2.9 | — | Baseline submitted for initial audit |
| V2.10 | — | Fixes for V2.9 findings |
| V2.11 | 3,468 | Fixes for V2.10 findings (partial) |
| V2.12 | 3,456 | Completes V2.11 partial fixes + new findings |
| V2.13 | 3,453 | Fixes for V2.12 findings |
| V2.14 | 3,466 | Fixes for V2.13 findings (partial) |
| V2.15 | 3,466 | Completes V2.14 fix |
| V2.16 | 3,474 | FINAL — fixes V2.15 finding. No open issues. |
Each review performed full source-level reading of the entire contract. Every identified issue was documented with exact line references, attack path or failure mode, severity rationale, and a concrete code fix. The following version re-verified fix correctness before beginning the next pass.
Areas covered per version: constructor and genesis state, oracle read paths, buyNBES() and sellNBES() (slippage, CEI, oracle call count), migration functions, emergency withdrawal paths, rescue mechanism, milestone burns, governance timelocks, ERC-20 standard functions, event integrity, return bomb protection, and reentrancy coverage.
| Function | executeMilestoneBurn() |
Milestone flags (milestone1Burned, milestone2Burned, etc.) were set BEFORE _executeMilestoneBurn() was called. If the internal call reverted, the flag set rolled back with it. In practice flags were never persistently saved, allowing the same milestone to be re-executed indefinitely until the 500M supply floor was reached.
Owner calls executeMilestoneBurn() for M1 → flag set → internal call fails → revert rolls back flag → repeat. Entire 500M above-floor supply burnable by owner without price milestone enforcement.
Flag set moved to AFTER _executeMilestoneBurn() completes successfully. Each milestone can now only execute once.
BEFORE: milestone1Burned = true; // set before execution — rolls back on revert
_executeMilestoneBurn(1, M1_BURN, BURN_M1);
AFTER: _executeMilestoneBurn(1, M1_BURN, BURN_M1);
milestone1Burned = true; // set only after success ✅
| Function | buyNBES() |
buyNBES() called getBuyNBESPrice() twice: once inside a helper and once explicitly. This resulted in 6 Chainlink latestRoundData() calls per purchase. A partial fix in V2.11 was incomplete (still two oracle paths). V2.12 resolved it fully.
No direct exploit, but excess gas cost per buy (~3× oracle overhead). During high gas periods or future gas repricing, this could make buying unexpectedly expensive or approach block gas limits.
Single formulaWei = getBuyNBESPrice() call at top of buyNBES(). Spread and buy price derived inline from the cached value.
uint256 formulaWei = getBuyNBESPrice(); // exactly 1 oracle round-trip ✅
uint256 spreadAmt_ = (formulaWei * buySpreadBps) / FEE_DENOM;
uint256 buyPriceWei = formulaWei + spreadAmt_;
// reuse formulaWei for spread accounting — zero extra oracle reads
| Function | cancelMigration() / executeMigration() |
signerApprovedMigration was never reset to false in either the cancel or execution path. A stale signer approval from a previous migration cycle could satisfy the approval check in a subsequent migration without fresh dual-approval.
1. Owner sets successor = addressA, calls initiateMigration()
2. Trusted signer calls approveMigration() → signerApprovedMigration = true
3. Owner calls cancelMigration() → flag NOT cleared ❌
4. Owner calls setSuccessor(attackerAddress) — no restriction at this point
5. Owner re-initiates migration
6. executeMigration() checks signerApprovedMigration → still true from step 2
7. Investment reserve transferred to attackerAddress without fresh signer approval
signerApprovedMigration = false added to both cancelMigration() and executeMigration().
function cancelMigration() external onlyOwner {
migrationInitiated = false;
migrationAt = 0;
signerApprovedMigration = false; // FIX: stale approval cleared on cancel ✅
emit MigrationCancelled(_msgSender());
}
| Function | Volume commit-reveal section |
Four @notice comment blocks (Steps 2, 3, Cancel, Get Status) existed in the source with no function bodies beneath them. The actual implementations sat ~40 lines lower. On-chain indexers and documentation generators would produce incorrect or incomplete output.
No security impact. Misleading code structure and incorrect auto-generated documentation.
Orphaned stubs removed. Function implementations now immediately follow their documentation.
| Function | setSuccessor() |
After a migration was initiated and signer approval obtained, the owner could call setSuccessor() to swap the destination address with no restriction. The signer's approval was tied to the migration process, not to a specific successor address.
1. Owner sets successor = addressA, calls initiateMigration()
2. Signer calls approveMigration() (believing migration goes to addressA)
3. Owner calls setSuccessor(attackerAddress) — no guard present ❌
4. After 48h, executeMigration() sends investment reserve to attackerAddress
require(!migrationInitiated, ...) guard added to setSuccessor(). Combined with F-3 fix, cancelling migration now also clears the stale approval, forcing fresh dual-approval for any new destination.
function setSuccessor(address _successor) external onlyOwner {
require(_successor != address(0), 'NBES: Zero address');
require(_successor != address(this), 'NBES: Cannot migrate to self');
require(!migrationInitiated,
'NBES: Cancel active migration before changing successor'); // FIX ✅
successor = _successor;
}
| Function | sellNBES() |
The slippage guard require(ethOutWei >= minETHOut) was positioned after the NBES burn (two SSTOREs to _balances and _totalSupply), the nbesReturn balance transfer, and the investmentReserve adjustment. On a slippage revert, the user had already paid for all those storage writes. A partial fix in V2.14 was incomplete. V2.15 completed it.
Not an exploit — impacts the seller. On a failed slippage check, the user pays for multiple SSTORE operations (~20,000 gas each) before the transaction reverts, losing gas for no state change.
Check moved to immediately after Phase 2 redirect calculation (which finalises ethOutWei) and BEFORE the // -- EFFECTS block. Zero state writes on slippage revert.
// Phase 2 finalises ethOutWei
if (reserveFeedEnabled && _totalSupply <= MIN_SUPPLY && ethOutWei > 0) {
redirectFeeETH = (ethOutWei * burnFee) / FEE_DENOM;
ethOutWei = ethOutWei - redirectFeeETH;
}
// V2.15 FIX: slippage check — zero SSTOREs on revert ✅
require(ethOutWei >= minETHOut, 'NBES: Slippage exceeded -- price moved');
// -- EFFECTS (all state writes begin here) --
| Function | executeMainReserveEmergency() |
This last-resort function (dual-sig + 60-day timelock) drains address(this).balance entirely — which includes both the main reserve AND the investment reserve. However, investmentReserve was not reset to zero after the withdrawal. The contract ETH balance became zero but the accounting variable retained its old value, creating phantom data in all view functions that reference it.
No fund-loss risk: getMaxSellETH() returns (0, EMPTY_RESERVE) when getMainReserve() == 0, causing sellNBES() to revert before any ETH transfer. However:
• _getCappedPremium() returned a non-zero premium from non-existent ETH
• getNBESPrice() / getBuyNBESPrice() showed inflated prices
• getPriceBreakdownV2() and getSellBreakdown() displayed phantom reserves
Inconsistency: executeEmergencyWithdraw() correctly writes investmentReserve = 0 — the main-reserve path did not.
investmentReserve = 0 added after flag resets, before the assembly ETH transfer.
uint256 amount = address(this).balance;
mainResEmergencyInitiated = false;
ownerApprovedMainEmerg = false;
signerApprovedMainEmerg = false;
investmentReserve = 0; // FIX: zero phantom reserve ✅
emit MainResEmergencyExecuted(amount);
bool ok;
assembly { ok := call(gas(), caller(), amount, 0, 0, 0, 0) }
require(ok, 'NBES: Transfer failed');
| ID | Severity | Function | Description | Fixed In |
|---|---|---|---|---|
| F-1 | MEDIUM | executeMilestoneBurn() | Milestone flags never persisted — re-execution possible until floor | V2.10 |
| F-2 | MEDIUM | buyNBES() | 6 Chainlink oracle reads per buy — double getBuyNBESPrice() call | V2.12 |
| F-3 | MEDIUM | cancelMigration() / executeMigration() | Stale signerApprovedMigration enabled successor-swap attack | V2.13 |
| F-4 | LOW | Volume commit-reveal section | Orphaned JSDoc stubs with no function bodies | V2.13 |
| F-5 | MEDIUM | setSuccessor() | No guard against active migration — successor swappable post-approval | V2.14 |
| F-6 | LOW | sellNBES() | minETHOut slippage check after SSTORE-heavy state writes | V2.15 |
| F-7 | LOW | executeMainReserveEmergency() | investmentReserve not zeroed after full balance withdrawal | V2.16 |
✓ All 7 findings resolved. Zero open findings in V2.16.
| Protection | Implementation |
|---|---|
| Staleness windows | BTC/ETH: 3 hours; SOL: 25 hours (matching Chainlink heartbeats) |
| Unanswered round detection | answeredInRound >= roundId in both oracle read paths |
| Last-known fallback | lastKnownBtcPrice / lastKnownEthPrice / lastKnownSolPrice updated on every fresh read |
| Return bomb protection | Assembly call with outSize=0 discards all return data |
| Return data size cap | Chainlink response validated to ≤320 bytes |
| Two oracle read paths | _readChainlinkPrice() (view, uses fallback) for display; _getChainlinkPrice() (state-writing) for buy/sell |
| Volume staleness fallback | If volumes not updated in 48h, equal-weight 33%/33%/33% applied instead of reverting |
| SOL oracle degradation | When SOL feed stale during buys: SOL weight redistributed to BTC/ETH; buyers never overcharged |
| Commit-reveal volumes | 24h timelock + hash commitment + salt; direction hidden from front-runners |
| Item | Decision |
|---|---|
| Oracle threading V3 refactor | sellNBES() makes ~9–17 Chainlink calls in worst case. Reducing to 3 via oracle caching is planned for V3. Tracked as V3-GAS-001. |
| Volume weights are owner-set | No on-chain 24h volume oracle exists (Chainlink/Pyth provide price only). Commit-reveal + dual-sig protects during Phase 1. Phase 2: Chainlink Functions / API3. |
| ethProtectionStale flag | Set/cleared by permissionless checkProtectionStaleness(). Keeper bot monitors ETHProtectionDeactivated event. V3: Chainlink Automation. |
| _totalSupply as formula denominator | Burns are the only _totalSupply changes — clean, predictable denominator. Circulating supply (changes on every transfer) would add noise. |
| Trusted signer — both wallets SKPH Capital | Phase 1 documented. Phase 2: Gnosis Safe. Phase 3: DAO governance. |
| 14-day owner-only feed update override | Prevents permanent oracle deadlock if trustedSigner is lost. Community has 48h + 14 days = 15.7 days notice before unilateral execution. |
NexumRise NBES V2.16 completes a seven-finding iterative audit cycle with all issues resolved. The contract correctly implements a novel formula-driven bonding curve with comprehensive liquidity protection, dual-reserve architecture, on-chain governance with community-visible timelocks, and a last-resort holder rescue mechanism.
The three medium findings addressed fundamental invariants: milestone irreversibility, oracle call efficiency, and migration authorization integrity. The four low findings addressed gas safety, event/data accuracy, and accounting consistency. No critical or high severity issues were identified at any stage.
V2.16 IS CLEARED FOR DEPLOYMENT ON ETHEREUM MAINNET.
Zero open findings. All critical, high, medium, and low issues resolved across the audit cycle.