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

Cross-chain VRF request-response scheme via ICM #56

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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
[submodule "lib/avalanche-interchain-token-transfer"]
path = lib/avalanche-interchain-token-transfer
url = https://github.com/ava-labs/avalanche-interchain-token-transfer
[submodule "lib/chainlink"]
path = lib/chainlink
url = https://github.com/smartcontractkit/chainlink
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// (c) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: Ecosystem

pragma solidity ^0.8.4;

import "@chainlink/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol";
import "@teleporter/ITeleporterReceiver.sol";
import "@teleporter/ITeleporterMessenger.sol";

contract CrossChainVRFConsumer is ITeleporterReceiver {

ITeleporterMessenger public teleporterMessenger;
address public vrfRequesterContract;

bytes32 constant DATASOURCE_BLOCKCHAIN_ID = 0x7fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d5;

struct CrossChainRequest {
bytes32 keyHash;
uint16 requestConfirmations;
uint32 callbackGasLimit;
uint32 numWords;
bool nativePayment;
}

struct CrossChainResponse {
uint256 requestId;
uint256[] randomWords;
}

event RandomWordsReceived(uint256 requestId);

constructor(address _teleporterMessenger, address _vrfRequesterContract) {
teleporterMessenger = ITeleporterMessenger(_teleporterMessenger);
vrfRequesterContract = _vrfRequesterContract;
}

function requestRandomWords(
bytes32 keyHash,
uint16 requestConfirmations,
uint32 callbackGasLimit,
uint32 numWords,
bool nativePayment,
uint32 requiredGasLimit
) external {
// Create CrossChainRequest struct
CrossChainRequest memory crossChainRequest = CrossChainRequest({
keyHash: keyHash,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: numWords,
nativePayment: nativePayment
});
// Send Teleporter message
bytes memory encodedMessage = abi.encode(crossChainRequest);
TeleporterMessageInput memory messageInput = TeleporterMessageInput({
destinationBlockchainID: DATASOURCE_BLOCKCHAIN_ID,
destinationAddress: vrfRequesterContract,
feeInfo: TeleporterFeeInfo({ feeTokenAddress: address(0), amount: 0 }),
requiredGasLimit: requiredGasLimit,
allowedRelayerAddresses: new address[](0),
message: encodedMessage
});
teleporterMessenger.sendCrossChainMessage(messageInput);
}

function receiveTeleporterMessage(
bytes32 originChainID,
address originSenderAddress,
bytes calldata message
) external {
require(originChainID == DATASOURCE_BLOCKCHAIN_ID, "Invalid originChainID");
require(msg.sender == address(teleporterMessenger), "Caller is not the TeleporterMessenger");
require(originSenderAddress == vrfRequesterContract, "Invalid sender");

// Decode the message to get the request ID and random words
CrossChainResponse memory response = abi.decode(message, (CrossChainResponse));

// Fulfill the request by calling the internal function
fulfillRandomWords(response.requestId, response.randomWords);
}

function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal {
// Logic to handle the fulfillment of random words
// Implement your custom logic here

// Emit event for received random words
emit RandomWordsReceived(requestId);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// (c) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: Ecosystem

pragma solidity ^0.8.4;

import "@chainlink/vrf/dev/VRFConsumerBaseV2Plus.sol";
import "@chainlink/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol";
import "@chainlink/vrf/dev/libraries/VRFV2PlusClient.sol";
import "@teleporter/ITeleporterReceiver.sol";
import "@teleporter/ITeleporterMessenger.sol";

contract CrossChainVRFWrapper is ITeleporterReceiver, VRFConsumerBaseV2Plus {

ITeleporterMessenger public teleporterMessenger;

struct SubscriptionInfo {
uint256 subscriptionId;
bool isAuthorized;
}
mapping(address => SubscriptionInfo) public authorizedSubscriptions;

// Avalanche Fuji VRF 2.5 Coordinator:
address s_vrfCoordinatorAddress = 0x5C210eF41CD1a72de73bF76eC39637bB0d3d7BEE;

struct CrossChainRequest {
bytes32 keyHash;
uint16 requestConfirmations;
uint32 callbackGasLimit;
uint32 numWords;
bool nativePayment;
}

struct CrossChainResponse {
uint256 requestId;
uint256[] randomWords;
}

struct CrossChainReceiver {
bytes32 destinationBlockchainId;
address destinationAddress;
}
mapping(uint256 => CrossChainReceiver) public pendingRequests;

constructor(address _teleporterMessenger) VRFConsumerBaseV2Plus(s_vrfCoordinatorAddress) {
teleporterMessenger = ITeleporterMessenger(_teleporterMessenger);
}

function receiveTeleporterMessage(
bytes32 originChainID,
address originSenderAddress,
bytes calldata message
) external {
require(msg.sender == address(teleporterMessenger), "Caller is not the TeleporterMessenger");
// Verify that the origin sender address is authorized
require(authorizedSubscriptions[originSenderAddress].isAuthorized, "Origin sender is not authorized");
uint256 subscriptionId = authorizedSubscriptions[originSenderAddress].subscriptionId;
// Verify that the subscription ID belongs to the correct owner
(,,,, address[] memory consumers) = s_vrfCoordinator.getSubscription(subscriptionId);
// Check wrapper contract is a consumer of the subscription
bool isConsumer = false;
for (uint256 i = 0; i < consumers.length; i++) {
if (consumers[i] == address(this)) {
isConsumer = true;
break;
}
}
require(isConsumer, "Contract is not a consumer of this subscription");
// Decode message to get the VRF parameters
CrossChainRequest memory vrfMessage = abi.decode(message, (CrossChainRequest));
// Request random words
VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({
keyHash: vrfMessage.keyHash,
subId: subscriptionId,
requestConfirmations: vrfMessage.requestConfirmations,
callbackGasLimit: vrfMessage.callbackGasLimit,
numWords: vrfMessage.numWords,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: vrfMessage.nativePayment}))
});
uint256 requestId = s_vrfCoordinator.requestRandomWords(req);
pendingRequests[requestId] = CrossChainReceiver({
destinationBlockchainId: originChainID,
destinationAddress: originSenderAddress
});
}

