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

RNG: Chainlink VRF + Fallback mechanism #966

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a1ca3ff
feat(contracts): vrf consumer + chainlink base
zmalatrax Jun 16, 2023
2554444
feat(contracts): vrf subscription manager + token interface
zmalatrax Jun 16, 2023
69d9844
test(contracts): chainlink vrf coordinator mock
zmalatrax Jun 16, 2023
03cd8ca
test(contracts): interface for mock coordinator
zmalatrax Jun 16, 2023
81d0c37
test(contracts): vrf consumer mock
zmalatrax Jun 16, 2023
f5f3a55
test(contracts): vrf subscription manager mock
zmalatrax Jun 16, 2023
298a67f
test(contracts): removed link token from mock
zmalatrax Jun 19, 2023
dd03101
style(contracts): add missing missing function section comments
zmalatrax Jun 19, 2023
0485596
fix(contracts): add missing storage visibility to public
zmalatrax Jun 19, 2023
4b01e8a
test(contracts): add deploy scripts for vrf
zmalatrax Jun 19, 2023
6180145
test(contracts): modify test to use vrf instead of randomizer
zmalatrax Jun 19, 2023
332b9df
refactor: remove unused mock contract
zmalatrax Jun 21, 2023
784287f
chore: add reference to vrf deployed contracts
zmalatrax Jun 21, 2023
006d48a
chore: add comments on deploy script
zmalatrax Jun 21, 2023
2e8f43a
refactor: change variable names
zmalatrax Jun 21, 2023
60232c1
test(rng): add distinct tests for Randomizer & VRF
zmalatrax Jun 21, 2023
eb49a06
feat(contracts): create subscription in constructor
zmalatrax Jun 21, 2023
1dea136
chore: remove unnecessary new subscription tx
zmalatrax Jun 21, 2023
841c73a
refactor: change variable name
zmalatrax Jun 21, 2023
ff1a5fd
feat(deploy): add deployment for public networks
zmalatrax Jun 21, 2023
d9384b6
fix(contracts): update mock to also create subscription in constructor
zmalatrax Jun 21, 2023
55987a8
feat(deploy): add task to credit LINK from deployer to VRF subscription
zmalatrax Jun 21, 2023
13c7b7d
refactor: change keyHash arbitrary address to 0
zmalatrax Jun 21, 2023
e846917
chore: update rng to last chainlink deploy version
zmalatrax Jun 21, 2023
0ca6b82
chore: clean unused imports
zmalatrax Jun 21, 2023
1bf82cb
refactor: remove nested ternary operator
zmalatrax Jun 21, 2023
9ce3f4b
refactor: restore 00-rng to its initial state
zmalatrax Jun 21, 2023
acfcd1b
fix(RNG): test fixes
unknownunknown1 Oct 29, 2023
0428693
feat(RNG): add fallback + upgradability
unknownunknown1 Oct 29, 2023
4b69a12
refactor: rename
jaybuidl Mar 12, 2024
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
114 changes: 110 additions & 4 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { DeployFunction, DeployResult } from "hardhat-deploy/types";
import { BigNumber } from "ethers";
import getContractAddress from "./utils/getContractAddress";
import { deployUpgradable } from "./utils/deployUpgradable";
import { HomeChains, isSkipped, isDevnet } from "./utils";
import { VRFSubscriptionManagerV2, VRFSubscriptionManagerV2Mock } from "../typechain-types";

