Browse Source

CCQ: EVM Library and Demo Contract

Bruce Riley 2 years ago
parent
commit
faea3bab8d

+ 103 - 0
ethereum/contracts/query/QueryDemo.sol

@@ -0,0 +1,103 @@
+// contracts/query/QueryDemo.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../libraries/external/BytesLib.sol";
+import "../interfaces/IWormhole.sol";
+import "@openzeppelin/contracts/access/Ownable.sol";
+import "@openzeppelin/contracts/utils/Context.sol";
+import "./QueryResponse.sol";
+
+/// @dev QueryDemo is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses.
+contract QueryDemo is Context, QueryResponse {
+    using BytesLib for bytes;
+
+    struct ChainEntry {
+        uint16 chainID;
+        address contractAddress;
+        uint256 counter;
+        uint256 blockNum;
+        uint256 blockTime;
+    }
+
+    address private immutable owner;
+    address private immutable wormhole;
+    uint16 private immutable myChainID;
+    mapping(uint16 => ChainEntry) private counters;
+    uint16[] private foreignChainIDs;
+
+    bytes4 GetMyCounter = bytes4(hex"916d5743");
+
+    constructor(address _owner, address _wormhole, uint16 _myChainID) {
+        owner = _owner;
+        wormhole = _wormhole;  
+        myChainID = _myChainID;
+        counters[_myChainID] = ChainEntry(_myChainID, address(this), 0, 0, 0);
+    }
+
+    // updateRegistration should be used to add the other chains and to set / update contract addresses.
+    function updateRegistration(uint16 _chainID, address _contractAddress) public onlyOwner {
+        if (counters[_chainID].chainID == 0) {
+            foreignChainIDs.push(_chainID);
+            counters[_chainID].chainID = _chainID;
+        }
+
+        counters[_chainID].contractAddress = _contractAddress;
+    }
+
+    // getMyCounter (call signature 916d5743) returns the counter value for this chain. It is meant to be used in a cross chain query.
+    function getMyCounter() public view returns (uint256) {
+        return counters[myChainID].counter;
+    }
+
+    // getState() returns this chain's view of all the counters. It is meant to be used in the front end.
+    function getState() public view returns (ChainEntry[] memory) {
+        ChainEntry[] memory ret = new ChainEntry[](foreignChainIDs.length + 1);
+        ret[0] = counters[myChainID];
+        for (uint idx=0; idx<foreignChainIDs.length; idx++) {
+            ret[idx+1] = counters[foreignChainIDs[idx]];
+        }      
+
+        return ret;
+    }
+
+    // updateCounters takes the cross chain query response for the two other counters, stores the results for the other chains, and updates the counter for this chain.
+    function updateCounters(bytes memory response, IWormhole.Signature[] memory signatures) public {
+        uint256 adjustedBlockTime;
+        ParsedQueryResponse memory r = parseAndVerifyQueryResponse(address(wormhole), response, signatures);
+        require(r.responses.length == foreignChainIDs.length, "unexpected number of results");
+        for (uint idx=0; idx<r.responses.length; idx++) {
+            require(counters[r.responses[idx].chainId].chainID == foreignChainIDs[idx], "unexpected foreign chain ID");
+            EthCallQueryResponse memory eqr = parseEthCallQueryResponse(r.responses[idx]);
+            require(eqr.blockNum > counters[r.responses[idx].chainId].blockNum, "update is obsolete");
+            // wormhole time is in microseconds, timestamp is in seconds
+            adjustedBlockTime = eqr.blockTime / 1_000_000;
+            require(adjustedBlockTime > block.timestamp - 300, "update is stale");
+            require(eqr.result.length == 1, "result mismatch");
+            require(eqr.result[0].contractAddress == counters[r.responses[idx].chainId].contractAddress, "contract address is wrong");
+
+            // TODO: Is there an easier way to verify that the call data is correct!
+            bytes memory callData = eqr.result[0].callData;
+            bytes4 result;
+            assembly {
+                    result := mload(add(callData, 32))
+            }
+            require(result == GetMyCounter, "unexpected callData");
+
+            require(eqr.result[0].result.length == 32, "result is not a uint256");
+            counters[r.responses[idx].chainId].blockNum = eqr.blockNum;
+            counters[r.responses[idx].chainId].blockTime = adjustedBlockTime;
+            counters[r.responses[idx].chainId].counter = abi.decode(eqr.result[0].result, (uint256));
+        }
+
+        counters[myChainID].blockNum = block.number;
+        counters[myChainID].blockTime = block.timestamp;
+        counters[myChainID].counter += 1;
+    }
+
+    modifier onlyOwner() {
+        require(owner == _msgSender(), "caller is not the owner");
+        _;
+    }
+}

