The Othentic Stack allows developers to write an AVSLogic contract, which provides pre and post execution hooks for task submission on-chain, and enables highly configurable and customized logic.
Interface
The interface defines two functions: beforeTaskSubmission and afterTaskSubmission. As their names suggest, these functions allow developers to implement custom AVS logic before and after task submission within the contract.
After implementation, you can deploy the contract on the same L2 network as the AttestationCenter.
Make sure to deploy the Othentic contracts before deploying the AVS Logic.
After deployment, you must call the setAvsLogic function on the AttestationCenter and provide the address of the AvsLogic contract as the parameter, as shown in the example below.
Note that only the AVS Governance Multisig is authorized to call this function.
Now, the AVS contracts are set and ready to be used.
Use cases
Handle Multiple Data Types
Add custom logic based on the Type of the Task data . By using type-prefixed encoding, you can specify the data type, and the AVS logic hook can efficiently decode and process it accordingly.
Key Components:
Type Identifier Prefix: First 4 bytes specify data type
Payload Structure: Remaining bytes contain the encoded data
Type-Specific Decoding: Switch logic based on data type
Usage
1
Define Supported Data Types
Define different data types you plan to use:
PRICE_TYPE: uint256
MESSAGE_TYPE: String
STATUS_TYPE: Tuple of (bool, address)
2
Encode Data with Type Prefix
Add data type identifier PRICE_TYPE at the beginning of the data field
The afterTaskSubmission function extracts the first 4 bytes to determine the data type, then decodes the payload accordingly.
// Add data type identifier at the beginning of the data field
bytes4 constant PRICE_TYPE = bytes4(keccak256("PRICE"));
bytes4 constant MESSAGE_TYPE = bytes4(keccak256("MESSAGE"));
bytes4 constant STATUS_TYPE = bytes4(keccak256("STATUS"));
function afterTaskSubmission() {
....
// Read the first 4 bytes to determine data type
bytes4 dataType = bytes4(_taskInfo.data[0:4]);
// Slice off the type identifier
bytes memory payload = _taskInfo.data[4:];
if (dataType == PRICE_TYPE) {
// Handle price update
uint256 price = abi.decode(payload, (uint256));
_updatePrice(price);
emit PriceUpdated(price);
}
else if (dataType == MESSAGE_TYPE) {
// Handle string message
string memory message = abi.decode(payload, (string));
_logMessage(message);
emit MessageLogged(message);
}
else if (dataType == STATUS_TYPE) {
// Handle boolean status with address
(bool status, address validator) = abi.decode(payload, (bool, address));
_updateValidatorStatus(validator, status);
emit ValidatorStatusUpdated(validator, status);
}
else {
revert UnsupportedDataType();
}
}
Task Management
In certain cases, it may be necessary to track and manage tasks even after their completion. For example, you can have an array of task results, allowing anyone to retrieve task history and data.
This functionality can be implemented using the AVS LogicafterTaskSubmission function to store task details on-chain, which other contracts can access via a getter function.
Random Number Generator: After completing a task, the generated random number is stored in the AvsLogic contract through afterTaskSubmission, making it available for retrieval.
EigenDA: A task is considered complete when the confirmBatch function is called. This function records batch details in a contract mapping, ensuring they are persistently stored.
Note that the function also checks the quorum and emits an event, but these steps have already been executed in the submitTask function.
Updating arbitrary contracts
Depending on your design, your AVS may follow an IFTTT (If This Then That) architecture, requiring on-chain post-execution tasks.
Usage
If the AVS functions as an Oracle service, it may fetch the latest query data. Once operators submit the final result and the task is processed, you might need to update a separate Oracle contract with the latest data.
Use AvsLogic hook to implement this by directly interacting with the contract, ensuring its state reflects the most recent task data.
Cross chain messaging
The AVS Logic hook enables seamless communication between different chains by dispatching messages after task execution. This ensures that data or task verification results can be reflected on another chain.
Usage
AVS tasks are executed off-chain, and task verification remains chain-agnostic and can take place on any blockchain. To maintain verification trails on a different chain, you can utilize the AVS Logic Hook to send a cross-chain message upon task completion.
For instance, if a task is executed on Base, the AVS Logic Hook can transmit a cross-chain message to notify the target blockchain. Additionally, if a particular chain is not natively supported by the Stack, this hook enables cross-chain messaging to ensure the task completion event is successfully relayed.
function afterTaskSubmission(
IAttestationCenter.TaskInfo calldata _taskInfo,
bool _isApproved,
bytes calldata _tpSignature,
uint256[2] calldata _taSignature,
uint256[] calldata _attestersIds
) external payable {
if (!_isApproved) return; // Only process approved tasks
// Decode the structured data
(bytes32 txUUID, uint256 agentId, uint256 timestamp, string memory status, string memory reason) =
abi.decode(_taskInfo.data, (bytes32, uint256, uint256, string, string));
// Emit event with extracted data
emit TaskDataParsed(txUUID, agentId, timestamp, status, reason);
// Encode message for LayerZero
bytes memory message = abi.encode(
string(_taskInfo.proofOfTask),
txUUID,
agentId,
timestamp,
status,
reason
);
// Estimate the gas fee for sending the message
(uint256 estimatedFee, ) = lzEndpoint.estimateFees(destinationChainId, address(this), message, false, "");
require(address(this).balance >= estimatedFee, "Insufficient funds for cross-chain message");
// Send the message via LayerZero
lzEndpoint.lzSend{value: estimatedFee}(
destinationChainId, // Target LayerZero chain ID
abi.encodePacked(policyCoordinator), // Recipient address on target chain
message, // Encoded payload
payable(msg.sender), // Refund address
address(0), // No ZRO token payment
bytes("") // Empty adapterParams (default settings)
);
emit CrosschainTaskDataSent(
block.timestamp, // Using block timestamp as message ID
string(_taskInfo.proofOfTask),
_taskInfo.data,
_taskInfo.taskDefinitionId
);
}