diff --git a/contracts/Donation.sol b/contracts/Donation.sol index e2df1995..5e7750e1 100644 --- a/contracts/Donation.sol +++ b/contracts/Donation.sol @@ -241,7 +241,7 @@ contract Donation is UUPSUpgradeable, OwnableUpgradeable, IOracleRewardCB { } } - function newOracleRandomWords(uint[3] calldata randomWords) external onlyWorld { + function newOracleRandomWords(uint _randomWord) external onlyWorld { uint16 _lastLotteryId = lastLotteryId; bool hasDonations = lastRaffleId != 0; @@ -253,7 +253,7 @@ contract Donation is UUPSUpgradeable, OwnableUpgradeable, IOracleRewardCB { if (hasDonations) { // Decide the winner - uint24 raffleIdWinner = uint24(randomWords[0] % lastRaffleId) + 1; + uint24 raffleIdWinner = uint24(_randomWord % lastRaffleId) + 1; winners[_lastLotteryId] = LotteryWinnerInfo({ lotteryId: _lastLotteryId, raffleId: raffleIdWinner, diff --git a/contracts/Players/PlayersImplRewards.sol b/contracts/Players/PlayersImplRewards.sol index 91d4e478..501997bf 100644 --- a/contracts/Players/PlayersImplRewards.sol +++ b/contracts/Players/PlayersImplRewards.sol @@ -828,18 +828,15 @@ contract PlayersImplRewards is PlayersImplBase, PlayersBase, IPlayersRewardsDele pendingRandomReward.boostItemTokenId < 10; uint numTickets = isCombat ? monstersKilled : pendingRandomReward.xpElapsedTime / 3600; - uint40 sentinelElapsedTime = pendingRandomReward.sentinelElapsedTime; - uint8 fullAttireBonusRewardsPercent = pendingRandomReward.fullAttireBonusRewardsPercent; - uint[] memory randomIds; uint[] memory randomAmounts; (randomIds, randomAmounts, processedAny) = _getRandomRewards( _playerId, - pendingRandomReward.startTime + sentinelElapsedTime, + pendingRandomReward.startTime + pendingRandomReward.sentinelElapsedTime, numTickets, actionRewards, successPercent, - fullAttireBonusRewardsPercent + pendingRandomReward.fullAttireBonusRewardsPercent ); if (processedAny) { diff --git a/contracts/Quests.sol b/contracts/Quests.sol index 22c9e980..f6be6daf 100644 --- a/contracts/Quests.sol +++ b/contracts/Quests.sol @@ -223,14 +223,14 @@ contract Quests is UUPSUpgradeable, OwnableUpgradeable, IOracleRewardCB { emit DeactivateQuest(_playerId, questId); } - function newOracleRandomWords(uint[3] calldata _randomWords) external override onlyWorld { + function newOracleRandomWords(uint _randomWord) external override onlyWorld { // Pick a random quest which is assigned to everyone (could be random later) uint length = randomQuests.length; if (length == 0) { return; // Don't revert as this would mess up the chainlink callback } - uint index = uint8(_randomWords[0]) % length; + uint index = uint8(_randomWord) % length; randomQuest = randomQuests[index]; uint oldQuestId = randomQuest.questId; uint newQuestId = randomQuestId++; diff --git a/contracts/World.sol b/contracts/World.sol index 07e4505c..d77b4a67 100644 --- a/contracts/World.sol +++ b/contracts/World.sol @@ -20,7 +20,7 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea using UnsafeMath for uint; event RequestSent(uint requestId, uint32 numWords, uint lastRandomWordsUpdatedTime); - event RequestFulfilled(uint requestId, uint[3] randomWords); + event RequestFulfilledV2(uint requestId, uint randomWord); event AddActionsV2(Action[] actions); event EditActionsV2(Action[] actions); event AddDynamicActions(uint16[] actionIds); @@ -29,7 +29,7 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea event EditActionChoicesV2(uint16 actionId, uint16[] actionChoiceIds, ActionChoiceInput[] choices); event RemoveActionChoicesV2(uint16 actionId, uint16[] actionChoiceIds); - // Legacy, just for ABI reasons + // Legacy, just for ABI reasons and old beta events event AddAction(ActionV1 action); event AddActions(ActionV1[] actions); event EditActions(ActionV1[] actions); @@ -37,6 +37,7 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea event AddActionChoices(uint16 actionId, uint16[] actionChoiceIds, ActionChoiceV1[] choices); event EditActionChoice(uint16 actionId, uint16 actionChoiceId, ActionChoiceV1 choice); event EditActionChoices_(uint16[] actionIds, uint16[] actionChoiceIds, ActionChoiceV1[] choices); + event RequestFulfilled(uint requestId, uint[3] randomWords); error RandomWordsCannotBeUpdatedYet(); error CanOnlyRequestAfterTheNextCheckpoint(uint currentTime, uint checkpoint); @@ -68,7 +69,7 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea // Past request ids uint[] public requestIds; // Each one is a set of random words for 1 day - mapping(uint requestId => uint[3] randomWord) public randomWords; + mapping(uint requestId => uint randomWord) public randomWords; uint40 public lastRandomWordsUpdatedTime; uint40 private startTime; uint40 private weeklyRewardCheckpoint; @@ -79,16 +80,16 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea // see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations bytes32 private constant KEY_HASH = 0x5881eea62f9876043df723cf89f0c2bb6f950da25e9dfe66995c24f919c8f8ab; - uint32 private constant CALLBACK_GAS_LIMIT = 500000; - // The default is 3, but you can set this higher. + uint32 private constant CALLBACK_GAS_LIMIT = 400000; uint16 private constant REQUEST_CONFIRMATIONS = 1; - // For this example, retrieve 3 random values in one request. // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS. - uint32 private constant NUM_WORDS = 3; + uint32 private constant NUM_WORDS = 1; uint32 public constant MIN_RANDOM_WORDS_UPDATE_TIME = 1 days; uint32 private constant MIN_DYNAMIC_ACTION_UPDATE_TIME = 1 days; + uint32 public constant NUM_DAYS_RANDOM_WORDS_INITIALIZED = 3; + mapping(uint actionId => ActionInfo actionInfo) public actions; uint16[] private lastAddedDynamicActions; uint private lastDynamicUpdatedTime; @@ -120,35 +121,35 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea COORDINATOR = _coordinator; subscriptionId = _subscriptionId; - startTime = uint40((block.timestamp / MIN_RANDOM_WORDS_UPDATE_TIME) * MIN_RANDOM_WORDS_UPDATE_TIME) - 5 days; // Floor to the nearest day 00:00 UTC - lastRandomWordsUpdatedTime = startTime + 4 days; + startTime = uint40( + (block.timestamp / MIN_RANDOM_WORDS_UPDATE_TIME) * + MIN_RANDOM_WORDS_UPDATE_TIME - + (NUM_DAYS_RANDOM_WORDS_INITIALIZED + 1) * + 1 days + ); // Floor to the nearest day 00:00 UTC + lastRandomWordsUpdatedTime = uint40(startTime + NUM_DAYS_RANDOM_WORDS_INITIALIZED * 1 days); weeklyRewardCheckpoint = uint40((block.timestamp - 4 days) / 1 weeks) * 1 weeks + 4 days + 1 weeks; - // Initialize 4 days worth of random words - for (U256 iter; iter.lt(4); iter = iter.inc()) { + // Initialize a few days worth of random words so that we have enough data to fetch the first day + for (U256 iter; iter.lt(NUM_DAYS_RANDOM_WORDS_INITIALIZED); iter = iter.inc()) { uint i = iter.asUint256(); uint requestId = 200 + i; requestIds.push(requestId); emit RequestSent(requestId, NUM_WORDS, startTime + (i * 1 days) + 1 days); - uint[] memory _randomWords = new uint[](3); + uint[] memory _randomWords = new uint[](1); _randomWords[0] = uint( - blockhash(block.number - 4 + i) ^ 0x3632d8eba811d69784e6904a58de6e0ab55f32638189623b309895beaa6920c4 - ); - _randomWords[1] = uint( - blockhash(block.number - 4 + i) ^ 0xca820e9e57e5e703aeebfa2dc60ae09067f931b6e888c0a7c7a15a76341ab2c2 - ); - _randomWords[2] = uint( - blockhash(block.number - 4 + i) ^ 0xd1f1b7d57307aee9687ae39dbb462b1c1f07a406d34cd380670360ef02f243b6 + blockhash(block.number - NUM_DAYS_RANDOM_WORDS_INITIALIZED + i) ^ + 0x3632d8eba811d69784e6904a58de6e0ab55f32638189623b309895beaa6920c4 ); fulfillRandomWords(requestId, _randomWords); } - thisWeeksRandomWordSegment = bytes8(uint64(randomWords[3][0])); + thisWeeksRandomWordSegment = bytes8(uint64(randomWords[0])); } function requestRandomWords() external returns (uint requestId) { // Last one has not been fulfilled yet - if (requestIds.length != 0 && randomWords[requestIds[requestIds.length - 1]][0] == 0) { + if (requestIds.length != 0 && randomWords[requestIds[requestIds.length - 1]] == 0) { revert RandomWordsCannotBeUpdatedYet(); } uint40 newLastRandomWordsUpdatedTime = lastRandomWordsUpdatedTime + MIN_RANDOM_WORDS_UPDATE_TIME; @@ -172,36 +173,33 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea } function fulfillRandomWords(uint _requestId, uint[] memory _randomWords) internal override { - if (randomWords[_requestId][0] != 0) { + if (randomWords[_requestId] != 0) { revert RequestAlreadyFulfilled(); } - uint[3] memory random = [_randomWords[0], _randomWords[1], _randomWords[2]]; + if (_randomWords.length != NUM_WORDS) { + revert LengthMismatch(); + } - if (random[0] == 0) { + uint randomWord = _randomWords[0]; + if (randomWord == 0) { // Not sure if 0 can be selected, but in case use previous block hash as pseudo random number - random[0] = uint(blockhash(block.number - 1)); - } - if (random[1] == 0) { - random[1] = uint(blockhash(block.number - 2)); - } - if (random[2] == 0) { - random[2] = uint(blockhash(block.number - 3)); + randomWord = uint(blockhash(block.number - 1)); } - randomWords[_requestId] = random; + randomWords[_requestId] = randomWord; if (address(quests) != address(0)) { - quests.newOracleRandomWords(random); + quests.newOracleRandomWords(randomWord); } if (address(donation) != address(0)) { - donation.newOracleRandomWords(random); + donation.newOracleRandomWords(randomWord); } - emit RequestFulfilled(_requestId, random); + emit RequestFulfilledV2(_requestId, randomWord); // Are we at the threshold for a new week if (weeklyRewardCheckpoint <= ((block.timestamp) / 1 days) * 1 days) { // Issue new daily rewards for each tier based on the new random words - thisWeeksRandomWordSegment = bytes8(uint64(random[0])); + thisWeeksRandomWordSegment = bytes8(uint64(randomWord)); weeklyRewardCheckpoint = uint40((block.timestamp - 4 days) / 1 weeks) * 1 weeks + 4 days + 1 weeks; } @@ -253,7 +251,7 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea if (offset < 0 || requestIds.length <= uint(offset)) { return 0; } - return randomWords[requestIds[uint(offset)]][0]; + return randomWords[requestIds[uint(offset)]]; } function hasRandomWord(uint _timestamp) external view returns (bool) { @@ -267,18 +265,10 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea } } - function getFullRandomWords(uint _timestamp) public view returns (uint[3] memory) { - int offset = _getRandomWordOffset(_timestamp); - if (offset < 0 || requestIds.length <= uint(offset)) { - revert NoValidRandomWord(); - } - return randomWords[requestIds[uint(offset)]]; - } - - function getMultipleFullRandomWords(uint _timestamp) public view returns (uint[3][5] memory words) { - for (U256 iter; iter.lt(5); iter = iter.inc()) { + function getMultipleWords(uint _timestamp) public view returns (uint[4] memory words) { + for (U256 iter; iter.lt(4); iter = iter.inc()) { uint i = iter.asUint256(); - words[i] = getFullRandomWords(_timestamp - i * 1 days); + words[i] = getRandomWord(_timestamp - (i * 1 days)); } } @@ -353,32 +343,20 @@ contract World is VRFConsumerBaseV2Upgradeable, UUPSUpgradeable, OwnableUpgradea // 32 bytes bytes32 word = bytes32(getRandomWord(_skillEndTime)); b = abi.encodePacked(_getRandomComponent(word, _skillEndTime, _playerId)); - } else if (_numTickets <= 48) { - uint[3] memory fullWords = getFullRandomWords(_skillEndTime); - // 3 * 32 bytes - for (U256 iter; iter.lt(3); iter = iter.inc()) { - uint i = iter.asUint256(); - fullWords[i] = uint(_getRandomComponent(bytes32(fullWords[i]), _skillEndTime, _playerId)); - } - b = abi.encodePacked(fullWords); - } else { - // 3 * 5 * 32 bytes - uint[3][5] memory multipleFullWords = getMultipleFullRandomWords(_skillEndTime); - for (U256 iter; iter.lt(5); iter = iter.inc()) { + } else if (_numTickets <= 64) { + // 4 * 32 bytes + uint[4] memory multipleWords = getMultipleWords(_skillEndTime); + for (U256 iter; iter.lt(4); iter = iter.inc()) { uint i = iter.asUint256(); - for (U256 jter; jter.lt(3); jter = jter.inc()) { - uint j = jter.asUint256(); - multipleFullWords[i][j] = uint( - _getRandomComponent(bytes32(multipleFullWords[i][j]), _skillEndTime, _playerId) - ); - // XOR all the full words with the first fresh random number to give more randomness to the existing random words - if (i != 0) { - multipleFullWords[i][j] = multipleFullWords[i][j] ^ multipleFullWords[0][j]; - } + multipleWords[i] = uint(_getRandomComponent(bytes32(multipleWords[i]), _skillEndTime, _playerId)); + // XOR all the words with the first fresh random number to give more randomness to the existing random words + if (i != 0) { + multipleWords[i] = uint(keccak256(abi.encodePacked(multipleWords[i] ^ multipleWords[0]))); } } - - b = abi.encodePacked(multipleFullWords); + b = abi.encodePacked(multipleWords); + } else { + assert(false); } } diff --git a/contracts/interfaces/IOracleRewardCB.sol b/contracts/interfaces/IOracleRewardCB.sol index 25955d68..bdd3f53d 100644 --- a/contracts/interfaces/IOracleRewardCB.sol +++ b/contracts/interfaces/IOracleRewardCB.sol @@ -2,5 +2,5 @@ pragma solidity ^0.8.20; interface IOracleRewardCB { - function newOracleRandomWords(uint[3] calldata randomWords) external; + function newOracleRandomWords(uint randomWord) external; } diff --git a/test/Players/Rewards.ts b/test/Players/Rewards.ts index 485a977c..12e57572 100644 --- a/test/Players/Rewards.ts +++ b/test/Players/Rewards.ts @@ -1088,14 +1088,13 @@ describe("Rewards", function () { await mockOracleClient.fulfill(requestId, world.address); let randomBytes = await world.getRandomBytes(numTickets, timestamp - 86400, playerId); expect(ethers.utils.hexDataLength(randomBytes)).to.be.eq(32); - numTickets = 48; + numTickets = MAX_UNIQUE_TICKETS; - const randomBytes1 = await world.getRandomBytes(numTickets, timestamp - 86400, playerId); - expect(ethers.utils.hexDataLength(randomBytes1)).to.be.eq(32 * 3); + randomBytes = await world.getRandomBytes(numTickets, timestamp - 86400, playerId); + expect(ethers.utils.hexDataLength(randomBytes)).to.be.eq(32 * 4); - numTickets = 49; - const randomBytes2 = await world.getRandomBytes(numTickets, timestamp - 86400, playerId); - expect(ethers.utils.hexDataLength(randomBytes2)).to.be.eq(32 * 3 * 5); + numTickets = MAX_UNIQUE_TICKETS + 1; + await expect(world.getRandomBytes(numTickets, timestamp - 86400, playerId)).to.be.reverted; }); it("Check past random rewards which are claimed the following day don't cause issues (many)", async function () { diff --git a/test/Quests.ts b/test/Quests.ts index 4f1202bf..118a51d7 100644 --- a/test/Quests.ts +++ b/test/Quests.ts @@ -240,10 +240,7 @@ describe("Quests", function () { it("Should fail to set the random quest for non-world", async function () { const {alice, quests} = await loadFixture(questsFixture); - await expect(quests.connect(alice).newOracleRandomWords([1, 2, 3])).to.be.revertedWithCustomError( - quests, - "NotWorld" - ); + await expect(quests.connect(alice).newOracleRandomWords(1)).to.be.revertedWithCustomError(quests, "NotWorld"); }); }); diff --git a/test/World.ts b/test/World.ts index f2fefbc9..928f00a9 100644 --- a/test/World.ts +++ b/test/World.ts @@ -38,11 +38,14 @@ describe("World", function () { const minRandomWordsUpdateTime = await world.MIN_RANDOM_WORDS_UPDATE_TIME(); + const numDaysRandomWordsInitialized = await world.NUM_DAYS_RANDOM_WORDS_INITIALIZED(); + return { world, worldLibrary, mockOracleClient, minRandomWordsUpdateTime, + numDaysRandomWordsInitialized, owner, alice, }; @@ -50,19 +53,21 @@ describe("World", function () { describe("Seed", function () { it("Requesting random words", async function () { - const {world, mockOracleClient, minRandomWordsUpdateTime} = await loadFixture(deployContracts); + const {world, mockOracleClient, minRandomWordsUpdateTime, numDaysRandomWordsInitialized} = await loadFixture( + deployContracts + ); await world.requestRandomWords(); - const startOffset = 4; + const startOffset = numDaysRandomWordsInitialized; let requestId = await world.requestIds(startOffset); expect(requestId).to.be.greaterThanOrEqual(1); - let randomWord = await world.randomWords(requestId, 0); + let randomWord = await world.randomWords(requestId); expect(randomWord).to.eq(0); // Retrieve the random number await mockOracleClient.fulfill(requestId, world.address); - randomWord = await world.randomWords(requestId, 0); + randomWord = await world.randomWords(requestId); expect(randomWord).to.not.eq(0); // Try fulfill same request should fail @@ -90,17 +95,19 @@ describe("World", function () { }); it("getRandomWord", async function () { - const {world, mockOracleClient, minRandomWordsUpdateTime} = await loadFixture(deployContracts); + const {world, mockOracleClient, minRandomWordsUpdateTime, numDaysRandomWordsInitialized} = await loadFixture( + deployContracts + ); const {timestamp: currentTimestamp} = await ethers.provider.getBlock("latest"); expect(await world.hasRandomWord(currentTimestamp)).to.be.false; await ethers.provider.send("evm_increaseTime", [minRandomWordsUpdateTime]); await world.requestRandomWords(); - await expect(world.requestIds(5)).to.be.reverted; - let requestId = await world.requestIds(4); + await expect(world.requestIds(numDaysRandomWordsInitialized + 1)).to.be.reverted; + let requestId = await world.requestIds(numDaysRandomWordsInitialized); await mockOracleClient.fulfill(requestId, world.address); expect(await world.hasRandomWord(currentTimestamp)).to.be.false; await world.requestRandomWords(); - requestId = await world.requestIds(5); + requestId = await world.requestIds(numDaysRandomWordsInitialized + 1); await mockOracleClient.fulfill(requestId, world.address); expect(await world.hasRandomWord(currentTimestamp)).to.be.true; await expect(world.getRandomWord(currentTimestamp)).to.not.be.reverted; @@ -117,42 +124,20 @@ describe("World", function () { ); }); - it("Get full/multiple words", async function () { - const {world, mockOracleClient, minRandomWordsUpdateTime} = await loadFixture(deployContracts); - const {timestamp: currentTimestamp} = await ethers.provider.getBlock("latest"); - await expect(world.getFullRandomWords(currentTimestamp)).to.be.revertedWithCustomError( - world, - "NoValidRandomWord" - ); - await expect(world.getMultipleFullRandomWords(currentTimestamp)).to.be.revertedWithCustomError( - world, - "NoValidRandomWord" + it("Get multiple words", async function () { + const {world, mockOracleClient, minRandomWordsUpdateTime, numDaysRandomWordsInitialized} = await loadFixture( + deployContracts ); + const {timestamp: currentTimestamp} = await ethers.provider.getBlock("latest"); + await expect(world.getMultipleWords(currentTimestamp)).to.be.revertedWithCustomError(world, "NoValidRandomWord"); await ethers.provider.send("evm_increaseTime", [minRandomWordsUpdateTime]); await world.requestRandomWords(); - let requestId = await world.requestIds(4); + let requestId = await world.requestIds(numDaysRandomWordsInitialized); await mockOracleClient.fulfill(requestId, world.address); - await expect(world.getFullRandomWords(currentTimestamp)).to.be.revertedWithCustomError( - world, - "NoValidRandomWord" - ); - await expect(world.getMultipleFullRandomWords(currentTimestamp)).to.be.revertedWithCustomError( - world, - "NoValidRandomWord" - ); + await expect(world.getMultipleWords(currentTimestamp)).to.be.revertedWithCustomError(world, "NoValidRandomWord"); await world.requestRandomWords(); - requestId = await world.requestIds(5); - await mockOracleClient.fulfill(requestId, world.address); - - const fullWords = await world.getFullRandomWords(currentTimestamp); - const multipleWords = await world.getMultipleFullRandomWords(currentTimestamp); - expect(fullWords).to.eql(multipleWords[0]); - expect(multipleWords.length).to.eq(5); - for (let i = 0; i < 5; ++i) { - expect(multipleWords[i][0]).to.not.eq(0); - expect(multipleWords[i][1]).to.not.eq(0); - expect(multipleWords[i][2]).to.not.eq(0); - } + requestId = await world.requestIds(numDaysRandomWordsInitialized + 1); + await expect(mockOracleClient.fulfill(requestId, world.address)).to.not.be.reverted; }); it("Test new random rewards", async function () {