Skip to content

Commit

Permalink
Modify L2Endpoint contract for VRF/RR (#910)
Browse files Browse the repository at this point in the history
* Created requestRandomWords function for VRF on L2 Endpoint

* Created L2VRFConsumerMock smc, added full fil function on L2Enpoint

* test case request fulfill for L2 VRFConsumerMock

* checked submmiter for fulfillRandomword function

* Resolved some comments

* fixed test L2 Endpoint
  • Loading branch information
KelvinThai authored Nov 15, 2023
1 parent 23117a8 commit ee5c8e4
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 26 deletions.
154 changes: 136 additions & 18 deletions contracts/src/v0.1/L2endpoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,173 @@
pragma solidity ^0.8.16;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IL2Aggregator.sol";
import "./VRFConsumerBase.sol";

contract L2Endpoint is Ownable {
uint256 public aggregatorCount;
uint256 public submitterCount;
uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
uint256 public sAggregatorCount;
uint256 public sSubmitterCount;
uint64 sNonce;
bool private sReentrancyLock;
struct RequestInfo {
address owner;
uint32 callbackGasLimit;
}
mapping(address => bool) sSubmitters;
mapping(address => bool) sAggregators;

mapping(address => bool) submitters;
mapping(address => bool) aggregators;
mapping(uint256 => RequestInfo) internal sRequestDetail;

error InvalidSubmitter(address submitter);
error InvalidAggregator(address aggregator);
error Reentrant();

event SubmitterAdded(address newSubmitter);
event SubmitterRemoved(address newSubmitter);
event AggregatorAdded(address newAggregator);
event AggregatorRemoved(address newAggregator);
event Submitted(uint256 roundId, int256 submission);
event RandomWordsRequested(
bytes32 indexed keyHash,
uint256 requestId,
uint256 preSeed,
uint64 indexed accId,
uint32 callbackGasLimit,
uint32 numWords,
address indexed sender
);
event RandomWordsFulfilled(uint256 indexed requestId, uint256[] randomWords, bool success);

modifier nonReentrant() {
if (sReentrancyLock) {
revert Reentrant();
}
_;
}

function addAggregator(address _newAggregator) external onlyOwner {
if (aggregators[_newAggregator]) revert InvalidAggregator(_newAggregator);
aggregators[_newAggregator] = true;
aggregatorCount += 1;
if (sAggregators[_newAggregator]) revert InvalidAggregator(_newAggregator);
sAggregators[_newAggregator] = true;
sAggregatorCount += 1;
emit AggregatorAdded(_newAggregator);
}

function removeAggregator(address _aggregator) external onlyOwner {
if (!aggregators[_aggregator]) revert InvalidAggregator(_aggregator);
delete aggregators[_aggregator];
aggregatorCount -= 1;
if (!sAggregators[_aggregator]) revert InvalidAggregator(_aggregator);
delete sAggregators[_aggregator];
sAggregatorCount -= 1;
emit AggregatorRemoved(_aggregator);
}

function addSubmitter(address _newSubmitter) external onlyOwner {
if (submitters[_newSubmitter]) revert InvalidSubmitter(_newSubmitter);
submitters[_newSubmitter] = true;
submitterCount += 1;
if (sSubmitters[_newSubmitter]) revert InvalidSubmitter(_newSubmitter);
sSubmitters[_newSubmitter] = true;
sSubmitterCount += 1;
emit SubmitterAdded(_newSubmitter);
}

function removeSubmitter(address _submitter) external onlyOwner {
if (!submitters[_submitter]) revert InvalidSubmitter(_submitter);
delete submitters[_submitter];
submitterCount -= 1;
if (!sSubmitters[_submitter]) revert InvalidSubmitter(_submitter);
delete sSubmitters[_submitter];
sSubmitterCount -= 1;
emit SubmitterRemoved(_submitter);
}

function submit(uint256 _roundId, int256 _submission, address _aggregator) external {
if (!submitters[msg.sender]) revert InvalidSubmitter(msg.sender);
if (!aggregators[_aggregator]) revert InvalidAggregator(_aggregator);
if (!sSubmitters[msg.sender]) revert InvalidSubmitter(msg.sender);
if (!sAggregators[_aggregator]) revert InvalidAggregator(_aggregator);
IL2Aggregator(_aggregator).submit(_roundId, _submission);
emit Submitted(_roundId, _submission);
}

function computeRequestId(
bytes32 keyHash,
address sender,
uint64 accId,
uint64 nonce
) private pure returns (uint256, uint256) {
uint256 preSeed = uint256(keccak256(abi.encode(keyHash, sender, accId, nonce)));
uint256 requestId = uint256(keccak256(abi.encode(keyHash, preSeed)));
return (requestId, preSeed);
}

/**
* @dev calls target address with exactly gasAmount gas and data as calldata
* or reverts if at least gasAmount gas is not available.
*/
function callWithExactGas(
uint256 gasAmount,
address target,
bytes memory data
) internal returns (bool success) {
// solhint-disable-next-line no-inline-assembly
assembly {
let g := gas()
// Compute g -= GAS_FOR_CALL_EXACT_CHECK and check for underflow
// The gas actually passed to the callee is min(gasAmount, 63//64*gas available).
// We want to ensure that we revert if gasAmount > 63//64*gas available
// as we do not want to provide them with less, however that check itself costs
// gas. GAS_FOR_CALL_EXACT_CHECK ensures we have at least enough gas to be able
// to revert if gasAmount > 63//64*gas available.
if lt(g, GAS_FOR_CALL_EXACT_CHECK) {
revert(0, 0)
}
g := sub(g, GAS_FOR_CALL_EXACT_CHECK)
// if g - g//64 <= gasAmount, revert
// (we subtract g//64 because of EIP-150)
if iszero(gt(sub(g, div(g, 64)), gasAmount)) {
revert(0, 0)
}
// solidity calls check that a contract actually exists at the destination, so we do the same
if iszero(extcodesize(target)) {
revert(0, 0)
}
// call and return whether we succeeded. ignore return data
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength)
success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)
}
return success;
}