function addAuthorizedAddress(address caller, uint256 subscriptionId) external {
// Verify that the subscription ID belongs to the correct owner
(,,, address owner,) = s_vrfCoordinator.getSubscription(subscriptionId);
require(owner == msg.sender, "Origin sender is not the owner of the subscription");
// Add subscription
authorizedSubscriptions[caller] = SubscriptionInfo({
subscriptionId: subscriptionId,
isAuthorized: true
});
}

function removeAuthorizedAddress(address _address) external {
require(authorizedSubscriptions[_address].isAuthorized, "Address is not authorized");
uint256 subscriptionId = authorizedSubscriptions[_address].subscriptionId;
// Verify that the subscription ID belongs to the correct owner
(,,, address owner,) = s_vrfCoordinator.getSubscription(subscriptionId);
require(owner == msg.sender, "Origin sender is not the owner of the subscription");
delete authorizedSubscriptions[_address];
}

function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
require(pendingRequests[requestId].destinationAddress != address(0), "Invalid request ID");
// Create CrossChainResponse struct
CrossChainResponse memory crossChainResponse = CrossChainResponse({
requestId: requestId,
randomWords: randomWords
});
bytes memory encodedMessage = abi.encode(crossChainResponse);
// Send cross chain message using ITeleporterMessenger interface
TeleporterMessageInput memory messageInput = TeleporterMessageInput({
destinationBlockchainID: pendingRequests[requestId].destinationBlockchainId,
destinationAddress: pendingRequests[requestId].destinationAddress,
feeInfo: TeleporterFeeInfo({ feeTokenAddress: address(0), amount: 0 }),
requiredGasLimit: 100000,
allowedRelayerAddresses: new address[](0),
message: encodedMessage
});
teleporterMessenger.sendCrossChainMessage(messageInput);
delete pendingRequests[requestId];
}
}
1 change: 1 addition & 0 deletions lib/chainlink
Submodule chainlink added at 5ebb63
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ forge-std/=lib/forge-std/src/
@openzeppelin/[email protected]/=lib/openzeppelin-contracts/contracts/
@avalabs/[email protected]/=lib/teleporter/contracts/lib/subnet-evm/contracts/
@teleporter/=lib/teleporter/contracts/src/Teleporter/
@avalanche-interchain-token-transfer/=lib/avalanche-interchain-token-transfer/contracts/src/
@avalanche-interchain-token-transfer/=lib/avalanche-interchain-token-transfer/contracts/src/
@chainlink/=lib/chainlink/contracts/src/v0.8/