Skip to main content

Economy

Overview​

Payment works the same way on both sides of the network. On Reactive, the system contract charges reactive contracts for executing react(). On a destination chain, the callback proxy charges callback contracts for delivering callbacks. Both inherit the same accounting logic, so the concepts and method names below apply to either side. Contracts must stay funded on both sides to remain active.

Balances​

Three separate numbers govern whether a contract can execute:

ValueHeld byMeaning
BalanceThe contract itselfFunds the contract can spend
ReservesSystem contract or callback proxyPre-paid credit for fees
DebtSystem contract or callback proxyFees owed but not yet paid

Charges consume reserves before debt accrues, and deposits clear debt before adding reserves, so a contract never holds both at once. Its balance is separate from both: a direct transfer raises the balance without creating reserves or settling debt.

Payment System

Any Debt Blocks Execution

Execution stops whenever debt is greater than zero, not merely when a contract is underfunded. The system contract won't call react() and the callback proxy won't deliver a callback while any amount remains outstanding.

System Contract Interactions​

Every reactive transaction runs through the system contract's trigger() method, and the ordering of its six steps explains most of the behaviour described on this page.

Reactive Tx Lifecycle

The debt check comes first, before any gas is spent, which is why an indebted contract goes quiet rather than failing loudly. And the charge comes after execution, calculated from gas actually consumed, which is why a reverting react() is still billed: the system contract catches the revert, logs it, and proceeds to step 5 regardless.

The kickback in step 6 goes to tx.origin, the validator that posted the transaction. Fees don't accumulate in the system contract; they pay whoever did the work of delivering the notification.

Fee Model​

Reactive transactions and callbacks share one pricing formula. The fee is charged in the same transaction as the execution it pays for, and goes to that transaction's originator: the validator that posted the reactive transaction or delivered the callback.

fee=pbase⋅C⋅(gused+K)fee = p_{base} ⋅ C ⋅ (g_{used} + K)

Where:

  • pbasep_{base} — the greater of tx.gasprice and block.basefee
  • CC — pricing coefficient, read from _gasPriceCoeffPer1000 and divided by 1000
  • gusedg_{used} — gas consumed during execution
  • KK — fixed gas surcharge, read from _extraGas

On Reactive, the system contract initializes the coefficient to 1000, the surcharge to 100,000 gas, and the payment gas budget to 50,000. The owner can change all three, so query them rather than rely on the defaults:

cast call $SYSTEM_CONTRACT_ADDR "_gasPriceCoeffPer1000()" --rpc-url $REACTIVE_RPC | cast to-dec
cast call $SYSTEM_CONTRACT_ADDR "_extraGas()" --rpc-url $REACTIVE_RPC | cast to-dec
cast call $SYSTEM_CONTRACT_ADDR "_maxChargeGas()" --rpc-url $REACTIVE_RPC | cast to-dec

Each callback proxy is configured at deployment and has no defaults, so values vary by destination chain and must be read from the proxy itself:

cast call $CALLBACK_PROXY_ADDR "_gasPriceCoeffPer1000()" --rpc-url $DESTINATION_RPC | cast to-dec
cast call $CALLBACK_PROXY_ADDR "_extraGas()" --rpc-url $DESTINATION_RPC | cast to-dec
cast call $CALLBACK_PROXY_ADDR "_maxChargeGas()" --rpc-url $DESTINATION_RPC | cast to-dec

_maxChargeGas doesn't enter the fee itself; it caps the gas available for settling payment, covered under Gas Limits.

Failed Execution Is Still Charged

A reverting react() is caught, logged as a ReactiveContractReverted event, and charged in full. A failing callback is logged as a CallbackFailure event and charged in full. Neither is refunded, so a contract that reverts on every notification will still exhaust its funds. The sole exception is a callback addressed to an account holding no code, which is rejected before any charge applies.

Gas Limits​

Reactive Transaction Gas Cap

