Skip to content

Commit 2a65d2c

Browse files
committed
fix: optimize tournament and gauntlet gas usage and VRF integration
- Cache skinRegistry reference in TournamentGame.queueForTournament to reduce external calls - Cache array lengths in loops to avoid repeated SLOAD operations - Fix Fisher-Yates shuffle to cache participants.length - Add name change ticket support to TournamentGame reward distribution - Replace string enum with ReplacementReason enum to fix stack depth errors - Ensure VRF randomness flows through reward distribution instead of pseudo-random - Fix GauntletGame deployment script XP permissions (experience: true) - Add roundWinners array emission to GauntletCompleted events with proper data
1 parent 565735a commit 2a65d2c

5 files changed

Lines changed: 184 additions & 63 deletions

File tree

CLAUDE.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -698,10 +698,3 @@ externalCall();
698698
- External registries for skins, names, etc.
699699
- Validation through registry interfaces
700700

701-
## TODO: Gas Optimization Issues to Fix Later
702-
703-
### GauntletGame Storage Waste
704-
- `gauntlet.winners[]` array is stored in expensive contract storage (~300k gas for 16 players)
705-
- Only used for emitting `GauntletCompleted` event at end
706-
- Should use memory array instead, emit event, never store in contract storage
707-
- Same pattern exists in both Gauntlet and Tournament - treating contract like database instead of using memory + events

script/deploy/GauntletGameDeploy.s.sol

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,8 @@ contract GauntletGameDeployScript is Script {
5858

5959
// Whitelist GauntletGame in Player contract
6060
Player playerContract = Player(playerAddr);
61-
IPlayer.GamePermissions memory perms = IPlayer.GamePermissions({
62-
record: true,
63-
retire: false,
64-
attributes: false,
65-
immortal: false,
66-
experience: false
67-
});
61+
IPlayer.GamePermissions memory perms =
62+
IPlayer.GamePermissions({record: true, retire: false, attributes: false, immortal: false, experience: true});
6863
playerContract.setGameContractPermission(address(gauntletGame), perms);
6964

7065
console2.log("\n=== Deployed Addresses ===");

src/game/modes/GauntletGame.sol

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
105105

106106
}
107107

108+
/// @notice Reasons why a player might be replaced in a gauntlet.
109+
enum ReplacementReason {
110+
PLAYER_RETIRED,
111+
SKIN_OWNERSHIP_LOST
112+
}
113+
108114
//==============================================================//
109115
// STRUCTS //
110116
//==============================================================//
@@ -287,7 +293,10 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
287293
event GauntletAutoRecovered(uint256 commitBlock, uint256 currentBlock, GauntletPhase phase);
288294
/// @notice Emitted when a player is replaced during gauntlet execution.
289295
event PlayerReplaced(
290-
uint256 indexed gauntletId, uint32 indexed originalPlayerId, uint32 indexed replacementPlayerId, string reason
296+
uint256 indexed gauntletId,
297+
uint32 indexed originalPlayerId,
298+
uint32 indexed replacementPlayerId,
299+
ReplacementReason reason
291300
);
292301
/// @notice Emitted when the default player contract address is updated.
293302
event DefaultPlayerContractSet(address indexed newContract);
@@ -664,16 +673,17 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
664673
function _distributeGauntletRewards(
665674
Gauntlet storage gauntlet,
666675
uint32[] memory eliminatedByRound,
667-
uint256 gauntletId
676+
uint256 gauntletId,
677+
uint256 randomness
668678
) private {
669679
// Reward champion
670680
if (_getFighterType(gauntlet.championId) == Fighter.FighterType.PLAYER) {
671-
_distributeReward(gauntletId, gauntlet.championId, championRewards);
681+
_distributeReward(gauntletId, gauntlet.championId, championRewards, randomness);
672682
}
673683

674684
// Reward runner-up
675685
if (_getFighterType(gauntlet.runnerUpId) == Fighter.FighterType.PLAYER) {
676-
_distributeReward(gauntletId, gauntlet.runnerUpId, runnerUpRewards);
686+
_distributeReward(gauntletId, gauntlet.runnerUpId, runnerUpRewards, randomness);
677687
}
678688

679689
// Reward 3rd-4th place (semi-final losers)
@@ -683,16 +693,19 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
683693
for (uint256 i = semiFinalistStart; i < semiFinalistEnd; i++) {
684694
uint32 playerId = eliminatedByRound[i];
685695
if (_getFighterType(playerId) == Fighter.FighterType.PLAYER) {
686-
_distributeReward(gauntletId, playerId, thirdFourthRewards);
696+
_distributeReward(gauntletId, playerId, thirdFourthRewards, randomness);
687697
}
688698
}
689699
}
690700

