performUpkeep is an external function that anyone can call — not just Chainlink Automation nodes. If your implementation has side effects (transferring funds, changing critical state), you need to ensure that unauthorized callers can’t exploit it.
There are two complementary strategies: re-validation (making the function safe for anyone to call) and access control (restricting who can call it). Use both when possible.
Strategy 1: Re-Validate On-Chain#
The simplest defense. Inside performUpkeep, re-check the same condition that checkUpkeep evaluated off-chain. If the condition no longer holds, revert.
|
|
This makes performUpkeep idempotent — calling it when the condition isn’t met is a no-op (reverts). An attacker gains nothing by calling it directly because either:
- The condition is met, in which case the work was going to happen anyway.
- The condition isn’t met, in which case the call reverts.
Re-validation is essential even when you also use access control, because the on-chain state can change between the off-chain checkUpkeep simulation and the on-chain performUpkeep execution.
Strategy 2: Forwarder-Based Access Control#
Restrict performUpkeep to calls from the upkeep’s forwarder contract. The forwarder address is stable and unique per upkeep, making it a reliable access control check.
|
|
When Access Control Matters Most#
Re-validation alone is sufficient when performUpkeep is naturally idempotent — the function does the same thing regardless of who calls it, and the condition gate prevents premature execution.
Add forwarder-based access control when:
- Order-dependent logic — e.g. processing a queue where the sequence of execution matters and a front-runner could manipulate it.
- MEV-sensitive operations — if
performDataencodes a price or trade route, an attacker who callsperformUpkeepwith crafted data could extract value. - Gas griefing — a caller invokes
performUpkeepto waste gas (and your LINK balance) even though the real condition isn’t met, in cases where the re-validation check is expensive.
Anti-Patterns#
Checking tx.origin — tx.origin is the EOA that submitted the transaction, not the Automation infrastructure. It rotates across node operators, can be spoofed in internal calls, and is broadly considered an unreliable access control mechanism.
Maintaining an allowlist of node addresses — node transmitter addresses change as the network evolves. You’d need to keep the allowlist in sync with the registry, which is fragile and unnecessary when the forwarder pattern exists.
No validation at all — relying on checkUpkeep to gate execution is insufficient. checkUpkeep runs off-chain; it doesn’t prevent direct on-chain calls to performUpkeep.
Decision Tree#
Is performUpkeep naturally idempotent?
├── Yes → Re-validation is sufficient for most cases
│ └── Does it handle MEV-sensitive data? → Add forwarder guard
└── No → Use forwarder access control + re-validationFurther Reading#
- Chainlink Docs — Security Considerations
- Forwarder — how the per-upkeep proxy works
- Automation Architecture