The maximum gas limit for reactive transactions is 900,000 units.

Callback Gas Limit

Reactive Network enforces a minimum callback gas limit of 100,000 gas. Callback requests below this threshold are ignored, as this minimum ensures sufficient gas for internal audits and computations required to process the callback.

Not All Requested Gas Reaches the Target

Before invoking a callback recipient, the callback proxy sets aside gas for its own accounting once the call returns (_extraGas) and for settling payment if the contract owes anything (_maxChargeGas). Only the remainder is forwarded, so size gas limits with both deductions in mind. Delivery fails with InsufficientGas if too little is left to cover them.

Funding Reactive Contracts​

Funds can go to one of two places. depositTo() credits reserves held by the system contract, which are spent automatically before any debt accrues. A transfer to the contract raises the contract's own balance, which the contract hands over when charged. Reserves can only ever be spent on fees, while a balance stays under the contract's control.

Deposit to Reserves via the System Contract​

depositTo() credits any contract from the caller's own funds. It settles outstanding debt first, then adds the remainder to reserves. The caller pays the transaction fee.

cast send --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY $SYSTEM_CONTRACT_ADDR "depositTo(address)" $REACTIVE_CONTRACT_ADDR --value 1ether

This requires no cooperation from the target contract, so it is also the way to revive a third-party contract that has stalled in debt.

Always Specify the Target Contract

Sending REACT directly to the system contract credits the sender's own address, not any contract. An externally owned account funding a contract this way credits an address that will never execute anything. Always pass the target through depositTo().

Direct Transfers & coverDebt()​

Transfer REACT to the contract at any time:

cast send $REACTIVE_CONTRACT_ADDR --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY --value 1ether

Funding at deployment works too, but only if the reactive contract declares a payable constructor. The base contracts don't, so --value on deployment reverts otherwise.

A balance sitting in the contract settles nothing on its own. The contract has to hand it to the system contract, and there are two ways that happens.

When a fee is charged and reserves are empty, the system contract calls pay() on the contract, asking for its entire outstanding debt. AbstractPayer implements this, and reactive contracts inherit it through AbstractReactive. The standard implementation verifies the caller, checks the balance, and transfers the amount owed. Settlement happens inside the same transaction as the charge, so the contract never goes inactive.

Gas Budget for pay()

The call to pay() is capped at _maxChargeGas. If a custom implementation exceeds that budget or the contract lacks funds, the call fails, a PaymentFailure event is emitted, and the debt remains outstanding. Keep custom payment logic minimal.

Otherwise, settlement has to be triggered by hand. AbstractPayer implements the logic as an internal _coverDebt() and leaves the decision to expose it to the contract author. To make it publicly callable, add the function to the reactive contract and make it external:

/// @notice Settles outstanding debt from this contract's own balance.
function coverDebt() external {
_coverDebt();
}

Add onlyOwner or any other access control if the operation should be restricted. A public version is useful for contracts serving third parties, since anyone can then fund the contract and restart it. Omitting the wrapper entirely is a valid choice.

Where the wrapper exists, settle like so:

cast send --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY $REACTIVE_CONTRACT_ADDR "coverDebt()"
Funds Sent to an Indebted Contract Can Be Stranded

A direct transfer neither settles debt nor creates reserves. If the contract exposes no way to pay the system contract from its balance, those funds can't be applied to the debt by anyone, and the contract stays inactive. Use depositTo() when unsure whether a contract exposes coverDebt().

Reactive Contract Status​

Reactive contract status is available on Reactscan.

  • active — contract executes normally
  • inactive — outstanding debt must be settled

Status follows debt automatically. When a contract's debt rises from zero, a BlacklistContract event is emitted and the network stops serving it. When a deposit or payment returns the debt to zero, a WhitelistContract event is emitted and service resumes. A contract that settles through pay() in the same transaction as the charge is never blacklisted at all.

Funding Callback Contracts​

