Skip to content

Commit

Permalink
Optimize oracle rewards, only request 1 random word now
Browse files Browse the repository at this point in the history
  • Loading branch information
0xSamWitch committed Aug 10, 2023
1 parent 8d9662a commit 1d68803
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 130 deletions.
4 changes: 2 additions & 2 deletions contracts/Donation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions contracts/Players/PlayersImplRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions contracts/Quests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
120 changes: 49 additions & 71 deletions contracts/World.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -29,14 +29,15 @@ 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);
event AddActionChoice(uint16 actionId, uint16 actionChoiceId, ActionChoiceV1 choice);
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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IOracleRewardCB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
pragma solidity ^0.8.20;

interface IOracleRewardCB {
function newOracleRandomWords(uint[3] calldata randomWords) external;
function newOracleRandomWords(uint randomWord) external;
}
11 changes: 5 additions & 6 deletions test/Players/Rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
5 changes: 1 addition & 4 deletions test/Quests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
Loading

0 comments on commit 1d68803

Please sign in to comment.