Skip to content

Boop Contracts Documentation

This documents the functionality of the Boop stack contracts. The contracts' source code is extremely well documented & readable, and should be considered as the canonical specification for their behaviour. This page extracts the essential information in a single place for convenience, and adds a few system-level notes.

Please checkout the Contracts Overview of the Architecture page for background information. We reproduce the diagram from that section below for context.

Boop Structure

This structure represents a boop (a smart transaction in the Boop stack). The following excerpt is copied from the Types.sol file.

struct Boop {
    // Account initiating the boop
    address account;
 
    // Destination address for the call carried by the boop
    address dest;
 
    // Fee payer. This can be:
    //   1. the account (if it's a self-paying transaction)
    //   2. an external paymaster contract (implementing {IPaymaster})
    //   3. 0x0...0: payment by a sponsoring submitter
    address payer;
 
    // Amount of gas tokens (in wei) to transfer to {dest}
    uint256 value;
 
    // Nonces are ordered within tracks;
    // there is no ordering constraintacross tracks
    uint192 nonceTrack;
 
    // Nonce sequence number within the nonce track
    uint64 nonceValue;
 
    // Maximum fee per gas unit paid by the payer
    uint256 maxFeePerGas;
 
    // Flat fee in gas token wei for the submitter
    //   - The submitter requests this on top of gas payment. This can be
    //     use to cover extra costs (e.g., DA costs on rollups, server
    //     costs), or as profit.
    //   - Acts as a rebate when negative (e.g., to refund part of the
    //     intrinsic transaction cost if the submitter batches multiple
    //     boops together). In no case does this lead to the submitter
    //     transferring funds to accounts.
    int256 submitterFee;
 
    // Global gas limit (maximum gas the account or paymaster will pay for)
    uint32 gasLimit;
 
    // Gas limit for {interfaces/IAccount.validate}
    uint32 validateGasLimit;
 
    // Gas limit for {interfaces/IPaymaster.validatePayment}
    uint32 validatePaymentGasLimit;
 
    // Gas limit for {interfaces/IAccount.execute}
    uint32 executeGasLimit;
 
    // Call data for the call carried by the boop
    bytes callData;
 
    // Extra data for validation (e.g., signatures)
    bytes validatorData;
 
    // Extra dictionary-structured data for extensions
    bytes extraData;
}

Encoding

We typically expect that the operation carried out by an account is a call or send to another address, and the fields in the Boop structure (dest, value, callData) reflect this — this is however not mandatory, it's up to the account implementation how to interpret these values. We might refer to "the call carried (or made) by the boop" in the rest of the documentation.

To save on calldata costs, boops are packed tightly when submitted to the chain. The first thing the EntryPoint contract does is unpack them into the struct shown above. You can read about the encoding logic in the Encoding.sol file, which you can vendor if you need to encode/decode boops on the chain yourself. Our client SDK expose decodeBoop and encodeBoop functions that do the same from JavaScript or TypeScript.

The extraData field encodes a key-value dictionary as a series of tightly-packed [key, length, data] triplets, where the key and the length are 3 bytes, while the data has length size. You can use getExtraData from Utils.sol to read from the dictionary on the contract size. Our client SDK exposes an encodeExtraData function to create extra data from JavaScript or TypeScript.

Boop Hash

Just like EVM transactions, boops are identified by their hashes. The boop hash is straightforwardly computed as the hash of its packed encoding with the chain ID appended. However, for boops whose fees are not paid by the account itself, the fee amounts and gas limits are zeroed before computing the hash. The validatorData field is set to zero-length before computing the hash in all cases.

Zeroing the fees and gas limits allows the submitter to set the fees for the boop (saving network roundtrips, especially in case the user would have to sign over these values before properly submitting), and potentially change them as network conditions evolve, while the boop preserves a stale identity. Emptying validatorData enables it to carry a signature over the boop hash to be verified, or even short-lived authorization data that could change with retries. If validation needs access to extra data that must be part of the boop's identity, it can be included in the extraData dictionary.

The Boop client SDK exposes a computeBoopHash function to compute a Boop's hash. You can see how this computation is done on the contract side in the validate function of our HappyAccount account implementation.

EntryPoint Contract

Any boop being sent must transit via the submit function of the EntryPoint contract. Its code can be found here.

Note that submit can be called at the top-level of an EVM transaction, but it can also be called by other smart accounts! This can be notably used to enable boop batching across multiple accounts (though note that once a boop is public, anybody can resubmit it independently, and there is no atomicity guarantees in any case).