The same two routes apply against the callback proxy, and AbstractCallback inherits AbstractPayer, so pay() and _coverDebt() behave exactly as described above. A callback contract in debt stops receiving callbacks until it's cleared.

Callback Payment

Callback contracts are funded in the destination chain's native token, ETH on Ethereum, BNB on BNB Chain, and so on, not in REACT.

Deposit through the callback proxy:

cast send --rpc-url $DESTINATION_RPC --private-key $DESTINATION_PRIVATE_KEY $CALLBACK_PROXY_ADDR "depositTo(address)" $CALLBACK_CONTRACT_ADDR --value 0.1ether

Or fund the contract directly and settle, if it exposes a coverDebt() wrapper:

cast send $CALLBACK_CONTRACT_ADDR --rpc-url $DESTINATION_RPC --private-key $DESTINATION_PRIVATE_KEY --value 0.1ether
cast send --rpc-url $DESTINATION_RPC --private-key $DESTINATION_PRIVATE_KEY $CALLBACK_CONTRACT_ADDR "coverDebt()"

Querying Balances​

The same three reads apply on both sides of the network: use the system contract over REACTIVE_RPC for reactive contracts, and the callback proxy over DESTINATION_RPC for callback contracts.

Balance​

Retrieve the balance of a reactive contract on Reactive:

cast balance $REACTIVE_CONTRACT_ADDR --rpc-url $REACTIVE_RPC

Retrieve the balance of a callback contract on a destination chain:

cast balance $CALLBACK_CONTRACT_ADDR --rpc-url $DESTINATION_RPC

Debt​

Query the debt recorded by the system contract on Reactive:

cast call $SYSTEM_CONTRACT_ADDR "debt(address)" $REACTIVE_CONTRACT_ADDR --rpc-url $REACTIVE_RPC | cast to-dec

Query the debt recorded by the callback proxy on a destination chain:

cast call $CALLBACK_PROXY_ADDR "debt(address)" $CALLBACK_CONTRACT_ADDR --rpc-url $DESTINATION_RPC | cast to-dec
Aliases

debts(address) returns the same value and is kept for compatibility with earlier tooling. New integrations should use debt(address), which is part of the IPayable interface.

Reserves​

Retrieve reserves held by the system contract on Reactive:

cast call $SYSTEM_CONTRACT_ADDR "reserves(address)" $REACTIVE_CONTRACT_ADDR --rpc-url $REACTIVE_RPC | cast to-dec

Retrieve reserves held by the callback proxy on a destination chain:

cast call $CALLBACK_PROXY_ADDR "reserves(address)" $CALLBACK_CONTRACT_ADDR --rpc-url $DESTINATION_RPC | cast to-dec

Service Provider​

Unlike the reads above, this one is called on the payer itself and confirms which contract it owes fees to.

On Reactive:

cast call $REACTIVE_CONTRACT_ADDR "_SERVICE_PROVIDER()" --rpc-url $REACTIVE_RPC

On a destination chain:

cast call $CALLBACK_CONTRACT_ADDR "_SERVICE_PROVIDER()" --rpc-url $DESTINATION_RPC

This returns the system contract for reactive contracts and the callback proxy for callback contracts.

Inspecting Callbacks​

The system contract records every callback posted to a destination chain, indexed by the hash of the reactive transaction that requested it. Because a single reactive transaction can request several callbacks, the call returns an array:

cast call $SYSTEM_CONTRACT_ADDR "getCallbacks(uint256)((uint256,address,uint256,uint256,uint256,bytes)[])" $REACTIVE_TX_HASH --rpc-url $REACTIVE_RPC

Each record carries the block number, the requesting reactive contract, the callback's index within the transaction, the destination chain ID, the destination transaction hash, and any error data returned by the destination contract. err is empty when the callback succeeded.

Validators write these records after delivery, so an empty array means no callback has been recorded yet rather than that one failed.