diff --git a/examples/token/contracts/Connected.sol b/examples/token/contracts/Connected.sol index e703e425..d5e2ec9d 100644 --- a/examples/token/contracts/Connected.sol +++ b/examples/token/contracts/Connected.sol @@ -5,11 +5,15 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol"; import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol"; +import "./shared/Events.sol"; -contract Connected is ERC20, Ownable { +contract Connected is ERC20, Ownable, Events { GatewayEVM public immutable gateway; address public counterparty; + error InvalidAddress(); + error Unauthorized(); + modifier onlyGateway() { require(msg.sender == address(gateway), "Caller is not the gateway"); _; @@ -17,12 +21,17 @@ contract Connected is ERC20, Ownable { function setCounterparty(address contractAddress) external onlyOwner { counterparty = contractAddress; + emit CounterpartySet(contractAddress); } constructor( address payable gatewayAddress, - address initialOwner - ) ERC20("MyToken", "MTK") Ownable(initialOwner) { + address owner, + string memory name, + string memory symbol + ) ERC20(name, symbol) Ownable(owner) { + if (gatewayAddress == address(0) || owner == address(0)) + revert InvalidAddress(); gateway = GatewayEVM(gatewayAddress); } @@ -31,42 +40,45 @@ contract Connected is ERC20, Ownable { } function transferCrossChain( - address receiver, address destination, + address receiver, uint256 amount ) external payable { _burn(msg.sender, amount); - bytes memory encodedData = abi.encode(receiver, amount, destination); + bytes memory message = abi.encode(destination, receiver, amount); RevertOptions memory revertOptions = RevertOptions( address(this), true, address(0), - encodedData, + message, 0 ); if (destination == address(0)) { - gateway.call(counterparty, encodedData, revertOptions); + gateway.call(counterparty, message, revertOptions); } else { gateway.depositAndCall{value: msg.value}( counterparty, - encodedData, + message, revertOptions ); } + emit TokenTransfer(destination, receiver, amount); } function onCall( - MessageContext calldata messageContext, + MessageContext calldata context, bytes calldata message ) external payable onlyGateway returns (bytes4) { - if (messageContext.sender != counterparty) revert("Unauthorized"); + if (context.sender != counterparty) revert Unauthorized(); (address receiver, uint256 amount) = abi.decode( message, (address, uint256) ); _mint(receiver, amount); + emit TokenTransferReceived(receiver, amount); + return ""; } function onRevert(RevertContext calldata context) external onlyGateway {} diff --git a/examples/token/contracts/Universal.sol b/examples/token/contracts/Universal.sol index 681a6a57..8ffee03d 100644 --- a/examples/token/contracts/Universal.sol +++ b/examples/token/contracts/Universal.sol @@ -9,21 +9,23 @@ import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol"; import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol"; import {SystemContract} from "@zetachain/toolkit/contracts/SystemContract.sol"; +import "./shared/Events.sol"; -contract Universal is ERC20, Ownable, UniversalContract { +contract Universal is ERC20, Ownable, UniversalContract, Events { GatewayZEVM public immutable gateway; SystemContract public immutable systemContract = SystemContract(0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9); uint256 private _nextTokenId; bool public isUniversal = true; - uint256 public gasLimit = 700000; + uint256 public gasLimit; error TransferFailed(); + error Unauthorized(); + error InvalidAddress(); + error InvalidGasLimit(); mapping(address => bytes) public counterparty; - event CounterpartySet(address indexed zrc20, bytes indexed contractAddress); - modifier onlyGateway() { require(msg.sender == address(gateway), "Caller is not the gateway"); _; @@ -31,9 +33,16 @@ contract Universal is ERC20, Ownable, UniversalContract { constructor( address payable gatewayAddress, - address initialOwner - ) ERC20("MyToken", "MTK") Ownable(initialOwner) { + address owner, + string memory name, + string memory symbol, + uint256 gas + ) ERC20(name, symbol) Ownable(owner) { + if (gatewayAddress == address(0) || owner == address(0)) + revert InvalidAddress(); + if (gas == 0) revert InvalidGasLimit(); gateway = GatewayZEVM(gatewayAddress); + gasLimit = gas; } function setCounterparty( @@ -41,22 +50,25 @@ contract Universal is ERC20, Ownable, UniversalContract { bytes memory contractAddress ) external onlyOwner { counterparty[zrc20] = contractAddress; - emit CounterpartySet(zrc20, contractAddress); + emit CounterpartyMappingSet(zrc20, contractAddress); } function transferCrossChain( + address destination, address receiver, - address zrc20, uint256 amount ) public { + if (receiver == address(0)) revert InvalidAddress(); _burn(msg.sender, amount); - (, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(gasLimit); - if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), gasFee)) - revert TransferFailed(); - IZRC20(zrc20).approve(address(gateway), gasFee); - - bytes memory encodedData = abi.encode(receiver, amount); + (, uint256 gasFee) = IZRC20(destination).withdrawGasFeeWithGasLimit( + gasLimit + ); + if ( + !IZRC20(destination).transferFrom(msg.sender, address(this), gasFee) + ) revert TransferFailed(); + IZRC20(destination).approve(address(gateway), gasFee); + bytes memory message = abi.encode(receiver, amount); CallOptions memory callOptions = CallOptions(gasLimit, false); @@ -64,17 +76,18 @@ contract Universal is ERC20, Ownable, UniversalContract { address(this), true, address(0), - encodedData, + message, gasLimit ); gateway.call( - counterparty[zrc20], - zrc20, - encodedData, + counterparty[destination], + destination, + message, callOptions, revertOptions ); + emit TokenTransfer(destination, receiver, amount); } function mint(address to, uint256 amount) public onlyOwner { @@ -82,20 +95,20 @@ contract Universal is ERC20, Ownable, UniversalContract { } function onCall( - MessageContext calldata messageContext, + MessageContext calldata context, address zrc20, uint256 amount, bytes calldata message ) external override onlyGateway { - if (keccak256(messageContext.origin) != keccak256(counterparty[zrc20])) - revert("Unauthorized"); - (address receiver, uint256 tokenAmount, address destination) = abi - .decode(message, (address, uint256, address)); + if (keccak256(context.origin) != keccak256(counterparty[zrc20])) + revert Unauthorized(); + (address destination, address receiver, uint256 tokenAmount) = abi + .decode(message, (address, address, uint256)); if (destination == address(0)) { _mint(receiver, tokenAmount); } else { (, uint256 gasFee) = IZRC20(destination).withdrawGasFeeWithGasLimit( - 700000 + gasLimit ); SwapHelperLib.swapExactTokensForTokens( systemContract, @@ -109,11 +122,19 @@ contract Universal is ERC20, Ownable, UniversalContract { counterparty[destination], destination, abi.encode(receiver, tokenAmount), - CallOptions(700000, false), + CallOptions(gasLimit, false), RevertOptions(address(0), false, address(0), "", 0) ); } + emit TokenTransferToDestination(destination, receiver, amount); } - function onRevert(RevertContext calldata context) external onlyGateway {} + function onRevert(RevertContext calldata context) external onlyGateway { + (address sender, uint256 amount) = abi.decode( + context.revertMessage, + (address, uint256) + ); + _mint(sender, amount); + emit TokenTransferReverted(sender, amount); + } } diff --git a/examples/token/contracts/shared/Events.sol b/examples/token/contracts/shared/Events.sol new file mode 100644 index 00000000..26fa43ed --- /dev/null +++ b/examples/token/contracts/shared/Events.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract Events { + event CounterpartyMappingSet( + address indexed zrc20, + bytes indexed contractAddress + ); + event CounterpartySet(address indexed contractAddress); + + event TokenMinted(address indexed to, uint256 amount); + event TokenTransfer( + address indexed destination, + address indexed receiver, + uint256 amount + ); + event TokenTransferReceived(address indexed receiver, uint256 amount); + event TokenTransferReverted(address indexed sender, uint256 amount); + event TokenTransferToDestination( + address indexed destination, + address indexed sender, + uint256 amount + ); +} diff --git a/examples/token/scripts/test.sh b/examples/token/scripts/test.sh index 25296410..43790de7 100755 --- a/examples/token/scripts/test.sh +++ b/examples/token/scripts/test.sh @@ -1,6 +1,7 @@ #!/bin/bash set -e +set -x if [ "$1" = "localnet" ]; then npx hardhat localnet --exit-on-error & sleep 10 @@ -28,7 +29,7 @@ GATEWAY_BNB=$(jq -r '.addresses[] | select(.type=="gatewayEVM" and .chain=="bnb" SENDER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -CONTRACT_ZETACHAIN=$(npx hardhat deploy --network localhost --json | jq -r '.contractAddress') +CONTRACT_ZETACHAIN=$(npx hardhat deploy --network localhost --gas-limit 700000 --json | jq -r '.contractAddress') echo -e "\nšŸš€ Deployed contract on ZetaChain: $CONTRACT_ZETACHAIN" CONTRACT_ETHEREUM=$(npx hardhat deploy --name Connected --json --network localhost --gateway "$GATEWAY_ETHEREUM" | jq -r '.contractAddress') diff --git a/examples/token/tasks/deploy.ts b/examples/token/tasks/deploy.ts index 1421fab3..534cced7 100644 --- a/examples/token/tasks/deploy.ts +++ b/examples/token/tasks/deploy.ts @@ -11,8 +11,14 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { ); } - const factory = await hre.ethers.getContractFactory(args.name); - const contract = await factory.deploy(args.gateway, signer.address); + const factory: any = await hre.ethers.getContractFactory(args.name); + const contract = await factory.deploy( + args.gateway, + signer.address, + args.tokenName, + args.tokenSymbol, + ...(args.gasLimit ? [args.gasLimit] : []) + ); await contract.deployed(); if (args.json) { @@ -33,7 +39,10 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { task("deploy", "Deploy the NFT contract", main) .addFlag("json", "Output the result in JSON format") + .addOptionalParam("tokenName", "Token name", "Universal Token") + .addOptionalParam("tokenSymbol", "Token symbol", "UFT") .addOptionalParam("name", "The contract name to deploy", "Universal") + .addOptionalParam("gasLimit", "Gas limit for the transaction") .addOptionalParam( "gateway", "Gateway address (default: ZetaChain Gateway)", diff --git a/examples/token/tasks/transfer.ts b/examples/token/tasks/transfer.ts index c82b7e9b..0016afdd 100644 --- a/examples/token/tasks/transfer.ts +++ b/examples/token/tasks/transfer.ts @@ -39,9 +39,13 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const receiver = args.receiver || signer.address; + console.log(args.to, receiver, args.amount, { + ...txOptions, + value: gasAmount, + }); tx = await (contract as any).transferCrossChain( - receiver, args.to, + receiver, args.amount, { ...txOptions, value: gasAmount } );