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);
}