+ 246 - 0
ethereum/contracts/query/QueryResponse.sol

@@ -0,0 +1,246 @@
+// contracts/query/QueryResponse.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import {BytesParsing} from "../relayer/libraries/BytesParsing.sol";
+import "../interfaces/IWormhole.sol";
+
+/// @dev QueryResponse is a library that implements the parsing and verification of Cross Chain Query (CCQ) responses.
+abstract contract QueryResponse {
+    using BytesParsing for bytes;
+
+    // Custom errors
+    error InvalidResponseVersion();
+    error VersionMismatch();
+    error NumberOfResponsesMismatch();
+    error ChainIdMismatch();
+    error RequestTypeMismatch();
+    error UnsupportedQueryType();
+    error UnexpectedNumberOfResults();
+    error InvalidPayloadLength(uint256 received, uint256 expected);
+       
+    /// @dev ParsedQueryResponse is returned by parseAndVerifyQueryResponse().
+    struct ParsedQueryResponse {
+        uint8   version;
+        uint16  senderChainId;
+        uint32  nonce;
+        bytes   requestId; // 65 byte sig for off-chain, 32 byte vaaHash for on-chain
+        ParsedPerChainQueryResponse [] responses;
+    }
+
+    /// @dev ParsedPerChainQueryResponse describes a single per-chain response.
+    struct ParsedPerChainQueryResponse {
+        uint16 chainId;
+        uint8 queryType;
+        bytes request;
+        bytes response;
+    }
+
+    /// @dev EthCallQueryResponse describes an ETH call per-chain query.
+    struct EthCallQueryResponse {
+        bytes requestBlockId;
+        uint64 blockNum;
+        uint64 blockTime;
+        bytes32 blockHash;
+        EthCallData [] result;
+    }
+
+    /// @dev EthCallData describes a single ETH call query / response pair.
+    struct EthCallData {
+        address contractAddress;
+        bytes callData;
+        bytes result;
+    }    
+
+    bytes public constant responsePrefix = bytes("query_response_0000000000000000000|");
+    uint8 public constant QT_ETH_CALL = 1;
+
+    /// @dev getResponseHash computes the hash of the specified query response.
+    function getResponseHash(bytes memory response) public pure returns (bytes32) {
+        return keccak256(response);
+    }
+
+    /// @dev getResponseDigest computes the digest of the specified query response.
+    function getResponseDigest(bytes memory response) public pure returns (bytes32) {
+        return keccak256(abi.encodePacked(responsePrefix,getResponseHash(response)));
+    }
+    
+    /// @dev parseAndVerifyQueryResponse verifies the query response and returns the parsed response.
+    function parseAndVerifyQueryResponse(address wormhole, bytes memory response, IWormhole.Signature[] memory signatures) public view returns (ParsedQueryResponse memory r) {
+        verifyQueryResponseSignatures(wormhole, response, signatures);
+
+        uint index = 0;
+        
+        (r.version, index) = response.asUint8Unchecked(index);
+        if (r.version != 1) {
+            revert InvalidResponseVersion();
+        }
+
+        (r.senderChainId, index) = response.asUint16Unchecked(index);
+
+        if (r.senderChainId == 0) {
+            (r.requestId, index) = response.sliceUnchecked(index, 65);
+        } else {
+            (r.requestId, index) = response.sliceUnchecked(index, 32);
+        }
+        
+        uint32 len;
+        (len, index) = response.asUint32Unchecked(index); // query_request_len
+        uint reqIdx = index;
+
+        uint8 version;
+        (version, reqIdx) = response.asUint8Unchecked(reqIdx);
+        if (version != r.version) {
+            revert VersionMismatch();
+        }
+
+        (r.nonce, reqIdx) = response.asUint32Unchecked(reqIdx);
+
+        uint8 numPerChainQueries;
+        (numPerChainQueries, reqIdx) = response.asUint8Unchecked(reqIdx);
+
+        // The response starts after the request.
+        uint respIdx = index + len;
+
+        uint8 respNumPerChainQueries;
+        (respNumPerChainQueries, respIdx) = response.asUint8Unchecked(respIdx);
+        if (respNumPerChainQueries != numPerChainQueries) {
+            revert NumberOfResponsesMismatch();
+        }
+
+        r.responses = new ParsedPerChainQueryResponse[](numPerChainQueries);
+
+        // Walk through the requests and responses in lock step.
+        for (uint idx = 0; idx < numPerChainQueries;) {
+            (r.responses[idx].chainId, reqIdx) = response.asUint16Unchecked(reqIdx);
+            uint16 respChainId;
+            (respChainId, respIdx) = response.asUint16Unchecked(respIdx);
+            if (respChainId != r.responses[idx].chainId) {
+                revert ChainIdMismatch();
+            }
+
+            (r.responses[idx].queryType, reqIdx) = response.asUint8Unchecked(reqIdx);
+            uint8 respQueryType;
+            (respQueryType, respIdx) = response.asUint8Unchecked(respIdx);
+            if (respQueryType != r.responses[idx].queryType) {
+                revert RequestTypeMismatch();
+            }
+            
+            if (r.responses[idx].queryType != QT_ETH_CALL) {
+                revert UnsupportedQueryType();
+            }
+
+            (len, reqIdx) = response.asUint32Unchecked(reqIdx);
+            (r.responses[idx].request, reqIdx) = response.sliceUnchecked(reqIdx, len);
+
+            (len, respIdx) = response.asUint32Unchecked(respIdx);
+            (r.responses[idx].response, respIdx) = response.sliceUnchecked(respIdx, len);
+
+            unchecked { ++idx; }
+        }
+
+        checkLength(response, respIdx);
+        return r;
+    }
+
+    /// @dev parseEthCallQueryResponse parses a ParsedPerChainQueryResponse for an ETH call per-chain query.
+    function parseEthCallQueryResponse(ParsedPerChainQueryResponse memory pcr) public pure returns (EthCallQueryResponse memory r) {
+        if (pcr.queryType != QT_ETH_CALL) {
+                revert UnsupportedQueryType();
+        }
+
+        uint reqIdx = 0;
+        uint respIdx = 0;
+
+        uint32 len;
+        (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // block_id_len
+
+        (r.requestBlockId, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len);
+
+        uint8 numBatchCallData;
+        (numBatchCallData, reqIdx) = pcr.request.asUint8Unchecked(reqIdx);
+
+        (r.blockNum, respIdx) = pcr.response.asUint64Unchecked(respIdx);
+
+        (r.blockHash, respIdx) = pcr.response.asBytes32Unchecked(respIdx);
+
+        (r.blockTime, respIdx) = pcr.response.asUint64Unchecked(respIdx);
+
+        uint8 respNumResults;
+        (respNumResults, respIdx) = pcr.response.asUint8Unchecked(respIdx);
+        if (respNumResults != numBatchCallData) {
+                revert UnexpectedNumberOfResults();
+        }
+
+        r.result = new EthCallData[](numBatchCallData);
+
+        // Walk through the call data and results in lock step.
+        for (uint idx = 0; idx < numBatchCallData;) {
+            (r.result[idx].contractAddress, reqIdx) = pcr.request.asAddressUnchecked(reqIdx);
+
+            (len, reqIdx) = pcr.request.asUint32Unchecked(reqIdx); // call_data_len
+            (r.result[idx].callData, reqIdx) = pcr.request.sliceUnchecked(reqIdx, len);
+
+            (len, respIdx) = pcr.response.asUint32Unchecked(respIdx); // result_len
+            (r.result[idx].result, respIdx) = pcr.response.sliceUnchecked(respIdx, len);
+
+            unchecked { ++idx; }
+        }
+
+        checkLength(pcr.request, reqIdx);
+        checkLength(pcr.response, respIdx);
+        return r;
+    }
+
+    /**
+     * @dev verifyQueryResponseSignatures verifies the signatures on a query response. It calls into the Wormhole contract.
+     * IWormhole.Signature expects the last byte to be bumped by 27 
+     * see https://github.com/wormhole-foundation/wormhole/blob/637b1ee657de7de05f783cbb2078dd7d8bfda4d0/ethereum/contracts/Messages.sol#L174
+     */
+    function verifyQueryResponseSignatures(address _wormhole, bytes memory response, IWormhole.Signature[] memory signatures) public view {
+        IWormhole wormhole = IWormhole(_wormhole);
+        // TODO: make a verifyCurrentQuorum call on the core bridge so that there is only 1 cross call instead of 4
+        uint32 gsi = wormhole.getCurrentGuardianSetIndex();
+        IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(gsi);
+
+        bytes32 responseHash = getResponseDigest(response);
+
+       /**
+        * @dev Checks whether the guardianSet has zero keys
+        * WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure
+        * that guardianSet key size doesn't fall to zero and negatively impact quorum assessment.  If guardianSet
+        * key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and
+        * signature verification.
+        */
+        if(guardianSet.keys.length == 0){
+            revert("invalid guardian set");
+        }
+
+       /**
+        * @dev We're using a fixed point number transformation with 1 decimal to deal with rounding.
+        *   WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM
+        *   if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and
+        *   vm.signatures length is 0, this could compromise the integrity of both vm and signature verification.
+        */
+        if (signatures.length < wormhole.quorum(guardianSet.keys.length)){
+            revert("no quorum");
+        }
+
+        /// @dev Verify the proposed vm.signatures against the guardianSet
+        (bool signaturesValid, string memory invalidReason) = wormhole.verifySignatures(responseHash, signatures, guardianSet);
+        if(!signaturesValid){
+            revert(invalidReason);
+        }
+
+        /// If we are here, we've validated the VM is a valid multi-sig that matches the current guardianSet.
+    }
+
+    /// @dev checkLength verifies that the message was fully consumed.
+    function checkLength(bytes memory encoded, uint256 expected) private pure {
+        if (encoded.length != expected) {
+            revert InvalidPayloadLength(encoded.length, expected);
+        }
+    }
+}
+

+ 146 - 0
ethereum/forge-test/query/Query.t.sol

@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache 2
+
+// forge test --match-contract QueryResponse
+
+pragma solidity ^0.8.0;
+
+import "../../contracts/query/QueryResponse.sol";
+import "../../contracts/Implementation.sol";
+import "../../contracts/Setup.sol";
+import "../../contracts/Wormhole.sol";
+import "forge-std/Test.sol";
+
+contract TestQueryResponse is Test, QueryResponse {
+    bytes resp = hex"010000ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f2000000005301dd9914c6010005010000004600000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd01000501000000b90000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a";
+
+    bytes32 sigR = hex"ba36cd576a0f9a8a37ec5ea6a174857922f2f170cd7ec62edcbe74b1cc7258d3";
+    bytes32 sigS = hex"01e8690cfd627e608d63b5d165e2190ba081bb84f5cf473fd353109e152f72fa";
+    uint8 sigV = 27; // last byte plus magic 27
+    uint8 sigGuardianIndex = 0;
+
+    bytes32 expectedHash = 0xed18e80906ffa80ce953a132a9cbbcf84186955f8fc8ce0322cd68622a58570e;
+    bytes32 expectedDigetst = 0x5b84b19c68ee0b37899230175a92ee6eda4c5192e8bffca1d057d811bb3660e2;
+
+    Wormhole wormhole;
+
+    function setUp() public {
+        wormhole = deployWormholeForTest();
+    }
+
+    uint16 constant TEST_CHAIN_ID = 2;
+    address constant DEVNET_GUARDIAN = 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe;
+    uint16 constant GOVERNANCE_CHAIN_ID = 1;
+    bytes32 constant GOVERNANCE_CONTRACT = 0x0000000000000000000000000000000000000000000000000000000000000004;
+
+    function deployWormholeForTest() public returns (Wormhole) {
+        // Deploy the Setup contract.
+        Setup setup = new Setup();
+
+        // Deploy the Implementation contract.
+        Implementation implementation = new Implementation();
+
+        address[] memory guardians = new address[](1);
+        guardians[0] = DEVNET_GUARDIAN;
+
+        // Deploy the Wormhole contract.
+        wormhole = new Wormhole(
+            address(setup),
+            abi.encodeWithSelector(
+                bytes4(keccak256("setup(address,address[],uint16,uint16,bytes32,uint256)")),
+                address(implementation),
+                guardians,
+                TEST_CHAIN_ID,
+                GOVERNANCE_CHAIN_ID,
+                GOVERNANCE_CONTRACT,
+                block.chainid // evm chain id
+            )
+        );
+
+        return wormhole;
+    }
+
+    function test_getResponseHash() public {
+        bytes32 hash = getResponseHash(resp);
+        assertEq(hash, expectedHash);
+    }
+
+    function test_getResponseDigest() public {
+        bytes32 digest = getResponseDigest(resp);
+        assertEq(digest, expectedDigetst);
+    }
+
+    function test_verifyQueryResponseSignatures() public view {
+        IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1);
+        signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex});
+        verifyQueryResponseSignatures(address(wormhole), resp, signatures);
+    }
+
+    function test_parseAndVerifyQueryResponse() public {
+        IWormhole.Signature[] memory signatures = new IWormhole.Signature[](1);
+        signatures[0] = IWormhole.Signature({r: sigR, s: sigS, v: sigV, guardianIndex: sigGuardianIndex});
+        ParsedQueryResponse memory r = parseAndVerifyQueryResponse(address(wormhole), resp, signatures);
+        assertEq(r.version, 1);
+        assertEq(r.senderChainId, 0);
+        assertEq(r.requestId, hex"ff0c222dc9e3655ec38e212e9792bf1860356d1277462b6bf747db865caca6fc08e6317b64ee3245264e371146b1d315d38c867fe1f69614368dc4430bb560f200");
+        assertEq(r.nonce, 3717797062);
+        assertEq(r.responses.length, 1);
+        assertEq(r.responses[0].chainId, 5);
+        assertEq(r.responses[0].queryType, 1);
+        assertEq(r.responses[0].request, hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd");
+        assertEq(r.responses[0].response, hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a");
+    }
+
+    function test_parseEthCallQueryResponse() public {
+        // Take the data extracted by the previous test and break it down even further.
+        ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({
+            chainId: 5,
+            queryType: 1,
+            request: hex"00000009307832613631616334020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd",
+            response: hex"0000000002a61ac4c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d040005ff312e4f90c002000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000000000200000000000000000000000000000000000000000007ae5649beabeddf889364a"
+            });
+
+        EthCallQueryResponse memory eqr = parseEthCallQueryResponse(r);
+        assertEq(eqr.requestBlockId, hex"307832613631616334");
+        assertEq(eqr.blockNum, 44440260);
+        assertEq(eqr.blockHash, hex"c1adff9f6e180309e7d0d94c063338ddc61c1c4474cd6957c960efe659534d04");
+        assertEq(eqr.blockTime, 1687961579000000);
+        assertEq(eqr.result.length, 2);
+
+        assertEq(eqr.result[0].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270));
+        assertEq(eqr.result[0].callData, hex"06fdde03");
+        assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d57726170706564204d6174696300000000000000000000000000000000000000");
+
+        assertEq(eqr.result[1].contractAddress, address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270));
+        assertEq(eqr.result[1].callData, hex"18160ddd");
+        assertEq(eqr.result[1].result, hex"0000000000000000000000000000000000000000007ae5649beabeddf889364a");
+    }
+
+    function test_parseEthCallQueryResponseComparison() public {
+        ParsedPerChainQueryResponse memory r = ParsedPerChainQueryResponse({
+            chainId: 23,
+            queryType: 1,
+            request: hex"00000009307832376433333433013ce792601c936b1c81f73ea2fa77208c0a478bae00000004916d5743",
+            response: hex"00000000027d3343b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c90006056cda47a84001000000200000000000000000000000000000000000000000000000000000000000000004"
+            });
+
+        EthCallQueryResponse memory eqr = parseEthCallQueryResponse(r);
+        assertEq(eqr.requestBlockId, "0x27d3343");
+        assertEq(eqr.blockNum, 0x27d3343);
+        assertEq(eqr.blockHash, hex"b9848f128b3658a0b9b50aa174e3ddc15ac4e54c84ee534b6d247adbdfc300c9");
+        vm.warp(1694814937);
+        assertEq(eqr.blockTime / 1_000_000, block.timestamp);
+        assertEq(eqr.result.length, 1);
+
+        assertEq(eqr.result[0].contractAddress, address(0x3ce792601c936b1c81f73Ea2fa77208C0A478BaE));
+        assertEq(eqr.result[0].callData, hex"916d5743");
+        bytes memory callData = eqr.result[0].callData;
+        bytes4 callSignature;
+        assembly {
+                callSignature := mload(add(callData, 32))
+            }
+        assertEq(callSignature, bytes4(keccak256("getMyCounter()")));
+        assertEq(eqr.result[0].result, hex"0000000000000000000000000000000000000000000000000000000000000004");
+        assertEq(abi.decode(eqr.result[0].result, (uint256)), 4);
+
+    }
+}

+ 26 - 0
ethereum/scripts/deploy_ccq_demo.js

@@ -0,0 +1,26 @@
+const QueryDemo = artifacts.require("QueryDemo");
+module.exports = async function(callback) {
+  const accounts = await web3.eth.getAccounts();
+  try {
+    // const ccqDemo = await QueryDemo.new(
+    //   accounts[0],
+    //   "0x0CBE91CF822c73C2315FB05100C2F714765d5c20",
+    //   5
+    // );
+    // const ccqDemo = await QueryDemo.new(
+    //   accounts[0],
+    //   "0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e",
+    //   23
+    // );
+    const ccqDemo = await QueryDemo.new(
+      accounts[0],
+      "0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35",
+      24
+    );
+    console.log("tx: " + ccqDemo.transactionHash);
+    console.log("QueryDemo address: " + ccqDemo.address);
+    callback();
+  } catch (e) {
+    callback(e);
+  }
+};