Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keepers example using function selector checkData #73

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 74 additions & 44 deletions contracts/KeepersCounter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,83 @@ contract KeepersCounter is KeeperCompatibleInterface {
*/
uint256 public counter;

/**
* Use an interval in seconds and a timestamp to slow execution of Upkeep
*/
uint256 public immutable interval;
uint256 public lastTimeStamp;
/**
* Public multiplier boolean variable
*/
bool public multiplierEnabled;

/**
* Use an interval in seconds and a timestamp to slow execution of Upkeep
*/
uint public immutable interval;
uint public lastTimeStamp;

// keccak256 hash of empty checkData
bytes32 constant public emptyCheckDataHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;

/**
* @notice Executes once when a contract is created to initialize state variables
*
* @param updateInterval - Period of time between two counter increments expressed as UNIX timestamp value
*/
constructor(uint256 updateInterval) {
interval = updateInterval;
lastTimeStamp = block.timestamp;
constructor(uint updateInterval, bool _multiplierEnabled) {
interval = updateInterval;
lastTimeStamp = block.timestamp;
counter = 0;
multiplierEnabled = _multiplierEnabled;
}

counter = 0;
}
function setMultiplierEnabled(bool _enabled) external {
multiplierEnabled = _enabled;
}

/**
* @notice Checks if the contract requires work to be done
*/
function checkUpkeep(
bytes memory /* checkData */
)
public
override
returns (
bool upkeepNeeded,
bytes memory /* performData */
)
{
upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
// We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
}
function checkMultiplier() external view returns (bool upkeepNeeded, bytes memory performData) {
upkeepNeeded = multiplierEnabled && (counter == 3 || counter == 10);
if (upkeepNeeded) {
uint256 multiplier = counter % 2 == 0 ? 10 : 3;
performData = abi.encodeWithSelector(this.multiplyCounter.selector, multiplier);
}
return (upkeepNeeded, performData);
}

/**
* @notice Performs the work on the contract, if instructed by :checkUpkeep():
*/
function performUpkeep(
bytes calldata /* performData */
) external override {
// add some verification
(bool upkeepNeeded, ) = checkUpkeep("");
require(upkeepNeeded, "Time interval not met");
function checkReset() external view returns (bool upkeepNeeded, bytes memory performData) {
upkeepNeeded = counter > 100;
performData = abi.encodePacked(bytes4(abi.encodeWithSelector(this.resetCounter.selector, "")));
return (upkeepNeeded, performData);
}

function multiplyCounter(uint256 _amount) external {
require(multiplierEnabled && (counter == 3 || counter == 10), "Upkeep not satisfied");
require(counter == _amount, "Only valid multiplier");
counter *= _amount;
}

function resetCounter() external {
require(counter > 100, "Too early");
counter = 0;
}

function checkUpkeep(bytes calldata checkData)
external
view
override
returns (bool upkeepNeeded, bytes memory performData)
{
if (keccak256(checkData) == emptyCheckDataHash) {
// We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
performData = "";
} else {
(bool success, bytes memory returnedData) = address(this).staticcall(checkData);
require(success);

(upkeepNeeded, performData) = abi.decode(returnedData, (bool, bytes));
}
}

lastTimeStamp = block.timestamp;
counter = counter + 1;
// We don't use the performData in this example. The performData is generated by the Keeper's call to your checkUpkeep function
}
function performUpkeep(bytes calldata performData) external override {
lastTimeStamp = block.timestamp;
if (keccak256(performData) == emptyCheckDataHash) {
// We don't use the performData in this example. The performData is generated by the Keeper's call to your checkUpkeep function
counter = counter + 1;
} else {
(bool success, ) = address(this).call(performData);
require(success);
}
}
}
2 changes: 1 addition & 1 deletion contracts/test/fuzzing/KeepersCounterEchidnaTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.7;
import "../../KeepersCounter.sol";

