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

feat: Snippets full integration #182

Merged
merged 10 commits into from
Apr 19, 2024
88 changes: 88 additions & 0 deletions examples/evm/multichain-game/MultichainGame.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import '@openzeppelin/contracts/utils/Strings.sol';
import { AddressToString, StringToAddress } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressString.sol';

import { MultichainGameReceiver } from './MultichainGameReceiver.sol';

contract MultichainGame is AxelarExecutable {
using AddressToString for address;
using StringToAddress for string;

IAxelarGasService public immutable gasService;

constructor(address _gateway, address _gasService) AxelarExecutable(_gateway) {
gasService = IAxelarGasService(_gasService);
}

function guessNumber(
string memory _destChain, //"" if not cross chain bet
string calldata _gameReceiver,
uint256 _guess,
string memory _symbol,
uint256 _amount
) external payable {
require(_guess >= 1 && _guess <= 6, 'Invalid guess');

address tokenAddress = gateway.tokenAddresses(_symbol);

require(tokenAddress != address(0), 'Invalid token');

if (bytes(_destChain).length == 0) {
//NO MULTICHAIN TX PLAYING ON SAME CHAIN
IERC20(tokenAddress).transferFrom(msg.sender, _gameReceiver.toAddress(), _amount);
_checkIfWinner(msg.sender, _guess, _gameReceiver.toAddress());
} else {
//MULTICHAIN TX FROM CHAIN A TO CHAIN B
require(msg.value > 0, 'Insufficient gas');

bytes memory encodedGuess = abi.encode(msg.sender, _guess);

IERC20(tokenAddress).transferFrom(msg.sender, address(this), _amount);
IERC20(tokenAddress).approve(address(gateway), _amount);

gasService.payNativeGasForContractCallWithToken{ value: msg.value }(
address(this),
_destChain,
_gameReceiver,
encodedGuess,
_symbol,
_amount,
msg.sender
);

gateway.callContractWithToken(_destChain, _gameReceiver, encodedGuess, _symbol, _amount);
}
}

function _executeWithToken(
string calldata,
string calldata,
bytes calldata _payload,
string calldata _symbol,
uint256 _amount
) internal override {
address player = abi.decode(_payload, (address));
address tokenAddress = gateway.tokenAddresses(_symbol);
IERC20(tokenAddress).transfer(player, _amount);
}

function _checkIfWinner(address _player, uint256 _guess, address _gameReceiver) internal {
uint256 diceResult = 5; //for testing
// uint256 diceResult = (block.timestamp % 6) + 1;

bool won = _guess == diceResult;

if (won) _payoutWinner(_player, _gameReceiver);
}

function _payoutWinner(address _player, address _gameReceiver) internal {
MultichainGameReceiver(_gameReceiver).payOutAllTokensToWinnerSameChain(_player, address(this).toString(), Strings.toString(block.chainid));
}
}
85 changes: 85 additions & 0 deletions examples/evm/multichain-game/MultichainGameReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import '@openzeppelin/contracts/utils/Strings.sol';