function requestRandomWords(
bytes32 keyHash,
uint64 accId,
uint32 callbackGasLimit,
uint32 numWords
) external nonReentrant returns (uint256) {
sNonce++;
(uint256 requestId, uint256 preSeed) = computeRequestId(keyHash, msg.sender, accId, sNonce);
sRequestDetail[requestId] = RequestInfo({
owner: msg.sender,
callbackGasLimit: callbackGasLimit
});
emit RandomWordsRequested(
keyHash,
requestId,
preSeed,
accId,
callbackGasLimit,
numWords,
msg.sender
);

return requestId;
}

function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) external nonReentrant {
if (!sSubmitters[msg.sender]) revert InvalidSubmitter(msg.sender);
RequestInfo memory detail = sRequestDetail[requestId];
bytes memory resp = abi.encodeWithSelector(
VRFConsumerBase.rawFulfillRandomWords.selector,
requestId,
randomWords
);
sReentrancyLock = true;
bool success = callWithExactGas(detail.callbackGasLimit, detail.owner, resp);
sReentrancyLock = false;
emit RandomWordsFulfilled(requestId, randomWords, success);
}
}
11 changes: 11 additions & 0 deletions contracts/src/v0.1/interfaces/IL2Endpoint.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

interface IL2Endpoint {
function requestRandomWords(
bytes32 keyHash,
uint64 accId,
uint32 callbackGasLimit,
uint32 numWords
) external returns (uint256);
}
45 changes: 45 additions & 0 deletions contracts/src/v0.1/mocks/L2VRFConsumerMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

import "../VRFConsumerBase.sol";
import "../interfaces/IL2Endpoint.sol";

contract L2VRFConsumerMock is VRFConsumerBase {
uint256 public sRandomWord;
address private sOwner;

IL2Endpoint L2ENDPOINT;

error OnlyOwner(address notOwner);

modifier onlyOwner() {
if (msg.sender != sOwner) {
revert OnlyOwner(msg.sender);
}
_;
}

constructor(address l2Endpoint) VRFConsumerBase(l2Endpoint) {
sOwner = msg.sender;
L2ENDPOINT = IL2Endpoint(l2Endpoint);
}

// Receive remaining payment from requestRandomWordsPayment
receive() external payable {}

function requestRandomWords(
bytes32 keyHash,
uint64 accId,
uint32 callbackGasLimit,
uint32 numWords
) public onlyOwner returns (uint256 requestId) {
requestId = L2ENDPOINT.requestRandomWords(keyHash, accId, callbackGasLimit, numWords);
}

function fulfillRandomWords(
uint256 /* requestId */,
uint256[] memory randomWords
) internal override {
sRandomWord = (randomWords[0] % 50) + 1;
}
}
16 changes: 8 additions & 8 deletions contracts/test/v0.1/non-vrf/L2Endpoint.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,19 @@ describe('L2Endpoint', function () {
await endpoint.contract.addAggregator(aggregator.contract.address)
await endpoint.contract.addSubmitter(endpoint.signer.address)

let aggreatorCount = await endpoint.contract.aggregatorCount()
let aggreatorCount = await endpoint.contract.sAggregatorCount()
expect(aggreatorCount).to.be.equal(1)

let submitterCount = await endpoint.contract.submitterCount()
let submitterCount = await endpoint.contract.sSubmitterCount()
expect(submitterCount).to.be.equal(1)

await endpoint.contract.removeAggregator(aggregator.contract.address)
await endpoint.contract.removeSubmitter(endpoint.signer.address)

aggreatorCount = await endpoint.contract.aggregatorCount()
aggreatorCount = await endpoint.contract.sAggregatorCount()
expect(aggreatorCount).to.be.equal(0)

submitterCount = await endpoint.contract.submitterCount()
submitterCount = await endpoint.contract.sSubmitterCount()
expect(submitterCount).to.be.equal(0)
})

