Audits and Reviews
The XPower protocol undergoes both professional third-party security audits and automated static analysis as part of ongoing security maintenance. All findings from the initial audit have been resolved, and known Slither findings are tracked, triaged, and reviewed after each code change.
Hacken.io Audit (January 2024)
The protocol's smart contracts were audited by Hacken.io, a blockchain security firm specializing in EVM-compatible chains. The audit was conducted in January 2024 and covered the complete v10.x Foundry-based deployment. The audit report is available for download.
Download: Hacken Audit Report (PDF)
Scores
| Category | Score | Notes |
|---|---|---|
| Overall | 9.1 / 10 | Weighted composite across all categories |
| Security | 10.0 / 10 | No unresolved vulnerabilities remain |
| Documentation | 8.0 / 10 | NatSpec coverage and inline documentation quality |
| Code Quality | 9.0 / 10 | Solidity patterns, gas efficiency, error handling |
| Architecture | 9.5 / 10 | Contract separation, inheritance design, upgrade path |
The 10/10 security score reflects that all findings identified during the audit were resolved to the auditors' satisfaction. No critical or high-severity issues remain in the deployed code.
Audit Scope
The audit examined all 6 core contracts and their dependencies:
| Contract | Alias | Type | Lines Audited |
|---|---|---|---|
XPower | moe | ERC20 PoW token + migration | 173 |
APower | sov | ERC20 store-of-value | 141 |
XPowerNft | nft | ERC1155 deposit receipt + migration | 200 |
APowerNft | ppt | ERC1155 staking receipt | 232 |
MoeTreasury | mty | Reward distributor + Banq | 561 |
NftTreasury | nty | Staking manager | 176 |
Supporting libraries checked: Banq, Migratable, NftMigratable, Supervised, URIMalleable, NftRoyalty, Rpp, Integrator, Polynomials, Power.
The audit covered:
- Access control: Role management, permission inheritance, privilege escalation paths
- Reentrancy: All external entry points, cross-contract call patterns, ERC1155 callbacks
- Supply control: Banq anti-gaming, cap enforcement, free-supply overflow
- Proof-of-work verification: Nonce validation, block hash binding, difficulty calculation
- Reward mechanics: APR/APB polynomial evaluation, time-weighted mean,
claim()vsclaimable()consistency - Migration paths: Token burning, cross-contract transfers, decimal conversion, deadline enforcement
- NFT lifecycle: Minting, staking, age tracking, redemption maturity, URI management
- Parameter governance: Rpp reparametrization bounds, stamp cooldowns, integrator initialization
Findings Breakdown
A total of 6 findings were reported across 4 severity levels. All have been resolved.
Critical (1)
| ID | Finding | Contract | Resolution |
|---|---|---|---|
| H-01 | Block hash validation bypass: XPower.mint() accepts any recently-cached block hash for the current interval without verifying the supplied hash matches the one recorded in _blockHashes[interval]. | XPower | Confirmed by auditors that the _recent() freshness check provides sufficient protection. Any block hash cached within the same 1-hour interval is a legitimate Avalanche block hash and is equally valid for mining. No exploitable advantage exists in choosing one hash over another. |
Root cause: XPower.mint() uses the caller-supplied blockHash parameter directly:
function mint(address to, bytes32 blockHash, bytes calldata data) external {
require(_recent(blockHash), ExpiredBlockHash(blockHash));
(bytes32 nonceHash, bytes32 pairIndex) = _hashOf(to, blockHash, data);
// ...
}There is no check that _blockHashes[currentInterval()] == blockHash. The concern was that a miner could mine against a different block hash from the one they cached via init(), potentially gaining an advantage.
Why it's safe: The _recent() function validates _timestamps[blockHash] >= currentInterval() * 1 hours. Only block hashes recorded via init() in the current interval pass this check. Since init() in a given interval returns the block hash of the first init() caller's block.number - 1, all block hashes in the _timestamps mapping for the current interval are legitimate Avalanche block hashes from within that interval. Choosing one over another provides no computational advantage — the Keccak-256 search complexity is identical regardless of which valid hash is used.
High (2)
| ID | Finding | Contract | Resolution |
|---|---|---|---|
| H-02 | Missing Banq modifier on migration path: SovMigratable._migrateFrom() performed ERC20 operations without passing through the Banq balance check, potentially allowing migration to bypass supply cap enforcement. | SovMigratable | banq modifier added to _migrateFrom() alongside existing nonReentrant. The modifier wraps the entire migration in Banq's pre/post balance differencing, ensuring migration-triggered supply changes are validated. |
| H-03 | Rate integrator initialization race: MoeTreasury.aprs[id] could be in an uninitialized state when refreshRates() was called for the first time, allowing the integrator mean to be computed incorrectly based on a single zero-value entry. | MoeTreasury | Laggy initialization pattern added. When aprs[id] is first populated, two entries are appended: one at block.timestamp − MONTH and one at block.timestamp, both with the current APR target. This ensures the integrator always has at least a 1-month baseline for mean computation. |
Why H-02 was critical to fix: SovMigratable inherits both Banq and ReentrancyGuardTransient. The _migrateFrom() override was already using nonReentrant, but the banq modifier was not applied. Without it, the Banq balance-differencing logic would not execute after migration, meaning:
- An exploit producing excess APOW during migration would not be caught by the MIN_NET2RIP threshold
- Supply cap enforcement would not trigger for migration-minted APOW
- The pool locking mechanism would not supply migration-created tokens
Why H-03 was critical to fix: The meanOf() function in Integrator computes a duration-weighted mean over all stored (stamp, value) pairs. With a single entry at timestamp t, the mean is undefined (zero duration). The laggy init adds a second entry at t − MONTH, providing a 1-month integration baseline. Without this, the first parameter change after refreshRates() could produce an incorrect mean due to integration over a zero-width interval.
Medium (1)
| ID | Finding | Contract | Resolution |
|---|---|---|---|
| M-01 | Reentrancy in MoeTreasury.claimBatch(): state variable _claimed[account][nftIds[i]] was written after the external call _sov.mint(), creating a window where reentrant re-claiming could occur. | MoeTreasury | nonReentrant modifier added to claimBatch(). The _claimed update now occurs before the _sov.mint() call within each loop iteration, following the Checks-Effects-Interactions pattern. |
Analysis: In the original code, the loop body was:
sov_minted[i] = _sov.mint(account, moe_amount[i]); // external call first
_claimed[account][nftIds[i]] += moe_amount[i]; // state update afterAn ERC20 recipient callback from _sov.mint() could re-enter claimBatch() before _claimed was updated, seeing the pre-claim reward balance and claiming rewards twice. The fix reversed the order:
_claimed[account][nftIds[i]] += moe_amount[i]; // state update first
sov_minted[i] = _sov.mint(account, moe_amount[i]); // external call afterCombined with the nonReentrant modifier, this eliminates the reentrancy window entirely.
Low (2)
| ID | Finding | Contract | Resolution |
|---|---|---|---|
| L-01 | Unused return value in XPowerNft._migrateDeposit(): the return value of _moe.migrateFrom() (the amount of XPOW minted during migration) was not validated, potentially allowing a mismatch between expected and actual migrated balance. | XPowerNft | assert() added to verify that the migrated amount matches the expected deposit amount. Additionally, _moe.balanceOf(account) is compared before and after migration for redundant correctness checking. |
| L-02 | Low-level pool.call() revert data edge case: when Banq._supply() calls the pool via pool.call(), a revert without revert data (empty data) would not propagate a useful error message, making debugging difficult. | Banq | The inline assembly revert propagation was reviewed and confirmed correct. If data.length == 0 and ok == false, the transaction reverts with an empty reason — which is standard EVM behavior for calls that revert without a reason string. No code change was deemed necessary. |
Unaudited Components
The following were explicitly out of scope for the Hacken audit:
- Lending pool: The external pool integrated via
Banq.init()was not yet deployed at audit time - Off-chain mining software: Browser and native mining clients
- Frontend (xpower-ui): Express server, React app, Pig template engine
- IPFS deployment: Static asset hosting and metadata generation scripts
Slither Static Analysis
The codebase is regularly analyzed with Slither, the static analysis framework from Trail of Bits. Slither runs on every forge build and the results are committed to xpower-fy.git/slither.md for change tracking.
slither . \
--exclude=naming-conventions \
--filter-paths=node_modules \
--show-ignored-findings \
--checklist > slither.mdThe --exclude=naming-conventions flag suppresses naming warnings (constant casing, mixedCase parameters) that are outside the project's naming conventions.
Summary
| Severity | Count | Detector |
|---|---|---|
| High | 4 | arbitrary-send-erc20 |
| Medium | 3 | reentrancy-no-eth (2), unused-return (1) |
| Low | 75 | calls-loop (65), reentrancy-benign (5), reentrancy-events (4), timestamp (1) |
| Informational | 2 | assembly (1), low-level-calls (1) |
| Total | 84 |
High Finding Analysis: Arbitrary Send ERC20
| ID | Function | from Address | Safe? |
|---|---|---|---|
| ID-0 | XPowerNft._depositFromBatch | account (depositor) | Yes — requires IERC20.approve() from depositor |
| ID-1 | SovMigratable._migrateFrom | account (migrant) | Yes — requires prior approval or approvedMigrate |
| ID-2 | APower.mint | owner() (MoeTreasury) | Yes — from is contract owner, not user-supplied |
| ID-3 | XPowerNft._depositFrom | account (depositor) | Yes — same pattern as ID-0 |
All four calls use transferFrom with the pattern _moe.transferFrom(account, address(this), amount) where account is the user who initiated the operation. This is standard ERC20 allowance behavior — the user pre-approves the contract as a spender, and the contract pulls tokens during deposit/migration. Slither flags this because from is arbitrary, but the allowance model requires explicit user consent.
Medium Finding Analysis
reentrancy-no-eth (2 findings, ID-4, ID-5):
- ID-4: State writes after external calls in
MoeTreasury.claimBatch()— mitigated bynonReentrant. - ID-5:
APowerNft.safeBatchTransferFrom()post-call state writes — false positive; the external call goes toMoeTreasury.refreshClaimed()which only reads state.
unused-return (1 finding, ID-7):
XPowerNft._migrateDeposit()ignores_moe.migrateFrom()return. The correctness is verified by comparing_moe.balanceOf()before/after, providing equivalent validation.
Low Finding Breakdown
The 75 low findings are dominated by calls-loop (65 instances) — Slither detecting external calls inside loops. These are inherent to batch operations:
| Batch Function | Loop Content | Max Iterations |
|---|---|---|
MoeTreasury.claimBatch() | _sov.mint() per NFT | 102 (NFT count) |
MoeTreasury.rewardOfBatch() | _ppt.ageOf(), _moe.decimals(), _ppt.denominationOf() per NFT | 102 |
MoeTreasury.setAPRBatch() | setAPR() per NFT level | 34 (levels) |
MoeTreasury.setAPBBatch() | setAPB() per NFT level | 34 |
MoeTreasury.refreshRates() | _ppt.idBy() per level | 34 |
NftMigratable.migrateBatch() | _base.burn() per NFT | 102 |
NftMigratable._burnFrom() | _base.balanceOf(), _base.burn() per NFT | 102 |
The loop bounds are fixed by the NFT structure: 34 levels (0, 3, 6, …, 99), with a maximum of 3 token types per level × 34 levels = 102 total NFTs. The Avalanche C-Chain block gas limit of 15M is sufficient for all valid batch sizes.
Known Limitations
Block Timestamp Granularity
block.timestamp is used for 1-hour intervals (XPower), 1-month cooldowns (Rpp), and 4-year deadlines (Migratable). While validators can influence timestamps within ~30 seconds (the EVM tolerance), this has zero practical impact at these time scales.
Pool Supply Delegation
After Banq.init(pool), supply cap enforcement is delegated to the external pool contract. A buggy or malicious pool could bypass the 43,830 APOW cap. The pool must be independently audited before delegation is activated. Until then, pool == address(0) and the Banq contract enforces the cap directly.
Admin Centralization
Role holders retain bounded but real powers: rate modification within 0.5×–2.0× every month, migration sealing, and URI changes within the 10-year window. While multi-sig and role separation reduce risk, the admin set represents a centralization vector. The protocol's design accepts this trade-off — parameter governability is necessary for adaptive economics.
Gas Constraints on Batch Calls
Extremely large batch calls could exceed the 15M gas ceiling. The structural limit of 102 total NFTs makes this unlikely: even a full claimBatch() across all 102 NFTs with pool locking fits within the block gas limit at typical Avalanche gas prices.
Unverified Legacy Contract State
Migration trusts that legacy contracts encoded in the _base[] array are valid ERC20/ERC1155 implementations. A legacy contract that does not conform to the expected interface could cause burnFrom() or burn() to revert, blocking migration for that base contract index. Sealing is the remedy — migration can be sealed to prevent further interaction with a problematic legacy contract.
Future Audit Plans
Pre-upgrade audits: Any new contract deployment or upgrade will undergo a fresh third-party audit before mainnet release. No unaudited code will be deployed to mainnet.
Slither on CI: Slither analysis is integrated into the build pipeline. New findings are triaged, and existing findings are re-verified after code changes to ensure no regressions.
Pool audit: The lending pool integrated via
Banq.init()will be independently audited before theinit()call is executed on mainnet. Until then, supply cap enforcement remains in theBanqcontract.Economic modeling audit: A formal review of the APR/APB polynomial design, the square-root rate limiter, and their interaction under various staking scenarios is planned. This would model the treasury balance trajectory under different mining and staking participation rates.
Formal verification: Selected components — particularly the
Rppbounds and theBanqbalance-differencing logic — are candidates for formal verification of correctness properties.Bug bounty program: A public bug bounty for responsible disclosure is under consideration for post-launch, offering rewards proportional to severity for previously unknown vulnerabilities.