Banq System
The Banq system is the core anti-gaming mechanism protecting APOW supply. It enforces a minimum ratio between newly minted tokens held ("net") and tokens burned ("rip"), and locks supply into a lending pool behind a proof-of-work nonce gate. The mechanism is implemented by the Banq contract at source/contracts/libs/Banq.sol and used as a modifier by MoeTreasury and the migration system.
Architecture
The Banq contract inherits from Supervised and is designed to be mixed into any contract that distributes APOW tokens. It defines a single modifier, banq, that wraps state-changing operations with pre- and post-execution hooks:
modifier banq(address account, uint256 amount, uint256 nonce) {
uint256 old_balance = _token.balanceOf(account);
_; // execute the body of the modified function
_banq(account, amount, nonce, old_balance);
}This atomic enforcement pattern ensures that every claim, migration, or mint operation passes through the Banq checks. The modifier cannot be bypassed — the body executes first, then the post-hook validates and routes the resulting balance.
Net and Rip Supply Tracking
After the modified function's body executes, _banq compares the token balance of the account before and after:
If
Here:
min_balance— the "net" supply: the legitimate portion limited by the claimedamountrip_balance— the "rip" supply: any excess beyond what was legitimately claimed
MIN_NET2RIP Threshold
The constant MIN_NET2RIP = 100 enforces the minimum ratio of net to rip supply:
If the ratio is below this threshold, the transaction reverts with InsufficientAmount. This means:
- For every 1 token burned, at most
tokens can be retained by the account - Equivalently, the account must retain at least
more than they burn - The threshold prevents attackers from using tiny legitimate claims as a vehicle for large illicit mints
Example: If a legitimate claim produces 1,000 APOW but a bug or exploit causes 10,000 to appear in the account (
Excess Burning
When rip_balance > 0 and the MIN_NET2RIP check passes, the excess tokens are burned via ERC20Burnable.burnFrom():
_token.burnFrom(account, rip_balance);This permanently destroys the excess supply, preventing it from ever entering circulation. The combination of ratio enforcement and burning means that any supply increase beyond the legitimate amount is automatically and atomically reversed.
Supply Cap Enforcement
If no lending pool is configured (pool == address(0)), the Banq contract enforces a free-supply cap:
if (_token.totalSupply() > _free_supply) {
revert Capped(_free_supply);
}The free-supply cap is set at construction time:
- MoeTreasury:
43,830 × 10^18APOW (the economic supply cap) - Migratable:
0(no cap — migration mirrors existing supply from old contracts; the cap was already enforced when that supply was originally minted)
When a pool is configured, the cap check is not performed — the pool's supply() method assumes responsibility for enforcing any supply constraints through its own logic.
Pool Locking Mechanism
When a pool address is set, the Banq contract supplies the net balance to a lending pool rather than leaving it in the account:
_supply(account, min_balance, nonce);The pool locking is implemented via a low-level pool.call():
bytes memory args1 = abi.encodeWithSelector(
0xf62256c7, // supply(address,address,uint256,uint256)
account, // address
_token, // address
amount, // supply amount
type(uint256).max, // lock (max = permanent)
nonce // PoW gate!
);
(bool ok, bytes memory data) = pool.call(args1);Nonce Gate
The nonce parameter is passed through to the pool's supply() function as a proof-of-work gate. Before tokens can be supplied to the pool (and thus exit the Banq-controlled state), the caller must provide a valid PoW nonce. This prevents unauthorized supply operations and ties the locking mechanism to the same computational work model used by XPOW mining.
Low-Level Call Pattern
The use of pool.call() rather than a typed interface call was a deliberate design choice, reviewed and accepted in audit. The pattern:
- Encodes the function selector and arguments via
abi.encodeWithSelector - Calls the pool contract with raw bytes
- Handles revert data propagation using inline assembly to bubble up revert reasons
This allows the Banq contract to interact with pools that may evolve independently, without requiring a shared interface definition. The revert propagation ensures that pool-level failures are visible in transaction traces.
Gas Cost
The banq modifier adds the following gas costs per invocation:
| Operation | Approximate Gas |
|---|---|
balanceOf (before) | ~2,600 (warm) |
balanceOf (after) | ~100 (warm, same slot) |
| Branch evaluation (if | ~50–200 |
burnFrom (when excess exists) | ~5,000+ |
totalSupply read | ~2,600 (warm) |
pool.call (when pool configured) | variable, ~30,000+ |
Typical total overhead: ~5,000–10,000 gas for normal paths; ~40,000+ gas when pool locking is active. These costs are dominated by the external calls to burnFrom and pool.call, which are infrequent relative to the protection they provide.
On Avalanche C-Chain at typical gas prices (~25 nAVAX), the overhead is approximately 0.0001–0.001 AVAX per transaction.
Contract Integration
MoeTreasury
The MoeTreasury contract inherits Banq and applies the modifier to both claim() and claimBatch():
function claim(address account, uint256 nftId, uint256 amount, uint256 nonce)
external nonReentrant banq(account, amount, nonce) { ... }The treasury sets _free_supply = 43,830 × 10^18 and receives a pool address via the init() function during deployment.
SovMigratable
The SovMigratable contract (base of APower) also inherits Banq but with _free_supply = 0. The banq modifier is applied to _migrateFrom(), ensuring migration operations pass through balance validation, though the cap check is a no-op (since 0 ≥ totalSupply is always false at migration time):
function _migrateFrom(address account, uint256 amount, uint256[] memory index)
internal override nonReentrant banq(account, newUnits(amount, index[0]), 0)
returns (uint256) { ... }Error Conditions
| Error | Condition |
|---|---|
InsufficientAmount(amount, net_balance, min_net2rip) | Net-to-rip ratio below MIN_NET2RIP |
Capped(free_supply) | Total supply exceeds free-supply cap (pool not yet initialized) |
AlreadyInitialized(pool) | init() called when pool is already set |
InvalidPool(pool) | init() called with zero address |
Security Model
The Banq system provides defense-in-depth against supply manipulation:
- Balance differencing catches any unexpected token accrual regardless of source
- Ratio enforcement prevents large-scale attacks masked by small legitimate operations
- Automatic burning removes illicit supply without requiring governance intervention
- Pool locking removes tokens from direct account control, raising the bar for theft
- Nonce gating ties pool access to proof-of-work, preventing unauthorized supply
- Atomic execution ensures no intermediate state can be exploited between check and enforcement
Since the modifier runs before-and-after within a single transaction, there is no window for reentrancy or cross-transaction manipulation. The check-apply pattern is atomic at the EVM level.