691701
/// @notice Distributes a single reward based on configured percentages.
692-
function _distributeReward(uint256 gauntletId, uint32 playerId, IPlayerTickets.RewardConfig memory config)
693-
private
694-
{
695-
uint256 random = uint256(keccak256(abi.encodePacked(gauntletId, playerId, block.timestamp)));
702+
function _distributeReward(
703+
uint256 gauntletId,
704+
uint32 playerId,
705+
IPlayerTickets.RewardConfig memory config,
706+
uint256 randomness
707+
) private {
708+
uint256 random = uint256(keccak256(abi.encodePacked(randomness, gauntletId, playerId)));
696709
uint256 roll = random % 10000; // 0-9999 for percentage precision
697710

698711
IPlayerTickets.RewardType rewardType;
@@ -901,23 +914,24 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
901914

902915
// Check if player needs replacement
903916
bool shouldReplace = false;
904-
string memory reason = "";
917+
ReplacementReason reason;
905918

906919
// Check if player is retired
907920
if (playerContract.isPlayerRetired(regPlayer.playerId)) {
908921
shouldReplace = true;
909-
reason = "PLAYER_RETIRED";
922+
reason = ReplacementReason.PLAYER_RETIRED;
910923
}
911924

912925
// Check if player still owns their skin
913926
if (!shouldReplace) {
914-
address playerOwner = playerContract.getPlayerOwner(regPlayer.playerId);
915-
try skinRegistry.validateSkinOwnership(regPlayer.loadout.skin, playerOwner) {
927+
try skinRegistry.validateSkinOwnership(
928+
regPlayer.loadout.skin, playerContract.getPlayerOwner(regPlayer.playerId)
929+
) {
916930
// Skin validation passed
917931
} catch {
918932
// Skin validation failed - player no longer owns the skin
919933
shouldReplace = true;
920-
reason = "SKIN_OWNERSHIP_LOST";
934+
reason = ReplacementReason.SKIN_OWNERSHIP_LOST;
921935
}
922936
}
923937

@@ -945,13 +959,14 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
945959
activeParticipants = _shuffleParticipants(activeParticipants, randomness);
946960

947961
// Run gauntlet rounds with improved structure
948-
uint32[] memory eliminatedByRound = _runGauntletRounds(gauntlet, gauntletId, activeParticipants, randomness);
962+
(uint32[] memory eliminatedByRound, uint32[] memory roundWinners) =
963+
_runGauntletRounds(gauntlet, gauntletId, activeParticipants, randomness);
949964

950965
// Award XP for levels 1-9 brackets, tickets for level 10
951966
if (levelBracket != LevelBracket.LEVEL_10) {
952967
_awardGauntletXP(gauntlet, eliminatedByRound, levelBracket);
953968
} else {
954-
_distributeGauntletRewards(gauntlet, eliminatedByRound, gauntletId);
969+
_distributeGauntletRewards(gauntlet, eliminatedByRound, gauntletId, randomness);
955970
}
956971

957972
// Clean up player statuses
@@ -967,7 +982,6 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
967982
// Extract participant IDs for event emission
968983
uint256 participantCount = activeParticipants.length;
969984
uint32[] memory participantIds = new uint32[](participantCount);
970-
uint32[] memory roundWinners = new uint32[](size - 1); // TODO: Get from _runGauntletRounds
971985
for (uint256 i = 0; i < participantCount; i++) {
972986
participantIds[i] = activeParticipants[i].playerId;
973987
}
@@ -978,18 +992,19 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
978992

979993
/// @notice Runs all gauntlet rounds with clean memory management.
980994
/// @return eliminatedByRound Array of player IDs eliminated in each round
995+
/// @return roundWinners Array of winner IDs for each match
981996
function _runGauntletRounds(
982997
Gauntlet storage gauntlet,
983998
uint256 gauntletId,
984999
ActiveParticipant[] memory participants,
9851000
uint256 randomness
986-
) private returns (uint32[] memory eliminatedByRound) {
1001+
) private returns (uint32[] memory eliminatedByRound, uint32[] memory roundWinners) {
9871002
uint8 size = gauntlet.size;
9881003

9891004
uint256 fightSeedBase = uint256(keccak256(abi.encodePacked(randomness, gauntletId)));
9901005

9911006
// Initialize memory arrays to track round winners and eliminations (not stored in contract)
992-
uint32[] memory roundWinners = new uint32[](size - 1);
1007+
roundWinners = new uint32[](size - 1);
9931008
eliminatedByRound = new uint32[](size);
9941009
uint256 winnerIndex = 0;
9951010
uint256 eliminatedIndex = 0;
@@ -1066,8 +1081,8 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
10661081
gauntlet.completionTimestamp = block.timestamp;
10671082
gauntlet.state = GauntletState.COMPLETED;
10681083

1069-
// Return elimination data for XP/reward processing
1070-
return eliminatedByRound;
1084+
// Return elimination data for XP/reward processing and round winners for event
1085+
return (eliminatedByRound, roundWinners);
10711086
}
10721087

10731088
//==============================================================//

src/game/modes/TournamentGame.sol

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
9797

9898
}
9999

100+
/// @notice Reasons why a player might be replaced in a tournament.
101+
enum ReplacementReason {
102+
PLAYER_RETIRED,
103+
SKIN_OWNERSHIP_LOST
104+
}
105+
100106
//==============================================================//
101107
// STRUCTS //
102108
//==============================================================//
@@ -261,7 +267,10 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
261267
);
262268
/// @notice Emitted when a player is replaced during tournament execution.
263269
event PlayerReplaced(
264-
uint256 indexed tournamentId, uint32 indexed originalPlayerId, uint32 indexed replacementPlayerId, string reason
270+
uint256 indexed tournamentId,
271+
uint32 indexed originalPlayerId,
272+
uint32 indexed replacementPlayerId,
273+
ReplacementReason reason
265274
);
266275
/// @notice Emitted when a tournament is auto-recovered due to blockhash expiration.
267276
event TournamentAutoRecovered(uint256 commitBlock, uint256 currentBlock, TournamentPhase phase);
@@ -335,16 +344,17 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
335344
// Check retirement status (single external call)
336345
if (playerContract.isPlayerRetired(loadout.playerId)) revert PlayerIsRetired();
337346

338-
// Cache equipment requirements to avoid repeated external calls
347+
// Cache equipment requirements and skin registry to avoid repeated external calls
339348
IEquipmentRequirements equipmentReqs = playerContract.equipmentRequirements();
349+
IPlayerSkinRegistry skinRegistry = playerContract.skinRegistry();
340350

341-
// Validate skin and equipment requirements via Player contract registries
342-
try playerContract.skinRegistry().validateSkinOwnership(loadout.skin, owner) {}
351+
// Validate skin and equipment requirements via cached registry reference
352+
try skinRegistry.validateSkinOwnership(loadout.skin, owner) {}
343353
catch {
344354
revert InvalidSkin();
345355
}
346-
try playerContract.skinRegistry().validateSkinRequirements(loadout.skin, playerStats.attributes, equipmentReqs)
347-
{} catch {
356+
try skinRegistry.validateSkinRequirements(loadout.skin, playerStats.attributes, equipmentReqs) {}
357+
catch {
348358
revert InvalidLoadout();
349359
}
350360

@@ -803,23 +813,24 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
803813
// Check if real player needs replacement
804814
if (!_isDefaultPlayerId(regPlayer.playerId)) {
805815
bool shouldReplace = false;
806-
string memory reason = "";
816+
ReplacementReason reason;
807817

808818
// Check if player is retired
809819
if (playerContract.isPlayerRetired(regPlayer.playerId)) {
810820
shouldReplace = true;
811-
reason = "PLAYER_RETIRED";
821+
reason = ReplacementReason.PLAYER_RETIRED;
812822
}
813823

814824
// Check if player still owns their skin
815825
if (!shouldReplace) {
816-
address playerOwner = playerContract.getPlayerOwner(regPlayer.playerId);
817-
try playerContract.skinRegistry().validateSkinOwnership(regPlayer.loadout.skin, playerOwner) {
826+
try playerContract.skinRegistry().validateSkinOwnership(
827+
regPlayer.loadout.skin, playerContract.getPlayerOwner(regPlayer.playerId)
828+
) {
818829
// Skin validation passed
819830
} catch {
820831
// Skin validation failed - player no longer owns the skin
821832
shouldReplace = true;
822-
reason = "SKIN_OWNERSHIP_LOST";
833+
reason = ReplacementReason.SKIN_OWNERSHIP_LOST;
823834
}
824835
}
825836

@@ -1003,7 +1014,7 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
10031014

10041015
// Award ratings and distribute rewards
10051016
_awardTournamentRatings(tournament, eliminatedByRound);
1006-
_distributeRewards(tournament, eliminatedByRound);
1017+
_distributeRewards(tournament, eliminatedByRound, randomness);
10071018

10081019
// Extract participant IDs for event emission
10091020
uint256 participantCount = participants.length;
@@ -1104,15 +1115,17 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11041115
}
11051116

11061117
/// @notice Distributes rewards to tournament winners.
1107-
function _distributeRewards(Tournament storage tournament, uint32[] memory eliminatedByRound) private {
1118+
function _distributeRewards(Tournament storage tournament, uint32[] memory eliminatedByRound, uint256 randomness)
1119+
private
1120+
{
11081121
// Reward champion
11091122
if (_getFighterType(tournament.championId) == Fighter.FighterType.PLAYER) {
1110-
_distributeReward(tournament.id, tournament.championId, winnerRewards);
1123+
_distributeReward(tournament.id, tournament.championId, winnerRewards, randomness);
11111124
}
11121125

11131126
// Reward runner-up
11141127
if (_getFighterType(tournament.runnerUpId) == Fighter.FighterType.PLAYER) {
1115-
_distributeReward(tournament.id, tournament.runnerUpId, runnerUpRewards);
1128+
_distributeReward(tournament.id, tournament.runnerUpId, runnerUpRewards, randomness);
11161129
}
11171130

11181131
// Reward 3rd-4th place (semi-final losers)
@@ -1122,16 +1135,19 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11221135
for (uint256 i = semiFinalistStart; i < semiFinalistEnd; i++) {
11231136
uint32 playerId = eliminatedByRound[i];
11241137
if (_getFighterType(playerId) == Fighter.FighterType.PLAYER) {
1125-
_distributeReward(tournament.id, playerId, thirdFourthRewards);
1138+
_distributeReward(tournament.id, playerId, thirdFourthRewards, randomness);
11261139
}
11271140
}
11281141
}
11291142

11301143
/// @notice Distributes a single reward based on configured percentages.
1131-
function _distributeReward(uint256 tournamentId, uint32 playerId, IPlayerTickets.RewardConfig memory config)
1132-
private
1133-
{
1134-
uint256 random = uint256(keccak256(abi.encodePacked(tournamentId, playerId, block.timestamp)));
1144+
function _distributeReward(
1145+
uint256 tournamentId,
1146+
uint32 playerId,
1147+
IPlayerTickets.RewardConfig memory config,
1148+
uint256 randomness
1149+
) private {
1150+
uint256 random = uint256(keccak256(abi.encodePacked(randomness, tournamentId, playerId)));
11351151
uint256 roll = random % 10000; // 0-9999 for percentage precision
11361152

11371153
IPlayerTickets.RewardType rewardType;
@@ -1164,20 +1180,30 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
11641180
) {
11651181
rewardType = IPlayerTickets.RewardType.ARMOR_SPECIALIZATION_TICKET;
11661182
ticketId = playerTickets.ARMOR_SPECIALIZATION_TICKET();
1167-
} else {
1183+
} else if (
1184+
roll
1185+
< config.attributeSwapPercent + config.createPlayerPercent + config.playerSlotPercent
1186+
+ config.weaponSpecPercent + config.armorSpecPercent + config.duelTicketPercent
1187+
) {
11681188
rewardType = IPlayerTickets.RewardType.DUEL_TICKET;
11691189
ticketId = playerTickets.DUEL_TICKET();
1190+
} else {
1191+
// Must be name change ticket (nameChangePercent is the remainder)
1192+
rewardType = IPlayerTickets.RewardType.NAME_CHANGE_TICKET;
11701193
}
11711194

1172-
// Get owner once and handle both reward types
1173-
address owner = playerContract.getPlayerOwner(playerId);
1174-
1195+
// Handle all reward types - no separate owner variable
11751196
if (rewardType == IPlayerTickets.RewardType.ATTRIBUTE_SWAP) {
11761197
// Award attribute swap charge directly to player
1177-
playerContract.awardAttributeSwap(owner);
1198+
playerContract.awardAttributeSwap(playerContract.getPlayerOwner(playerId));
1199+
} else if (rewardType == IPlayerTickets.RewardType.NAME_CHANGE_TICKET) {
1200+
// Mint name change NFT with VRF randomness
1201+
ticketId = playerTickets.mintNameChangeNFT(playerContract.getPlayerOwner(playerId), random);
1202+
emit RewardDistributed(tournamentId, playerId, rewardType, ticketId);
1203+
return; // Early return for name change tickets
11781204
} else if (ticketId > 0) {
1179-
// Mint ticket for other reward types
1180-
playerTickets.mintFungibleTicket(owner, ticketId, 1);
1205+
// Mint fungible ticket for other reward types
1206+
playerTickets.mintFungibleTicket(playerContract.getPlayerOwner(playerId), ticketId, 1);
11811207
}
11821208

11831209
emit RewardDistributed(tournamentId, playerId, rewardType, ticketId);
@@ -1255,7 +1281,8 @@ contract TournamentGame is BaseGame, ReentrancyGuard {
12551281
returns (ActiveParticipant[] memory)
12561282
{
12571283
// True Fisher-Yates shuffle - clean and simple!
1258-
for (uint256 i = participants.length - 1; i > 0; i--) {
1284+
uint256 participantCount = participants.length;
1285+
for (uint256 i = participantCount - 1; i > 0; i--) {
12591286
seed = uint256(keccak256(abi.encodePacked(seed, i)));
12601287
uint256 j = seed % (i + 1);
12611288

0 commit comments

Comments
 (0)