contract KeepersCounterEchidnaTest is KeepersCounter {
constructor() KeepersCounter(8 days) {}
constructor() KeepersCounter(8 days, true) {}

function echidna_test_perform_upkeep_gate() public view returns (bool) {
return counter == 0;
Expand Down
3 changes: 2 additions & 1 deletion deploy/04_Deploy_KeepersCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ module.exports = async ({ getNamedAccounts, deployments, getChainId }) => {
const { deployer } = await getNamedAccounts()
const chainId = network.config.chainId
const keepersUpdateInterval = networkConfig[chainId]["keepersUpdateInterval"] || "30"
const multiplierEnabled = networkConfig[chainId]['multiplierEnabled'] || true
// Price Feed Address, values can be obtained at https://docs.chain.link/docs/reference-contracts
// Default one below is ETH/USD contract on Kovan
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
const args = [keepersUpdateInterval]
const args = [keepersUpdateInterval, multiplierEnabled]
const keepersCounter = await deploy("KeepersCounter", {
from: deployer,
args: args,
Expand Down
9 changes: 9 additions & 0 deletions helper-hardhat-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const networkConfig = {
jobId: "29fa9aa13bf1468788b7cc4a500a45b8",
fundAmount: "1000000000000000000",
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
31337: {
name: "localhost",
Expand All @@ -14,6 +15,7 @@ const networkConfig = {
jobId: "29fa9aa13bf1468788b7cc4a500a45b8",
fundAmount: "1000000000000000000",
keepersUpdateInterval: "30",
multiplierEnabled: "true",
ethUsdPriceFeed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
},
42: {
Expand All @@ -25,6 +27,7 @@ const networkConfig = {
fee: "100000000000000000",
fundAmount: "100000000000000000", // 0.1
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
4: {
name: "rinkeby",
Expand All @@ -37,17 +40,21 @@ const networkConfig = {
fee: "100000000000000000",
fundAmount: "100000000000000000", // 0.1
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
1: {
name: "mainnet",
linkToken: "0x514910771af9ca656af840dff83e8264ecf986ca",
fundAmount: "0",
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
5: {
name: "goerli",
linkToken: "0x326c977e6efc84e512bb9c30f76e30c160ed06fb",
fundAmount: "0",
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
137: {
name: "polygon",
Expand All @@ -57,6 +64,8 @@ const networkConfig = {
jobId: "12b86114fa9e46bab3ca436f88e1a912",
fee: "100000000000000",
fundAmount: "100000000000000",
keepersUpdateInterval: "30",
multiplierEnabled: "true",
},
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13",
"@nomiclabs/hardhat-etherscan": "^3.0.0",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@nomiclabs/hardhat-web3": "^2.0.0",
"@openzeppelin/test-helpers": "^0.5.15",
"chai": "^4.3.4",
"ethereum-waffle": "^3.4.0",
"ethers": "^5.5.1",
Expand Down
126 changes: 110 additions & 16 deletions test/unit/KeepersCounter_unit_test.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,126 @@
const { assert, expect } = require("chai")
const { network, deployments, ethers } = require("hardhat")
const { developmentChains } = require("../../helper-hardhat-config")
const { developmentChains, networkConfig } = require("../../helper-hardhat-config")
const { expectRevert } = require('@openzeppelin/test-helpers')

!developmentChains.includes(network.name)
? describe.skip
: describe("Keepers Counter Unit Tests", async function () {
beforeEach(async () => {
await deployments.fixture(["mocks", "keepers"])
counter = await ethers.getContract("KeepersCounter")
chainId = await getChainId()
})

context("Basic upkeep", async () => {
it('should be able to call checkUpkeep', async () => {
const checkData = ethers.utils.hexlify("0x");
const { upkeepNeeded, performData } = await counter.checkUpkeep(checkData);
})

it('should be able to call performUpkeep', async () => {
const checkData = ethers.utils.hexlify("0x");
await counter.performUpkeep(checkData);
//now get the new counter value
const result = await counter.counter()
console.log("Keepers Counter value: ", new ethers.BigNumber.from(result._hex).toString())
expect(new ethers.BigNumber.from(result).toString()).to.be.a.bignumber.that.is.greaterThan(new ethers.BigNumber.from(0).toString())
})
})

context("Upkeep with checkData - unhappy paths", async () => {
before(function() {
if(networkConfig[chainId].multiplierEnabled == "false") this.skip();
});
it("Should not require upkeep from checkMultiplier if not enabled", async () => {
await counter.setMultiplierEnabled(false);
const returnData = await counter.checkMultiplier()
expect(returnData[0]).to.be.equal(false)
})

it("Should not multiply counter if not enabled", async () => {
await expectRevert(counter.multiplyCounter(3), "Upkeep not satisfied")
})

it("Should not require upkeep from checkMultiplier if not valid value", async () => {
await counter.setMultiplierEnabled(true);
const returnData = await counter.checkMultiplier()
expect(returnData[0]).to.be.equal(false)
})

it("Should not multiply counter if not valid value", async () => {
for(let i=0; i<3; i++) {
await counter.performUpkeep(ethers.utils.hexlify("0x"));
}
await expectRevert(counter.multiplyCounter(2), "Only valid multiplier")
})

it("Should not require upkeep from checkReset if too early", async () => {
const returnData = await counter.checkReset()
expect(returnData[0]).to.be.equal(false)
})

it("Should not reset counter if too early", async () => {
await expectRevert(counter.resetCounter(), "Too early")
})
})

context("Upkeep with checkData - happy paths", async () => {
before(function() {
if(networkConfig[chainId].multiplierEnabled == "false") this.skip();
});
beforeEach(async () => {
await deployments.fixture(["mocks", "keepers"])
counter = await ethers.getContract("KeepersCounter")
for(let i=0; i<3; i++) {
await counter.performUpkeep(ethers.utils.hexlify("0x"));
}
})

it("should be able to call checkUpkeep", async () => {
const checkData = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(""))
const { upkeepNeeded } = await counter.callStatic.checkUpkeep(checkData)
assert.equal(upkeepNeeded, false)
it("Should be able to call checkUpkeep for checkMultiplier", async () => {
const abi = ["function checkMultiplier()", "function multiplyCounter(uint)"]
const iface = new ethers.utils.Interface(abi)
const checkData = iface.encodeFunctionData("checkMultiplier", [])
console.log(`checkMultiplier checkData: ${checkData}`)
const { upkeepNeeded, performData } = await counter.checkUpkeep(checkData)
const calldata = iface.encodeFunctionData("multiplyCounter", ["3"])
expect(upkeepNeeded).to.be.equal(true)
expect(performData).to.be.equal(calldata)
})

it("Should call performUpkeep for multiplyCounter", async () => {
const abi = ["function multiplyCounter(uint)"]
const iface = new ethers.utils.Interface(abi)
const checkData = iface.encodeFunctionData("multiplyCounter", ["3"])
await counter.performUpkeep(checkData)
const result = await counter.counter()
expect(result).to.be.equal(new ethers.BigNumber.from("9"))
})

it("should not be able to call perform upkeep without the time passed interval", async () => {
const checkData = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(""))
await expect(counter.performUpkeep(checkData)).to.be.revertedWith("Time interval not met")
it("Should be able to call checkUpkeep for checkReset", async () => {
await counter.multiplyCounter(3);
await counter.performUpkeep(ethers.utils.hexlify("0x"));
await counter.multiplyCounter(10);
await counter.performUpkeep(ethers.utils.hexlify("0x"));
const abi = ["function checkReset()", "function resetCounter()"]
const iface = new ethers.utils.Interface(abi)
const checkData = iface.encodeFunctionData("checkReset", [])
console.log(`checkReset checkData: ${checkData}`)
const { upkeepNeeded, performData } = await counter.checkUpkeep(checkData)
const calldata = iface.encodeFunctionData("resetCounter", [])
expect(upkeepNeeded).to.be.equal(true)
expect(performData).to.be.equal(calldata)
})

it("should be able to call performUpkeep after time passes", async () => {
const startingCount = await counter.counter()
const checkData = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(""))
const interval = await counter.interval()
await network.provider.send("evm_increaseTime", [interval.toNumber() + 1])
it("Should call performUpkeep for resetCounter", async () => {
await counter.multiplyCounter(3);
await counter.performUpkeep(ethers.utils.hexlify("0x"));
await counter.multiplyCounter(10);
await counter.performUpkeep(ethers.utils.hexlify("0x"));
const abi = ["function resetCounter()"]
const iface = new ethers.utils.Interface(abi)
const checkData = iface.encodeFunctionData("resetCounter", [])
await counter.performUpkeep(checkData)
assert.equal(startingCount + 1, (await counter.counter()).toNumber())
const result = await counter.counter()
expect(result).to.be.equal(new ethers.BigNumber.from("0"))
})
})
})
Loading