Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cowprotocol/solver-rewards/llms.txt

Use this file to discover all available pages before exploring further.

An overdraft occurs when a solver’s net payout for an accounting period is negative — meaning the solver’s token deficit (negative slippage) is larger than all rewards combined. Rather than ignoring this, the pipeline records the debt on-chain in a dedicated overdraft management contract.

What constitutes an overdraft?

The overdraft condition is checked via RewardAndPenaltyDatum.is_overdraft():
# src/fetch/payouts.py
def is_overdraft(self) -> bool:
    """
    True if the solver's complete combined data results in a net negative
    """
    return self.total_outgoing_eth() < 0
And total_outgoing_eth() is:
def total_outgoing_eth(self) -> int:
    """Total outgoing amount (including slippage) for the payout."""
    return self.total_eth_reward() + self.slippage_eth
So an overdraft occurs when:
total_eth_reward + slippage_eth < 0
In practice this means the solver experienced a token loss during the period (e.g., due to adverse AMM price movement or MEV) that was large enough to wipe out all batch rewards and produce a net negative balance.

The Overdraft dataclass

Overdraft data is represented by a simple dataclass defined in src/models/overdraft.py:
@dataclass
class Overdraft:
    """
    Contains the data for a solver's overdraft;
    Namely, overdraft = |transfer - negative slippage| when the difference is negative
    """

    period: AccountingPeriod
    account: Address
    name: str
    wei: int

    @property
    def eth(self) -> float:
        """Returns amount in units"""
        return self.wei / 10**18
FieldTypeDescription
periodAccountingPeriodThe accounting period in which the overdraft occurred
accountAddressThe solver’s submission address
namestrHuman-readable solver name
weiintOverdraft amount in wei (always positive; represents the absolute value of the negative payout)
wei is always a positive integer. It is the absolute value of total_outgoing_eth(). The sign is implicit — an Overdraft object always represents a debt owed by the solver.

String representation

Overdraft.__str__() produces a human-readable log line:
def __str__(self) -> str:
    return (
        f"Overdraft(solver={self.account} ({self.name}),"
        f"period={self.period},owed={self.eth} native token units)"
    )
Example output:
Overdraft(solver=0xabc...def (SolverName),period=2024-01-09-to-2024-01-16,owed=0.042 native token units)

How overdrafts are detected and excluded

In prepare_payouts(), solvers are iterated and each is checked for the overdraft condition:
# src/fetch/payouts.py
for _, payment in solver_payouts.iterrows():
    payout_datum = RewardAndPenaltyDatum.from_series(payment)
    if payout_datum.is_overdraft():
        overdraft = Overdraft(
            period=period,
            account=payout_datum.solver,
            name=payout_datum.solver_name,
            wei=-int(payout_datum.total_outgoing_eth()),
        )
        print(f"Solver Overdraft! {overdraft}")
        overdrafts.append(overdraft)
    transfers += payout_datum.as_payouts()
Note that as_payouts() is still called on an overdraft solver. However, inside as_payouts(), the overdraft check causes an early return:
def as_payouts(self) -> list[Transfer]:
    quote_reward_cow = int(self.reward_scaling() * self.quote_reward_cow)
    result = []
    if quote_reward_cow > 0:
        result.append(Transfer(...))  # Quote reward is still paid
    if self.is_overdraft():
        return result  # Early return — no primary reward or reimbursement transfers
    ...
This means:
  • The quote reward (if any) is still paid to the solver in COW
  • The primary batch reward and any ETH reimbursement are not transferred
  • The overdraft amount is recorded in the contract instead

The overdraft contract

All overdrafts across all networks are recorded in a single shared contract at:
0x8Fd67Ea651329fD142D7Cfd8e90406F133F26E8a
This address is the same for every supported network and is configured in OverdraftConfig:
# src/config.py
@dataclass(frozen=True)
class OverdraftConfig:
    """Configuration for the overdrafts management contract."""
    contract_address: Address

    @staticmethod
    def from_network(network: Network) -> OverdraftConfig:
        match network:
            case (
                Network.MAINNET
                | Network.GNOSIS
                | Network.ARBITRUM_ONE
                | Network.BASE
                | Network.AVALANCHE
                | Network.POLYGON
                | Network.BNB
                | Network.LINEA
                | Network.PLASMA
                | Network.INK
            ):
                contract_address = Address("0x8Fd67Ea651329fD142D7Cfd8e90406F133F26E8a")

The addOverdraft call

Each Overdraft is converted to a multisend transaction via as_multisend_tx():
# src/models/overdraft.py
def as_multisend_tx(self) -> MultiSendTx:
    """Converts Overdraft into encoded MultiSendTx bytes"""
    network = Network(os.getenv("NETWORK"))
    config = OverdraftConfig.from_network(network)
    contract_address = Web3.to_checksum_address(config.contract_address.address)
    return MultiSendTx(
        operation=MultiSendOperation.CALL,
        to=contract_address,
        value=0,
        data=OVERDRAFTS_CONTRACT.encode_abi(
            abi_element_identifier="addOverdraft",
            args=[Web3.to_checksum_address(self.account.address), self.wei],
        ),
    )
This encodes a call to addOverdraft(address solver, uint256 amount) on the overdraft management contract, registering the debt against the solver’s address on-chain.

The separate overdraft transaction

At payout time, three separate multisend transactions are posted to the Gnosis Safe queue:
1

COW transfers

A multisend on Ethereum mainnet from payment_safe_address_cow containing all COW token reward transfers for the period.
2

Native token transfers

A multisend on the target chain from payment_safe_address_native containing all native-token reimbursements and partner fee transfers.
3

Overdraft recording

A separate multisend on the target chain from payment_safe_address_native containing one addOverdraft call per overdraft solver.
# src/fetch/transfer_file.py
ovedrafts_txs = [ov.as_multisend_tx() for ov in overdrafts]

nonce_overdrafts = post_multisend(
    safe_address=config.payment_config.payment_safe_address_native,
    transactions=ovedrafts_txs,
    network=config.payment_config.network,
    signing_key=signing_key,
    client=client,
    nonce_modifier=...,
)
The Slack notification sent to #dev-multisig after auto-proposal includes all three transaction nonces and Safe queue links.

When to pause a payout

The README states: “it might happen that the slippage of a solver is bigger than the ETH payout. In this case, please do not proceed with the payout, until the root cause is known.”If you observe an unusually large overdraft, do not post any of the three transactions. Reach out to the project maintainers to investigate whether the root cause is:
  • A data pipeline error (incorrect price data, missing transfers)
  • Legitimate adverse execution that should be accounted for
  • A Dune query bug producing incorrect slippage values
Overdrafts appear in the pipeline log under the OVERDRAFT category and are also printed to stdout with the Solver Overdraft! prefix, making them easy to spot before any transaction is posted.

Build docs developers (and LLMs) love