Building Consumer Contracts
When your workflow writes data to the blockchain, it doesn't call your contract directly. Instead, it submits a signed report to a Chainlink KeystoneForwarder contract, which then calls your contract.
This guide explains how to build a consumer contract that can securely receive and process data from a CRE workflow.
In this guide:
- Core Concepts: The Onchain Data Flow
- The IReceiver Standard
- Using ReceiverTemplate
- Working with Simulation
- Advanced Usage
- Complete Examples
- Security Considerations
1. Core Concepts: The Onchain Data Flow
- Workflow Execution: Your workflow produces a final, signed report.
- EVM Write: The EVM capability sends this report to the Chainlink-managed
KeystoneForwardercontract. - Forwarder Validation: The
KeystoneForwardervalidates the report's signatures. - Callback to Your Contract: If the report is valid, the forwarder calls a designated function (
onReport) on your consumer contract to deliver the data.
2. The IReceiver Standard
To be a valid target for the KeystoneForwarder, your consumer contract must satisfy two main requirements:
2.1 Implement the IReceiver Interface
The KeystoneForwarder needs a standardized function to call. This is defined by the IReceiver interface, which mandates an onReport function.
undefined
metadata: Workflow identity fields for the receiver. See Metadata length and layout below.report: The raw, ABI-encoded data payload from your workflow.
Metadata length and layout
The logical packing matches abi.encodePacked(bytes32 workflowId, bytes10 workflowName, address workflowOwner), which is 62 bytes (32 + 10 + 20).
Production KeystoneForwarder does not pass exactly those 62 bytes as the metadata argument. It forwards the calldata slice rawReport[FORWARDER_METADATA_LENGTH:METADATA_LENGTH] from report, which is 64 bytes wide (METADATA_LENGTH - FORWARDER_METADATA_LENGTH). That slice is word-aligned to a 32-byte boundary: you get the same 62-byte packed workflow identity as above, plus two trailing bytes. Workflow configuration does not change this length; it is fixed by the forwarder's report layout.
Those trailing bytes are the reportId field (bytes2) at calldata indices 62 and 63. Field offsets for the full signed rawReport (including report_id at offset 139 in that buffer) are documented in KeystoneForwarder._getMetadata. If reportId is zero, those bytes read as 0x0000 in a hex dump; they still occupy the reportId field rather than arbitrary alignment filler you can drop when sizing metadata. In production delivery, metadata.length is 64. A require(metadata.length == 62) (or similar) in your own code will revert; the sample ReceiverTemplate does not enforce length 62 on metadata.
Extract reportId from metadata like this:
// metadata.length == 64 on production KeystoneForwarder delivery
bytes2 reportId = bytes2(metadata[62:64]);
The first 62 bytes remain decodable with the same layout as ReceiverTemplate._decodeMetadata (workflow ID, ten-byte workflow name, owner address). The template ignores the final two bytes for identity checks; read them only if your logic needs reportId.
Link to the Chainlink KeystoneForwarder contract: KeystoneForwarder.sol
2.2 Support ERC165 Interface Detection
ERC165 is a standard that allows contracts to publish the interfaces they support. The KeystoneForwarder uses this to check if your contract supports the IReceiver interface before sending a report.
Link to the IERC165 interface: IERC165.sol
3. Using ReceiverTemplate
3.1 Overview
While you can implement these standards manually, we provide an abstract contract, ReceiverTemplate.sol, that does the heavy lifting for you. Inheriting from it is the recommended best practice.
Key features:
- Secure by Default: Requires forwarder address at deployment, ensuring your contract is protected from the start
- Layered Security: Add optional workflow ID validation, workflow owner verification, or any combination for defense-in-depth
- Flexible Configuration: All permission settings can be updated via setter functions after deployment
- Simplified Logic: You only need to implement
_processReport(bytes calldata report)with your business logic - Built-in Access Control: Includes OpenZeppelin's
Ownablefor secure permission management - ERC165 Support: Includes the necessary
supportsInterfacefunction - Metadata Access: Helper function to decode workflow ID, name, and owner for custom validation logic
3.2 Contract Source Code
undefined
3.3 Quick Start
The simplest way to use ReceiverTemplate is to inherit from it and implement the _processReport function:
undefined
3.4 Configuring Permissions
The forwarder address is configured at deployment via the constructor and provides your first line of defense. After deploying your contract, the owner can configure additional security checks or update the forwarder address if needed.
Configuration examples:
// Example: Update forwarder address (e.g., when moving from simulation to production)
myConsumer.setForwarderAddress(0xF8344CFd5c43616a4366C34E3EEE75af79a74482); // Ethereum Sepolia KeystoneForwarder
// Example: Add workflow ID check for additional security
myConsumer.setExpectedWorkflowId(0x1234...); // Your specific workflow ID
// Example: Add workflow owner check
myConsumer.setExpectedAuthor(0xYourAddress...);
// Example: Add workflow name check (requires author validation to be set)
myConsumer.setExpectedWorkflowName("my_workflow");
// Example: Disable a check later
myConsumer.setExpectedWorkflowName(""); // Empty string disables the check
What the template handles for you:
- Validates the caller address against the configured forwarder (required at deployment)
- Validates the workflow ID (if
expectedWorkflowIdis configured) - Validates the workflow owner (if
expectedAuthoris configured) - Validates the workflow name (if both
expectedWorkflowNameANDexpectedAuthorare configured) - Implements ERC165 interface detection
- Provides access control via OpenZeppelin's
Ownable - Calls your
_processReportfunction with validated data
What you implement:
- Pass the forwarder address to the constructor during deployment
- Your business logic in
_processReport - (Optional) Configure additional permissions after deployment using setter functions
How workflow names are encoded
The workflowName field in the metadata uses the bytes10 type rather than plaintext strings. When you call setExpectedWorkflowName("my_workflow"), the ReceiverTemplate automatically encodes it using the same algorithm as the CRE engine:
- Compute SHA256 hash of the workflow name
- Convert hash to hex string (64 characters)
- Take the first 10 hex characters (e.g.,
"b76f3ae1de") - Hex-encode those 10 ASCII characters to get
bytes10(20 hex characters / 10 bytes)
Example: "my_workflow" ā SHA256 ā "b76f3ae1de..." ā hex-encode ā 0x62373666336165316465
This encoding ensures consistent, fixed-size representation regardless of the original workflow name length.
Usage:
// Set the expected author first (required)
myConsumer.setExpectedAuthor(0xYourAddress...);
// Then set the expected workflow name (only works with author validation)
myConsumer.setExpectedWorkflowName("my_workflow");
// To disable the workflow name check
myConsumer.setExpectedWorkflowName(""); // Empty string clears the stored value
4. Working with Simulation
When you run cre workflow simulate, your workflow interacts with a MockKeystoneForwarder contract that does not provide workflow metadata (workflow_name, workflow_owner).
Deploying for Simulation
When deploying your consumer contract for simulation, pass the Mock Forwarder address to the constructor:
// Deploy with MockForwarder address for Ethereum Sepolia simulation
address mockForwarder = 0x15fC6ae953E024d975e77382eEeC56A9101f9F88; // Ethereum Sepolia MockForwarder
MyConsumer myConsumer = new MyConsumer(mockForwarder);
Find Mock Forwarder addresses for all networks in the Forwarder Directory page.
Metadata-based validation
Do not configure these validation checks during simulation - they require metadata that MockKeystoneForwarder doesn't provide:
setExpectedWorkflowId()setExpectedAuthor()setExpectedWorkflowName()
Setting any of these will cause your simulation to fail.
Transitioning to Production
Once you're ready to deploy your workflow to production:
Option 1: Deploy a new contract instance
// Deploy with production KeystoneForwarder address
address keystoneForwarder = 0xF8344CFd5c43616a4366C34E3EEE75af79a74482; // Ethereum Sepolia
MyConsumer myConsumer = new MyConsumer(keystoneForwarder);
// Configure additional security checks
myConsumer.setExpectedWorkflowId(0xYourWorkflowId);
Option 2: Update existing contract's forwarder
// Update forwarder to production KeystoneForwarder
myConsumer.setForwarderAddress(0xF8344CFd5c43616a4366C34E3EEE75af79a74482); // Ethereum Sepolia
// Add metadata-based validation
myConsumer.setExpectedWorkflowId(0xYourWorkflowId);
See Configuring Permissions for complete details.
5. Advanced Usage (Optional)
5.1 Custom Validation Logic
You can override onReport to add your own validation logic before or after the standard checks:
import { ReceiverTemplate } from "./ReceiverTemplate.sol";
contract AdvancedConsumer is ReceiverTemplate {
uint256 private s_minReportInterval = 1 hours;
uint256 private s_lastReportTime;
error ReportTooFrequent(uint256 timeSinceLastReport, uint256 minInterval);
event MinReportIntervalUpdated(uint256 previousInterval, uint256 newInterval);
constructor(address _forwarderAddress) ReceiverTemplate(_forwarderAddress) {}
// Add custom validation before parent's checks
function onReport(bytes calldata metadata, bytes calldata report) external override {
// Custom check: Rate limiting
if (block.timestamp < s_lastReportTime + s_minReportInterval) {
revert ReportTooFrequent(block.timestamp - s_lastReportTime, s_minReportInterval);
}
// Call parent implementation for standard permission checks
super.onReport(metadata, report);
s_lastReportTime = block.timestamp;
}
function _processReport(bytes calldata report) internal override {
// Your business logic here
uint256 value = abi.decode(report, (uint256));
// ... store or process the value ...
}
/// @notice Returns the minimum interval between reports
/// @return The minimum interval in seconds
function getMinReportInterval() external view returns (uint256) {
return s_minReportInterval;
}
/// @notice Returns the timestamp of the last report
/// @return The last report timestamp
function getLastReportTime() external view returns (uint256) {
return s_lastReportTime;
}
/// @notice Updates the minimum interval between reports
/// @param _interval The new minimum interval in seconds
function setMinReportInterval(uint256 _interval) external onlyOwner {
uint256 previousInterval = s_minReportInterval;
s_minReportInterval = _interval;
emit MinReportIntervalUpdated(previousInterval, _interval);
}
}
5.2 Using Metadata Fields in Your Logic
The _decodeMetadata helper function is available for use in your _processReport implementation. This allows you to access workflow metadata for custom business logic:
contract MetadataAwareConsumer is ReceiverTemplate {
mapping(bytes32 => uint256) public s_reportCountByWorkflow;
constructor(address _forwarderAddress) ReceiverTemplate(_forwarderAddress) {}
function _processReport(bytes calldata report) internal override {
// Access the metadata to get workflow ID
bytes calldata metadata = msg.data[4:]; // Skip function selector
(bytes32 workflowId, , ) = _decodeMetadata(metadata);
// Use workflow ID in your business logic
s_reportCountByWorkflow[workflowId]++;
// Process the report data
uint256 value = abi.decode(report, (uint256));
// ... your logic here ...
}
}
6. Complete Examples
Example 1: Simple Consumer Contract
This example inherits from ReceiverTemplate to store a temperature value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { ReceiverTemplate } from "./ReceiverTemplate.sol";
contract TemperatureConsumer is ReceiverTemplate {
int256 public s_currentTemperature;
event TemperatureUpdated(int256 newTemperature);
// Constructor requires forwarder address
constructor(address _forwarderAddress) ReceiverTemplate(_forwarderAddress) {}
function _processReport(bytes calldata report) internal override {
int256 newTemperature = abi.decode(report, (int256));
s_currentTemperature = newTemperature;
emit TemperatureUpdated(newTemperature);
}
}
Deployment:
// For simulation: Use MockForwarder address
address mockForwarder = 0x15fC6ae953E024d975e77382eEeC56A9101f9F88; // e.g. Ethereum Sepolia
TemperatureConsumer temperatureConsumer = new TemperatureConsumer(mockForwarder);
// For production: Use KeystoneForwarder address
address keystoneForwarder = 0xF8344CFd5c43616a4366C34E3EEE75af79a74482; // e.g. Ethereum Sepolia
TemperatureConsumer temperatureConsumer = new TemperatureConsumer(keystoneForwarder);
Adding additional security after deployment:
// Add workflow ID check for highest security
temperatureConsumer.setExpectedWorkflowId(0xYourWorkflowId...);
Example 2: The Proxy Pattern
For more complex scenarios, it's best to separate your Chainlink-aware code from your core business logic. The Proxy Pattern is a robust architecture that uses two contracts to achieve this:
- A Logic Contract: Holds the state and the core functions of your application. It knows nothing about the Forwarder contract or the
onReportfunction. - A Proxy Contract: Acts as the secure entry point. It inherits from
ReceiverTemplateand forwards validated reports to the Logic Contract.
This separation makes your business logic more modular and reusable.
The Logic Contract (ReserveManager.sol)
This contract, our "vault", holds the state and the updateReserves function. For security, it only accepts calls from its trusted Proxy. It also includes an owner-only function to update the proxy address, making the system upgradeable without requiring a migration.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
contract ReserveManager is Ownable {
struct UpdateReserves {
uint256 ethPrice;
uint256 btcPrice;
}
address private s_proxyAddress;
uint256 private s_lastEthPrice;
uint256 private s_lastBtcPrice;
uint256 private s_lastUpdateTime;
event ReservesUpdated(uint256 ethPrice, uint256 btcPrice, uint256 updateTime);
event ProxyAddressUpdated(address indexed previousProxy, address indexed newProxy);
modifier onlyProxy() {
require(msg.sender == s_proxyAddress, "Caller is not the authorized proxy");
_;
}
constructor() Ownable(msg.sender) {}
/// @notice Returns the proxy address
/// @return The authorized proxy address
function getProxyAddress() external view returns (address) {
return s_proxyAddress;
}
/// @notice Returns the last ETH price
/// @return The last recorded ETH price
function getLastEthPrice() external view returns (uint256) {
return s_lastEthPrice;
}
/// @notice Returns the last BTC price
/// @return The last recorded BTC price
function getLastBtcPrice() external view returns (uint256) {
return s_lastBtcPrice;
}
/// @notice Returns the last update timestamp
/// @return The timestamp of the last update
function getLastUpdateTime() external view returns (uint256) {
return s_lastUpdateTime;
}
/// @notice Updates the authorized proxy address
/// @param _proxyAddress The new proxy address
function setProxyAddress(address _proxyAddress) external onlyOwner {
address previousProxy = s_proxyAddress;
s_proxyAddress = _proxyAddress;
emit ProxyAddressUpdated(previousProxy, _proxyAddress);
}
/// @notice Updates the reserve prices
/// @param data The new reserve data containing ETH and BTC prices
function updateReserves(UpdateReserves memory data) external onlyProxy {
s_lastEthPrice = data.ethPrice;
s_lastBtcPrice = data.btcPrice;
s_lastUpdateTime = block.timestamp;
emit ReservesUpdated(data.ethPrice, data.btcPrice, block.timestamp);
}
}
The Proxy Contract (UpdateReservesProxy.sol)
This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits ReceiverTemplate to validate incoming reports and then calls the ReserveManager.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ReserveManager } from "./ReserveManager.sol";
import { ReceiverTemplate } from "./ReceiverTemplate.sol";
contract UpdateReservesProxy is ReceiverTemplate {
ReserveManager private s_reserveManager;
constructor(address _forwarderAddress, address reserveManagerAddress) ReceiverTemplate(_forwarderAddress) {
s_reserveManager = ReserveManager(reserveManagerAddress);
}
/// @notice Returns the reserve manager contract address
/// @return The ReserveManager contract instance
function getReserveManager() external view returns (ReserveManager) {
return s_reserveManager;
}
/// @inheritdoc ReceiverTemplate
function _processReport(bytes calldata report) internal override {
ReserveManager.UpdateReserves memory updateReservesData = abi.decode(report, (ReserveManager.UpdateReserves));
s_reserveManager.updateReserves(updateReservesData);
}
}
Configuring permissions after deployment:
// Additional validation can be added after deployment
updateReservesProxy.setExpectedWorkflowId(0xYourWorkflowId...);
How it Works
The deployment and configuration process involves these steps:
- Deploy the Logic Contract: Deploy
ReserveManager.sol. The wallet that deploys this contract becomes itsowner. - Deploy the Proxy Contract: Deploy
UpdateReservesProxy.sol, passing the forwarder address and the address of the deployedReserveManagercontract to its constructor. - Link the Contracts: The
ownerof theReserveManagercontract must call itssetProxyAddressfunction, passing in the address of theUpdateReservesProxycontract. This authorizes the proxy to call the logic contract. - Configure Permissions (Recommended): The
ownerof the proxy should call setter functions to enable security checks:updateReservesProxy.setForwarderAddress(0xF8344CFd5c43616a4366C34E3EEE75af79a74482); updateReservesProxy.setExpectedWorkflowId(0xYourWorkflowId...); - Configure Workflow: In your workflow's
config.json, use the address of the Proxy Contract as the receiver address. - Execution Flow: When your workflow runs:
- The Chainlink Forwarder calls
onReporton your Proxy - The Proxy validates the report (forwarder address is verified automatically; additional checks like workflow ID can be added)
- The Proxy's
_processReportfunction calls theupdateReservesfunction on your Logic Contract - Because the caller is the trusted proxy, the
onlyProxycheck passes, and your state is securely updated
- The Chainlink Forwarder calls
- (Optional) Upgrade: If you later need to deploy a new proxy, the owner can:
- Deploy the new proxy contract with the appropriate forwarder address
- Call
setProxyAddresson theReserveManagerto point it to the new proxy's address - Update the workflow configuration to use the new proxy address
End-to-End Sequence
7. Security Considerations
Forwarder address
The forwarder address is the foundation of your contract's security. The KeystoneForwarder contract performs cryptographic verification of DON signatures before calling your consumer. By requiring the forwarder address in the constructor, ReceiverTemplate ensures your contract is secure from deployment.
Replay attacks
CRE reports carry DON signatures that any compatible KeystoneForwarder will accept. This creates two distinct replay vectors that workflow authors must explicitly protect against by embedding protective metadata in their report payloads and verifying it in their consumer contracts.
Cross-chain replay
The risk: While publishing a single signed report to multiple chains simultaneously enables patterns like Proof of Reserve (PoR) or feed-style publish-once-post-many, it also means anyone holding a valid report can replay it on any chain that recognizes the DON's signing keys.
The forwarder validates cryptographic signatures but those signatures do not commit to a specific chain; without additional protection in your consumer contract, a replayed report can land on an unintended chain.
The mitigation: Embed the target chain selector in the report payload. The consumer contract decodes this value and rejects reports not intended for the current chain. Chain selectors are uint64 identifiers used throughout the CRE platform to identify blockchain networks; see Chain Selectors for the full list of constants and the ChainSelectorFromName helper.
Workflow (embed chain selector in the report payload):
// Define your report struct with a ChainSelector field.
// ChainSelector is a uint64, the same type used when instantiating evm.Client.
type PaymentReport struct {
Recipient common.Address
Amount *big.Int
ChainSelector uint64 // Target chain; used by the consumer to reject cross-chain replays
}
paymentReport := PaymentReport{
Recipient: common.HexToAddress(config.Recipient),
Amount: big.NewInt(100_000_000), // e.g., 100 USDC (6 decimals)
ChainSelector: config.ChainSelector, // e.g., 16015286601757825753 for Ethereum Sepolia
}
// ABI-encode paymentReport and pass to runtime.GenerateReport() as normal
Consumer contract (verify the embedded chain selector):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { ReceiverTemplate } from "./ReceiverTemplate.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ChainRestrictedConsumer is ReceiverTemplate {
IERC20 public immutable i_token;
uint64 public immutable i_expectedChainSelector;
error UnexpectedChainSelector(uint64 received, uint64 expected);
constructor(
address _forwarderAddress,
address _token,
uint64 _expectedChainSelector
) ReceiverTemplate(_forwarderAddress) {
i_token = IERC20(_token);
i_expectedChainSelector = _expectedChainSelector;
}
function _processReport(bytes calldata report) internal override {
(address recipient, uint256 amount, uint64 chainSelector) = abi.decode(
report,
(address, uint256, uint64)
);
if (chainSelector != i_expectedChainSelector) {
revert UnexpectedChainSelector(chainSelector, i_expectedChainSelector);
}
i_token.transfer(recipient, amount);
}
}
Same-chain replay on failure
The risk: While allowing failed deliveries to be retried without requiring a new signed report enables permissionless recovery from transient failures, it also means the forwarder does not mark reverted transmissions as used.
A malicious actor can exploit this window: after your workflow has already reacted to a failure (for example, scheduled a corrective action), an attacker can replay the original signed report once conditions recover, causing double-execution.
The mitigation: Embed the scheduled execution timestamp in the report payload. The consumer contract stores the last accepted timestamp and rejects any report with a timestamp equal to or earlier than the stored value. Once a later execution has been accepted, earlier failed reports can never land.
Workflow (embed scheduled execution timestamp in the report payload):
// Use the trigger's scheduled slot time; deterministic across all DON nodes.
// Refer to the cron trigger reference for the exact field name on cron.Payload.
scheduledAt := trigger.ScheduledAt.Unix()
// Define your report struct with a ScheduledAt field
type PaymentReport struct {
Recipient common.Address
Amount *big.Int
ScheduledAt *big.Int // Monotonic execution timestamp; used to reject stale replays
}
paymentReport := PaymentReport{
Recipient: common.HexToAddress(config.Recipient),
Amount: big.NewInt(100_000_000), // e.g., 100 USDC (6 decimals)
ScheduledAt: big.NewInt(scheduledAt),
}
// ABI-encode paymentReport and pass to runtime.GenerateReport() as normal
Consumer contract (reject reports from earlier executions):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { ReceiverTemplate } from "./ReceiverTemplate.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ScheduledPaymentConsumer is ReceiverTemplate {
IERC20 public immutable i_token;
uint256 public s_lastAcceptedTimestamp;
error ReportTooOld(uint256 reportTimestamp, uint256 lastAccepted);
event PaymentProcessed(address indexed recipient, uint256 amount, uint256 scheduledAt);
constructor(address _forwarderAddress, address _token) ReceiverTemplate(_forwarderAddress) {
i_token = IERC20(_token);
}
function _processReport(bytes calldata report) internal override {
(address recipient, uint256 amount, uint256 scheduledAt) = abi.decode(
report,
(address, uint256, uint256)
);
if (scheduledAt <= s_lastAcceptedTimestamp) {
revert ReportTooOld(scheduledAt, s_lastAcceptedTimestamp);
}
s_lastAcceptedTimestamp = scheduledAt;
i_token.transfer(recipient, amount);
emit PaymentProcessed(recipient, amount, scheduledAt);
}
}
Additional validation layers
The forwarder address provides baseline security, but you can add additional validation for defense-in-depth:
expectedWorkflowId: Ensures only one specific workflow can update your contract. Use this when a single workflow writes to your consumer (highest security for single-workflow scenarios).expectedAuthor: Restricts to workflows owned by a specific address. Use this when multiple workflows from the same owner should access your contract.expectedWorkflowName: Can be used in combination withexpectedAuthorfor additional validation. Requires author validation to be configured. See Workflow name validation below.
Workflow name validation
Best practices
- Always deploy with a valid forwarder address - The constructor requires this for security. Use
MockForwarderfor simulation,KeystoneForwarderfor production. Forwarder addresses are available in the Forwarder Directory page. - Add additional validation for production:
- Single workflow: Use
setExpectedWorkflowId()to restrict to one specific workflow (highest security) - Multiple workflows from same owner: Use
setExpectedAuthor()to restrict to workflows you own - Multiple workflows from different owners: Implement custom validation logic in your
onReport()override
- Single workflow: Use
- Protect against replay attacks - For any workflow that performs state-changing actions (payments, minting, position updates):
- Embed the target chain selector in the report payload and verify it in
_processReportto prevent cross-chain replay - Embed a monotonic execution timestamp from the cron trigger and reject reports with a timestamp ⤠the last accepted value to prevent same-chain replay on failure
- See Replay attacks for complete code examples
- Embed the target chain selector in the report payload and verify it in
- Keep your owner key secure - The owner can update all permission settings
- Test permission configurations - Verify your security settings work as expected before production deployment
- Workflow name validation - Can be used with
setExpectedWorkflowName()but requiressetExpectedAuthor()to also be configured for security