contract MultichainGameReceiver is AxelarExecutable {
string[] public uniqueTokens;

IAxelarGasService public immutable gasService;

address multichainGame;

constructor(address _gateway, address _gasService, address _game) AxelarExecutable(_gateway) {
gasService = IAxelarGasService(_gasService);
multichainGame = _game;
}

function payOutAllTokensToWinnerSameChain(address _player, string calldata _sourceAddress, string calldata _winnersChain) external {
require(msg.sender == multichainGame, 'invalid sender');
_payout(_player, _sourceAddress, _winnersChain);
}

function _executeWithToken(
string calldata _sourceChain,
string calldata _sourceAddress,
bytes calldata _payload,
string calldata _symbol,
uint256
) internal override {
_checkIfWinner(_payload, _symbol, _sourceAddress, _sourceChain);
}

function _checkIfWinner(
bytes calldata _payload,
string memory _tokenSymbol,
string calldata _sourceAddress,
string calldata _sourceChain
) internal {
(address player, uint256 guess) = abi.decode(_payload, (address, uint256));

uint256 diceResult = 5; // for testing
// uint256 diceResult = (block.timestamp % 6) + 1;

_addUniqueTokenSymbol(_tokenSymbol);

bool won = guess == diceResult;

if (won) _payOutAllTokensToWinnerInternal(player, _sourceAddress, _sourceChain);
}

function _addUniqueTokenSymbol(string memory _tokenSymbol) internal {
bool found = false;

for (uint i = 0; i < uniqueTokens.length; i++) {
if (keccak256(abi.encode(uniqueTokens[i])) == keccak256(abi.encode(_tokenSymbol))) {
found = true;
break;
}
}
if (!found) uniqueTokens.push(_tokenSymbol);
}

function _payOutAllTokensToWinnerInternal(address _player, string calldata _sourceAddress, string calldata _winnersChain) internal {
_payout(_player, _sourceAddress, _winnersChain);
}

function _payout(address _player, string calldata _sourceAddress, string calldata _winnersChain) internal {
for (uint i = 0; i < uniqueTokens.length; i++) {
string memory tokenSymbol = uniqueTokens[i];
address tokenAddress = gateway.tokenAddresses(tokenSymbol);
uint256 transferAmount = IERC20(tokenAddress).balanceOf(address(this));
if (keccak256(abi.encode(_winnersChain)) == keccak256(abi.encode(Strings.toString(block.chainid)))) {
IERC20(tokenAddress).transfer(_player, transferAmount);
} else {
IERC20(tokenAddress).approve(address(gateway), transferAmount);
gateway.callContractWithToken(_winnersChain, _sourceAddress, abi.encode(_player), tokenSymbol, transferAmount);
}
}
}
}
42 changes: 42 additions & 0 deletions examples/evm/multichain-game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Multichain Game

This example demonstrates a simple guessing game where users can guess a number between 1-6 and stake some funds for each guess. If the correct number is guessed then you win the pot! The `MultichainGameReceiver` exists on a single chain and can hold a variety of different tokens. It pays back the winner playing the game via the `MultiChainGame` contract on one of the satellite chains the game is deployed on.

### Deployment

```bash
npm run deploy evm/multichain-game [local|testnet]
```
This will deploy the MultichainGame and MultichainGame Receiver contract.

### Execution

To execute the Game run the following command.

```bash
npm run execute evm/multichain-game local [local|testnet]
```

### Example

```bash
npm run deploy evm/multichain-game local
npm run execute evm/multichain-game local "Avalanche" "Fantom" 3 "aUSDC" 3
```

Output:
```
--- Initially ---
your balance at Avalanche is 1000000000000000000
pot balance at Fantom is 0
guess on Fantom chain guess was: 3
you guessed wrong!
guess on Fantom chain guess was: 4
you guessed wrong!
pot balance is now 3
guess on Fantom chain guess was: 5
you guessed right!
---- AFTER CORRECT GUESS ----
pot balance at Fantom is now 0
your balance at Avalanche is now 1000000000000000003
```
114 changes: 114 additions & 0 deletions examples/evm/multichain-game/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict';

const {
utils: { deployContract },
} = require('@axelar-network/axelar-local-dev');

const MultichainGame = rootRequire('./artifacts/examples/evm/multichain-game/MultichainGame.sol/MultichainGame.json');
const MultichainGameReceiver = rootRequire(
'./artifacts/examples/evm/multichain-game/MultichainGameReceiver.sol/MultichainGameReceiver.json',
);

async function deploy(chain, wallet) {
chain.multichainGame = await deployContract(wallet, MultichainGame, [chain.gateway, chain.gasService]);
console.log(`Deployed Multichain Game for ${chain.name} at address: ${chain.multichainGame.address}.`);

chain.multichainReceiver = await deployContract(wallet, MultichainGameReceiver, [
chain.gateway,
chain.gasService,
chain.multichainGame.address,
]);
console.log(`Deployed Multichain Receiver for ${chain.name} at ${chain.multichainReceiver.address}.`);

chain.wallet = wallet;
}

