diff --git a/README.md b/README.md
index 6928a29..4850a8d 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,17 @@ Navigate to each directory and follow the instructions in their respective READM
## Solution Overview
-
+
+
+Code references:
+
+1. Predict game result: [PredictionGame.sol#L116](./contracts/contracts/SportsPredictionGame.sol#L116)
+2. Check for finished games: [PredictionGame.sol#379](./contracts/contracts/SportsPredictionGame.sol#L379)
+3. Request game result: [ResultsConsumer.sol#L65](./contracts/contracts/ResultsConsumer.sol#L65)
+4. Fetch game result: [sports-api.js#L63](./contracts/sports-api.js#L63)
+5. Fulfill game result request [ResultsConsumer.sol#L105](./contracts/contracts/ResultsConsumer.sol#L105)
+6. Claim winnings [SportsPredictionGame.sol#L151](./contracts/contracts/SportsPredictionGame.sol#L151)
+ 6.1 Transfer winnings [NativeTokenSender.sol#L66](./contracts/contracts/ccip/NativeTokenSender.sol#L66)
## Resources
diff --git a/contracts/contracts/ResultsConsumer.sol b/contracts/contracts/ResultsConsumer.sol
index 96264ed..b6db7c5 100644
--- a/contracts/contracts/ResultsConsumer.sol
+++ b/contracts/contracts/ResultsConsumer.sol
@@ -63,11 +63,14 @@ abstract contract ResultsConsumer is FunctionsClient {
/// @param externalId The ID of the game on the external sports API
/// @return requestId The Chainlink Functions request ID
function _requestResult(uint256 sportId, uint256 externalId) internal returns (bytes32 requestId) {
+ // Prepare the arguments for the Chainlink Functions request
string[] memory args = new string[](2);
args[0] = Strings.toString(sportId);
args[1] = Strings.toString(externalId);
+ // Send the Chainlink Functions request
requestId = _executeRequest(args);
+ // Store the request and the associated data for the callback
pending[requestId] = PendingRequest({sportId: sportId, externalId: externalId});
emit RequestedResult(sportId, externalId, requestId);
}
@@ -101,17 +104,20 @@ abstract contract ResultsConsumer is FunctionsClient {
/// @dev This function is called by the oracle
function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
PendingRequest memory request = pending[requestId];
+ // Check if there is a sent request
if (request.sportId == 0) {
emit NoPendingRequest();
return;
}
delete pending[requestId];
+ // Check if the Functions script failed
if (err.length > 0) {
emit RequestFailed(err);
return;
}
emit ResultReceived(requestId, response);
+ // Call the child contract to process the result
_processResult(request.sportId, request.externalId, response);
}
}
diff --git a/contracts/contracts/SportsPredictionGame.sol b/contracts/contracts/SportsPredictionGame.sol
index c81d8c0..4a16629 100644
--- a/contracts/contracts/SportsPredictionGame.sol
+++ b/contracts/contracts/SportsPredictionGame.sol
@@ -117,16 +117,19 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
Game memory game = games[gameId];
uint256 wagerAmount = msg.value;
+ // Check if the prediction is valid
if (game.externalId == 0) revert GameNotRegistered();
if (game.resolved) revert GameIsResolved();
if (game.timestamp < block.timestamp) revert GameAlreadyStarted();
if (wagerAmount < MIN_WAGER) revert InsufficientValue();
if (wagerAmount > MAX_WAGER) revert ValueTooHigh();
+ // Update the game pool amounts
if (result == Result.Home) games[gameId].homeWagerAmount += wagerAmount;
else if (result == Result.Away) games[gameId].awayWagerAmount += wagerAmount;
else revert InvalidResult();
+ // Add the prediction to the user's list of predictions
predictions[msg.sender][gameId].push(Prediction(gameId, result, wagerAmount, false));
emit Predicted(msg.sender, gameId, result, wagerAmount);
}
@@ -151,14 +154,18 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
if (!game.resolved) revert GameNotResolved();
+ // Calculate the total winnings and mark the predictions as claimed
uint256 totalWinnings = 0;
Prediction[] memory userPredictions = predictions[user][gameId];
for (uint256 i = 0; i < userPredictions.length; i++) {
Prediction memory prediction = userPredictions[i];
+ // Skip if the prediction has already been claimed
if (prediction.claimed) continue;
if (game.result == Result.None) {
+ // For a draw, the user gets their tokens back
totalWinnings += prediction.amount;
} else if (prediction.result == game.result) {
+ // Calculate the winnings for correct predictions
uint256 winnings = calculateWinnings(gameId, prediction.amount, prediction.result);
totalWinnings += winnings;
}
@@ -167,9 +174,12 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
if (totalWinnings == 0) revert NothingToClaim();
+ // Claim winnings depending on the transfer parameter
if (transfer) {
+ // Transfer the winnings to the user on the another chain
_sendTransferRequest(user, totalWinnings);
} else {
+ // Transfer the winnings to the user on the same chain
payable(user).transfer(totalWinnings);
}
@@ -186,10 +196,13 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
function _registerGame(uint256 sportId, uint256 externalId, uint256 timestamp) internal returns (uint256 gameId) {
gameId = getGameId(sportId, externalId);
+ // Check if the game can be registered
if (games[gameId].externalId != 0) revert GameAlreadyRegistered();
if (timestamp < block.timestamp) revert TimestampInPast();
+ // Store the game data
games[gameId] = Game(sportId, externalId, timestamp, 0, 0, false, Result.None);
+ // Add the game to the active games list
activeGames.push(gameId);
emit GameRegistered(gameId);
@@ -201,11 +214,14 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
function _requestResolve(uint256 gameId) internal {
Game memory game = games[gameId];
+ // Check if the game can be resolved
if (pendingRequests[gameId] != 0) revert ResolveAlreadyRequested();
if (game.externalId == 0) revert GameNotRegistered();
if (game.resolved) revert GameIsResolved();
if (!readyToResolve(gameId)) revert GameNotReadyToResolve();
+ // Request the result of the game via ResultsConsumer contract
+ // Store the Chainlink Functions request ID to prevent duplicate requests
pendingRequests[gameId] = _requestResult(game.sportId, game.externalId);
}
@@ -225,9 +241,11 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
/// @param result The result of the game
/// @dev Removes the game from the active games list
function _resolveGame(uint256 gameId, Result result) internal {
+ // Store the game result and mark the game as finished
games[gameId].result = result;
games[gameId].resolved = true;
+ // Add the game to the finished games list
resolvedGames.push(gameId);
_removeFromActiveGames(gameId);
@@ -338,7 +356,9 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
/// @dev The game must be registered
function calculateWinnings(uint256 gameId, uint256 wager, Result result) public view returns (uint256) {
Game memory game = games[gameId];
+ // Calculate the total amount of tokens wagered on the game
uint256 totalWager = game.homeWagerAmount + game.awayWagerAmount;
+ // Calculate the winnings based on the result and the total amount of tokens wagered
uint256 winnings = (wager * totalWager) / (result == Result.Home ? game.homeWagerAmount : game.awayWagerAmount);
return winnings;
}
@@ -357,10 +377,13 @@ contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationC
/// @notice Check if any games are ready to be resolved
/// @dev Called by Chainlink Automation to determine if a game result should be requested
function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
+ // Get all games that can be resolved
Game[] memory activeGamesArray = getActiveGames();
+ // Check if any game is ready to be resolved and have not already been requested
for (uint256 i = 0; i < activeGamesArray.length; i++) {
uint256 gameId = getGameId(activeGamesArray[i].sportId, activeGamesArray[i].externalId);
if (readyToResolve(gameId) && pendingRequests[gameId] == 0) {
+ // Signal that a game is ready to be resolved to Chainlink Automation
return (true, abi.encodePacked(gameId));
}
}
diff --git a/contracts/contracts/ccip/NativeTokenReceiver.sol b/contracts/contracts/ccip/NativeTokenReceiver.sol
index 5889392..ca466cf 100644
--- a/contracts/contracts/ccip/NativeTokenReceiver.sol
+++ b/contracts/contracts/ccip/NativeTokenReceiver.sol
@@ -64,9 +64,11 @@ contract NativeTokenReceiver is ProgrammableTokenReceiver {
function _onTokenReceived(bytes32, bytes memory data, address tokenAddress, uint256 tokenAmount) internal override {
if (tokenAddress != exchangeToken) revert InvalidTokenAddress(tokenAddress);
+ // Swap exchange token to native token
address tokenRecipient = abi.decode(data, (address));
uint256 nativeTokenAmount = _swapExchangeTokenToNative(tokenAmount);
+ // Transfer native token to the recipient
(bool success, ) = tokenRecipient.call{value: nativeTokenAmount}("");
if (!success) revert FailedToTransferNativeToken(tokenRecipient);
@@ -77,7 +79,9 @@ contract NativeTokenReceiver is ProgrammableTokenReceiver {
/// @param _exchangeTokenAmount The amount of exchange token to swap
/// @return nativeTokenAmount The amount of native token received
function _swapExchangeTokenToNative(uint256 _exchangeTokenAmount) internal returns (uint256 nativeTokenAmount) {
+ // Approve the Uniswap V3 router to spend the exchange token
TransferHelper.safeApprove(exchangeToken, uniswapV3Router, _exchangeTokenAmount);
+ // Swap the exchange token to wrapped native token
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: exchangeToken,
tokenOut: weth9Token,
@@ -89,6 +93,7 @@ contract NativeTokenReceiver is ProgrammableTokenReceiver {
sqrtPriceLimitX96: 0
});
nativeTokenAmount = ISwapRouter(uniswapV3Router).exactInputSingle(params);
+ // Swap the wrapped native token to native token
IWETH9(weth9Token).withdraw(nativeTokenAmount);
}
diff --git a/contracts/contracts/ccip/NativeTokenSender.sol b/contracts/contracts/ccip/NativeTokenSender.sol
index c06bca8..0415b0a 100644
--- a/contracts/contracts/ccip/NativeTokenSender.sol
+++ b/contracts/contracts/ccip/NativeTokenSender.sol
@@ -65,7 +65,9 @@ abstract contract NativeTokenSender is ProgrammableTokenSender {
/// @return requestId The ID of the CCIP message that was sent
function _sendTransferRequest(address _to, uint256 _amount) internal returns (bytes32 requestId) {
if (destinationContractReceiver == address(0)) revert NoDestinationContractReceiver();
+ // Swap native token to exchange token
uint256 exchangeTokenAmount = _swapNativeToExchangeToken(_amount);
+ // Send the transfer request to CCIP
requestId = _sendMessagePayLINK(
destinationChainSelector,
destinationContractReceiver,