Skip to content

Commit

Permalink
Merge pull request #1 from hackbg/feature/improve-docs
Browse files Browse the repository at this point in the history
Feature/improve docs
  • Loading branch information
danielgruesso authored Oct 24, 2023
2 parents 55c38c4 + 827e57e commit d4fdd84
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 1 deletion.
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

0 comments on commit d4fdd84

Please sign in to comment.