Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/improve docs #1

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ Navigate to each directory and follow the instructions in their respective READM

## Solution Overview

<img src="./solution-diagram.png" alt="Solution" width="100%"/>
<img src="./solution-diagram.png" alt="Solution" width="100%"/><br>

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)<br>
6.1 Transfer winnings [NativeTokenSender.sol#L66](./contracts/contracts/ccip/NativeTokenSender.sol#L66)

## Resources

Expand Down
6 changes: 6 additions & 0 deletions contracts/contracts/ResultsConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
23 changes: 23 additions & 0 deletions contracts/contracts/SportsPredictionGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -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);
}

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

Expand All @@ -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);

Expand Down Expand Up @@ -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;
}
Expand All @@ -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));
}
}
Expand Down
5 changes: 5 additions & 0 deletions contracts/contracts/ccip/NativeTokenReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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,
Expand All @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions contracts/contracts/ccip/NativeTokenSender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading