Skip to content

Commit

Permalink
grinding automation
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPereira committed Oct 19, 2023
1 parent 1f1046f commit 1cda182
Show file tree
Hide file tree
Showing 13 changed files with 558 additions and 52 deletions.
4 changes: 2 additions & 2 deletions packages/hardhat/contracts/AggregatorV3Consumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ contract AggregatorV3Consumer {

// Get the latest price from the price feed contract
function getLatestPrice() public view returns (int) {
(, /* uint80 roundID */ int answer, , , ) = /*uint startedAt*/ /*uint timeStamp*/ /*uint80 answeredInRound*/
i_priceFeed.latestRoundData();
// latestRoundData() returns roundID, answer, startedAt, timeStamp, answeredInRound
(, int answer, , , ) = i_priceFeed.latestRoundData();
return answer;
}

Expand Down
136 changes: 125 additions & 11 deletions packages/hardhat/contracts/AutomationConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,159 @@
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

// Errors
error AutomationConsumer__UpkeepNotNeeded();

/**
*
* user guesses if the next price returned by chainlink will be higher or lower than the current price
* bets are placed in LINK
*/
contract AutomationConsumer is AutomationCompatibleInterface {
// Types
enum Prediction {
Lower, // 0
Higher // 1
}
struct Bet {
address bettor;
uint256 amount;
int referencePrice;
Prediction prediction;
}

// State variables
AggregatorV3Interface internal immutable i_priceFeed;
IERC20 internal immutable i_linkToken;
mapping(uint => Bet) public bets; // betId => Bet struct
uint[] public activeBets; // betIds of bets that are still active
uint public nextBetId = 0;

/** Chainlink Keeper nodes call checkUpkeep to see if upkeepNeeded is true
*
// Events
event BetPlaced(
uint256 indexed betId,
address indexed bettor,
int256 indexed referencePrice,
uint256 amount,
Prediction prediction
);
event BetWon(uint betId, address indexed bettor, uint256 payout);
event BetLost(uint betId, address indexed bettor, uint256 amountLost);

// Functions
constructor(address priceFeedAddress, address linkTokenAddress) {
i_priceFeed = AggregatorV3Interface(priceFeedAddress);
i_linkToken = IERC20(linkTokenAddress);
}

/** Allows user to place bet on whether price of asset will be higher or lower than current price
* @param amount amount of LINK to wager
* @param prediction Lower or Higher than the price at time of bet
*/
function placeBet(uint256 amount, Prediction prediction) public {
require(
amount >= 1 ether && amount <= 5 ether,
"You must wager between 1 and 5 LINK"
);
require(
i_linkToken.allowance(msg.sender, address(this)) >= amount,
"Insufficient LINK token allowance"
);
require(
i_linkToken.transferFrom(msg.sender, address(this), amount),
"Failed to transfer LINK"
);
(, int answer, , , ) = i_priceFeed.latestRoundData();
bets[nextBetId] = Bet({
bettor: msg.sender,
referencePrice: answer,
amount: amount,
prediction: prediction
});
activeBets.push(nextBetId);
emit BetPlaced(nextBetId, msg.sender, answer, amount, prediction);
nextBetId++;
}

/** Chainlink Keeper nodes call checkUpkeep to see if it returns true
* https://docs.chain.link/chainlink-automation/compatible-contracts#checkupkeep-function
*
* @dev returns true if there are active bets and the latest price is different than
* the reference price of the most recently placed bet
*/
function checkUpkeep(
bytes memory /* checkData */
)
public
pure
view
override
returns (bool upkeepNeeded, bytes memory /* performData */)
{
upkeepNeeded = true;
if (activeBets.length == 0) {
upkeepNeeded = false;
}
(, int latestPrice, , , ) = i_priceFeed.latestRoundData();
// execute the automation if the latest price is different than the reference price of the most recently placed bet
upkeepNeeded =
bets[activeBets[activeBets.length - 1]].referencePrice !=
latestPrice;
}

/**
* @dev Once `checkUpkeep` returns true, this function triggers
* the Chainlink VRF node being called to generate a random number
/** Once `checkUpkeep` returns true, `performUpkeep` will be triggered by chainlink node
* https://docs.chain.link/chainlink-automation/compatible-contracts#performupkeep-function
*
* @dev then the `fullfillRandomWords` function is automatically called once the Chainlink VRF node
* returns the random number because of this contract inherting from `VRFConsumerBaseV2`
* @notice pays out winners and remove bets that have been settled
*
* @dev https://docs.chain.link/chainlink-automation/compatible-contracts#performupkeep-function
* - winners get paid double their bet amount
* - losers lose their bet amount
*
* TODO: handle if price is still the same somehow?
*/
function performUpkeep(bytes calldata /* performData */) external override {
// best practice to revalidate the upkeepNeeded
(bool upkeepNeeded, ) = checkUpkeep(bytes("0x"));
if (!upkeepNeeded) {
revert AutomationConsumer__UpkeepNotNeeded();
}

(, int latestPrice, , , ) = i_priceFeed.latestRoundData();

while (activeBets.length > 0) {
// pointer to end of array enables .pop() for efficiency
uint betId = activeBets[activeBets.length - 1];
Bet memory bet = bets[betId];

bool isWinner = (bet.referencePrice > latestPrice &&
bet.prediction == Prediction.Lower) ||
(bet.referencePrice < latestPrice &&
bet.prediction == Prediction.Higher);

if (isWinner) {
uint256 payout = bet.amount * 2;
i_linkToken.transfer(bet.bettor, payout);
emit BetWon(betId, bet.bettor, payout);
} else {
emit BetLost(betId, bet.bettor, bet.amount);
}

activeBets.pop();
}
}

// Getters
function getLatestRoundData()
public
view
returns (
uint80 roundId,
int price,
uint256 startedAt,
uint256 updatedAt
)
{
(roundId, price, startedAt, updatedAt, ) = i_priceFeed
.latestRoundData();
}
}
2 changes: 1 addition & 1 deletion packages/hardhat/contracts/VRFConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "@chainlink/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";

/**
*
* if you send 1 link user can spin more than once
*/
contract VRFConsumer is VRFV2WrapperConsumerBase, ConfirmedOwner {
// State variables
Expand Down
4 changes: 2 additions & 2 deletions packages/hardhat/deploy/01_AggregatorV3Consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { developmentChains, networkConfig } from "../helper-hardhat-config";
import { network } from "hardhat";

/**
* Deploy PriceFeedConsumer contract
* Deploy AggregatorV3Consumer contract
*
* @param hre HardhatRuntimeEnvironment object.
*/
Expand All @@ -24,7 +24,7 @@ const deployAggregatorV3Consumer: DeployFunction = async function (hre: HardhatR
priceFeedAddress = MockV3Aggregator.address;
} else {
// use address from helper-hardhat-config if on testnet or live network
priceFeedAddress = networkConfig[chainId].priceFeedAddress?.ETH_USD;
priceFeedAddress = networkConfig[chainId].AggregatorV3Consumer?.ETH_USD;
}

const args = [priceFeedAddress];
Expand Down
6 changes: 4 additions & 2 deletions packages/hardhat/deploy/03_VRFConsumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const deployVRFConsumer: DeployFunction = async function (hre: HardhatRuntimeEnv
console.log(`The current chain ID is: ${chainId}`);

log("------------------------------------");
const { wrapperAddress, linkAddress } = networkConfig[chainId].vrfCoordinator;
const { VRFV2WrapperAddress, linkTokenAddress } = networkConfig[chainId].VRFConsumer;

const args = [linkAddress, wrapperAddress];
const args = [linkTokenAddress, VRFV2WrapperAddress];

const VRFConsumer = await deploy("VRFConsumer", {
from: deployer,
Expand All @@ -31,3 +31,5 @@ const deployVRFConsumer: DeployFunction = async function (hre: HardhatRuntimeEnv
};

export default deployVRFConsumer;

deployVRFConsumer.tags = ["vrf", "all"];
34 changes: 34 additions & 0 deletions packages/hardhat/deploy/04_AutomationConsumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { networkConfig } from "../helper-hardhat-config";

/** Deploy the "AutomationConsumer" contract
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployAutomationConsumer: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const { deploy, log } = hre.deployments;

const chainId = await hre.ethers.provider.getNetwork().then(network => network.chainId);

log("------------------------------------");
const { priceFeedAddress, linkTokenAddress } = networkConfig[chainId].AutomationConsumer;

const args = [priceFeedAddress, linkTokenAddress];

const AutomationConsumer = await deploy("AutomationConsumer", {
from: deployer,
args: args,
log: true,
autoMine: true,
});

log("VRFConsumer deployed at:", AutomationConsumer.address);

// HOW TO VERIFY : https://docs.scaffoldeth.io/deploying/deploy-smart-contracts#4-verify-your-smart-contract
};

export default deployAutomationConsumer;

deployAutomationConsumer.tags = ["automation", "all"];
32 changes: 17 additions & 15 deletions packages/hardhat/helper-hardhat-config.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
interface NetworkConfigEntryTypes {
name: string;
priceFeedAddress?: {
AggregatorV3Consumer?: {
BTC_USD: string;
ETH_USD: string;
LINK_USD: string;
};
vrfCoordinator: {
wrapperAddress?: string;
linkAddress?: string;
VRFConsumer: {
VRFV2WrapperAddress?: string;
linkTokenAddress?: string;
};
AutomationConsumer: {
priceFeedAddress?: string;
linkTokenAddress?: string;
};
}

// Contracts have constructors that require contract address args that are network specific
const networkConfig: { [key: number]: NetworkConfigEntryTypes } = {
31337: {
name: "hardhat",
vrfCoordinator: {
wrapperAddress: "",
linkAddress: "",
},
},
11155111: {
name: "sepolia",
vrfCoordinator: {
wrapperAddress: "0xab18414CD93297B0d12ac29E63Ca20f515b3DB46",
linkAddress: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
AutomationConsumer: {
priceFeedAddress: "0x694AA1769357215DE4FAC081bf1f309aDC325306", // ETH/USD
linkTokenAddress: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
},
VRFConsumer: {
VRFV2WrapperAddress: "0xab18414CD93297B0d12ac29E63Ca20f515b3DB46",
linkTokenAddress: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
},
priceFeedAddress: {
AggregatorV3Consumer: {
BTC_USD: "0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43",
ETH_USD: "0x694AA1769357215DE4FAC081bf1f309aDC325306",
LINK_USD: "0xc59E3633BAAC79493d908e63626716e204A45EdF",
Expand Down
28 changes: 26 additions & 2 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import React, { useCallback, useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { Bars3Icon, BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { BanknotesIcon, Bars3Icon, BugAntIcon, LinkIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
import { useOutsideClick } from "~~/hooks/scaffold-eth";
import { getTargetNetwork } from "~~/utils/scaffold-eth";

const NavLink = ({ href, children }: { href: string; children: React.ReactNode }) => {
interface NavLinkProps {
href: string;
children: React.ReactNode;
newTab?: boolean; // new prop to decide whether to open the link in a new tab or not
}

const NavLink: React.FC<NavLinkProps> = ({ href, children, newTab = false }) => {
const router = useRouter();
const isActive = router.pathname === href;

return (
<Link
href={href}
passHref
target={newTab ? "_blank" : "_self"}
rel={newTab ? "noopener noreferrer" : ""}
className={`${
isActive ? "bg-secondary shadow-md" : ""
} hover:bg-secondary hover:shadow-md focus:!bg-secondary active:!text-neutral py-1.5 px-3 text-lg rounded-full gap-2 grid grid-flow-col`}
Expand Down Expand Up @@ -55,6 +63,22 @@ export const Header = () => {
Debug Contracts
</NavLink>
</li>
{network.id === 11155111 && (
<>
<li>
<NavLink newTab href="https://faucets.chain.link/sepolia">
<LinkIcon className="h-4 w-4" />
Link Faucet
</NavLink>
</li>
<li>
<NavLink newTab href="https://sepoliafaucet.com/">
<BanknotesIcon className="h-4 w-4" />
Sepolia Faucet
</NavLink>
</li>
</>
)}
{network.id === 31337 && (
<li>
<NavLink href="/blockexplorer">
Expand Down
Loading

0 comments on commit 1cda182

Please sign in to comment.