The pipeline batches all solver payment transfers into Gnosis Safe multisend transactions so that the entire week’s payouts can be authorized with a single Safe signature round. TheDocumentation 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.
post_multisend() function in src/multisend.py handles encoding and submission; Transfer.as_multisend_tx() in src/models/transfer.py handles per-transfer encoding.
Why multisend
A typical payout run involves tens to hundreds of individual transfers — COW tokens to each solver on mainnet, native tokens to each solver on the target chain, and overdraft state updates. Submitting each transfer as a standalone Safe transaction would require a separate round of co-signer approvals for every one. Multisend bundles them all into a singleDELEGATE_CALL to the Safe’s canonical multisend contract (0x40A2aCCbd92BCA938b02010E17A5b8929b49130D), reducing the approval burden to a fixed three transactions per payout run.
Three transactions per payout run
Each run ofauto_propose() in src/fetch/transfer_file.py posts exactly three multisend transactions:
COW transfers
ERC-20 COW token transfers to every eligible solver. Always posted to Ethereum mainnet regardless of the target network being processed.
Native transfers
Native gas token transfers to eligible solvers on the target network (ETH, xDAI, AVAX, etc.).
Overdraft updates
Contract calls to the overdraft management contract (
0x8Fd67Ea651329fD142D7Cfd8e90406F133F26E8a) on the target network, recording any solvers that received less than their owed amount.Transaction posting flow
Encode individual transfers
Each
Transfer object calls as_multisend_tx() to produce a MultiSendTx. Native transfers send ETH directly to the recipient; ERC-20 transfers call the token contract’s transfer(address,uint256) function:src/models/transfer.py
Prepend WETH unwrap if necessary
Before encoding the COW and native transfer batches,
prepend_unwrap_if_necessary() checks whether the Safe’s native ETH balance covers the total outgoing value. If not, it prepends a WETH withdraw() call to the transaction list to unwrap enough WETH first.src/multisend.py
In production (
auto_propose()), both the COW and native batches pass skip_validation=True. This allows the transaction to be proposed to the Safe queue even when the balance is temporarily insufficient — for example, when a previous payout’s execution is still pending.Build and encode the multisend
build_encoded_multisend() passes the list of MultiSendTx objects to safe_eth’s MultiSend.build_tx_data(), which ABI-encodes them into the packed byte format that the multisend contract expects:src/multisend.py
Build the Safe transaction
post_multisend() wraps the encoded multisend bytes in a Safe multisig transaction targeting the multisend contract via DELEGATE_CALL. The Safe’s current on-chain nonce is fetched and the nonce_modifier offset is added:src/multisend.py
Sign and post to the Safe Transaction Service
The transaction is signed with the proposer’s private key (from
PROPOSER_PK) and submitted to the Safe Transaction Service via TransactionServiceApi.post_transaction(). A 2-second sleep follows each post to respect the Safe API’s rate limits.src/multisend.py
Nonce modifier system
When the pipeline runs across multiple networks in the same week, it proposes all COW-transfer transactions against the same Ethereum mainnet Safe. To allow all of them to sit in the Safe’s queue simultaneously — each with a unique nonce — thenonce_modifier shifts the Safe’s current nonce by a fixed, per-network offset.
The offsets are derived at startup from the Network enum order:
src/config.py
INK an offset of 0, and increments toward MAINNET which gets the highest offset. For example, with 10 networks (len(Network) == 10):
| Network | COW tx nonce offset | Native tx nonce offset |
|---|---|---|
MAINNET | 9 | 10 (= len(Network)) |
BASE | 8 | 0 |
ARBITRUM_ONE | 7 | 0 |
BNB | 6 | 0 |
AVALANCHE | 5 | 0 |
POLYGON | 4 | 0 |
GNOSIS | 3 | 0 |
LINEA | 2 | 0 |
PLASMA | 1 | 0 |
INK | 0 | 0 |
len(Network) (10) and the overdraft update uses len(Network) + 1 (11) to avoid colliding with the COW transfer at offset 9.
Required environment variables
| Variable | Description |
|---|---|
PROPOSER_PK | Private key of the Safe proposer account used to sign multisend transactions. |
PAYOUTS_SAFE_ADDRESS_MAINNET | Checksum address of the Ethereum mainnet Safe used for COW token transfers. |
PAYOUTS_SAFE_ADDRESS | Checksum address of the target-network Safe used for native token and overdraft transfers. |
SAFE_API_KEY | Optional API key for the Safe Transaction Service, read via os.getenv("SAFE_API_KEY"). |
NODE_URL_MAINNET | RPC endpoint for Ethereum mainnet (used for COW transfer Safe). |
NODE_URL | RPC endpoint for the target network (used for native transfer Safe). |
NONCE_MODIFIER | Optional override for the per-network nonce offset. Defaults to the computed value from nonce_modifier_dict. |
Monitoring pending transactions
After posting, the Safe queue URLs are constructed inPaymentConfig using the network’s short name:
src/config.py