Expand All @@ -127,10 +127,10 @@ describe('L2Endpoint', function () {
endpoint.contract.addSubmitter(endpoint.signer.address)
).to.be.revertedWithCustomError(endpoint.contract, 'InvalidSubmitter')

let aggreatorCount = await endpoint.contract.aggregatorCount()
let aggreatorCount = await endpoint.contract.sAggregatorCount()
expect(aggreatorCount).to.be.equal(1)

let submitterCount = await endpoint.contract.submitterCount()
let submitterCount = await endpoint.contract.sSubmitterCount()
expect(submitterCount).to.be.equal(1)

await endpoint.contract.removeAggregator(aggregator.contract.address)
Expand All @@ -143,10 +143,10 @@ describe('L2Endpoint', function () {
endpoint.contract.removeSubmitter(endpoint.signer.address)
).to.be.revertedWithCustomError(endpoint.contract, 'InvalidSubmitter')

aggreatorCount = await endpoint.contract.aggregatorCount()
aggreatorCount = await endpoint.contract.sAggregatorCount()
expect(aggreatorCount).to.be.equal(0)

submitterCount = await endpoint.contract.submitterCount()
submitterCount = await endpoint.contract.sSubmitterCount()
expect(submitterCount).to.be.equal(0)
})

Expand Down
73 changes: 73 additions & 0 deletions contracts/test/v0.1/vrf/L2VRFConsumerMock.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { expect } = require('chai')
const { ethers } = require('hardhat')
const { time, loadFixture } = require('@nomicfoundation/hardhat-network-helpers')

const { createSigners } = require('../utils.cjs')
const { vrfConfig } = require('./VRFCoordinator.config.cjs')

async function deploy() {
const {
account0: deployerSigner,
account1: consumerSigner,
account2,
account3,
account4,
account5
} = await createSigners()

// L2 endpoint
let l2EndpointContract = await ethers.getContractFactory('L2Endpoint', { signer: deployerSigner })
l2EndpointContract = await l2EndpointContract.deploy()
await l2EndpointContract.deployed()

const endpoint = {
contract: l2EndpointContract,
signer: deployerSigner
}

// L2 consumer
let l2VRFConsumerMock = await ethers.getContractFactory('L2VRFConsumerMock', {
signer: deployerSigner
})
l2VRFConsumerMock = await l2VRFConsumerMock.deploy(l2EndpointContract.address)
await l2VRFConsumerMock.deployed()

const consumer = {
contract: l2VRFConsumerMock,
signer: deployerSigner
}

return {
endpoint,
consumer,
account2,
account3,
account4,
account5
}
}

describe('Consumer', function () {
it('Request and fullfil', async function () {
const { consumer, endpoint } = await loadFixture(deploy)
const { maxGasLimit: callbackGasLimit, keyHash } = vrfConfig()
const accMock = 1
const numWords = 1
const txRequestRandomWords = await (
await consumer.contract.requestRandomWords(keyHash, accMock, callbackGasLimit, numWords)
).wait()
const event = endpoint.contract.interface.parseLog(txRequestRandomWords.events[0])
expect(event.name).to.be.equal('RandomWordsRequested')
const { requestId } = event.args

const randomWords = [1]
await expect(
endpoint.contract.fulfillRandomWords(requestId, randomWords)
).revertedWithCustomError(endpoint.contract, 'InvalidSubmitter')

await (await endpoint.contract.addSubmitter(endpoint.signer.address)).wait()
await (await endpoint.contract.fulfillRandomWords(requestId, randomWords)).wait()
const result = await consumer.contract.sRandomWord()
expect(result).to.be.equal((randomWords % 50) + 1)
})
})

0 comments on commit ee5c8e4

Please sign in to comment.