At a high level, this function performs the following tasks:

  1. It validates the gas price, checks the paymaster's staking balance (if a paymaster is specified), validates and updates the account nonce.

  2. It calls the account to validate the boop. (IAccount.validate, see below)

  3. It calls the paymaster to validate payment if a paymaster is specified. (IPaymaster.validatePayment, see below)

  4. It calls the account to execute the call specified by the boop. (IAccount.execute, see below). It is possible the call carried by the boop will fail — however execute itself is not allowed to fail. Even if the carried call fails, execution proceeds, fees are paid and the nonce is incremented. This mimics the behaviour of EVM transactions, and ensures that submitters are not on the hook for reverts in arbitrary external contracts.

  5. It collects payment from the paymaster or account if the transaction is not sponsored by the submitter. Payment is taken from the paymaster's stake or solicited from the account by calling IAccount.payout.

The EntryPoint has special provisions for simulation. If simulated with an EVM transaction sender (tx.origin) with the zero address, the submit function will enter simulation mode, omit certain checks (gas price & nonce) and will not use the provided gas limits. It will also return data that includes useful information (for instance, if the nonce validation failed because of a future nonce, as well as estimated gas limits). This enables us to get all the information we need to validate a boop and "fill in" missing values like gas limits in a single call to the blockchain node.

Finally, the EntryPoint manages the deposits of paymasters, which are used to pay back submitters. This is implemented in Staking.sol.

The BoopSubmitted Event

If a boop is successfully included onchain (nonce incremented), a BoopSubmitted event is emitted from the EVM transaction that carries it. Its topics include all the fields in the boop, and it can be used for indexing boops.

It also conveniently shows up at the top of the logs list on block explorers, and is a great way to get some level of observability for boops without needing any custom block explorer support.

Account Contract

The user can use any account implementation of their choice that implement the IAccount or IExtensibleAccount interface. We provide our own HappyAccount compliant implementation — note that despite the name, the implementation is not HappyChain-specific.

Here are the core functions the account must implement, refer to interface for more details, including support for EIP-1271 contract signatures

  • validate(Boop) — validates whether the boop is authorized by the account, returning a success code or an error code depending. A common way to validate validity is to verify a EOA signature included in the boop's validationData field. Because this signature must in some circumstances sign over the gas values, it would sometimes fail during simulation. In those cases, validate can return UnknownDuringSimulation in simulation mode to let the EntryPoint proceed with execution.

  • execute(Boop) — This executes the operation specified by the boop. The function returns a structure indicating the outcome: whether the operation succeeded, reverted, or whether the account rejected the operation (typically because of malformed data). Since the function is not allowed to revert, it must catch reverts if calling external contracts that may revert. As indicated earlier, even if the call fails, fees will be paid and the nonce will be incremented.

  • payout(amount) — This functions pays the entrypoint the given amount in wei — it's implementation should strictly be payable(tx.origin).call{value: amount}("");.

These functions share a few more things in common:

  • They can only be called by the EntryPoint, but are not allowed to otherwise revert.

  • They should consume roughly the same amount of gas during simulation as they do during actual onchain execution (as the gas limits are estimated from the simulation run).

To support account extensions, accounts should implement IExtensibleAccount which extends IAccount with extension-related functions. See the Extensions page for more information.

Paymaster Contract

Paymasters are contracts that can pay transaction fees to the submitter (which fronts them) on behalf of accounts. Any account can use paymasters, which are specified in the payer field of the Boop structure.

Paymaster contracts must implement the IPaymaster interface.

We provide an example implementation, the HappyPaymaster. This implementation is relatively specific to the HappyChain use case giving every user a 24-hours budget that streams back over time.

To preserve Boop's good latency properties, we advise against using the "signing paymasters" that are popular with ERC-4337 account abstraction. Those have you incur an network roundtrip to collect a signature from a paymaster EOA which is then verified by the paymaster contract. Instead, we recommend to register your users in your paymaster contracts and perform all your checks onchain — at least on chains where gas costs allow it.

A paymaster only needs to implement a single method — validatePayment(Boop). Its role is to ensure that the paymaster does authorize payment of the fees of the given boop. The interface of this function is identical to IAccount.validate, so refer to the account section or the source code for more information.

Bundling

Boop does not build in support for bundling multiple boops from different account. However this can be trivially achieved at the contract level — for instance via a multicall — since a smart contract can call into the EntryPoint and every boop is given its own gas limit. It is a more involved feature at the submitter level. This is currently not a priority feature, but reach out at hello@happy.tech if interested.

Multiple boops from a single account can be bundled via the BatchCallExecutor extension — however this is something initiated by the user — the submitter requires no awareness of it.