Collect this post as an NFT.
Smart contracts are powerful tools for automating processes on the blockchain, but they come with a challenge: they rely on external triggers to execute functions. These triggers, typically coming from an Externally Owned Account (EOA) or another contract, can create bottlenecks that are particularly problematic for contracts requiring frequent updates or real-time responsiveness.
Now, what if I told you that your smart contracts could maintain themselves, freeing you from the constant need to trigger them manually?
Well, thanks to Chainlink Automation, they make automating smart contracts feel like a walk-in-the-park. With Chainlink's Decentralized Oracle Network (Chainlink DON) and onchain Registry architecture, they are able to automate your smart contracts so that you don't have to worry about dealing with setup costs, ongoing maintenance, and risks associated with a centralized automation stack.
In this Builder Guide, I'll be walking you through the process of creating automation-compatible contracts and programmatically registering upkeeps. This will give you full control over the custom logic you want on Chainlink's onchain registry. In return, you’ll receive an upkeepID
, which you can use to check balances, fund, modify, or cancel the upkeep all via the registry.
P.S. The Chainlink Automation App interface also provides pre-built options to register upkeeps with custom triggers, time-based triggers, or log-based triggers if you choose not to programatically register your upkeeps.
The diagram below shows a high-level overview of the flow of how Chainlink Automation works.
At its core, the Chainlink Automation Network relies on the Automation Registry, which manages network actors and ensures Automation Nodes are compensated for successfully executing upkeeps.
Automation Nodes operate as a peer-to-peer network using Chainlink’s OCR3 protocol. These nodes rely on the Automation Registry to identify which upkeeps to service and independently simulate checkUpkeep
functions on their own block nodes to determine when an upkeep is eligible for execution. Once an upkeep is identified, nodes reach consensus through OCR3 and generate a signed report containing the performData
, which is then validated on the Registry before execution. This process ensures cryptographic guarantees, enhancing security and reliability. Additionally, the network is designed with built-in redundancy, ensuring upkeeps are executed even if some nodes go offline.
Chainlink Automation leverages the same battle-tested transaction manager mechanism used in Chainlink Data Feeds. This ensures a highly reliable automation service capable of executing and confirming transactions even during extreme gas spikes or on chains experiencing significant reorgs. By combining robust consensus mechanisms with proven transaction management, Chainlink Automation provides a resilient and efficient solution for decentralized task execution.
To set the stage, this guide will be based on the Base Sepolia network, utilizing Solidity as the programming language and Foundry as the contract deployment toolkit. To better illustrate the entire automation flow, I will demonstrate it using a Dutch Auction for Token ICOs project that I previously built. This project is very similar to the concept of Liquidity Bootstrapping Pools.
Fundamentally, there are 2 main contracts that are used to programatically register automation-compatible contracts:
Auctioneer.sol - A Dutch Auction Factory contract for creating and managing Dutch Auctions. This contract contains an internal function to registerUpkeep
onto Chainlink.
DutchAuction.sol - A Dutch Auction contract designed for token ICOs. It includes the checkUpkeep
and performUpkeep
functions, which automate auction finalization, airdrop tokens to users, refund surplus ETH, and burn any remaining tokens.
forge install
Chainlink's Smart Contract Kit.
forge install
Open Zeppelin Contracts.
Get Base Sepolia ETH from Alchemy.
Get LINK Tokens on Base Sepolia Network.
P.S. I will be skipping the core logic of the smart contracts and focus primarily on the Chainlink Automation functions within them. If you’re interested in the full source code, you can find it in this Github repository.
In our context, theDutchAuction.sol
will be an automation-compatible smart contract that contains checkUpkeep
and performUpkeep
functions. These functions allows the Chainlink Automation Network to determine if, when, and how the contract should be automated.
checkUpkeep
contains the logic that will be executed offchain such that if the criteria in this function that you have set has been met, then the Chainlink Automation Network will automatically trigger performUpkeep
.
performUpkeep
contains the logic that will be executed onchain when checkUpkeep
returns true. In our case, this function contains the endAuction
, airdropToken
, refundETH
and burnTokens
logic that we want to be automatically executed for the Dutch Auction.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Token} from "./Token.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
contract DutchAuction is ReentrancyGuard, Pausable, Ownable, AutomationCompatibleInterface {
// ... initialise variables, errors, events, mappings and functions
/////////////////////////////////////////
///// CHAINLINK AUTOMATION FUNCTIONS ////
/////////////////////////////////////////
/**
* @notice Chainlink Automation: Check if upkeep is needed
*/
function checkUpkeep(bytes calldata /* checkData */ )
external
view
override
returns (bool upkeepNeeded, bytes memory /* performData */ )
{
upkeepNeeded = (startTime != 0) // auction has started
&& !auctionEnded // auction hasn't ended yet
&& (block.timestamp >= endTime || getRemainingTokens() == 0); // time is up OR remaining tokens is 0
return (upkeepNeeded, "");
}
/**
* @notice Chainlink Automation: Perform the upkeep
*/
function performUpkeep(bytes calldata /* performData */ ) external override {
if (startTime == 0) revert AuctionNotStarted();
if (auctionEnded) revert AuctionAlreadyEnded();
if (block.timestamp < endTime && getRemainingTokens() > 0) revert AuctionNotEnded();
// Ends the auction
auctionEnded = true;
// Tokens to distribute are the total tokens sold
uint256 tokensToDistribute = getSoldTokens();
// Get the clearing price
clearingPrice = getCurrentPrice();
// Airdrop tokens to bidders proportionally
for (uint256 i = 0; i < bidders.length; i++) {
address bidder = bidders[i];
uint256 ethContribution = ethContributed[bidder];
// Calculate tokens for this bidder based on their ETH contribution
uint256 tokenAmount = (ethContribution * tokensToDistribute) / totalEthRaised;
uint256 ethNeeded = tokenAmount * clearingPrice;
uint256 refund = ethContribution - ethNeeded;
if (refund > 0) {
(bool success,) = bidder.call{value: refund}("");
if (!success) revert RefundFailed();
}
if (tokenAmount > 0 && !hasClaimedTokens[bidder]) {
hasClaimedTokens[bidder] = true;
bool success = token.transfer(bidder, tokenAmount * 1e18);
if (!success) {
hasClaimedTokens[bidder] = false;
revert TransferFailed();
}
emit TokensClaimed(bidder, tokenAmount * 1e18);
}
}
// Burn any remaining tokens
uint256 remainingTokens = totalTokensForSale - tokensToDistribute;
if (remainingTokens > 0) {
token.burn(remainingTokens * 1e18);
}
emit AuctionEnded(clearingPrice, tokensToDistribute, totalEthRaised);
}
}
** Please note that the logic in checkUpkeep
and performUpkeep
is completely customisable based on your automation trigger. The only important thing you need to do is to inherit the AutomationCompatibleInterface
from Chainlink.
Now that we have created an automation-compatible contract - DutchAuction.sol
, we can create Auctioneer.sol
, which includes the registerUpkeep
function. This function is triggered whenever a user deploys DutchAuction.sol
, programmatically registering it onto the Chainlink Automation Network.
The Auctioneer.sol
covers the following main areas:
Import LinkTokenInterface
to instantiate an ERC20 token interface for transferring of LINK.
Create a RegistrationParams
struct for when calling registerUpkeep
function on the Chainlink Registrar contract.
Instantiate an interface called AutomationRegistrarInterface
that contains the registerUpkeep
function.
Create an internal function that approve the transfer of LINK tokens, initialises the registration parameters and call registerUpkeep
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {DutchAuction} from "./DutchAuction.sol";
import {IERC20Metadata as IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
// Create a RegistrationParams struct
struct RegistrationParams {
address upkeepContract;
uint96 amount;
address adminAddress;
uint32 gasLimit;
uint8 triggerType;
IERC20 billingToken;
string name;
bytes encryptedEmail;
bytes checkData;
bytes triggerConfig;
bytes offchainConfig;
}
// Instantiate an Automation Registrar Interface
interface AutomationRegistrarInterface {
function registerUpkeep(RegistrationParams calldata requestParams) external returns (uint256);
}
contract Auctioneer {
LinkTokenInterface public immutable i_link;
AutomationRegistrarInterface public immutable i_registrar;
// ... initialise variables, errors, events, mappings and functions
/**
* When deploying Auctioneer.sol, you would need the LINK token address, Chainlink Registrar address and the Chainlink Registry address
*/
constructor(address _link, address _registrar, address _registry) {
if (_link == address(0) || _registrar == address(0) || _registry == address(0)) {
revert InvalidAddresses();
}
i_link = LinkTokenInterface(_link);
i_registrar = AutomationRegistrarInterface(_registrar);
i_registry = _registry;
}
/**
* @notice Create a new Dutch auction
*/
function createAuction(
string memory _name,
string memory _symbol,
uint256 _totalSupply,
uint256 _initialPrice,
uint256 _reservePrice,
uint256 _minimumBid
) external returns (address) {
// Create new auction
DutchAuction newAuction = new DutchAuction(
_name, _symbol, _totalSupply, _initialPrice, _reservePrice, _minimumBid, msg.sender, address(this)
);
// Transfer LINK tokens from msg.sender to this contract
if (!i_link.transferFrom(msg.sender, address(this), UPKEEP_MINIMUM_FUNDS)) {
revert LinkTransferFailed();
}
// Register the upkeep
uint256 upkeepId = _registerAuctionUpkeep(address(newAuction));
// Setup auction with registry (not registrar)
auctions.push(newAuction);
isValidAuction[address(newAuction)] = true;
auctionUpkeepIds[address(newAuction)] = upkeepId;
emit AuctionCreated(
address(newAuction), _name, _symbol, _totalSupply, _initialPrice, _reservePrice, _minimumBid, upkeepId
);
return address(newAuction);
}
//////////////////////////////////////////
///// CHAINLINK AUTOMATION FUNCTIONS /////
//////////////////////////////////////////
/**
* @notice Internal function to register upkeep for an auction
* @param auctionAddress The address of the auction to register upkeep for
* @return upkeepId The ID of the registered upkeep
*/
function _registerAuctionUpkeep(address auctionAddress) internal returns (uint256) {
// Approve LINK transfer to registrar
i_link.approve(address(i_registrar), UPKEEP_MINIMUM_FUNDS);
// Prepare registration parameters
RegistrationParams memory params = RegistrationParams({
name: "Dutch Auction Automation",
encryptedEmail: bytes(""),
upkeepContract: auctionAddress,
gasLimit: UPKEEP_GAS_LIMIT,
adminAddress: msg.sender,
triggerType: 0,
checkData: bytes(""),
triggerConfig: bytes(""),
offchainConfig: bytes(""),
amount: UPKEEP_MINIMUM_FUNDS,
billingToken: IERC20(address(i_link))
});
// Register upkeep
try i_registrar.registerUpkeep(params) returns (uint256 upkeepId) {
return upkeepId;
} catch {
revert UpkeepRegistrationFailed();
}
}
}
** For the LINK token address, Automation Registry address and Automation Registrar address on Base Sepolia network, you can refer to this deployAuctioneer.s.sol
file.
Important to note: Chainlink Automation deploys different versions of AutomationRegistry.sol
across various chains. For example, Ethereum Sepolia uses AutomationRegistry2_1.sol
, while Base Sepolia uses AutomationRegistry2_3.sol
. The key difference to be aware of is that the RegistrationParams
struct varies between versions:
AutomationRegistry2_1.sol
on Ethereum Sepolia does not require billingToken
in the struct
// RegistrationParams for AutomationRegistry2_1.sol
struct RegistrationParams {
string name;
bytes encryptedEmail;
address upkeepContract;
uint32 gasLimit;
address adminAddress;
uint8 triggerType;
bytes checkData;
bytes triggerConfig;
bytes offchainConfig;
uint96 amount;
}
AutomationRegistry2_3.sol
on Base Sepolia requires billingToken
in the struct
// RegistrationParams for AutomationRegistry2_3.sol
struct RegistrationParams {
address upkeepContract;
uint96 amount;
address adminAddress;
uint32 gasLimit;
uint8 triggerType;
IERC20 billingToken;
string name;
bytes encryptedEmail;
bytes checkData;
bytes triggerConfig;
bytes offchainConfig;
}
Putting it all together, this guide walks you through the process of creating automation-compatible smart contracts and seamlessly registering upkeeps on the Chainlink Automation Network. The two key contracts — DutchAuction.sol
and Auctioneer.sol
— work hand in hand, with DutchAuction.sol
implementing Chainlink’s checkUpkeep
and performUpkeep
functions to automate auction finalization, while Auctioneer.sol
handles programmatic upkeep registration via the registerUpkeep
function.
By leveraging Chainlink Automation, we can eliminate the need for manual smart contract triggers, allowing decentralized applications to function autonomously with greater reliability and efficiency.
I hope that the structured approach used in this guide is able to assist you in your journey of integrating automation into smart contracts, making it easier to deploy, manage, and scale your decentralized system.
With a solid understanding of upkeep registration and automation logic, you are now equipped with the power to build resilient and self-sustaining smart contracts.
Let's work towards building together a fully autonomous onchain future together.