diff --git a/target_chains/ethereum/contracts/test/MockEntropy.t.sol b/target_chains/ethereum/contracts/test/MockEntropy.t.sol new file mode 100644 index 0000000000..f9e74d4a89 --- /dev/null +++ b/target_chains/ethereum/contracts/test/MockEntropy.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "@pythnetwork/entropy-sdk-solidity/MockEntropy.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; +import "@pythnetwork/entropy-sdk-solidity/EntropyStructsV2.sol"; + +contract MockEntropyConsumer is IEntropyConsumer { + address public entropy; + bytes32 public lastRandomNumber; + uint64 public lastSequenceNumber; + address public lastProvider; + uint256 public callbackCount; + + constructor(address _entropy) { + entropy = _entropy; + } + + function requestRandomNumber() external payable returns (uint64) { + return MockEntropy(entropy).requestV2{value: msg.value}(); + } + + function requestRandomNumberWithGasLimit( + uint32 gasLimit + ) external payable returns (uint64) { + return MockEntropy(entropy).requestV2{value: msg.value}(gasLimit); + } + + function requestRandomNumberFromProvider( + address provider, + uint32 gasLimit + ) external payable returns (uint64) { + return + MockEntropy(entropy).requestV2{value: msg.value}( + provider, + gasLimit + ); + } + + function getEntropy() internal view override returns (address) { + return entropy; + } + + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) internal override { + lastSequenceNumber = sequenceNumber; + lastProvider = provider; + lastRandomNumber = randomNumber; + callbackCount += 1; + } +} + +contract MockEntropyTest is Test { + MockEntropy public entropy; + MockEntropyConsumer public consumer; + address public provider; + + function setUp() public { + provider = address(0x1234); + entropy = new MockEntropy(provider); + consumer = new MockEntropyConsumer(address(entropy)); + } + + function testBasicRequestAndReveal() public { + uint64 seq = consumer.requestRandomNumber(); + assertEq(seq, 1, "Sequence number should be 1"); + + bytes32 randomNumber = bytes32(uint256(42)); + entropy.mockReveal(provider, seq, randomNumber); + + assertEq( + consumer.lastSequenceNumber(), + seq, + "Callback sequence number mismatch" + ); + assertEq( + consumer.lastProvider(), + provider, + "Callback provider mismatch" + ); + assertEq( + consumer.lastRandomNumber(), + randomNumber, + "Random number mismatch" + ); + assertEq(consumer.callbackCount(), 1, "Callback should be called once"); + } + + function testDifferentInterleavings() public { + uint64 seq1 = consumer.requestRandomNumber(); + uint64 seq2 = consumer.requestRandomNumber(); + uint64 seq3 = consumer.requestRandomNumber(); + + assertEq(seq1, 1, "First sequence should be 1"); + assertEq(seq2, 2, "Second sequence should be 2"); + assertEq(seq3, 3, "Third sequence should be 3"); + + bytes32 random2 = bytes32(uint256(200)); + bytes32 random3 = bytes32(uint256(300)); + bytes32 random1 = bytes32(uint256(100)); + + entropy.mockReveal(provider, seq2, random2); + assertEq( + consumer.lastRandomNumber(), + random2, + "Should reveal seq2 first" + ); + assertEq(consumer.lastSequenceNumber(), seq2, "Sequence should be 2"); + + entropy.mockReveal(provider, seq3, random3); + assertEq( + consumer.lastRandomNumber(), + random3, + "Should reveal seq3 second" + ); + assertEq(consumer.lastSequenceNumber(), seq3, "Sequence should be 3"); + + entropy.mockReveal(provider, seq1, random1); + assertEq( + consumer.lastRandomNumber(), + random1, + "Should reveal seq1 last" + ); + assertEq(consumer.lastSequenceNumber(), seq1, "Sequence should be 1"); + + assertEq( + consumer.callbackCount(), + 3, + "All three callbacks should be called" + ); + } + + function testDifferentProviders() public { + address provider2 = address(0x5678); + + uint64 seq1 = consumer.requestRandomNumberFromProvider( + provider, + 100000 + ); + uint64 seq2 = consumer.requestRandomNumberFromProvider( + provider2, + 100000 + ); + + assertEq(seq1, 1, "Provider 1 first sequence should be 1"); + assertEq(seq2, 1, "Provider 2 first sequence should be 1"); + + bytes32 random1 = bytes32(uint256(111)); + bytes32 random2 = bytes32(uint256(222)); + + entropy.mockReveal(provider, seq1, random1); + assertEq( + consumer.lastRandomNumber(), + random1, + "First provider random number" + ); + + entropy.mockReveal(provider2, seq2, random2); + assertEq( + consumer.lastRandomNumber(), + random2, + "Second provider random number" + ); + } + + function testRequestWithGasLimit() public { + uint64 seq = consumer.requestRandomNumberWithGasLimit(200000); + + assertEq(seq, 1, "Sequence should be 1"); + + EntropyStructsV2.Request memory req = entropy.getRequestV2( + provider, + seq + ); + assertEq(req.gasLimit10k, 20, "Gas limit should be 20 (200k / 10k)"); + + bytes32 randomNumber = bytes32(uint256(999)); + entropy.mockReveal(provider, seq, randomNumber); + + assertEq( + consumer.lastRandomNumber(), + randomNumber, + "Random number should match" + ); + } + + function testGetProviderInfo() public { + EntropyStructsV2.ProviderInfo memory info = entropy.getProviderInfoV2( + provider + ); + assertEq(info.feeInWei, 1, "Fee should be 1"); + assertEq( + info.defaultGasLimit, + 100000, + "Default gas limit should be 100000" + ); + assertEq(info.sequenceNumber, 1, "Sequence number should start at 1"); + } + + function testGetDefaultProvider() public { + assertEq( + entropy.getDefaultProvider(), + provider, + "Default provider should match" + ); + } + + function testFeesReturnZero() public { + assertEq(entropy.getFeeV2(), 0, "getFeeV2() should return 0"); + assertEq( + entropy.getFeeV2(100000), + 0, + "getFeeV2(gasLimit) should return 0" + ); + assertEq( + entropy.getFeeV2(provider, 100000), + 0, + "getFeeV2(provider, gasLimit) should return 0" + ); + } + + function testCustomRandomNumbers() public { + uint64 seq = consumer.requestRandomNumber(); + + bytes32[] memory randomNumbers = new bytes32[](3); + randomNumbers[0] = bytes32(uint256(0)); + randomNumbers[1] = bytes32(type(uint256).max); + randomNumbers[2] = keccak256("custom random value"); + + for (uint i = 0; i < randomNumbers.length; i++) { + if (i > 0) { + seq = consumer.requestRandomNumber(); + } + entropy.mockReveal(provider, seq, randomNumbers[i]); + assertEq( + consumer.lastRandomNumber(), + randomNumbers[i], + "Custom random number should match" + ); + } + } +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol new file mode 100644 index 0000000000..e5d4e791d4 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.0; + +import "./IEntropyV2.sol"; +import "./IEntropyConsumer.sol"; +import "./EntropyStructsV2.sol"; +import "./EntropyEventsV2.sol"; + +contract MockEntropy is IEntropyV2 { + address public defaultProvider; + + mapping(address => EntropyStructsV2.ProviderInfo) public providers; + mapping(address => mapping(uint64 => EntropyStructsV2.Request)) + public requests; + + constructor(address _defaultProvider) { + require(_defaultProvider != address(0), "Invalid default provider"); + defaultProvider = _defaultProvider; + + providers[_defaultProvider].sequenceNumber = 1; + providers[_defaultProvider].feeInWei = 1; + providers[_defaultProvider].defaultGasLimit = 100000; + } + + function requestV2() + external + payable + override + returns (uint64 assignedSequenceNumber) + { + return _requestV2(defaultProvider, bytes32(0), 0); + } + + function requestV2( + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(defaultProvider, bytes32(0), gasLimit); + } + + function requestV2( + address provider, + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(provider, bytes32(0), gasLimit); + } + + function requestV2( + address provider, + bytes32 userRandomNumber, + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(provider, userRandomNumber, gasLimit); + } + + function _requestV2( + address provider, + bytes32 userRandomNumber, + uint32 gasLimit + ) internal returns (uint64 assignedSequenceNumber) { + EntropyStructsV2.ProviderInfo storage providerInfo = providers[ + provider + ]; + + if (providerInfo.sequenceNumber == 0) { + providerInfo.sequenceNumber = 1; + providerInfo.feeInWei = 1; + providerInfo.defaultGasLimit = 100000; + } + + assignedSequenceNumber = providerInfo.sequenceNumber; + providerInfo.sequenceNumber += 1; + + uint32 effectiveGasLimit = gasLimit == 0 + ? providerInfo.defaultGasLimit + : gasLimit; + + EntropyStructsV2.Request storage req = requests[provider][ + assignedSequenceNumber + ]; + req.provider = provider; + req.sequenceNumber = assignedSequenceNumber; + req.requester = msg.sender; + req.blockNumber = uint64(block.number); + req.useBlockhash = false; + req.gasLimit10k = uint16(effectiveGasLimit / 10000); + + emit Requested( + provider, + msg.sender, + assignedSequenceNumber, + userRandomNumber, + effectiveGasLimit, + bytes("") + ); + + return assignedSequenceNumber; + } + + function mockReveal( + address provider, + uint64 sequenceNumber, + bytes32 randomNumber + ) external { + EntropyStructsV2.Request storage req = requests[provider][ + sequenceNumber + ]; + require(req.requester != address(0), "Request not found"); + + address requester = req.requester; + + emit Revealed( + provider, + requester, + sequenceNumber, + randomNumber, + bytes32(0), + bytes32(0), + false, + bytes(""), + 0, + bytes("") + ); + + delete requests[provider][sequenceNumber]; + + uint256 codeSize; + assembly { + codeSize := extcodesize(requester) + } + + if (codeSize > 0) { + IEntropyConsumer(requester)._entropyCallback( + sequenceNumber, + provider, + randomNumber + ); + } + } + + function getProviderInfoV2( + address provider + ) external view override returns (EntropyStructsV2.ProviderInfo memory) { + return providers[provider]; + } + + function getDefaultProvider() external view override returns (address) { + return defaultProvider; + } + + function getRequestV2( + address provider, + uint64 sequenceNumber + ) external view override returns (EntropyStructsV2.Request memory) { + return requests[provider][sequenceNumber]; + } + + function getFeeV2() external pure override returns (uint128) { + return 0; + } + + function getFeeV2(uint32) external pure override returns (uint128) { + return 0; + } + + function getFeeV2( + address, + uint32 + ) external pure override returns (uint128) { + return 0; + } +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/package.json b/target_chains/ethereum/entropy_sdk/solidity/package.json index 216cecadf1..ba2d35c454 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/package.json +++ b/target_chains/ethereum/entropy_sdk/solidity/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/entropy-sdk-solidity", - "version": "2.0.0", + "version": "2.1.0", "description": "Generate secure random numbers with Pyth Entropy", "type": "module", "repository": {