const pnkByChain = new Map<HomeChains, string>([
[HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"],
Expand All @@ -18,12 +19,30 @@ const randomizerByChain = new Map<HomeChains, string>([

const daiByChain = new Map<HomeChains, string>([[HomeChains.ARBITRUM_ONE, "??"]]);
const wethByChain = new Map<HomeChains, string>([[HomeChains.ARBITRUM_ONE, "??"]]);
// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum
const linkByChain = new Map<HomeChains, string>([
[HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"],
[HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"],
]);

// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet
const keyHashByChain = new Map<HomeChains, string>([
[HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash
[HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"],
[HomeChains.HARDHAT, "0x0000000000000000000000000000000000000000000000000000000000000000"], // arbitrary value
]);

// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet
const vrfCoordinatorByChain = new Map<HomeChains, string>([
[HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"],
[HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"],
]);

const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
const { deploy, execute } = deployments;
const { AddressZero } = hre.ethers.constants;
const RNG_LOOKAHEAD = 20;
const RNG_FALLBACK = 150;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand All @@ -42,7 +61,18 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const erc20Address = await deployERC20AndFaucet(hre, deployer, "WETH");
wethByChain.set(HomeChains[HomeChains[chainId]], erc20Address);
}

if (chainId === HomeChains.HARDHAT) {
vrfCoordinatorByChain.set(
HomeChains.HARDHAT,
(
await deploy("VRFCoordinatorV2Mock", {
from: deployer,
args: [BigNumber.from(10).pow(18), 1000000000], // base_fee: 1 LINK, gas_price_link: 0.000000001 LINK per gas, from chainlink mock scripts
log: true,
})
).address
);
}
if (!randomizerByChain.get(chainId)) {
const randomizerMock = await deploy("RandomizerMock", {
from: deployer,
Expand Down Expand Up @@ -80,7 +110,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const maxFreezingTime = devnet ? 600 : 1800;
const sortitionModule = await deployUpgradable(deployments, "SortitionModule", {
from: deployer,
args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, RNG_LOOKAHEAD],
args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, RNG_FALLBACK],
log: true,
}); // nonce (implementation), nonce+1 (proxy)

Expand Down Expand Up @@ -119,6 +149,82 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", pnk, 12225583, 12);
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", dai, 60327783, 11);
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", weth, 1, 1);

const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; // LINK not needed on hardhat local node
const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero;
const requestConfirmations = 3; // Paramater to be fixed, range [1 ; 200] on Arbitrum
const callbackGasLimit = 100000; // Parameter to be fixed, 50000 on RandomizerRNG but no external call to sortitionModule.passPhase() in the callback
const numWords = 1;
const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero;
// Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks.
let vrfSubscriptionManager: DeployResult | string;
if (vrfCoordinator) {
vrfSubscriptionManager =
chainId === HomeChains.HARDHAT
? await deploy("VRFSubscriptionManagerV2Mock", {
from: deployer,
args: [deployer, vrfCoordinator],
log: true,
})
: await deploy("VRFSubscriptionManagerV2", {
from: deployer,
args: [deployer, vrfCoordinator, link],
log: true,
});
} else {
vrfSubscriptionManager = AddressZero;
}

// Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node
// The Sortition Module rng source is not changed to the VRF Consumer.
if (vrfSubscriptionManager) {
if (chainId === HomeChains.HARDHAT) {
const vrfSubscriptionManagerContract = (await hre.ethers.getContract(
"VRFSubscriptionManagerV2Mock"
)) as VRFSubscriptionManagerV2Mock;
await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK
const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId();
const vrfConsumer = await deployUpgradable(deployments, "VRFConsumerV2", {
from: deployer,
args: [
deployer,
vrfCoordinator,
sortitionModule.address,
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords,
AddressZero,
AddressZero,
],
log: true,
});
await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address);
}
} else {
const vrfSubscriptionManagerContract = (await hre.ethers.getContract(
"VRFSubscriptionManagerV2"
)) as VRFSubscriptionManagerV2;
const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId();
const vrfConsumer = await deployUpgradable(deployments, "VRFConsumerV2", {
from: deployer,
args: [
deployer,
vrfCoordinator,
sortitionModule.address,
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords,
AddressZero,
AddressZero,
],
log: true,
});
await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address);
}
};

deployArbitration.tags = ["Arbitration"];
Expand Down
5 changes: 2 additions & 3 deletions contracts/deploy/00-rng.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { SortitionModule, RandomizerRNG } from "../typechain-types";
import { SortitionModule } from "../typechain-types";
import { HomeChains, isSkipped } from "./utils";
import { deployUpgradable } from "./utils/deployUpgradable";

Expand All @@ -18,7 +18,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const { deployments, getNamedAccounts, getChainId } = hre;
const { deploy, execute } = deployments;
const { AddressZero } = hre.ethers.constants;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand Down Expand Up @@ -56,7 +55,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
});

const sortitionModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule;
await sortitionModule.changeRandomNumberGenerator(rng.address, RNG_LOOKAHEAD);
await sortitionModule.changeRandomNumberGenerator(rng.address);
};

deployArbitration.tags = ["RNG"];
Expand Down
4 changes: 2 additions & 2 deletions contracts/deploy/upgrade-sortition-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum HomeChains {

const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts, getChainId } = hre;
const RNG_LOOKAHEAD = 20;
const RNG_FALLBACK = 150;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand All @@ -32,7 +32,7 @@ const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeE
1800, // minStakingTime
1800, // maxFreezingTime
rng.address,
RNG_LOOKAHEAD,
RNG_FALLBACK,
],
});
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "hardhat-docgen";
import "hardhat-contract-sizer";
import "hardhat-tracer";
require("./scripts/simulations/tasks");
require("./scripts/creditLink");

dotenv.config();

Expand Down
55 changes: 55 additions & 0 deletions contracts/scripts/creditLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { LinkTokenInterface, VRFSubscriptionManagerV2 } from "../typechain-types";
import { task } from "hardhat/config";
import { BigNumber } from "ethers";

enum HomeChains {
ARBITRUM_ONE = 42161,
ARBITRUM_GOERLI = 421613,
}

// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum
const linkByChain = new Map<HomeChains, string>([
[HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"],
[HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"],
]);

const ONE_LINK_WEI = BigNumber.from(10).pow(18);

task("credit-link", "Credit LINK tokens to the current Chainlink VRF Subscription")
.addParam("amount", "(in normal values, not wei!) The amount of LINK tokens to credit")
.setAction(async (taskArgs, hre) => {
const { deployments, getNamedAccounts, getChainId, ethers } = hre;
const { AddressZero } = ethers.constants;
const deployer = (await getNamedAccounts()).deployer ?? AddressZero;

const chainId = Number(await getChainId());
if (!HomeChains[chainId]) {
console.error(`Aborting: script is not compatible with ${chainId}`);
return;
} else {
console.log("Crediting %d LINK to %s with deployer %s", HomeChains[chainId], deployer);
}

const { amount } = taskArgs;
const amountInWei = BigNumber.from(amount).mul(ONE_LINK_WEI);

// Retrieve LINK token contract from artifact to interact with it
const linkTokenAddress = linkByChain.get(Number(await getChainId())) ?? AddressZero;
const linkTokenArtifact = await hre.artifacts.readArtifact("LinkTokenInterface");
const linkToken = (await ethers.getContractAtFromArtifact(
linkTokenArtifact,
linkTokenAddress
)) as LinkTokenInterface;

const vrfSubscriptionManagerDeployment = await deployments.get("VRFSubscriptionManagerV2");
const vrfSubscriptionManager = (await ethers.getContractAt(
"VRFSubscriptionManagerV2",
vrfSubscriptionManagerDeployment.address
)) as VRFSubscriptionManagerV2;

// Transfer LINK from deployer to the Subscription Manager
await linkToken.transfer(vrfSubscriptionManager.address, amountInWei);

// // Fund the subscription, sending `amount` LINK tokens
await vrfSubscriptionManager.topUpSubscription(amountInWei);
});
2 changes: 0 additions & 2 deletions contracts/scripts/simulations/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,6 @@ task("simulate:to-freezing-and-generating-phase", "Pass phase from 'staking' to
if (isNetworkLocal(hre)) {
const { sortition, randomizerMock, randomizerRng } = await getContracts(hre);
const { wallet } = await getWallet(hre, walletindex);
const numberOfBlocksToMine = Number(await sortition.rngLookahead());
await mineBlocks(numberOfBlocksToMine, hre.network);
await randomizerMock.connect(wallet).relay(randomizerRng.address, 0, utils.randomBytes(32));
}
});
Expand Down
44 changes: 28 additions & 16 deletions contracts/src/arbitration/SortitionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors.
RNG public rng; // The random number generator.
uint256 public randomNumber; // Random number returned by RNG.
uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number.
uint256 public rngFallbackTimeout; // Time after which RNG fallback will be used if no random number was received.
uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped.
uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped.
mapping(bytes32 => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys.
Expand Down Expand Up @@ -92,22 +92,22 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
/// @param _minStakingTime Minimal time to stake
/// @param _maxDrawingTime Time after which the drawing phase can be switched
/// @param _rng The random number generator.
/// @param _rngLookahead Lookahead value for rng.
/// @param _rngFallbackTimeout RNG fallback timeout in seconds.
function initialize(
address _governor,
KlerosCore _core,
uint256 _minStakingTime,
uint256 _maxDrawingTime,
RNG _rng,
uint256 _rngLookahead
uint256 _rngFallbackTimeout
) external reinitializer(1) {
governor = _governor;
core = _core;
minStakingTime = _minStakingTime;
maxDrawingTime = _maxDrawingTime;
lastPhaseChange = block.timestamp;
rng = _rng;
rngLookahead = _rngLookahead;
rngFallbackTimeout = _rngFallbackTimeout;
delayedStakeReadIndex = 1;
}

Expand Down Expand Up @@ -135,18 +135,22 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
maxDrawingTime = _maxDrawingTime;
}

/// @dev Changes the `_rng` and `_rngLookahead` storage variables.
/// @dev Changes the `_rng` storage variable.
/// @param _rng The new value for the `RNGenerator` storage variable.
/// @param _rngLookahead The new value for the `rngLookahead` storage variable.
function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor {
function changeRandomNumberGenerator(RNG _rng) external onlyByGovernor {
rng = _rng;
rngLookahead = _rngLookahead;
if (phase == Phase.generating) {
rng.requestRandomness(block.number + rngLookahead);
rng.requestRandomness(block.number);
randomNumberRequestBlock = block.number;
}
}

/// @dev Changes the `rngFallbackTimeout` storage variable.
/// @param _rngFallbackTimeout The new value for the `rngFallbackTimeout` storage variable.
function changeRNGFallbackTimeout(uint256 _rngFallbackTimeout) external onlyByGovernor {
rngFallbackTimeout = _rngFallbackTimeout;
}

// ************************************* //
// * State Modifiers * //
// ************************************* //
Expand All @@ -158,23 +162,31 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
"The minimum staking time has not passed yet."
);
require(disputesWithoutJurors > 0, "There are no disputes that need jurors.");
rng.requestRandomness(block.number + rngLookahead);
rng.requestRandomness(block.number);
randomNumberRequestBlock = block.number;
phase = Phase.generating;
lastPhaseChange = block.timestamp;
emit NewPhase(phase);
} else if (phase == Phase.generating) {
randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead);
require(randomNumber != 0, "Random number is not ready yet");
phase = Phase.drawing;
randomNumber = rng.receiveRandomness(randomNumberRequestBlock);
if (randomNumber == 0) {
if (block.number > randomNumberRequestBlock + rngFallbackTimeout) {
rng.receiveRandomnessFallback(block.number);
}
} else {
phase = Phase.drawing;
lastPhaseChange = block.timestamp;
emit NewPhase(phase);
}
} else if (phase == Phase.drawing) {
require(
disputesWithoutJurors == 0 || block.timestamp - lastPhaseChange >= maxDrawingTime,
"There are still disputes without jurors and the maximum drawing time has not passed yet."
);
phase = Phase.staking;
lastPhaseChange = block.timestamp;
emit NewPhase(phase);
}

lastPhaseChange = block.timestamp;
emit NewPhase(phase);
}

/// @dev Create a sortition sum tree at the specified key.
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/rng/BlockhashRNG.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ contract BlockHashRNG is RNG {
}
randomNumbers[_block] = randomNumber;
}

function receiveRandomnessFallback(uint256 _block) external {}
}
2 changes: 2 additions & 0 deletions contracts/src/rng/IncrementalNG.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ contract IncrementalNG is RNG {
return number++;
}
}

function receiveRandomnessFallback(uint256 _block) external {}
}
2 changes: 2 additions & 0 deletions contracts/src/rng/RNG.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ interface RNG {
/// @param _block Block the random number is linked to.
/// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead.
function receiveRandomness(uint256 _block) external returns (uint256 randomNumber);

function receiveRandomnessFallback(uint256 _block) external;
}
Loading