Skip to content

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:

Δ=balancenewbalanceold

If Δ0 (no net increase), no further action is taken. If Δ>0, the system decomposes it:

min_balance=min(amount,Δ)rip_balance=Δmin_balance

Here:

  • min_balance — the "net" supply: the legitimate portion limited by the claimed amount
  • rip_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:

Δrip_balance>MIN_NET2RIP

If the ratio is below this threshold, the transaction reverts with InsufficientAmount. This means:

  • For every 1 token burned, at most Δ100 tokens can be retained by the account
  • Equivalently, the account must retain at least 100× 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 (Δ=11,000), then rip_balance=10,000 and 11,00010,000=1.1100, triggering a revert.

Excess Burning

When rip_balance > 0 and the MIN_NET2RIP check passes, the excess tokens are burned via ERC20Burnable.burnFrom():

solidity
_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:

solidity
if (_token.totalSupply() > _free_supply) {
    revert Capped(_free_supply);
}

The free-supply cap is set at construction time:

  • MoeTreasury: 43,830 × 10^18 APOW (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:

solidity
_supply(account, min_balance, nonce);

The pool locking is implemented via a low-level pool.call():

solidity
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:

  1. Encodes the function selector and arguments via abi.encodeWithSelector
  2. Calls the pool contract with raw bytes
  3. 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:

OperationApproximate Gas
balanceOf (before)~2,600 (warm)
balanceOf (after)~100 (warm, same slot)
Branch evaluation (if Δ>0)~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():

solidity
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):

solidity
function _migrateFrom(address account, uint256 amount, uint256[] memory index)
    internal override nonReentrant banq(account, newUnits(amount, index[0]), 0)
    returns (uint256) { ... }

Error Conditions

ErrorCondition
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:

  1. Balance differencing catches any unexpected token accrual regardless of source
  2. Ratio enforcement prevents large-scale attacks masked by small legitimate operations
  3. Automatic burning removes illicit supply without requiring governance intervention
  4. Pool locking removes tokens from direct account control, raising the bar for theft
  5. Nonce gating ties pool access to proof-of-work, preventing unauthorized supply
  6. 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.