async function execute(chains, wallet, options) {
const args = options.args || [];
let guess;
const { source, destination, calculateBridgeFee } = options;
guess = args[2];
const token = args[3];
const amount = args[4];

const originalBalance = await source.usdc.balanceOf(wallet.address);

function log(guess) {
console.log(`guess on ${destination.name} chain guess was: ${guess}`);
guess != 5 ? console.log('you guessed wrong!') : console.log('you guessed right!');
}

console.log('--- Initially ---');
console.log(`your balance at ${source.name} is ${originalBalance.toString()}`);
console.log(`pot balance at ${destination.name} is ${await destination.usdc.balanceOf(destination.multichainReceiver.address)}`);

const fee = await calculateBridgeFee(source, destination);

//approve on dest
const destChainApproveTx = await destination.usdc.approve(destination.multichainGame.address, amount);
await destChainApproveTx.wait();

//approve on src
const srcChainApproveTx = await source.usdc.approve(source.multichainGame.address, amount * 2);
await srcChainApproveTx.wait();

//guess incorrect on dest
const incorrectGuessTxSameChain = await destination.multichainGame.guessNumber(
'',
destination.multichainReceiver.address,
guess === 5 ? (guess = 4) : guess,
token,
amount,
);
await incorrectGuessTxSameChain.wait();
log(guess);

//guess incorrect 1 on src
const incorrectGuessTxMultiChain = await source.multichainGame.guessNumber(
destination.name,
destination.multichainReceiver.address,
guess != 5 ? (guess = 4) : guess,
token,
amount,
{ value: fee },
);
await incorrectGuessTxMultiChain.wait();
log(guess);

console.log(`pot balance is now ${await destination.usdc.balanceOf(destination.multichainReceiver.address)}`);

//guess correct on src
const correctGuessTxMultiChain = await source.multichainGame.guessNumber(
destination.name,
destination.multichainReceiver.address,
guess != 5 ? (guess = 5) : guess,
token,
amount,
{
value: fee,
},
);
await correctGuessTxMultiChain.wait();
log(guess);

console.log('---- AFTER CORRECT GUESS ----');
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

while (true) {
const updatedBalance = await source.usdc.balanceOf(wallet.address);
if (updatedBalance.gt(originalBalance)) {
const potUpdatedBalance = await destination.usdc.balanceOf(destination.multichainReceiver.address);
const userUpdatedBalance = await source.usdc.balanceOf(wallet.address);
console.log(`pot balance at ${destination.name} is now ${potUpdatedBalance}`);
console.log(`your balance at ${source.name} is now ${userUpdatedBalance}`);
break;
}
await sleep(6000);

}
}

module.exports = {
deploy,
execute,
};
45 changes: 45 additions & 0 deletions examples/evm/multichain-nft-mint/MultichainNFTMint.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import { ERC721 } from '@openzeppelin/contracts/token/ERC721/ERC721.sol';

import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';

contract MultichainNFTMint is AxelarExecutable {
IAxelarGasService public immutable gasService;
ERC721 nft;

constructor(address _gateway, address _gasService, address _nftAddr) AxelarExecutable(_gateway) {
gasService = IAxelarGasService(_gasService);
nft = ERC721(_nftAddr);
}

function mintNftOnDestChain(
string memory _destChain,
string memory _destContractAddr,
address receivingAddr,
uint256 _tokenId
) external payable {
require(msg.value > 0, 'Gas payment required');

bytes memory mintNftPayload = abi.encodeWithSignature('safeMint(address,uint256)', receivingAddr, _tokenId);

//pay gas from source chain
gasService.payNativeGasForContractCall{ value: msg.value }(
address(this), //sender
_destChain,
_destContractAddr,
mintNftPayload,
msg.sender
);

gateway.callContract(_destChain, _destContractAddr, mintNftPayload);
}

function _execute(string calldata, string calldata, bytes calldata _payload) internal override {
(bool success, ) = address(nft).call(_payload);
require(success, 'safeMint() call failed');
}
}
Loading
Loading