Skip to content

Creating a Trap 🌱

The section below outlines the anatomy of a Trap and how to deploy a Trap to the Drosera Network.

Trap Anatomy

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
struct EventLog {
    // The topics of the log, including the signature, if any.
    bytes32[] topics;
    // The raw data of the log.
    bytes data;
    // The address of the log's emitter.
    address emitter;
}
 
struct EventFilter {
    // The address of the contract to filter logs from.
    address contractAddress;
    // The topics to filter logs by.
    string signature;
}
 
abstract contract Trap {
    EventLog[] private eventLogs;
 
    function collect() external view virtual returns (bytes memory);
 
    function shouldRespond(
        bytes[] calldata data
    ) external pure virtual returns (bool, bytes memory);
 
    function eventLogFilters() public view virtual returns (EventFilter[] memory) {
        EventFilter[] memory filters = new EventFilter[](0);
        return filters;
    }
 
    function version() public pure returns (string memory) {
        return "2.0";
    }
 
    function setEventLogs(EventLog[] calldata logs) public {
       EventLog[] storage storageArray = eventLogs;
      
        // Set new logs
        for (uint256 i = 0; i < logs.length; i++) {
            storageArray.push(EventLog({
                emitter: logs[i].emitter,
                topics: logs[i].topics,
                data: logs[i].data
            }));
        }
    }
 
    function getEventLogs() public view returns (EventLog[] memory) {
        EventLog[] storage storageArray = eventLogs;
        EventLog[] memory logs = new EventLog[](storageArray.length);
 
        for (uint256 i = 0; i < storageArray.length; i++) {
            logs[i] = EventLog({
                emitter: storageArray[i].emitter,
                topics: storageArray[i].topics,
                data: storageArray[i].data
            });
        }
        return logs;
    }
}

Source code for the abstract Trap contract can be found here.

Templates for common monitoring behavior can be found here.

Collect Function

The collect function is responsible for gathering data from the blockchain and returning it in a standardized format. This function is called by Operators on every new block and the output is stored off-chain on the Operator node.

Example:

function collect() external view returns (bytes memory) {
    uint256 totalSupply = IERC20(0x1234).totalSupply();
    return abi.encode(totalSupply);
}

Should Respond Function

The shouldRespond function is responsible for validating the data returned by the collect function. This function is called by the Operator on every new block and is used to determine whether or not the Trap response should be executed on-chain. The collect function is called first followed by the shouldRespond function.

The shouldRespond function takes an array of bytes as an argument. The Operator will call the shouldRespond function with the previous data returned by the collect function.

The outputs are ordered from newest to oldest. The last element in the array is the oldest block of data returned by the collect function. The first element in the array is the most recent block of data returned by the collect function.

The shouldRespond function must return a tuple (bool, bytes memory). If the tuple contains true then the Trap response will be executed. If the tuple contains false then the Trap response will not be executed by the Operators. The second element of the tuple is passed as an argument to the defined response function.

Example:

function shouldRespond(
    bytes[] calldata data
) external override pure returns (bool, bytes memory) {
    // Grab the total supply from the first element in the array (newest block)
    uint256 totalSupply = abi.decode(data[0], (uint256));
    if (totalSupply < 1000000) {
        return (true, abi.encode(totalSupply));
    }
    return (false, abi.encode(""));
}

Set Event Logs Function

The setEventLogs function is responsible for setting the event logs emitted in a block in the Trap. This function is called by the Operator on every new block and is used to store the event logs and be made available to the collect function. This function is designated to be used by the off-chain operator node and is not intended to be called by the Trap developer.

Get Event Logs Function

The getEventLogs function is responsible for returning the event logs emitted in a block in the Trap. This function can be called in the collect function to retrieve the event logs stored in the Trap.

Example:

function collect() external view override returns (bytes memory) {
    EventLog[] memory logs = getEventLogs();
    EventFilter[] memory filters = eventLogFilters();
 
    uint256 totalTransferAmount = 0;
    for (uint256 i = 0; i < logs.length; i++) {
        EventLog memory log = logs[i];
 
        // Check if the log matches the filter for Transfer events by contract address and signature
        if (filters[0].matches(log)) {
            (,, uint256 amount) = parseTransferEvent(log);
            totalTransferAmount += amount;
        }
 
        // Check if the log matches the filter for Transfer events by signature
        if (filters[0].matches_signature(log)) {
            (,, uint256 amount) = parseTransferEvent(log);
            totalTransferAmount += amount;
        }
    }
 
    CollectOutput memory output = CollectOutput({
        totalTransferAmount: totalTransferAmount
    });
 
    return abi.encode(output);
}

Event Log Filters Function

The eventLogFilters function is responsible for returning an array of event filters that the Trap wants to evaluate. The event filters are used to match against event logs emitted in a block. This is purely a convenience function for the Trap developer to define the event filters they want to evaluate.

Full and partial event filter matching is supported. For example, if the Trap wants to match against all Transfer events, it can return the following:

Example:

function eventLogFilters() public pure returns (EventFilter[] memory) {
    EventFilter[] memory filters = new EventFilter[](1);
    filters[0] = EventFilter({
        contractAddress: address(0x0),
        // "Transfer(address indexed from, address indexed to, uint256 value)"
        signature: "Transfer(address,address,uint256)"
    });
}

If the Trap wants to match against all Transfer events from the 0x1234 contract, it can return the following:

function eventLogFilters() public pure returns (EventFilter[] memory) {
    EventFilter[] memory filters = new EventFilter[](1);
    filters[0] = EventFilter({
        contractAddress: address(0x1234),
        // "Transfer(address indexed from, address indexed to, uint256 value)"
        signature: "Transfer(address,address,uint256)"
    });
}

Version Function

The version function is responsible for returning the version of the Trap. This function is used to identify the version of the Trap and is used by the Operator nodes to determine how to execute the Trap.

Deploying a Trap

Once you have created and tested your Trap, you can deploy it to the Drosera Network. Run the drosera apply command to deploy your Trap to the network.

drosera apply

A successful Trap creation will output the following:

1. Created Trap Config for basic_trap: (Gas Used: 1,678,320)
  - address: 0x7ab4C4804197531f7ed6A6bc0f0781f706ff7953
  - block: 321

The address represents the address of the Trap Config contract on the blockchain. The address will be used to interact with the Trap Config contract in the future.

The Trap Config is used to store the hash of the Trap bytecode and the configuration of the Trap. The Trap Config is used by the Operators to determine which Traps to opt into and execute. The hash of the Trap bytecode is used to verify the Trap bytecode has not been tampered with and to request it from the configured Drosera RPC node.

The response contract address and function signature are also stored in the Trap Config. Once a Trap indicates a response should be triggered, the Operators will submit a claim on-chain which will subsequently trigger the response function.

Once the Trap Config has been created, the Trap bytecode will be sent to the configured Drosera RPC node. The Drosera RPC node will store the Trap bytecode and provide it to Operators when they opt into the Trap. The hash of the Trap bytecode is verified against the hash stored in the Trap Config to ensure the bytecode has not been tampered with.