EVHEthereum
Virtual Hook
SYNCINGBLOCK EPOCH PLUGINS ASSIGNED POOLS LAST DISPATCH HYPERVISOR
Documentation · 02 / 11

The plugin ABI

Two functions are the whole guest interface. Anything that implements them will run.

interface IPlugin {
    /// Self-description, read at registration. Must be view/pure.
    function spec() external view returns (PluginSpec memory);

    /// Called once per assigned phase per pool operation, under a hard
    /// fuel cap, by the PluginRuntime. Reverts are captured, never
    /// propagated to the pool. May return a Verdict.FEE_BID.
    function run(Invocation calldata inv) external returns (bytes memory out);
}

struct PluginSpec {
    string label;
    string semver;
    uint16 phaseMask;   // bit i => Phase(i)
    uint32 fuelCap;     // gas per frame
    uint8  permits;     // Permits bitmask
}

struct Invocation {
    bytes32 poolId;   bytes32 pluginId;  address origin;  address hypervisor;
    uint8   phase;    uint16  rank;      uint32  fuelCap;
    uint64  blockNumber;      uint64  timestamp;
    int24   tick;     uint160 sqrtPriceX96;  uint128 liquidity;  uint24 lpFee;
    bytes   data;     // Envelopes.Swap | Envelopes.Liquidity | Envelopes.Genesis
}

Reference: SwapCounterPlugin

PluginBase refuses calls that do not come from the runtime, decodes envelopes, and wraps the return conventions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.37;

import {PluginBase} from "evh/plugins/PluginBase.sol";
import {PluginSpec} from "evh/types/PluginTypes.sol";
import {Invocation} from "evh/types/Invocation.sol";
import {Phase} from "evh/types/Phase.sol";
import {Permits} from "evh/libraries/Permits.sol";
import {PhaseMask} from "evh/libraries/PhaseMask.sol";

/// The smallest plugin that does something visible: it counts swaps
/// and mirrors the count into PluginMemory.
contract SwapCounterPlugin is PluginBase {
    bytes32 internal constant KEY_SWAPS = keccak256("swapcounter.swaps");

    uint256 public swaps;
    mapping(bytes32 => uint256) public swapsIn;

    event SwapCounted(bytes32 indexed poolId, address indexed origin, uint256 total);

    constructor(address runtime_, address pluginMemory_)
        PluginBase(runtime_, pluginMemory_) {}

    function spec() external pure override returns (PluginSpec memory) {
        return PluginSpec({
            label: "SwapCounter",
            semver: "1.0",
            phaseMask: PhaseMask.bit(Phase.PostSwap),
            fuelCap: 120_000,
            permits: Permits.READ_STATE | Permits.WRITE_MEMORY | Permits.EMIT_LOGS
        });
    }

    function _onRun(Invocation calldata inv) internal override returns (bytes memory) {
        unchecked {
            swaps += 1;
            swapsIn[inv.poolId] += 1;
        }
        // A fresh namespace pays cold SSTORE prices on its first write:
        // the fuel cap must cover the cold start, not the steady state.
        _store(KEY_SWAPS, bytes32(swaps));
        emit SwapCounted(inv.poolId, inv.origin, swaps);
        return abi.encode(swaps);
    }
}

Bidding a fee

A plugin holding SET_FEE returns abi.encode(Verdict.FEE_BID, feePips) from pre-swap. The last bidder in rank order wins, and the hypervisor clamps the bid to the pool's band.

function _onRun(Invocation calldata inv) internal override returns (bytes memory) {
    Envelopes.Swap memory s = _swapEnvelope(inv);
    // Charge more for large exact-input swaps. The hypervisor clamps
    // whatever you bid to the pool's [feeFloor, feeCeiling] band.
    uint24 fee = s.amountSpecified < -1e18 ? 10_000 : 3_000;
    return _bidFee(fee);
}