├── web-apps ├── src │ └── app │ │ ├── favicon.ico │ │ ├── components │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ └── Input.tsx │ │ ├── layout.tsx │ │ ├── balance-app │ │ ├── layout.tsx │ │ └── page.tsx │ │ ├── basic-explorer │ │ ├── layout.tsx │ │ └── page.tsx │ │ ├── basic-wallet │ │ ├── layout.tsx │ │ ├── providers.tsx │ │ └── page.tsx │ │ ├── chains │ │ └── definitions │ │ │ ├── echo.ts │ │ │ ├── dispatch.ts │ │ │ └── fuji.ts │ │ ├── globals.css │ │ ├── api │ │ ├── faucet │ │ │ ├── config │ │ │ │ └── route.ts │ │ │ └── send │ │ │ │ └── route.ts │ │ ├── balance │ │ │ └── route.ts │ │ ├── explorer │ │ │ └── route.ts │ │ └── wallet │ │ │ └── route.ts │ │ ├── utils │ │ └── RateLimiter.ts │ │ ├── faucet │ │ └── page.tsx │ │ ├── ictt │ │ └── page.tsx │ │ ├── constants.ts │ │ └── page.tsx ├── public │ ├── chains │ │ └── logo │ │ │ ├── 173750.png │ │ │ ├── 43113.png │ │ │ └── 779672.png │ ├── tokens │ │ └── logo │ │ │ └── 43113 │ │ │ └── native.png │ ├── vercel.svg │ ├── next.svg │ ├── avax.svg │ └── ava-labs.svg ├── postcss.config.mjs ├── next-env.d.ts ├── next.config.mjs ├── tailwind.config.ts ├── tsconfig.json ├── package.json └── .gitignore ├── contracts ├── misc │ ├── creating-contracts │ │ ├── BridgeActions.sol │ │ ├── MyERC20Token.sol │ │ ├── BridgeReceiverOnSubnet.sol │ │ └── BridgeSenderOnCChain.sol │ └── erc721-bridge │ │ ├── ExampleERC721.sol │ │ ├── BridgeNFT.sol │ │ ├── README.md │ │ ├── IERC721Bridge.sol │ │ ├── ERC721Bridge.sol │ │ └── ERC721BridgeTests.t.sol ├── interchain-token-transfer │ ├── cross-chain-token-swaps │ │ ├── interfaces │ │ │ ├── IWAVAX.sol │ │ │ ├── IUniswapFactory.sol │ │ │ └── IUniswapPair.sol │ │ └── DexERC20Wrapper.sol │ ├── MyToken.sol │ └── ExampleWNATV.sol └── interchain-messaging │ ├── invoking-functions │ ├── CalculatorActions.sol │ ├── SimpleCalculatorReceiverOnSubnet.sol │ ├── SimpleCalculatorSenderOnCChain.sol │ ├── CalculatorSenderOnCChain.sol │ └── CalculatorReceiverOnSubnet.sol │ ├── invoking-functions-assignment-solution │ ├── ExtendedCalculatorActions.sol │ ├── ExtendedCalculatorReceiverOnSubnet.sol │ └── ExtendedCalculatorSenderOnCChain.sol │ ├── send-receive │ ├── receiverOnSubnet.sol │ └── senderOnCChain.sol │ ├── incentivize-relayer │ ├── receiverWithFees.sol │ └── senderWithFees.sol │ ├── send-receive-assignment-solution │ ├── extendedReceiverOnSubnet.sol │ └── extendedSenderOnCChain.sol │ ├── registry │ ├── ReceiverOnSubnetWithRegistry.sol │ └── SenderOnCChainWithRegistry.sol │ └── send-roundtrip │ ├── receiverOnSubnet.sol │ └── senderOnCChain.sol ├── .devcontainer ├── configure-ports.sh ├── Dockerfile └── devcontainer.json ├── .gitignore ├── .gitmodules ├── remappings.txt ├── .vscode └── settings.json ├── foundry.toml └── README.md /web-apps/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/HEAD/web-apps/src/app/favicon.ico -------------------------------------------------------------------------------- /web-apps/public/chains/logo/173750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/HEAD/web-apps/public/chains/logo/173750.png -------------------------------------------------------------------------------- /web-apps/public/chains/logo/43113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/HEAD/web-apps/public/chains/logo/43113.png -------------------------------------------------------------------------------- /web-apps/public/chains/logo/779672.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/HEAD/web-apps/public/chains/logo/779672.png -------------------------------------------------------------------------------- /web-apps/public/tokens/logo/43113/native.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/avalanche-starter-kit/HEAD/web-apps/public/tokens/logo/43113/native.png -------------------------------------------------------------------------------- /contracts/misc/creating-contracts/BridgeActions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.18; 3 | 4 | enum BridgeAction { 5 | createToken, 6 | mintToken 7 | } 8 | -------------------------------------------------------------------------------- /web-apps/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.devcontainer/configure-ports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Configuring ports..." 4 | 5 | gh codespace ports visibility 3000:public -c $CODESPACE_NAME 6 | gh codespace ports visibility 9650:public -c $CODESPACE_NAME 7 | 8 | echo "Ports configuration completed!" 9 | -------------------------------------------------------------------------------- /web-apps/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/cross-chain-token-swaps/interfaces/IWAVAX.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.18; 3 | 4 | interface IWAVAX { 5 | function withdraw(uint256 amount) external; 6 | 7 | function deposit() external payable; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/cross-chain-token-swaps/interfaces/IUniswapFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.18; 3 | 4 | interface IUniswapFactory { 5 | function getPair(address tokenA, address tokenB) external view returns (address pair); 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Dotenv file 11 | .env 12 | 13 | .yarn/cache 14 | .yarn/unplugged 15 | .yarn/build-state.yml 16 | .yarn/install-state.gz 17 | .pnp.* 18 | 19 | yarn.lock -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions/CalculatorActions.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | enum CalculatorAction { 9 | add, 10 | concatenate 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions-assignment-solution/ExtendedCalculatorActions.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | enum CalculatorAction { 9 | add, 10 | concatenate, 11 | tripleSum 12 | } 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | branch = v4.8 8 | [submodule "lib/icm-contracts"] 9 | path = lib/icm-contracts 10 | url = https://github.com/ava-labs/icm-contracts 11 | branch = main 12 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | 4 | @openzeppelin/contracts@4.8.1/=lib/openzeppelin-contracts/contracts/ 5 | @avalabs/subnet-evm-contracts@1.2.0=lib/icm-contracts/lib/subnet-evm/contracts/ 6 | @teleporter/=lib/icm-contracts/contracts/teleporter/ 7 | @teleporter-mocks/=lib/icm-contracts/contracts/mocks/ 8 | @avalanche-interchain-token-transfer/=lib/icm-contracts/contracts/ictt/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[solidity]": { 4 | "editor.defaultFormatter": "JuanBlanco.solidity" 5 | }, 6 | "solidity.formatter": "forge", 7 | "solidity.packageDefaultDependenciesContractsDirectory": "src", 8 | "solidity.packageDefaultDependenciesDirectory": "lib", 9 | "solidity.defaultCompiler": "localNodeModule", 10 | "solidity.compileUsingRemoteVersion": "v0.8.18+commit.87f61d96", 11 | } -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/MyToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Compatible with OpenZeppelin Contracts ^5.0.0 3 | pragma solidity ^ 0.8.18; 4 | 5 | import "@openzeppelin/contracts@4.8.1/token/ERC20/ERC20.sol"; 6 | 7 | /* this is an example ERC20 token called TOK */ 8 | 9 | contract TOK is ERC20 { 10 | constructor() ERC20("TOK", "TOK") { 11 | _mint(msg.sender, 100000 * 10 ** decimals()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/misc/creating-contracts/MyERC20Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.18; 3 | 4 | import "@openzeppelin/contracts@4.8.1/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts@4.8.1/access/Ownable.sol"; 6 | 7 | contract myToken is ERC20, Ownable { 8 | constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} 9 | 10 | function mint(address to, uint256 amount) public onlyOwner { 11 | _mint(to, amount); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web-apps/src/app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, MouseEventHandler } from 'react'; 2 | 3 | interface ButtonProps { 4 | children: ReactNode; 5 | onClick: MouseEventHandler; 6 | className?: string; 7 | } 8 | 9 | export const Button = ({ children, onClick, className = '' }: ButtonProps) => ( 10 | 16 | ) -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "contracts" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.25" 6 | evm_version = "shanghai" 7 | 8 | [rpc_endpoints] 9 | local-c = "http://localhost:9650/ext/bc/C/rpc" 10 | myblockchain = "http://localhost:9650/ext/bc/myblockchain/rpc" 11 | fuji-c = "https://api.avax-test.network/ext/bc/C/rpc" 12 | dispatch = "https://subnets.avax.network/dispatch/testnet/rpc" 13 | mysubnet = "http://localhost:9650/ext/bc/mysubnet/rpc" 14 | 15 | 16 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 17 | -------------------------------------------------------------------------------- /web-apps/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack: config => { 5 | config.externals.push('pino-pretty', 'lokijs', 'encoding'); 6 | return config; 7 | }, 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: 'https', 12 | hostname: 'images.ctfassets.net', 13 | }, 14 | { 15 | protocol: 'https', 16 | hostname: 'cdn.salvor.io', 17 | } 18 | ], 19 | }, 20 | }; 21 | 22 | export default nextConfig; 23 | -------------------------------------------------------------------------------- /web-apps/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Avalanche Starter Kit", 9 | description: "Explore Developer Tools for Avalanche", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /web-apps/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /web-apps/src/app/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface CardProps { 4 | title: string; 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export const Card = ({ title, children, className = '' }: CardProps) => ( 10 |
11 |
12 |

{title}

13 |
14 |
15 | {children} 16 |
17 |
18 | ) -------------------------------------------------------------------------------- /web-apps/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-apps/src/app/balance-app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "../globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Glacier Token Balance App", 9 | description: "Learn more about the Glacier SDK and API by AvaCloud", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /web-apps/src/app/basic-explorer/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "../globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Glacier Basic Explorer", 9 | description: "Learn more about the Glacier SDK and API by AvaCloud", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /web-apps/src/app/basic-wallet/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@rainbow-me/rainbowkit/styles.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import "../globals.css"; 5 | import Providers from "./providers"; 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Glacier Basic Wallet", 10 | description: "Learn more about the Glacier SDK and API by AvaCloud", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode; 17 | }>) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /web-apps/src/app/components/Input.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { ChangeEvent } from 'react'; 3 | 4 | interface InputProps { 5 | value: string; 6 | onChange: (event: ChangeEvent) => void; 7 | placeholder?: string; 8 | className?: string; 9 | } 10 | 11 | export const Input: React.FC = ({ value, onChange, placeholder, className = '' }) => ( 12 | 19 | ) -------------------------------------------------------------------------------- /web-apps/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /web-apps/src/app/chains/definitions/echo.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "viem"; 2 | 3 | export const echo = defineChain({ 4 | id: 173750, 5 | name: 'Echo L1', 6 | network: 'echo', 7 | nativeCurrency: { 8 | decimals: 18, 9 | name: 'Ech', 10 | symbol: 'ECH', 11 | }, 12 | rpcUrls: { 13 | default: { 14 | http: ['https://subnets.avax.network/echo/testnet/rpc'] 15 | }, 16 | }, 17 | blockExplorers: { 18 | default: { name: 'Explorer', url: 'https://subnets-test.avax.network/echo' }, 19 | }, 20 | // Custom variables 21 | iconUrl: "/chains/logo/173750.png", 22 | icm_registry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228" 23 | }); -------------------------------------------------------------------------------- /web-apps/src/app/chains/definitions/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "viem"; 2 | 3 | export const dispatch = defineChain({ 4 | id: 779672, 5 | name: 'Dispatch L1', 6 | network: 'dispatch', 7 | nativeCurrency: { 8 | decimals: 18, 9 | name: 'DIS', 10 | symbol: 'DIS', 11 | }, 12 | rpcUrls: { 13 | default: { 14 | http: ['https://subnets.avax.network/dispatch/testnet/rpc'] 15 | }, 16 | }, 17 | blockExplorers: { 18 | default: { name: 'Explorer', url: 'https://subnets-test.avax.network/dispatch' }, 19 | }, 20 | // Custom variables 21 | iconUrl: "/chains/logo/779672.png", 22 | icm_registry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228" 23 | }); -------------------------------------------------------------------------------- /web-apps/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web-apps/src/app/api/faucet/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { 4 | CHAINS 5 | } from './../../../constants' 6 | 7 | import { Wallet } from "ethers"; 8 | 9 | export async function GET() { 10 | const fauced_supported_chains = CHAINS.filter((c: any) => c.faucet !== undefined); 11 | return NextResponse.json({ 12 | chains: fauced_supported_chains.map((c: any) => { 13 | const { id, blockExplorers, faucet } = c; 14 | const pk = process.env[`PK_${id}`]; 15 | if (pk === undefined) { 16 | return; 17 | } 18 | const wallet = new Wallet(pk); 19 | faucet.address = wallet.address; 20 | return { id, blockExplorers, faucet }; 21 | }) 22 | }); 23 | } -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/cross-chain-token-swaps/interfaces/IUniswapPair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.18; 3 | 4 | interface IUniswapPair { 5 | event Swap( 6 | address indexed sender, 7 | uint256 amount0In, 8 | uint256 amount1In, 9 | uint256 amount0Out, 10 | uint256 amount1Out, 11 | address indexed to 12 | ); 13 | 14 | function factory() external view returns (address); 15 | 16 | function token0() external view returns (address); 17 | 18 | function token1() external view returns (address); 19 | 20 | function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 21 | 22 | function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; 23 | } 24 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-receive/receiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract ReceiverOnSubnet is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | string public lastMessage; 15 | 16 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 17 | // Only the Teleporter receiver can deliver a message. 18 | require(msg.sender == address(messenger), "ReceiverOnSubnet: unauthorized TeleporterMessenger"); 19 | 20 | // Store the message. 21 | lastMessage = abi.decode(message, (string)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web-apps/src/app/utils/RateLimiter.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from 'lru-cache' 2 | 3 | const requests = new LRUCache({ 4 | ttl: 1000 * 60 * 60 * 24, 5 | ttlAutopurge: true 6 | }); 7 | 8 | export function rateLimiter(ip: string, max_limit: number, window_size: number) { 9 | const now = Date.now(); 10 | const data: any = requests.get(ip); 11 | // First request 12 | if (data === undefined) { 13 | requests.set(ip, { count: 1, timestamp: now }); 14 | return true; 15 | } 16 | // Available on the cache 17 | const { count, timestamp } = data; 18 | if (now - timestamp < window_size) { 19 | if (count >= max_limit) { 20 | return false; 21 | } else { 22 | requests.set(ip, { count: count + 1, timestamp: timestamp }); 23 | return true; 24 | } 25 | } else { 26 | requests.set(ip, { count: 1, timestamp: now }); 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/incentivize-relayer/receiverWithFees.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract ReceiverOnSubnet is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | string public lastMessage; 15 | 16 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 17 | // Only the Teleporter receiver can deliver a message. 18 | require(msg.sender == address(messenger), "ReceiverOnSubnet: unauthorized TeleporterMessenger"); 19 | 20 | // Store the message. 21 | lastMessage = abi.decode(message, (string)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web-apps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glacier-starter-kit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@avalabs/avacloud-sdk": "^0.3.1", 13 | "@rainbow-me/rainbowkit": "^2.1.6", 14 | "@tanstack/react-query": "^5.56.2", 15 | "lucide-react": "^0.441.0", 16 | "next": "14.2.10", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "viem": "2.x", 20 | "wagmi": "^2.12.11", 21 | "zod": "^3.23.8", 22 | "@0xstt/builderkit": "^0.0.1", 23 | "lru-cache": "^11.0.1" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20", 27 | "@types/react": "^18", 28 | "@types/react-dom": "^18", 29 | "eslint": "^8", 30 | "eslint-config-next": "14.2.7", 31 | "postcss": "^8", 32 | "tailwindcss": "^3.4.1", 33 | "typescript": "^5" 34 | } 35 | } -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-receive-assignment-solution/extendedReceiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract ReceiverOnSubnet is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | uint public sum = 0; 15 | 16 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 17 | // Only the Teleporter receiver can deliver a message. 18 | require(msg.sender == address(messenger), "ReceiverOnSubnet: unauthorized TeleporterMessenger"); 19 | 20 | // Store the message. 21 | sum = sum + abi.decode(message, (uint256)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web-apps/src/app/faucet/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Faucet, PoweredByAvalanche, Web3Provider } from "@0xstt/builderkit"; 4 | import { CHAINS, TOKENS } from "./../constants"; 5 | 6 | export default function Home() { 7 | 8 | return ( 9 | 10 |
11 | {/* Faucet Example */} 12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/registry/ReceiverOnSubnetWithRegistry.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/upgrades/TeleporterRegistry.sol"; 9 | import "@teleporter/ITeleporterMessenger.sol"; 10 | import "@teleporter/ITeleporterReceiver.sol"; 11 | 12 | contract ReceiverOnDispatchWithRegistry is ITeleporterReceiver { 13 | // The Teleporter registry contract manages different Teleporter contract versions. 14 | TeleporterRegistry public immutable teleporterRegistry = 15 | TeleporterRegistry(0x827364Da64e8f8466c23520d81731e94c8DDe510); 16 | 17 | string public lastMessage; 18 | 19 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 20 | // Only a Teleporter Messenger registered in the registry can deliver a message. 21 | // Function throws an error if msg.sender is not registered. 22 | teleporterRegistry.getVersionFromAddress(msg.sender); 23 | 24 | // Store the message. 25 | lastMessage = abi.decode(message, (string)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions/SimpleCalculatorReceiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract SimpleCalculatorReceiverOnSubnet is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable teleporterMessenger = 13 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 14 | 15 | uint256 public result_num; 16 | 17 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 18 | // Only the Teleporter receiver can deliver a message. 19 | require( 20 | msg.sender == address(teleporterMessenger), "CalculatorReceiverOnSubnet: unauthorized TeleporterMessenger" 21 | ); 22 | 23 | (uint256 a, uint256 b) = abi.decode(message, (uint256, uint256)); 24 | _calculatorAdd(a, b); 25 | } 26 | 27 | function _calculatorAdd(uint256 _num1, uint256 _num2) internal { 28 | result_num = _num1 + _num2; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-receive/senderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | 10 | contract SenderOnCChain { 11 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 12 | 13 | /** 14 | * @dev Sends a message to another chain. 15 | */ 16 | function sendMessage(address destinationAddress, string calldata message) external { 17 | messenger.sendCrossChainMessage( 18 | TeleporterMessageInput({ 19 | // Replace with blockchainID of your Subnet (see instructions in Readme) 20 | destinationBlockchainID: 0x108ce15038973062d8628fd20c8c657effe993dd8324297353e350dfc05dacad, 21 | destinationAddress: destinationAddress, 22 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 23 | requiredGasLimit: 100000, 24 | allowedRelayerAddresses: new address[](0), 25 | message: abi.encode(message) 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-receive-assignment-solution/extendedSenderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | 10 | contract SenderOnCChain { 11 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 12 | 13 | /** 14 | * @dev Sends a message to another chain. 15 | */ 16 | function sendMessage(address destinationAddress, uint256 message) external { 17 | messenger.sendCrossChainMessage( 18 | TeleporterMessageInput({ 19 | // Replace with blockchainID of your Subnet (see instructions in Readme) 20 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 21 | destinationAddress: destinationAddress, 22 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 23 | requiredGasLimit: 100000, 24 | allowedRelayerAddresses: new address[](0), 25 | message: abi.encode(message) 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/ExampleERC721.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | /** 9 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 10 | * DO NOT USE THIS CODE IN PRODUCTION. 11 | */ 12 | import {ERC721} from "@openzeppelin/contracts@4.8.1/token/ERC721/ERC721.sol"; 13 | import {ERC721Burnable} from "@openzeppelin/contracts@4.8.1/token/ERC721/extensions/ERC721Burnable.sol"; 14 | 15 | contract ExampleERC721 is ERC721, ERC721Burnable { 16 | string private constant _TOKEN_NAME = "Mock Token"; 17 | string private constant _TOKEN_SYMBOL = "EXMP"; 18 | string private constant _TOKEN_URI = "https://example.com/ipfs/"; 19 | 20 | constructor() ERC721(_TOKEN_NAME, _TOKEN_SYMBOL) {} 21 | 22 | function mint(uint256 tokenId) external { 23 | require(_ownerOf(tokenId) == address(0), "Token already minted"); 24 | _mint(msg.sender, tokenId); 25 | } 26 | 27 | function _baseURI() internal view virtual override returns (string memory) { 28 | return _TOKEN_URI; 29 | } 30 | 31 | function baseUri() external pure returns (string memory) { 32 | return _TOKEN_URI; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/ExampleWNATV.sol: -------------------------------------------------------------------------------- 1 | // (c) 2024, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity 0.8.25; 7 | 8 | import {IWrappedNativeToken} from "@avalanche-interchain-token-transfer/interfaces/IWrappedNativeToken.sol"; 9 | import {ERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/ERC20.sol"; 10 | import {Address} from "@openzeppelin/contracts@5.0.2/utils/Address.sol"; 11 | 12 | /** 13 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 14 | * DO NOT USE THIS CODE IN PRODUCTION. 15 | */ 16 | contract WNATV is IWrappedNativeToken, ERC20 { 17 | using Address for address payable; 18 | 19 | constructor() ERC20("Wrapped NATV", "WNATV") {} 20 | 21 | receive() external payable { 22 | deposit(); 23 | } 24 | 25 | fallback() external payable { 26 | deposit(); 27 | } 28 | 29 | function withdraw(uint256 amount) external { 30 | _burn(msg.sender, amount); 31 | emit Withdrawal(msg.sender, amount); 32 | payable(msg.sender).sendValue(amount); 33 | } 34 | 35 | function deposit() public payable { 36 | _mint(msg.sender, msg.value); 37 | emit Deposit(msg.sender, msg.value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web-apps/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions/SimpleCalculatorSenderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | 10 | contract SimpleCalculatorSenderOnCChain { 11 | ITeleporterMessenger public immutable teleporterMessenger = 12 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | function sendAddMessage(address destinationAddress, uint256 num1, uint256 num2) external { 15 | teleporterMessenger.sendCrossChainMessage( 16 | TeleporterMessageInput({ 17 | // Replace with chain id of your Subnet (see instructions in Readme) 18 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 19 | destinationAddress: destinationAddress, 20 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 21 | requiredGasLimit: 100000, 22 | allowedRelayerAddresses: new address[](0), 23 | message: encodeAddData(num1, num2) 24 | }) 25 | ); 26 | } 27 | 28 | //Encode helpers 29 | function encodeAddData(uint256 a, uint256 b) public pure returns (bytes memory) { 30 | bytes memory paramsData = abi.encode(a, b); 31 | return paramsData; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/registry/SenderOnCChainWithRegistry.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/upgrades/TeleporterRegistry.sol"; 9 | import "@teleporter/ITeleporterMessenger.sol"; 10 | 11 | contract SenderOnCChain { 12 | // The Teleporter registry contract manages different Teleporter contract versions. 13 | TeleporterRegistry public immutable teleporterRegistry = 14 | TeleporterRegistry(0x827364Da64e8f8466c23520d81731e94c8DDe510); 15 | 16 | /** 17 | * @dev Sends a message to another chain. 18 | */ 19 | function sendMessage(address destinationAddress, string calldata message) external { 20 | ITeleporterMessenger messenger = teleporterRegistry.getLatestTeleporter(); 21 | 22 | messenger.sendCrossChainMessage( 23 | TeleporterMessageInput({ 24 | // Replace with blockchainID of your Subnet (see instructions in Readme) 25 | destinationBlockchainID: 0x92756d698399805f0088fc07fc42af47c67e1d38c576667ac6c7031b8df05293, 26 | destinationAddress: destinationAddress, 27 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 28 | requiredGasLimit: 100000, 29 | allowedRelayerAddresses: new address[](0), 30 | message: abi.encode(message) 31 | }) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web-apps/src/app/basic-wallet/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { WagmiProvider, cookieToInitialState } from "wagmi"; 4 | import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit"; 5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 6 | 7 | import { http, createStorage, cookieStorage } from 'wagmi'; 8 | import { avalanche, avalancheFuji } from 'wagmi/chains'; 9 | import { Chain, getDefaultConfig } from '@rainbow-me/rainbowkit'; 10 | 11 | const projectId = "glacier-starter-kit"; 12 | 13 | const supportedChains: Chain[] = [ avalanche, avalancheFuji ]; 14 | 15 | export const config = getDefaultConfig({ 16 | appName: "Glacier Starter Kit", 17 | projectId, 18 | chains: supportedChains as any, 19 | ssr: true, 20 | storage: createStorage({ 21 | storage: cookieStorage, 22 | }), 23 | transports: supportedChains.reduce((obj, chain) => ({ ...obj, [chain.id]: http() }), {}) 24 | }); 25 | 26 | const queryClient = new QueryClient(); 27 | 28 | type Props = { 29 | children: React.ReactNode; 30 | cookie?: string | null; 31 | }; 32 | 33 | export default function Providers({ children, cookie }: Props) { 34 | const initialState = cookieToInitialState(config, cookie); 35 | 36 | return ( 37 | 38 | 39 | 40 | {children} 41 | 42 | 43 | 44 | ); 45 | } -------------------------------------------------------------------------------- /web-apps/src/app/ictt/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ICTT, PoweredByAvalanche, Web3Provider } from "@0xstt/builderkit"; 4 | import { Info } from 'lucide-react'; 5 | 6 | import { CHAINS, TOKENS } from "./../constants"; 7 | 8 | export default function Home() { 9 | 10 | return ( 11 | 12 |
13 | {/* ICTT Example */} 14 |
15 | 25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-roundtrip/receiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract ReceiverOnSubnet is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | function receiveTeleporterMessage(bytes32 sourceBlockchainID, address originSenderAddress, bytes calldata message) 15 | external 16 | { 17 | // Only the Teleporter receiver can deliver a message. 18 | require(msg.sender == address(messenger), "ReceiverOnSubnet: unauthorized TeleporterMessenger"); 19 | 20 | // Send Roundtrip message back to sender 21 | string memory response = string.concat(abi.decode(message, (string)), " World!"); 22 | 23 | messenger.sendCrossChainMessage( 24 | TeleporterMessageInput({ 25 | // Blockchain ID of C-Chain 26 | destinationBlockchainID: sourceBlockchainID, 27 | destinationAddress: originSenderAddress, 28 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 29 | requiredGasLimit: 100000, 30 | allowedRelayerAddresses: new address[](0), 31 | message: abi.encode(response) 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /web-apps/public/avax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/incentivize-relayer/senderWithFees.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import {IERC20} from "@openzeppelin/contracts@4.8.1/token/ERC20/IERC20.sol"; 10 | 11 | contract SenderWithFeesOnCChain { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | /** 14 | * @dev Sends a message to another chain. 15 | */ 16 | 17 | function sendMessage(address destinationAddress, string calldata message, address feeAddress) external { 18 | IERC20 feeContract = IERC20(feeAddress); 19 | uint256 feeAmount = 500000000000000; 20 | feeContract.transferFrom(msg.sender, address(this), feeAmount); 21 | feeContract.approve(address(messenger), feeAmount); 22 | 23 | messenger.sendCrossChainMessage( 24 | TeleporterMessageInput({ 25 | // Replace with blockchainID of your Subnet (see instructions in Readme) 26 | destinationBlockchainID: 0x52f2c4d51ef13a5781babe42c1b916e98fc88fc72919b20527782c939c8be71d, 27 | destinationAddress: destinationAddress, 28 | feeInfo: TeleporterFeeInfo({feeTokenAddress: feeAddress, amount: 0}), 29 | requiredGasLimit: 100000, 30 | allowedRelayerAddresses: new address[](0), 31 | message: abi.encode(message) 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/send-roundtrip/senderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | 11 | contract SenderOnCChain is ITeleporterReceiver { 12 | ITeleporterMessenger public immutable messenger = ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 13 | 14 | string public roundtripMessage; 15 | 16 | /** 17 | * @dev Sends a message to another chain. 18 | */ 19 | function sendMessage(address destinationAddress) external { 20 | messenger.sendCrossChainMessage( 21 | TeleporterMessageInput({ 22 | // Replace with blockchainID of your Subnet (see instructions in Readme) 23 | destinationBlockchainID: 0x609fa1886e1fbc9585127fa22b6e77a785ebdeecc01049f294638c7909e53c5e, 24 | destinationAddress: destinationAddress, 25 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 26 | requiredGasLimit: 200000, 27 | allowedRelayerAddresses: new address[](0), 28 | message: abi.encode("Hello") 29 | }) 30 | ); 31 | } 32 | 33 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 34 | // Only the Teleporter receiver can deliver a message. 35 | require(msg.sender == address(messenger), "SenderOnCChain: unauthorized TeleporterMessenger"); 36 | 37 | // Store the message. 38 | roundtripMessage = abi.decode(message, (string)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web-apps/src/app/chains/definitions/fuji.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "viem"; 2 | 3 | export const fuji = defineChain({ 4 | id: 43113, 5 | name: 'Avalanche Fuji', 6 | nativeCurrency: { 7 | decimals: 18, 8 | name: 'Avalanche Fuji', 9 | symbol: 'AVAX', 10 | }, 11 | rpcUrls: { 12 | default: { http: ['https://api.avax-test.network/ext/bc/C/rpc'] }, 13 | }, 14 | blockExplorers: { 15 | default: { 16 | name: 'SnowTrace', 17 | url: 'https://testnet.snowtrace.io', 18 | apiUrl: 'https://api-testnet.snowtrace.io', 19 | }, 20 | }, 21 | contracts: { 22 | multicall3: { 23 | address: '0xca11bde05977b3631167028862be2a173976ca11', 24 | blockCreated: 7096959, 25 | }, 26 | }, 27 | testnet: true, 28 | // Custom variables 29 | icm_registry: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228", 30 | faucet: { 31 | recalibrate: 30, 32 | assets: [ 33 | { 34 | address: "native", 35 | decimals: 18, 36 | drip_amount: 0.05, // max .05 token per request 37 | rate_limit: { // max 1 request in 24hrs 38 | max_limit: 1, 39 | window_size: 24 * 60 * 60 * 1000 40 | } 41 | }, 42 | { 43 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 44 | decimals: 18, 45 | drip_amount: 2, // max 2 token per request 46 | rate_limit: { // max 1 request in 24hrs 47 | max_limit: 1, 48 | window_size: 24 * 60 * 60 * 1000 49 | } 50 | } 51 | ] 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /contracts/misc/creating-contracts/BridgeReceiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/upgrades/TeleporterRegistry.sol"; 9 | import "@teleporter/ITeleporterMessenger.sol"; 10 | import "@teleporter/ITeleporterReceiver.sol"; 11 | import "./MyERC20Token.sol"; 12 | import "./BridgeActions.sol"; 13 | 14 | contract TokenMinterReceiverOnBulletin is ITeleporterReceiver { 15 | TeleporterRegistry public immutable teleporterRegistry = 16 | TeleporterRegistry(0x827364Da64e8f8466c23520d81731e94c8DDe510); 17 | address public tokenAddress; 18 | 19 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 20 | // Only a Teleporter Messenger registered in the registry can deliver a message. 21 | // Function throws an error if msg.sender is not registered. 22 | teleporterRegistry.getVersionFromAddress(msg.sender); 23 | 24 | // Decoding the Action type: 25 | (BridgeAction actionType, bytes memory paramsData) = abi.decode(message, (BridgeAction, bytes)); 26 | 27 | // Route to the appropriate function. 28 | if (actionType == BridgeAction.createToken) { 29 | (string memory name, string memory symbol) = abi.decode(paramsData, (string, string)); 30 | tokenAddress = address(new myToken(name, symbol)); 31 | } else if (actionType == BridgeAction.mintToken) { 32 | (address recipient, uint256 amount) = abi.decode(paramsData, (address, uint256)); 33 | myToken(tokenAddress).mint(recipient, amount); 34 | } else { 35 | revert("Receiver: invalid action"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web-apps/src/app/api/balance/route.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { NextResponse } from 'next/server' 3 | import { AvaCloudSDK} from "@avalabs/avacloud-sdk"; 4 | import { Erc20TokenBalance } from '@avalabs/avacloud-sdk/models/components/erc20tokenbalance'; 5 | 6 | const avaCloudSDK = new AvaCloudSDK({ 7 | apiKey: process.env.GLACIER_API_KEY, 8 | chainId: "43114", // Avalanche Mainnet 9 | network: "mainnet", 10 | }); 11 | 12 | export async function GET(request: Request) { 13 | const { searchParams } = new URL(request.url) 14 | const method = searchParams.get('method') 15 | try { 16 | let result 17 | switch (method) { 18 | case 'getBlockHeight': 19 | result = await getBlockHeight() 20 | break 21 | case 'listErc20Balances': 22 | const address: string = searchParams.get('address')! 23 | const blockNumber: string = searchParams.get('blockNumber')! 24 | result = await listErc20Balances(address, blockNumber); 25 | break 26 | default: 27 | return NextResponse.json({ error: 'Invalid method' }, { status: 400 }) 28 | } 29 | return NextResponse.json(result) 30 | } catch (error) { 31 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) 32 | } 33 | } 34 | 35 | async function getBlockHeight() { 36 | const result = await avaCloudSDK.data.evm.blocks.getLatestBlocks({ 37 | pageSize: 1, 38 | }); 39 | return result.result.blocks[0].blockNumber 40 | } 41 | 42 | async function listErc20Balances(address: string, blockNumber: string) { 43 | const result = await avaCloudSDK.data.evm.balances.listErc20Balances({ 44 | blockNumber: blockNumber, 45 | pageSize: 10, 46 | address: address, 47 | }); 48 | const balances: Erc20TokenBalance[] = []; 49 | for await (const page of result) { 50 | balances.push(...page.result.erc20TokenBalances); 51 | } 52 | return balances 53 | } 54 | -------------------------------------------------------------------------------- /web-apps/src/app/api/explorer/route.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { NextResponse } from 'next/server' 3 | import { AvaCloudSDK} from "@avalabs/avacloud-sdk"; 4 | import { NativeTransaction, EvmBlock } from '@avalabs/avacloud-sdk/models/components'; 5 | 6 | const avaCloudSDK = new AvaCloudSDK({ 7 | apiKey: process.env.GLACIER_API_KEY, 8 | chainId: "43114", // Avalanche Mainnet 9 | network: "mainnet", 10 | }); 11 | 12 | export async function GET(request: Request) { 13 | const { searchParams } = new URL(request.url) 14 | const method = searchParams.get('method') 15 | try { 16 | let result 17 | switch (method) { 18 | case 'getRecentTransactions': 19 | result = await getRecentTransactions() 20 | break 21 | case 'getRecentBlocks': 22 | result = await getRecentBlocks() 23 | break 24 | default: 25 | return NextResponse.json({ error: 'Invalid method' }, { status: 400 }) 26 | } 27 | return NextResponse.json(result) 28 | } catch (error) { 29 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) 30 | } 31 | } 32 | 33 | 34 | const getRecentBlocks = async () => { 35 | const result = await avaCloudSDK.data.evm.blocks.getLatestBlocks({ 36 | pageSize: 1, 37 | }); 38 | 39 | let count = 0; 40 | const blocks: EvmBlock[] = []; 41 | for await (const page of result) { 42 | if (count === 20) { 43 | break; 44 | } 45 | blocks.push(...page.result.blocks); 46 | count++; 47 | } 48 | return blocks 49 | } 50 | 51 | const getRecentTransactions = async () => { 52 | const result = await avaCloudSDK.data.evm.transactions.listLatestTransactions({ 53 | pageSize: 3, 54 | }); 55 | 56 | let count = 0; 57 | const transactions: NativeTransaction[] = []; 58 | for await (const page of result) { 59 | if (count === 20) { 60 | break; 61 | } 62 | transactions.push(...page.result.transactions); 63 | count++; 64 | } 65 | return transactions; 66 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM avaplatform/avalanche-cli:v1.8.4 AS avalanche-cli 2 | FROM avaplatform/awm-relayer:latest AS awm-relayer 3 | FROM --platform=linux/amd64 ghcr.io/foundry-rs/foundry:latest AS foundry 4 | FROM mcr.microsoft.com/devcontainers/base 5 | 6 | COPY --from=avalanche-cli /avalanche /usr/local/bin/avalanche 7 | COPY --from=awm-relayer /usr/bin/awm-relayer /usr/local/bin/awm-relayer 8 | COPY --from=foundry /usr/local/bin/forge /usr/local/bin/forge 9 | COPY --from=foundry /usr/local/bin/cast /usr/local/bin/cast 10 | COPY --from=foundry /usr/local/bin/anvil /usr/local/bin/anvil 11 | COPY --from=foundry /usr/local/bin/chisel /usr/local/bin/chisel 12 | 13 | RUN mkdir -p /home/vscode/.foundry/bin 14 | COPY --from=foundry /usr/local/bin/forge /home/vscode/.foundry/bin/forge 15 | 16 | # Switch to root user to install system packages 17 | USER root 18 | 19 | # Install Git and other dependencies 20 | RUN apt-get update && apt-get install -y \ 21 | git \ 22 | curl \ 23 | ca-certificates \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # Ensure vscode owns its home directory 27 | RUN chown -R vscode:vscode /home/vscode 28 | 29 | # Set HOME environment variable 30 | ENV HOME=/home/vscode 31 | 32 | # Switch back to vscode user 33 | USER vscode 34 | 35 | # Install nvm, Node.js, and TypeScript 36 | ENV NVM_DIR=/home/vscode/.nvm 37 | ENV NODE_VERSION=23.0.0 38 | 39 | # Install nvm and Node.js 40 | RUN mkdir -p $NVM_DIR \ 41 | && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash \ 42 | && . $NVM_DIR/nvm.sh \ 43 | && nvm install $NODE_VERSION \ 44 | && nvm alias default $NODE_VERSION 45 | 46 | # Update PATH for nvm and Node.js 47 | ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH 48 | 49 | # Install TypeScript globally 50 | RUN npm install -g typescript 51 | 52 | # Ensure Node.js and npm are accessible in future shells 53 | RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $HOME/.bashrc \ 54 | && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $HOME/.bashrc \ 55 | && echo 'export PATH="$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH"' >> $HOME/.bashrc 56 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. 2 | { 3 | "name": "Avalanche Starter Kit", 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "runArgs": [ 8 | "--network=host" 9 | ], 10 | "remoteEnv": { 11 | "PATH": "${containerEnv:PATH}:/usr/local/bin/", 12 | "PK": "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027", 13 | "KEYSTORE": "${containerWorkspaceFolder}/keystore", 14 | "AVALANCHE_CLI_GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}", 15 | "FUNDED_ADDRESS": "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC", 16 | "TELEPORTER_REGISTRY_C_CHAIN": "0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25", 17 | "C_CHAIN_BLOCKCHAIN_ID_HEX": "0x31233cae135e3974afa396e90f465aa28027de5f97f729238c310d2ed2f71902", 18 | "PK_43113": "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027" 19 | }, 20 | "postCreateCommand": { 21 | "init-git-submodules": "git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive" 22 | }, 23 | "postStartCommand": { 24 | "update-git-submodules": "git submodule update --recursive", 25 | "enable-avalanche-cli-metrics": "avalanche config metrics enable", 26 | "configure-ports": "chmod +x .devcontainer/configure-ports.sh && .devcontainer/configure-ports.sh" 27 | }, 28 | "features": { 29 | "ghcr.io/devcontainers/features/github-cli:1": {}, 30 | "ghcr.io/devcontainers/features/docker-in-docker:1": {} 31 | }, 32 | "forwardPorts": [ 33 | 3000, 34 | 9650, 35 | 9652, 36 | 9654, 37 | 9656, 38 | 9658 39 | ], 40 | "portsAttributes": { 41 | "3000": { 42 | "label": "Frontend" 43 | }, 44 | "9650": { 45 | "label": "Node-1" 46 | }, 47 | "9652": { 48 | "label": "Node-2" 49 | }, 50 | "9654": { 51 | "label": "Node-3" 52 | }, 53 | "9656": { 54 | "label": "Node-4" 55 | }, 56 | "9658": { 57 | "label": "Node-5" 58 | } 59 | }, 60 | // Configure tool-specific properties. 61 | "customizations": { 62 | "vscode": { 63 | "settings": { 64 | "git.autofetch": true 65 | }, 66 | "extensions": [ 67 | "juanblanco.solidity" 68 | ] 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/BridgeNFT.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import {ERC721} from "@openzeppelin/contracts@4.8.1/token/ERC721/ERC721.sol"; 9 | 10 | /** 11 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 12 | * DO NOT USE THIS CODE IN PRODUCTION. 13 | */ 14 | error Unauthorized(); 15 | 16 | /** 17 | * @dev BridgeNFT is an ERC721 token contract that is associated with a specific native chain bridge and asset, and is only mintable by the bridge contract on this chain. 18 | */ 19 | contract BridgeNFT is ERC721 { 20 | address public immutable bridgeContract; 21 | bytes32 public immutable nativeBlockchainID; 22 | address public immutable nativeBridge; 23 | address public immutable nativeAsset; 24 | string public nativeTokenURI; 25 | 26 | /** 27 | * @dev Initializes a BridgeNFT instance. 28 | */ 29 | constructor( 30 | bytes32 sourceBlockchainID, 31 | address sourceBridge, 32 | address sourceAsset, 33 | string memory tokenName, 34 | string memory tokenSymbol, 35 | string memory tokenURI 36 | ) ERC721(tokenName, tokenSymbol) { 37 | bridgeContract = msg.sender; 38 | nativeBlockchainID = sourceBlockchainID; 39 | nativeBridge = sourceBridge; 40 | nativeAsset = sourceAsset; 41 | nativeTokenURI = tokenURI; 42 | } 43 | 44 | /** 45 | * @dev Mints tokens to `account` if called by original `bridgeContract`. 46 | */ 47 | function mint(address account, uint256 tokenId) external { 48 | _authorize(); 49 | _mint(account, tokenId); 50 | } 51 | 52 | function burn(uint256 tokenId) external { 53 | _authorize(); 54 | 55 | require(_isApprovedOrOwner(msg.sender, tokenId), "BridgeNFT: caller is not token owner or approved"); 56 | _burn(tokenId); 57 | } 58 | 59 | function _baseURI() internal view virtual override returns (string memory) { 60 | return nativeTokenURI; 61 | } 62 | 63 | function _authorize() internal view { 64 | if (msg.sender != bridgeContract) { 65 | revert Unauthorized(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions/CalculatorSenderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "./CalculatorActions.sol"; 10 | 11 | contract CalculatorSenderOnCChain { 12 | ITeleporterMessenger public immutable teleporterMessenger = 13 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 14 | 15 | function sendAddMessage(address destinationAddress, uint256 num1, uint256 num2) external { 16 | teleporterMessenger.sendCrossChainMessage( 17 | TeleporterMessageInput({ 18 | // Replace with chain id of your Subnet (see instructions in Readme) 19 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 20 | destinationAddress: destinationAddress, 21 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 22 | requiredGasLimit: 100000, 23 | allowedRelayerAddresses: new address[](0), 24 | message: encodeAddData(num1, num2) 25 | }) 26 | ); 27 | } 28 | 29 | function sendConcatenateMessage(address destinationAddress, string memory text1, string memory text2) external { 30 | teleporterMessenger.sendCrossChainMessage( 31 | TeleporterMessageInput({ 32 | destinationBlockchainID: 0x382d2a20c299b03b638dd4d42b96e7401f6c3e88209b764abce83cf71c0c30cd, 33 | destinationAddress: destinationAddress, 34 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 35 | requiredGasLimit: 100000, 36 | allowedRelayerAddresses: new address[](0), 37 | message: encodeConcatenateData(text1, text2) 38 | }) 39 | ); 40 | } 41 | 42 | //Encode helpers 43 | function encodeAddData(uint256 a, uint256 b) public pure returns (bytes memory) { 44 | bytes memory paramsData = abi.encode(a, b); 45 | return abi.encode(CalculatorAction.add, paramsData); 46 | } 47 | 48 | function encodeConcatenateData(string memory a, string memory b) public pure returns (bytes memory) { 49 | bytes memory paramsData = abi.encode(a, b); 50 | return abi.encode(CalculatorAction.concatenate, paramsData); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions/CalculatorReceiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | import "./CalculatorActions.sol"; 11 | 12 | contract CalculatorReceiverOnSubnet is ITeleporterReceiver { 13 | ITeleporterMessenger public immutable teleporterMessenger = 14 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 15 | uint256 public result_num; 16 | string public result_string; 17 | 18 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 19 | // Only the Teleporter receiver can deliver a message. 20 | require( 21 | msg.sender == address(teleporterMessenger), "CalculatorReceiverOnSubnet: unauthorized TeleporterMessenger" 22 | ); 23 | 24 | // Decoding the Action type: 25 | (CalculatorAction actionType, bytes memory paramsData) = abi.decode(message, (CalculatorAction, bytes)); 26 | 27 | // Route to the appropriate function. 28 | if (actionType == CalculatorAction.add) { 29 | (uint256 a, uint256 b) = abi.decode(paramsData, (uint256, uint256)); 30 | _calculatorAdd(a, b); 31 | } else if (actionType == CalculatorAction.concatenate) { 32 | (string memory text1, string memory text2) = abi.decode(paramsData, (string, string)); 33 | _calculatorConcatenateStrings(text1, text2); 34 | } else { 35 | revert("CalculatorReceiverOnSubnet: invalid action"); 36 | } 37 | } 38 | 39 | function _calculatorAdd(uint256 _num1, uint256 _num2) internal { 40 | result_num = _num1 + _num2; 41 | } 42 | 43 | function _calculatorConcatenateStrings(string memory str1, string memory str2) internal { 44 | bytes memory str1Bytes = bytes(str1); 45 | bytes memory str2Bytes = bytes(str2); 46 | 47 | bytes memory combined = new bytes(str1Bytes.length + str2Bytes.length + 1); 48 | 49 | for (uint256 i = 0; i < str1Bytes.length; i++) { 50 | combined[i] = str1Bytes[i]; 51 | } 52 | combined[str1Bytes.length] = " "; 53 | for (uint256 i = 0; i < str2Bytes.length; i++) { 54 | combined[str1Bytes.length + i + 1] = str2Bytes[i]; 55 | } 56 | 57 | result_string = string(combined); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web-apps/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | 134 | yarn.lock -------------------------------------------------------------------------------- /contracts/misc/creating-contracts/BridgeSenderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/upgrades/TeleporterRegistry.sol"; 9 | import "@teleporter/ITeleporterMessenger.sol"; 10 | import "./BridgeActions.sol"; 11 | 12 | contract ERC20MinterSenderOnCChain { 13 | // The Teleporter registry contract manages different Teleporter contract versions. 14 | TeleporterRegistry public immutable teleporterRegistry = 15 | TeleporterRegistry(0x827364Da64e8f8466c23520d81731e94c8DDe510); 16 | 17 | /** 18 | * @dev Sends a message to another chain. 19 | */ 20 | function sendCreateTokenMessage(address destinationAddress, string memory name, string memory symbol) external { 21 | ITeleporterMessenger messenger = teleporterRegistry.getLatestTeleporter(); 22 | 23 | messenger.sendCrossChainMessage( 24 | TeleporterMessageInput({ 25 | // Replace with chain id of your Subnet (see instructions in Readme) 26 | destinationBlockchainID: 0xd7cdc6f08b167595d1577e24838113a88b1005b471a6c430d79c48b4c89cfc53, 27 | destinationAddress: destinationAddress, 28 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 29 | requiredGasLimit: 100000, 30 | allowedRelayerAddresses: new address[](0), 31 | message: encodeCreateTokenData(name, symbol) 32 | }) 33 | ); 34 | } 35 | 36 | function sendMintTokenMessage(address destinationAddress, address to, uint256 amount) external { 37 | ITeleporterMessenger messenger = teleporterRegistry.getLatestTeleporter(); 38 | 39 | messenger.sendCrossChainMessage( 40 | TeleporterMessageInput({ 41 | destinationBlockchainID: 0xd7cdc6f08b167595d1577e24838113a88b1005b471a6c430d79c48b4c89cfc53, 42 | destinationAddress: destinationAddress, 43 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 44 | requiredGasLimit: 100000, 45 | allowedRelayerAddresses: new address[](0), 46 | message: encodeMintTokenData(to, amount) 47 | }) 48 | ); 49 | } 50 | 51 | //Encode helpers 52 | function encodeCreateTokenData(string memory name, string memory symbol) public pure returns (bytes memory) { 53 | bytes memory paramsData = abi.encode(name, symbol); 54 | return abi.encode(BridgeAction.createToken, paramsData); 55 | } 56 | 57 | function encodeMintTokenData(address to, uint256 amount) public pure returns (bytes memory) { 58 | bytes memory paramsData = abi.encode(to, amount); 59 | return abi.encode(BridgeAction.mintToken, paramsData); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions-assignment-solution/ExtendedCalculatorReceiverOnSubnet.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "@teleporter/ITeleporterReceiver.sol"; 10 | import "./ExtendedCalculatorActions.sol"; 11 | 12 | contract CalculatorReceiverOnSubnet is ITeleporterReceiver { 13 | ITeleporterMessenger public immutable teleporterMessenger = 14 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 15 | uint256 public result_num; 16 | string public result_string; 17 | 18 | function receiveTeleporterMessage(bytes32, address, bytes calldata message) external { 19 | // Only the Teleporter receiver can deliver a message. 20 | require( 21 | msg.sender == address(teleporterMessenger), "ExtendedCalculatorReceiverOnSubnet: unauthorized TeleporterMessenger" 22 | ); 23 | 24 | // Decoding the Action type: 25 | (CalculatorAction actionType, bytes memory paramsData) = abi.decode(message, (CalculatorAction, bytes)); 26 | 27 | // Route to the appropriate function. 28 | if (actionType == CalculatorAction.add) { 29 | (uint256 a, uint256 b) = abi.decode(paramsData, (uint256, uint256)); 30 | _calculatorAdd(a, b); 31 | } else if (actionType == CalculatorAction.concatenate) { 32 | (string memory text1, string memory text2) = abi.decode(paramsData, (string, string)); 33 | _calculatorConcatenateStrings(text1, text2); 34 | } else if (actionType == CalculatorAction.tripleSum) { 35 | (uint256 a, uint256 b, uint256 c) = abi.decode(paramsData, (uint256, uint256, uint256)); 36 | _calculatorTripleSum(a, b,c); 37 | } else { 38 | revert("ReceiverOnSubnet: invalid action"); 39 | } 40 | } 41 | 42 | function _calculatorAdd(uint256 _num1, uint256 _num2) internal { 43 | result_num = _num1 + _num2; 44 | } 45 | 46 | function _calculatorConcatenateStrings(string memory str1, string memory str2) internal { 47 | bytes memory str1Bytes = bytes(str1); 48 | bytes memory str2Bytes = bytes(str2); 49 | 50 | bytes memory combined = new bytes(str1Bytes.length + str2Bytes.length + 1); 51 | 52 | for (uint256 i = 0; i < str1Bytes.length; i++) { 53 | combined[i] = str1Bytes[i]; 54 | } 55 | combined[str1Bytes.length] = " "; 56 | for (uint256 i = 0; i < str2Bytes.length; i++) { 57 | combined[str1Bytes.length + i + 1] = str2Bytes[i]; 58 | } 59 | 60 | result_string = string(combined); 61 | } 62 | 63 | function _calculatorTripleSum(uint256 _num1, uint256 _num2, uint256 _num3) internal { 64 | result_num = _num1 + _num2 + _num3; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /contracts/interchain-messaging/invoking-functions-assignment-solution/ExtendedCalculatorSenderOnCChain.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import "@teleporter/ITeleporterMessenger.sol"; 9 | import "./ExtendedCalculatorActions.sol"; 10 | 11 | contract CalculatorSenderOnCChain { 12 | ITeleporterMessenger public immutable teleporterMessenger = 13 | ITeleporterMessenger(0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf); 14 | 15 | function sendAddMessage(address destinationAddress, uint256 num1, uint256 num2) external { 16 | teleporterMessenger.sendCrossChainMessage( 17 | TeleporterMessageInput({ 18 | // Replace with chain id of your Subnet (see instructions in Readme) 19 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 20 | destinationAddress: destinationAddress, 21 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 22 | requiredGasLimit: 100000, 23 | allowedRelayerAddresses: new address[](0), 24 | message: encodeAddData(num1, num2) 25 | }) 26 | ); 27 | } 28 | 29 | function sendConcatenateMessage(address destinationAddress, string memory text1, string memory text2) external { 30 | teleporterMessenger.sendCrossChainMessage( 31 | TeleporterMessageInput({ 32 | // Replace with chain id of your Subnet (see instructions in Readme) 33 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 34 | destinationAddress: destinationAddress, 35 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 36 | requiredGasLimit: 100000, 37 | allowedRelayerAddresses: new address[](0), 38 | message: encodeConcatenateData(text1, text2) 39 | }) 40 | ); 41 | } 42 | 43 | function sendTripleSumMessage(address destinationAddress, uint256 num1, uint256 num2, uint256 num3) external { 44 | teleporterMessenger.sendCrossChainMessage( 45 | TeleporterMessageInput({ 46 | // Replace with chain id of your Subnet (see instructions in Readme) 47 | destinationBlockchainID: 0xd7ed7b978d4d6c478123bf9b326d47e69f959206d34e42ea4de2d1d2acbc93ea, 48 | destinationAddress: destinationAddress, 49 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 50 | requiredGasLimit: 100000, 51 | allowedRelayerAddresses: new address[](0), 52 | message: encodeTripleSumData(num1, num2, num3) 53 | }) 54 | ); 55 | } 56 | 57 | //Encode helpers 58 | function encodeAddData(uint256 a, uint256 b) public pure returns (bytes memory) { 59 | bytes memory paramsData = abi.encode(a, b); 60 | return abi.encode(CalculatorAction.add, paramsData); 61 | } 62 | 63 | function encodeConcatenateData(string memory a, string memory b) public pure returns (bytes memory) { 64 | bytes memory paramsData = abi.encode(a, b); 65 | return abi.encode(CalculatorAction.concatenate, paramsData); 66 | } 67 | 68 | function encodeTripleSumData(uint256 a, uint256 b, uint256 c) public pure returns (bytes memory) { 69 | bytes memory paramsData = abi.encode(a, b, c); 70 | return abi.encode(CalculatorAction.tripleSum, paramsData); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /web-apps/src/app/constants.ts: -------------------------------------------------------------------------------- 1 | import { fuji } from "./chains/definitions/fuji"; 2 | import { echo } from "./chains/definitions/echo"; 3 | import { dispatch } from "./chains/definitions/dispatch"; 4 | 5 | export const CHAINS = [fuji, echo, dispatch]; 6 | export const TOKENS = [ 7 | { 8 | address: "native", 9 | name: "Avalanche", 10 | symbol: "AVAX", 11 | decimals: 18, 12 | chain_id: 43113 13 | }, 14 | { 15 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 16 | name: "TOK", 17 | symbol: "TOK", 18 | decimals: 18, 19 | chain_id: 43113, 20 | supports_ictt: true, 21 | transferer: "0xD63c60859e6648b20c38092cCceb92c5751E32fF", 22 | mirrors: [ 23 | { 24 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 25 | transferer: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 26 | chain_id: 173750, 27 | decimals: 18 28 | } 29 | ] 30 | }, 31 | { 32 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 33 | name: "TOK.e", 34 | symbol: "TOK.e", 35 | decimals: 18, 36 | chain_id: 173750, 37 | supports_ictt: true, 38 | is_transferer: true, 39 | mirrors: [ 40 | { 41 | home: true, 42 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 43 | transferer: "0xD63c60859e6648b20c38092cCceb92c5751E32fF", 44 | chain_id: 43113, 45 | decimals: 18 46 | } 47 | ] 48 | }, 49 | { 50 | address: "0xD737192fB95e5D106a459a69Faec4a7bD38c2A17", 51 | name: "STT", 52 | symbol: "STT", 53 | decimals: 18, 54 | chain_id: 43113, 55 | supports_ictt: true, 56 | transferer: "0x8a6A0605556ec621EB75F27954C32f048B51d8e9", 57 | mirrors: [ 58 | { 59 | address: "0x96cA8090Ab3748C0697058C06FBdcF0813Cd9576", 60 | transferer: "0x96cA8090Ab3748C0697058C06FBdcF0813Cd9576", 61 | chain_id: 173750, 62 | decimals: 18 63 | }, 64 | { 65 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 66 | transferer: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 67 | chain_id: 779672, 68 | decimals: 18 69 | } 70 | ] 71 | }, 72 | { 73 | address: "0x96cA8090Ab3748C0697058C06FBdcF0813Cd9576", 74 | name: "STT.e", 75 | symbol: "STT.e", 76 | decimals: 18, 77 | chain_id: 173750, 78 | supports_ictt: true, 79 | is_transferer: true, 80 | mirrors: [ 81 | { 82 | home: true, 83 | address: "0xD737192fB95e5D106a459a69Faec4a7bD38c2A17", 84 | transferer: "0x8a6A0605556ec621EB75F27954C32f048B51d8e9", 85 | chain_id: 43113, 86 | decimals: 18 87 | }, 88 | { 89 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 90 | transferer: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 91 | chain_id: 779672, 92 | decimals: 18 93 | } 94 | ] 95 | }, 96 | { 97 | address: "0x8D6f0E153B1D4Efb46c510278Db3678Bb1Cc823d", 98 | name: "STT.d", 99 | symbol: "STT.d", 100 | decimals: 18, 101 | chain_id: 779672, 102 | supports_ictt: true, 103 | is_transferer: true, 104 | mirrors: [ 105 | { 106 | home: true, 107 | address: "0xD737192fB95e5D106a459a69Faec4a7bD38c2A17", 108 | transferer: "0x8a6A0605556ec621EB75F27954C32f048B51d8e9", 109 | chain_id: 43113, 110 | decimals: 18 111 | }, 112 | { 113 | address: "0x96cA8090Ab3748C0697058C06FBdcF0813Cd9576", 114 | transferer: "0x96cA8090Ab3748C0697058C06FBdcF0813Cd9576", 115 | chain_id: 173750, 116 | decimals: 18 117 | } 118 | ] 119 | } 120 | ]; -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/README.md: -------------------------------------------------------------------------------- 1 | # Generic ERC721 Token Bridge 2 | 3 | An ERC721 token bridge contract built on top of Teleporter to support bridging any ERC721 tokens between any `subnet-evm` instance. 4 | 5 | ## Design 6 | The generic ERC721 bridge is implemented using two primary contracts. 7 | - `BridgeNFT` 8 | - A simple extension of OpenZeppelin's `ERC721` contract. 9 | - Representation of an ERC721 token from another chain. 10 | - Identifies the specific native ERC721 contract and bridge it represents. 11 | - Automatically deployed by `ERC721Bridge` when new tokens are added to be supported by a bridge instance. 12 | - `ERC721Bridge` 13 | - Bridge contract that uses Teleporter to facilitate the bridging operations of adding new supported tokens and moving tokens between chains. 14 | - Supports bridging of simple ERC721 tokens between any `subnet-evm` instance. Note: Some custom ERC721 implementations may include custom logic for minting, metadata(such as variants, rarity, etc.), and/or other features. This bridge implementation does not support bridging of these custom features. 15 | - Primary functions include: 16 | - `submitCreateBridgeNFT`: Called on the origin chain to add support for an ERC721 on a different chain's bridge instance. Submits a Teleporter message that invokes `createBridgeNFT` on the destination chain. 17 | - `_createBridgeNFTContract`: Called when a new `BridgeAction.Create` Teleporter message is received on the destinations chain. Deploys a new `BridgeNFT` contract to represent a native ERC721 token, if it already does not exist. 18 | - `bridgeToken`: Called to move supported ERC721 token from one chain to another. If a token is moved from native to bridged contract, the ERC721 token is locked on the native chain by being transferred to the ERC721Bridge (requires the ERC721Bridge to be approved as operator of the token) and then minted to the used on the destination chain. If a token is moved from a bridged contract to a native contract, the bridged ERC721 token is burned (also requires approve) on the bridge chain and then "unlocked" by being transferred back to the recipient on the native chain. 19 | - `_mintBridgeNFT`: Called when a new `BridgeAction.Mint` Teleporter message is received on the destinations chain to mint the bridged ERC721 token to the receiver address. 20 | - `_transferBridgeNFT`: Called when a new `BridgeAction.Transfer` Teleporter message is received to transfer bridged tokens to the native chain. 21 | 22 | Note: This implementation currently does not support chain-hopping for ERC721 tokens. This means if a user wants to re-bridge an ERC721 token from one chain to another, they must first bridge it back to the native chain and then bridge it to the new destination chain manually. 23 | 24 | ## End-to-end test 25 | An end-to-end test demonstrating the use of the generic ERC721 token bridge contracts can be found in [here](/tests/flows/erc721_native_token_bridge.go). This test implements the following flow and checks that each step is successful: 26 | 1. An example "native" [ERC721](/contracts/src/Mocks/ExampleERC721.sol) token is deployed on subnet A 27 | 2. The [ERC721Bridge](/contracts/src/CrossChainApplications/examples/ERC721Bridge/ERC721Bridge.sol) contract is deployed on subnets A and B. 28 | 4. Teleporter message is sent from subnet A to B to create a new [BridgeNFT](/contracts/misc/erc721-bridge/BridgeNFT.sol) contract instance to represent the native ERC721 token on subnet B. 29 | 5. An NFT is minted on subnet A to the user address. 30 | 6. ERC721Bridge on subnet A is approved as operator of the NFT. 31 | 7. `bridgeToken` is called on subnet A to lock the NFT and bridge it to subnet B. 32 | 8. The NFT is transferred to the ERC721Bridge contract on subnet A and then a Teleporter message is sent to subnet B to mint the NFT to the user on the BridgeNFT contract on subnet B. 33 | 9. The user on subnet B approves the BridgeNFT contract on subnet B as operator of the NFT. 34 | 10. The user on subnet B calls `bridgeToken` on the BridgeNFT contract on subnet B to burn the NFT and bridge it to subnet A. 35 | 36 | ## Local testing 37 | 1. Run a local network following the instructions [here](/README.md#run-a-local-testnet-in-docker). 38 | 2. An example workflow is provided via the [erc721_send_and_receive.sh](scripts/local/examples/basic_send_receive.sh) script. 39 | -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/IERC721Bridge.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import {ExampleERC721} from "./ExampleERC721.sol"; 9 | import {ITeleporterMessenger} from "@teleporter/ITeleporterMessenger.sol"; 10 | 11 | /** 12 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 13 | * DO NOT USE THIS CODE IN PRODUCTION. 14 | */ 15 | 16 | /** 17 | * @dev Interface that describes functionalities for a cross-chain ERC20 bridge. 18 | */ 19 | interface IERC721Bridge { 20 | error InvalidBridgeAction(); 21 | error InvalidDestinationBlockchainId(); 22 | error InvalidDestinationBridgeAddress(); 23 | error InvalidTokenId(); 24 | error TokenContractAlreadyBridged(); 25 | error TokenContractNotBridged(); 26 | error ZeroTokenContractAddress(); 27 | error ZeroRecipientAddress(); 28 | error ZeroDestinationBridgeAddress(); 29 | error ZeroFeeAssetAddress(); 30 | 31 | struct BridgedTokenTransferInfo { 32 | bytes32 destinationBlockchainID; 33 | address destinationBridgeAddress; 34 | ITeleporterMessenger teleporterMessenger; 35 | address bridgedNFTContractAddress; 36 | address recipient; 37 | uint256 tokenId; 38 | address messageFeeAsset; 39 | uint256 messageFeeAmount; 40 | } 41 | 42 | struct NativeTokenTransferInfo { 43 | bytes32 destinationBlockchainID; 44 | address destinationBridgeAddress; 45 | ITeleporterMessenger teleporterMessenger; 46 | address nativeContractAddress; 47 | address recipient; 48 | uint256 tokenId; 49 | address messageFeeAsset; 50 | uint256 messageFeeAmount; 51 | } 52 | 53 | struct CreateBridgeNFTData { 54 | address nativeContractAddress; 55 | string nativeName; 56 | string nativeSymbol; 57 | string nativeTokenURI; 58 | } 59 | 60 | struct MintBridgeNFTData { 61 | address nativeContractAddress; 62 | address recipient; 63 | uint256 tokenId; 64 | } 65 | 66 | struct TransferBridgeNFTData { 67 | bytes32 destinationBlockchainID; 68 | address destinationBridgeAddress; 69 | address nativeContractAddress; 70 | address recipient; 71 | uint256 tokenId; 72 | } 73 | 74 | /** 75 | * @dev Enum representing the action to take on receiving a Teleporter message. 76 | */ 77 | enum BridgeAction { 78 | Create, 79 | Mint, 80 | Transfer 81 | } 82 | 83 | /** 84 | * @dev Emitted when tokens are locked in this bridge contract to be bridged to another chain. 85 | */ 86 | event BridgeToken( 87 | address indexed tokenContractAddress, 88 | bytes32 indexed destinationBlockchainID, 89 | bytes32 indexed teleporterMessageID, 90 | address destinationBridgeAddress, 91 | address recipient, 92 | uint256 tokenId 93 | ); 94 | 95 | /** 96 | * @dev Emitted when submitting a request to create a new bridge token on another chain. 97 | */ 98 | event SubmitCreateBridgeNFT( 99 | bytes32 indexed destinationBlockchainID, 100 | address indexed destinationBridgeAddress, 101 | address indexed nativeContractAddress, 102 | bytes32 teleporterMessageID 103 | ); 104 | 105 | /** 106 | * @dev Emitted when creating a new bridge token. 107 | */ 108 | event CreateBridgeNFT( 109 | bytes32 indexed nativeBlockchainID, 110 | address indexed nativeBridgeAddress, 111 | address indexed nativeContractAddress, 112 | address bridgeTokenAddress 113 | ); 114 | 115 | /** 116 | * @dev Emitted when minting bridge tokens. 117 | */ 118 | event MintBridgeNFT(address indexed contractAddress, address recipient, uint256 tokenId); 119 | 120 | /** 121 | * @dev Transfers ERC721 token to another chain. 122 | * 123 | * This can be wrapping, unwrapping, and transferring a wrapped token between two non-native chains. 124 | */ 125 | function bridgeToken( 126 | bytes32 destinationBlockchainID, 127 | address destinationBridgeAddress, 128 | address tokenContractAddress, 129 | address recipient, 130 | uint256 tokenId, 131 | address feeTokenAddress, 132 | uint256 feeAmount 133 | ) external; 134 | 135 | /** 136 | * @dev Creates a new bridge token on another chain. 137 | */ 138 | function submitCreateBridgeNFT( 139 | bytes32 destinationBlockchainID, 140 | address destinationBridgeAddress, 141 | ExampleERC721 nativeContract, 142 | address feeTokenAddress, 143 | uint256 feeAmount 144 | ) external; 145 | } 146 | -------------------------------------------------------------------------------- /web-apps/public/ava-labs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web-apps/src/app/api/faucet/send/route.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from "./../../../constants"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { Chain, erc20Abi, isAddress, toHex } from "viem"; 4 | import { ethers, Wallet } from "ethers"; 5 | import { BigNumber } from "bignumber.js"; 6 | import { rateLimiter } from "./../../../utils/RateLimiter"; 7 | 8 | const getFaucetBalance = async (chain: any, address: string, wallet: Wallet) => { 9 | let rpc_url = chain.rpcUrls.default.http[0]; 10 | let provider = new ethers.JsonRpcProvider(rpc_url, { chainId: chain.id, name: "NW" }); 11 | let balance; 12 | if (address === "native") { 13 | balance = await provider.getBalance(wallet.address); 14 | } else { 15 | const contract = new ethers.Contract(address, erc20Abi, provider); 16 | balance = await contract.balanceOf(wallet); 17 | } 18 | return new BigNumber(balance.toString()); 19 | } 20 | 21 | const getFeeData = async (chain: any) => { 22 | let rpc_url = chain.rpcUrls.default.http[0]; 23 | let provider = new ethers.JsonRpcProvider(rpc_url, { chainId: chain.id, name: "NW" }); 24 | return await provider.getFeeData(); 25 | } 26 | 27 | const send = async (chain: any, address: string, wallet: Wallet, receiver: string, amount: BigNumber) => { 28 | let fee_data = await getFeeData(chain); 29 | if (fee_data.gasPrice === null) { 30 | throw new Error("TransactionSender cound not fetch the actual fee data."); 31 | } 32 | let adjusted_gas_price = BigInt(new BigNumber(fee_data.gasPrice.toString()).times(1.25).toFixed(0)); 33 | if (address === "native") { 34 | const transaction = await wallet.sendTransaction({ 35 | to: receiver, 36 | value: amount.toString(), 37 | gasPrice: toHex(adjusted_gas_price), 38 | gasLimit: 21000 39 | }); 40 | return transaction.hash; 41 | } else { 42 | const intf = new ethers.Interface(erc20Abi); 43 | const amount_hex = toHex(BigInt(amount.toFixed(0))); 44 | let data = intf.encodeFunctionData("transfer", [receiver, amount_hex]); 45 | const transaction = await wallet.sendTransaction({ 46 | to: address, 47 | value: '0x0', 48 | gasPrice: toHex(adjusted_gas_price), 49 | gasLimit: 65000, 50 | data: data 51 | }); 52 | return transaction.hash; 53 | } 54 | } 55 | 56 | export async function POST(req: NextRequest) { 57 | const body = await req.json(); 58 | const { chain_id, address, receiver } = body; 59 | // Check parameters 60 | if (chain_id === undefined || (address !== "native" && isAddress(address) === false) || isAddress(receiver) === false) { 61 | return NextResponse.json({ message: 'Invalid parameters passed!' }, { status: 400 }); 62 | } 63 | // Check chain is supporting faucet 64 | const fauced_supported_chains = CHAINS.filter((c: any) => c.faucet !== undefined); 65 | const chain: any = fauced_supported_chains.find(c => c.id === chain_id); 66 | if (chain === undefined) { 67 | return NextResponse.json({ message: 'Faucet config cannot be found!' }, { status: 400 }); 68 | } 69 | // Check faucet is supporting asset 70 | const { faucet } = chain; 71 | const asset = faucet.assets.find((a: any) => a.address === address); 72 | if (asset === undefined) { 73 | return NextResponse.json({ message: 'Asset config cannot be found!' }, { status: 400 }); 74 | } 75 | const { decimals, drip_amount, rate_limit } = asset; 76 | // Check rate limit 77 | const client_ip = req.headers.get('x-forwarded-for') || req.ip || 'unknown'; 78 | const { max_limit, window_size } = rate_limit; 79 | if (rateLimiter(client_ip, max_limit, window_size) === false) { 80 | return NextResponse.json({ message: 'Too many requests, please try again later.' }, { status: 429 }); 81 | } 82 | // Get faucet wallet 83 | const pk = process.env[`PK_${chain.id}`]; 84 | if (pk === undefined) { 85 | return NextResponse.json({ message: 'Faucet wallet cannot be found!' }, { status: 400 }); 86 | } 87 | let rpc_url = chain.rpcUrls.default.http[0]; 88 | let provider = new ethers.JsonRpcProvider(rpc_url, { chainId: chain.id, name: "NW" }); 89 | const wallet = new Wallet(pk, provider); 90 | // Get faucet asset balance 91 | const balance = await getFaucetBalance(chain, address, wallet); 92 | // Check drip amount bigger than faucet balance 93 | const drip_amount_bn = new BigNumber(drip_amount).times(10 ** decimals); 94 | if (drip_amount_bn.gte(balance)) { 95 | return NextResponse.json({ message: 'Faucet balance is not enough!' }, { status: 400 }); 96 | } 97 | // Send asset 98 | try { 99 | const hash = await send(chain, address, wallet, receiver, drip_amount_bn); 100 | return NextResponse.json({ hash }); 101 | } catch (err) { 102 | return NextResponse.json({ message: err }, { status: 400 }); 103 | } 104 | } -------------------------------------------------------------------------------- /web-apps/src/app/api/wallet/route.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { NextResponse } from 'next/server' 3 | import { AvaCloudSDK} from "@avalabs/avacloud-sdk"; 4 | import { Erc721TokenBalance } from '@avalabs/avacloud-sdk/models/components/erc721tokenbalance'; 5 | import { Erc1155TokenBalance } from '@avalabs/avacloud-sdk/models/components/erc1155tokenbalance'; 6 | import { TransactionDetails } from '@avalabs/avacloud-sdk/models/components/transactiondetails'; 7 | 8 | const avaCloudSDK = new AvaCloudSDK({ 9 | apiKey: process.env.GLACIER_API_KEY, 10 | chainId: "43114", // Avalanche Mainnet 11 | network: "mainnet", 12 | }); 13 | 14 | export async function GET(request: Request) { 15 | const { searchParams } = new URL(request.url) 16 | const method = searchParams.get('method') 17 | let address 18 | try { 19 | let result 20 | switch (method) { 21 | case 'listERC721Balances': 22 | address = searchParams.get('address')! 23 | result = await listERC721Balances(address) 24 | break 25 | case 'listERC1155Balances': 26 | address = searchParams.get('address')! 27 | result = await listErc1155Balances(address) 28 | break 29 | case 'listRecentTransactions': 30 | address = searchParams.get('address')! 31 | result = await listRecentTransactions(address) 32 | break 33 | default: 34 | return NextResponse.json({ error: 'Invalid method' }, { status: 400 }) 35 | } 36 | return NextResponse.json(result) 37 | } catch (error) { 38 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) 39 | } 40 | } 41 | 42 | async function getBlockHeight() { 43 | const result = await avaCloudSDK.data.evm.blocks.getLatestBlocks({ 44 | pageSize: 1, 45 | }); 46 | return Number(result.result.blocks[0].blockNumber) 47 | } 48 | 49 | const listERC721Balances = async (address: string) => { 50 | const result = await avaCloudSDK.data.evm.balances.listErc721Balances({ 51 | pageSize: 10, 52 | address: address, 53 | }); 54 | const balances: Erc721TokenBalance[] = []; 55 | for await (const page of result) { 56 | balances.push(...page.result.erc721TokenBalances); 57 | } 58 | return balances 59 | } 60 | 61 | const listErc1155Balances = async (address: string) => { 62 | const result = await avaCloudSDK.data.evm.balances.listErc1155Balances({ 63 | pageSize: 10, 64 | address: address, 65 | }); 66 | const balances: Erc1155TokenBalance[] = []; 67 | for await (const page of result) { 68 | balances.push(...page.result.erc1155TokenBalances); 69 | } 70 | return balances 71 | } 72 | 73 | const listRecentTransactions = async (address: string) => { 74 | const blockHeight = await getBlockHeight() 75 | const result = await avaCloudSDK.data.evm.transactions.listTransactions({ 76 | pageSize: 10, 77 | startBlock: blockHeight - 100000, 78 | endBlock: blockHeight, 79 | address: address, 80 | sortOrder: "desc", 81 | }); 82 | const transactions: TransactionDetails = { 83 | erc20Transfers: [], 84 | erc721Transfers: [], 85 | erc1155Transfers: [], 86 | nativeTransaction: { 87 | blockNumber: '', 88 | blockTimestamp: 0, 89 | blockHash: '', 90 | blockIndex: 0, 91 | txHash: '', 92 | txStatus: '', 93 | txType: 0, 94 | gasLimit: '', 95 | gasUsed: '', 96 | gasPrice: '', 97 | nonce: '', 98 | from: { 99 | name: undefined, 100 | symbol: undefined, 101 | decimals: undefined, 102 | logoUri: undefined, 103 | address: '' 104 | }, 105 | to: { 106 | name: undefined, 107 | symbol: undefined, 108 | decimals: undefined, 109 | logoUri: undefined, 110 | address: '' 111 | }, 112 | value: '' 113 | }, 114 | } 115 | for await (const page of result) { 116 | for (const transaction of page.result.transactions) { 117 | if (transaction.erc20Transfers) { 118 | if (transactions.erc20Transfers) { 119 | transactions.erc20Transfers.push(...transaction.erc20Transfers); 120 | } 121 | } 122 | else if (transaction.erc721Transfers) { 123 | if (transactions.erc721Transfers) { 124 | transactions.erc721Transfers.push(...transaction.erc721Transfers); 125 | } 126 | } 127 | else if (transaction.erc1155Transfers) { 128 | if (transactions.erc1155Transfers) { 129 | transactions.erc1155Transfers.push(...transaction.erc1155Transfers); 130 | } 131 | } 132 | } 133 | } 134 | return transactions 135 | } -------------------------------------------------------------------------------- /web-apps/src/app/balance-app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client"; 3 | import Image from "next/image"; 4 | import { useState } from "react"; 5 | import { Erc20TokenBalance } from "@avalabs/avacloud-sdk/models/components/erc20tokenbalance"; 6 | 7 | export default function BalanceApp() { 8 | const [address, setAddress] = useState(); 9 | const [balances, setBalances] = useState([]); 10 | 11 | const handleSetAddress = async () => { 12 | const addressInput = document.getElementById("address") as HTMLInputElement; 13 | const address = addressInput.value; 14 | const addressPattern = /^0x[a-fA-F0-9]{40}$/; 15 | 16 | if (addressInput && addressPattern.test(address)) { 17 | setAddress(address); 18 | setBalances(await fetchERC20Balances(address)); 19 | } 20 | }; 21 | 22 | const fetchERC20Balances = async (address: string) => { 23 | const blockResult = await fetch("api/balance?method=getBlockHeight"); 24 | const blockNumber = await blockResult.json(); 25 | const balanceResult = await fetch("api/balance?method=listErc20Balances&address=" + address + "&blockNumber=" + blockNumber); 26 | const balances = await balanceResult.json(); 27 | return balances as Erc20TokenBalance[]; 28 | }; 29 | 30 | 31 | return ( 32 |
33 |
34 | 52 |
53 | {address ? ( 54 |

Address: {address}

55 | ) : ( 56 | <> 57 | 60 |
61 | 67 | 73 |
74 | 75 | )} 76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {Array.isArray(balances) && 89 | balances.map((token) => { 90 | return ( 91 | 92 | 102 | 111 | 112 | 113 | 114 | ); 115 | })} 116 | 117 |
LogoContract AddressToken NameBalance
93 | {token.logoUri && ( 94 | Token Logo 100 | )} 101 | 103 | 108 | {token.address} 109 | 110 | {token.name}{Number(token.balance) / 10 ** Number(token.decimals)}
118 |
119 | ); 120 | } 121 | 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avalanche Starter Kit 2 | 3 | This starter kit will get you started with developing solidity smart contract dApps on the C-Chain or on an Avalanche L1. It provides all tools to build cross-L1 dApps using Teleporter. It includes: 4 | 5 | - **Avalanche CLI**: Run a local Avalanche Network 6 | - **Foundry**: 7 | - Forge: Compile and Deploy smart contracts to the local network, Fuji Testnet or Mainnet 8 | - Cast: Interact with these smart contracts 9 | - **Teleporter**: All contracts you may want to interact with Teleporter 10 | - **AWM Relayer**: The binary to run your own relayer 11 | - **Examples**: Contracts showcasing how to achieve common patterns, such as sending simple messages, call functions of a contract on another blockchain and bridging assets. Please note that these example contracts have not been audited and are for educational purposes only 12 | - **BuilderKit**: A component library providing UI elements for ICTT bridges, Cross-Chain swaps, ... 13 | 14 | ## Set Up 15 | 16 | This starter kit utilizes a Dev Container specification. Dev Containers use containerization to create consistent and isolated development environments. All of the above mentioned components are pre-installed in that container. These containers can be run using GitHub Codespaces or locally using Docker and VS Code. You can switch back and forth between the two options. 17 | 18 | ### Run on Github Codespace 19 | 20 | You can run them directly on Github by clicking **Code**, switching to the **Codespaces** tab and clicking **Create codespace on main**. A new window will open that loads the codespace. Afterwards you will see a browser version of VS code with all the dependencies installed. Codespace time out after some time of inactivity, but can be restarted. 21 | 22 | ### Run Dev Container locally with Docker 23 | 24 | Alternatively, you can run them locally. You need [docker](https://www.docker.com/products/docker-desktop/) installed and [VS Code](https://code.visualstudio.com/) with the extensions [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). Then clone the repository and open it in VS Code. VS Code will ask you if you want to reopen the project in a container. 25 | 26 | ## Starting a local Avalanche Network 27 | 28 | To start a local Avalanche network with your own teleporter-enabled L1 inside the container follow these commands. Your Avalanche network will be completely independent of the Avalanche Mainnet and Fuji Testnet. It will have its own Primary Network (C-Chain, X-Chain & P-Chain). You will not have access to services available on Fuji (such as Chainlink services or bridges). If you require these, go to the [Fuji Testnet](#fuji-testnet) section. 29 | 30 | First let's create out L1 configuration. Follow the dialog and if you don't have special requirements for precompiles just follow the suggested options. For the Airdrop of the native token select "Airdrop 1 million tokens to the default ewoq address (do not use in production)". Keep the name `myblockchain` to avoid additional configuration. 31 | 32 | ``` 33 | avalanche blockchain create myblockchain 34 | ``` 35 | 36 | Now let's spin up the local Avalanche network and deploy our L1. This will also deploy the Teleporter messenger and the registry on our L1 and the C-Chain. 37 | 38 | ```bash 39 | avalanche blockchain deploy myblockchain 40 | ``` 41 | 42 | Make sure to add the RPC Url to the `foundry.toml` file if you have chosen a different name than `myblockchain`. If you've used `myblockchain` the rpc is already configured. 43 | 44 | ```toml 45 | [rpc_endpoints] 46 | local-c = "http://localhost:9650/ext/bc/C/rpc" 47 | myblockchain = "http://localhost:9650/ext/bc/myblockchain/rpc" 48 | anotherblockchain = "http://localhost:9650/ext/bc/BASE58_BLOCKCHAIN_ID/rpc" 49 | ``` 50 | 51 | ## Code Examples 52 | 53 | ### Interchain Messaging 54 | - [send-receive](https://academy.avax.network/course/interchain-messaging/04-icm-basics/01-icm-basics) 55 | - [send-roundtrip](https://academy.avax.network/course/interchain-messaging/05-two-way-communication/01-two-way-communication) 56 | - [invoking-functions](https://academy.avax.network/course/interchain-messaging/06-invoking-functions/01-invoking-functions) 57 | - [registry](https://academy.avax.network/course/interchain-messaging/07-icm-registry/01-icm-registry) 58 | - [incentivized-relayer](https://academy.avax.network/course/interchain-messaging/12-incentivizing-a-relayer/01-incentivizing-a-relayer) 59 | 60 | ### Interchain Token Transfer 61 | - [erc20-to-erc20](https://academy.avax.network/course/interchain-token-transfer/06-erc-20-to-erc-20-bridge/01-erc-20-to-erc-20-bridge) 62 | - [native-to-erc20](https://academy.avax.network/course/interchain-token-transfer/08-native-to-erc-20-bridge/01-native-to-erc-20-bridge) 63 | - [native-to-native](https://academy.avax.network/course/l1-tokenomics/03-multi-chain-ecosystems/04-use-any-native-as-native-token) 64 | - [erc20-to-native](https://academy.avax.network/course/l1-tokenomics/03-multi-chain-ecosystems/03-use-erc20-as-native-token) 65 | - [cross-chain-token-swaps](https://academy.avax.network/course/interchain-token-transfer/13-cross-chain-token-swaps/07-exchange-contract) 66 | 67 | ### Misc 68 | - [creating-contracts](contracts/misc/creating-contracts/REAEDME.md) 69 | - [erc721-bridge](contracts/misc/erc721-bridge/README.md) 70 | 71 | 72 | ## Web-Apps 73 | - [AvaCloud APIs](https://academy.avax.network/course/avacloudapis) 74 | -------------------------------------------------------------------------------- /contracts/interchain-token-transfer/cross-chain-token-swaps/DexERC20Wrapper.sol: -------------------------------------------------------------------------------- 1 | // (c) 2024, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.25; 7 | 8 | import {IERC20SendAndCallReceiver} from "@avalanche-interchain-token-transfer/interfaces/IERC20SendAndCallReceiver.sol"; 9 | import {SafeERC20TransferFrom} from "@avalanche-interchain-token-transfer/../utilities/SafeERC20TransferFrom.sol"; 10 | 11 | import {SafeERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; 12 | import {IERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; 13 | import {Context} from "@openzeppelin/contracts@5.0.2/utils/Context.sol"; 14 | 15 | import {IWAVAX} from "./interfaces/IWAVAX.sol"; 16 | import {IUniswapFactory} from "./interfaces/IUniswapFactory.sol"; 17 | import {IUniswapPair} from "./interfaces/IUniswapPair.sol"; 18 | 19 | /** 20 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 21 | * DO NOT USE THIS CODE IN PRODUCTION. 22 | */ 23 | contract DexERC20Wrapper is Context, IERC20SendAndCallReceiver { 24 | using SafeERC20 for IERC20; 25 | 26 | address public immutable WNATIVE; 27 | address public immutable factory; 28 | 29 | struct SwapOptions { 30 | address tokenOut; 31 | uint256 minAmountOut; 32 | } 33 | 34 | constructor(address wrappedNativeAddress, address dexFactoryAddress) { 35 | WNATIVE = wrappedNativeAddress; 36 | factory = dexFactoryAddress; 37 | } 38 | 39 | event TokensReceived( 40 | bytes32 indexed sourceBlockchainID, 41 | address indexed originTokenTransferrerAddress, 42 | address indexed originSenderAddress, 43 | address token, 44 | uint256 amount, 45 | bytes payload 46 | ); 47 | 48 | // To receive native when another contract called. 49 | receive() external payable {} 50 | 51 | function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) 52 | internal 53 | pure 54 | returns (uint256 amountOut) 55 | { 56 | uint256 amountInWithFee = amountIn * 997; 57 | uint256 numerator = amountInWithFee * reserveOut; 58 | uint256 denominator = reserveIn * 1e3 + amountInWithFee; 59 | amountOut = numerator / denominator; 60 | } 61 | 62 | function query(uint256 amountIn, address tokenIn, address tokenOut) internal view returns (uint256 amountOut) { 63 | if (tokenIn == tokenOut || amountIn == 0) { 64 | return 0; 65 | } 66 | address pair = IUniswapFactory(factory).getPair(tokenIn, tokenOut); 67 | if (pair == address(0)) { 68 | return 0; 69 | } 70 | (uint256 r0, uint256 r1,) = IUniswapPair(pair).getReserves(); 71 | (uint256 reserveIn, uint256 reserveOut) = tokenIn < tokenOut ? (r0, r1) : (r1, r0); 72 | if (reserveIn > 0 && reserveOut > 0) { 73 | amountOut = getAmountOut(amountIn, reserveIn, reserveOut); 74 | } 75 | } 76 | 77 | function swap(uint256 amountIn, uint256 amountOut, address tokenIn, address tokenOut, address to) internal { 78 | address pair = IUniswapFactory(factory).getPair(tokenIn, tokenOut); 79 | (uint256 amount0Out, uint256 amount1Out) = 80 | (tokenIn < tokenOut) ? (uint256(0), amountOut) : (amountOut, uint256(0)); 81 | IERC20(tokenIn).safeTransfer(pair, amountIn); 82 | IUniswapPair(pair).swap(amount0Out, amount1Out, to, new bytes(0)); 83 | } 84 | 85 | function receiveTokens( 86 | bytes32 sourceBlockchainID, 87 | address originTokenTransferrerAddress, 88 | address originSenderAddress, 89 | address token, 90 | uint256 amount, 91 | bytes calldata payload 92 | ) external { 93 | emit TokensReceived({ 94 | sourceBlockchainID: sourceBlockchainID, 95 | originTokenTransferrerAddress: originTokenTransferrerAddress, 96 | originSenderAddress: originSenderAddress, 97 | token: token, 98 | amount: amount, 99 | payload: payload 100 | }); 101 | 102 | require(payload.length > 0, "DexERC20Wrapper: empty payload"); 103 | 104 | IERC20 _token = IERC20(token); 105 | // Receives teleported assets to be used for different purposes. 106 | SafeERC20TransferFrom.safeTransferFrom(_token, _msgSender(), amount); 107 | 108 | // Requests a quote from the Uniswap V2-like contract. 109 | uint256 amountOut = query(amount, token, WNATIVE); 110 | require(amountOut > 0, "DexERC20Wrapper: insufficient liquidity"); 111 | 112 | // Parses the payload of the message. 113 | SwapOptions memory swapOptions = abi.decode(payload, (SwapOptions)); 114 | // Checks if the target swap price is still valid. 115 | require(amountOut >= swapOptions.minAmountOut, "DexERC20Wrapper: slippage exceeded"); 116 | 117 | // Verifies if the desired tokenOut is a native or wrapped asset. 118 | if (swapOptions.tokenOut == address(0)) { 119 | swap(amount, amountOut, token, WNATIVE, address(this)); 120 | IWAVAX(WNATIVE).withdraw(amountOut); 121 | payable(originSenderAddress).transfer(amountOut); 122 | } else { 123 | swap(amount, amountOut, token, WNATIVE, originSenderAddress); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /web-apps/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 31 | 117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /web-apps/src/app/basic-explorer/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from 'next/image' 3 | import { useEffect, useState } from 'react' 4 | import { Card } from '@/app/components/Card' 5 | import { Input } from '@/app/components/Input' 6 | import { Button } from '@/app/components/Button' 7 | import { NativeTransaction, EvmBlock } from '@avalabs/avacloud-sdk/models/components'; 8 | 9 | 10 | interface SelectedItem { 11 | type: string; 12 | id: string; 13 | from: string; 14 | to: string; 15 | value: string; 16 | } 17 | 18 | export default function BlockchainExplorer() { 19 | const [searchTerm, setSearchTerm] = useState('') 20 | const [selectedItem, setSelectedItem] = useState(null) 21 | const [recentTransactions, setRecentTransactions] = useState([]) 22 | const [recentBlocks, setRecentBlocks] = useState([]) 23 | 24 | const handleSearch = () => { 25 | setSelectedItem({ type: 'transaction', id: searchTerm, from: '0xSender', to: '0xReceiver', value: '1.5 ETH' }) 26 | } 27 | 28 | const fetchRecentTransactions = async () => { 29 | const response = await fetch(`/api/explorer?method=getRecentTransactions`) 30 | const data = await response.json() 31 | return data as NativeTransaction[] 32 | } 33 | const fetchRecentBlocks = async () => { 34 | const response = await fetch(`/api/explorer?method=getRecentBlocks`) 35 | const data = await response.json() 36 | return data as EvmBlock[] 37 | } 38 | 39 | useEffect(() => { 40 | fetchRecentTransactions().then(setRecentTransactions) 41 | fetchRecentBlocks().then(setRecentBlocks) 42 | }, []) 43 | 44 | return ( 45 | 46 |
47 |

Blockchain Explorer

48 | 66 |
67 |
68 | setSearchTerm(e.target.value)} 71 | placeholder="Search by transaction, block, contract or address" 72 | className="flex-grow" 73 | /> 74 | 77 |
78 |
79 | 80 |
81 | 82 |
    83 | {recentTransactions.map((tx) => ( 84 |
  • 85 |
    86 | {tx.txHash.substring(0, 12)}... 87 | 88 | {Math.floor((Date.now() - new Date(tx.blockTimestamp * 1000).getTime()) / 1000)}s ago 89 | 90 |
    91 |
    92 | From: {tx.from.address.substring(0, 12)} 93 | To: {tx.to.address.substring(0, 12)} 94 |
    95 |
    96 | {(parseFloat(tx.value) / 10 ** 18).toString().substring(0,6)} AVAX 97 |
    98 |
  • 99 | ))} 100 |
101 |
102 | 103 | 104 |
    105 | {recentBlocks.map((block) => ( 106 |
  • 107 |
    108 | #{block.blockNumber} 109 | 110 | {Math.floor((Date.now() - new Date(block.blockTimestamp * 1000).getTime()) / 1000)}s ago 111 | 112 |
    113 | {block.txCount} transactions 114 |
    115 | {(parseFloat(block.feesSpent) / 10 ** 18).toString().substring(0,6)} AVAX 116 |
    117 |
  • 118 | ))} 119 |
120 |
121 |
122 | 123 | {selectedItem && ( 124 | 125 |
126 | ID: 127 | {selectedItem.id} 128 | From: 129 | {selectedItem.from} 130 | To: 131 | {selectedItem.to} 132 | Value: 133 | {selectedItem.value} 134 |
135 |
136 | )} 137 |
138 | ) 139 | } -------------------------------------------------------------------------------- /web-apps/src/app/basic-wallet/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from 'next/image' 3 | import { useEffect, useState } from 'react' 4 | import { useAccount } from 'wagmi'; 5 | import { ConnectButton } from '@rainbow-me/rainbowkit' 6 | import { Erc1155TokenBalance, Erc20TokenBalance, TransactionDetails } from '@avalabs/avacloud-sdk/models/components'; 7 | import { Erc721TokenBalance } from '@avalabs/avacloud-sdk/models/components/erc721tokenbalance'; 8 | 9 | export default function BasicWallet() { 10 | const { address } = useAccount() 11 | const [erc20Balances, setErc20Balances] = useState([]) 12 | const [erc721Balances, setErc721Balances] = useState([]) 13 | const [erc1155Balances, setErc1155Balances] = useState([]) 14 | const [recentTransactions, setRecentTransactions] = useState() 15 | const [activeTab, setActiveTab] = useState('nfts') 16 | 17 | const fetchERC20Balances = async (address: string) => { 18 | const blockResult = await fetch("api/balance?method=getBlockHeight"); 19 | const blockNumber = await blockResult.json(); 20 | const balanceResult = await fetch("api/balance?method=listErc20Balances&address=" + address + "&blockNumber=" + blockNumber); 21 | const balances = await balanceResult.json(); 22 | return balances as Erc20TokenBalance[]; 23 | }; 24 | 25 | const fetchERC721Balances = async (address: string) => { 26 | const result = await fetch(`api/wallet?method=listERC721Balances&address=${address}`); 27 | const balances = await result.json(); 28 | return balances as Erc721TokenBalance[]; 29 | } 30 | 31 | const fetchERC1155Balances = async (address: string) => { 32 | const result = await fetch(`api/wallet?method=listERC1155Balances&address=${address}`); 33 | const balances = await result.json(); 34 | return balances as Erc1155TokenBalance[]; 35 | } 36 | 37 | const fetchRecentTransactions = async (address: string) => { 38 | const result = await fetch(`api/wallet?method=listRecentTransactions&address=${address}`); 39 | const transactions = await result.json(); 40 | return transactions as TransactionDetails; 41 | } 42 | 43 | useEffect(() => { 44 | if (address) { 45 | fetchERC20Balances("0xd26C04bE22Cb25c7727504daF4304919cA26e301").then(setErc20Balances); 46 | fetchERC721Balances("0xd26C04bE22Cb25c7727504daF4304919cA26e301").then(setErc721Balances); 47 | fetchERC1155Balances("0xd26C04bE22Cb25c7727504daF4304919cA26e301").then(setErc1155Balances); 48 | fetchRecentTransactions("0xd26C04bE22Cb25c7727504daF4304919cA26e301").then(setRecentTransactions); 49 | } 50 | }, [address]); 51 | 52 | return ( 53 |
54 |
55 |

Wallet Portfolio

56 | 74 |
75 | 76 |
77 |
78 | 79 |
80 |
81 |
82 | 97 |
98 | 99 | {activeTab === 'nfts' && ( 100 |
101 | {erc721Balances.length > 0 && erc721Balances.map((nft) => ( 102 | 109 |
110 |
111 |

{nft.name}

112 |

#{nft.tokenId}

113 |
114 | {nft.metadata.imageUri && ( 115 | {nft.symbol} 116 | )} 117 |
118 |
119 | ))} 120 |
121 | )} 122 | 123 | {activeTab === 'erc20' && ( 124 | 144 | )} 145 | 146 | {activeTab === 'erc1155' && ( 147 |
148 | {erc1155Balances.length > 0 && erc1155Balances.map((token) => ( 149 |
150 |

{token.metadata.name}

151 |

Balance: {token.balance}

152 |
153 | ))} 154 |
155 | )} 156 |
157 | 158 | 204 |
205 |
206 | ) 207 | } -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/ERC721Bridge.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import {BridgeNFT} from "./BridgeNFT.sol"; 9 | import {ExampleERC721} from "./ExampleERC721.sol"; 10 | import {IERC721Bridge} from "./IERC721Bridge.sol"; 11 | import {ITeleporterMessenger, TeleporterMessageInput, TeleporterFeeInfo} from "@teleporter/ITeleporterMessenger.sol"; 12 | import {TeleporterOwnerUpgradeable} from "@teleporter/upgrades/TeleporterOwnerUpgradeable.sol"; 13 | import {IWarpMessenger} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/IWarpMessenger.sol"; 14 | import {IERC721} from "@openzeppelin/contracts@4.8.1/token/ERC721/ERC721.sol"; 15 | import {IERC721Receiver} from "@openzeppelin/contracts@4.8.1/token/ERC721/IERC721Receiver.sol"; 16 | import {IERC20, ERC20} from "@openzeppelin/contracts@4.8.1/token/ERC20/ERC20.sol"; 17 | import {SafeERC20} from "@openzeppelin/contracts@4.8.1/token/ERC20/utils/SafeERC20.sol"; 18 | import {SafeERC20TransferFrom} from "@teleporter/SafeERC20TransferFrom.sol"; 19 | 20 | /** 21 | * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. 22 | * DO NOT USE THIS CODE IN PRODUCTION. 23 | */ 24 | 25 | /** 26 | * @dev Implementation of the {IERC721Bridge} interface. 27 | * 28 | * This implementation uses the {BridgeToken} contract to represent tokens on this chain, and uses 29 | * {ITeleporterMessenger} to send and receive messages to other chains. 30 | */ 31 | contract ERC721Bridge is IERC721Bridge, TeleporterOwnerUpgradeable, IERC721Receiver { 32 | using SafeERC20 for IERC20; 33 | 34 | address public constant WARP_PRECOMPILE_ADDRESS = 0x0200000000000000000000000000000000000005; 35 | bytes32 public immutable currentBlockchainID; 36 | uint256 public constant CREATE_BRIDGE_TOKENS_REQUIRED_GAS = 2_000_000; 37 | uint256 public constant MINT_BRIDGE_TOKENS_REQUIRED_GAS = 200_000; 38 | uint256 public constant TRANSFER_BRIDGE_TOKENS_REQUIRED_GAS = 300_000; 39 | 40 | // Mapping to keep track of submitted create bridge token requests 41 | mapping( 42 | bytes32 destinationBlockchainID 43 | => mapping(address destinationBridgeAddress => mapping(address erc721Contract => bool submitted)) 44 | ) public submittedBridgeNFTCreations; 45 | 46 | // Set of BridgeNFT contracts created by this ERC721Bridge instance. 47 | mapping(address bridgeNFT => bool bridgeTokenExists) public bridgedNFTContracts; 48 | 49 | // Tracks the wrapped bridge token contract address for each native token bridged to this bridge instance. 50 | // (nativeBlockchainID, nativeBridgeAddress, nativeTokenAddress) -> bridgeTokenAddress 51 | mapping( 52 | bytes32 nativeBlockchainID 53 | => mapping(address nativeBridgeAddress => mapping(address nativeTokenAddress => address bridgeNFTAddress)) 54 | ) public nativeToBridgedNFT; 55 | 56 | mapping(address bridgeNft => mapping(uint256 tokenId => bool bridged)) public bridgedTokens; 57 | 58 | /** 59 | * @dev Initializes the Teleporter Messenger used for sending and receiving messages, 60 | * and initializes the current chain ID. 61 | */ 62 | constructor(address teleporterRegistryAddress) TeleporterOwnerUpgradeable(teleporterRegistryAddress, msg.sender) { 63 | currentBlockchainID = IWarpMessenger(WARP_PRECOMPILE_ADDRESS).getBlockchainID(); 64 | } 65 | 66 | // Required in order to be able to hold ERC721 tokens in this contract. 67 | function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { 68 | return this.onERC721Received.selector; 69 | } 70 | 71 | /** 72 | * @dev See {IERC721Bridge-bridgeToken}. 73 | * 74 | * Requirements: 75 | * 76 | * - `destinationBlockchainID` cannot be the same as the current chain ID. 77 | * - `recipient` cannot be the zero address. 78 | * - `destinationBridgeAddress` cannot be the zero address. 79 | * - `tokenContractAddress` must be a valid ERC721 contract. 80 | * - `tokenId` must be a valid token ID for the ERC721 contract. 81 | * 82 | * Emits a {BridgeToken} event. 83 | */ 84 | function bridgeToken( 85 | bytes32 destinationBlockchainID, 86 | address destinationBridgeAddress, 87 | address tokenContractAddress, 88 | address recipient, 89 | uint256 tokenId, 90 | address messageFeeAsset, 91 | uint256 messageFeeAmount 92 | ) external nonReentrant { 93 | // Bridging tokens within a single chain is not allowed. 94 | if (destinationBlockchainID == currentBlockchainID) { 95 | revert InvalidDestinationBlockchainId(); 96 | } 97 | 98 | // Neither the recipient, nor the NFT contract, nor the Remote Bridge can be the zero address. 99 | if (tokenContractAddress == address(0)) { 100 | revert ZeroTokenContractAddress(); 101 | } 102 | if (recipient == address(0)) { 103 | revert ZeroRecipientAddress(); 104 | } 105 | if (destinationBridgeAddress == address(0)) { 106 | revert ZeroDestinationBridgeAddress(); 107 | } 108 | 109 | ITeleporterMessenger teleporterMessenger = _getTeleporterMessenger(); 110 | 111 | _manageFee(teleporterMessenger, messageFeeAsset, messageFeeAmount); 112 | 113 | // If the token to be bridged is a bridged NFT of this bridge, 114 | // then handle it by burning the NFT on this chain, and sending a message 115 | // back to the native chain. 116 | // Otherwise, handle it by locking the NFT in this bridge instance, 117 | // and sending a message to the destination to mint the equivalent NFT on the destination chain. 118 | if (bridgedNFTContracts[tokenContractAddress]) { 119 | return _processBridgedTokenTransfer( 120 | BridgedTokenTransferInfo({ 121 | destinationBlockchainID: destinationBlockchainID, 122 | destinationBridgeAddress: destinationBridgeAddress, 123 | teleporterMessenger: teleporterMessenger, 124 | bridgedNFTContractAddress: tokenContractAddress, 125 | recipient: recipient, 126 | tokenId: tokenId, 127 | messageFeeAsset: messageFeeAsset, 128 | messageFeeAmount: messageFeeAmount 129 | }) 130 | ); 131 | } 132 | 133 | // Check if requests to create a BridgeNFT contract on the destination chain has been submitted. 134 | // This does not guarantee that the BridgeNFT contract has been created on the destination chain, 135 | // due to different factors preventing the message from being delivered, or the contract creation. 136 | if (!submittedBridgeNFTCreations[destinationBlockchainID][destinationBridgeAddress][tokenContractAddress]) { 137 | revert TokenContractNotBridged(); 138 | } 139 | 140 | // Check that the token ID is not already bridged 141 | // If the owner of the token is this contract, then the token is already bridged. 142 | address tokenOwner = IERC721(tokenContractAddress).ownerOf(tokenId); 143 | if (tokenOwner == address(this) || tokenOwner != msg.sender) { 144 | revert InvalidTokenId(); 145 | } 146 | 147 | // Lock the NFT by transferring it to this contract. 148 | IERC721(tokenContractAddress).safeTransferFrom(msg.sender, address(this), tokenId); 149 | 150 | // Send a message to the destination chain and bridge to mint the equivalent NFT on the destination chain. 151 | _processNativeTokenTransfer( 152 | NativeTokenTransferInfo({ 153 | destinationBlockchainID: destinationBlockchainID, 154 | destinationBridgeAddress: destinationBridgeAddress, 155 | teleporterMessenger: teleporterMessenger, 156 | nativeContractAddress: tokenContractAddress, 157 | recipient: recipient, 158 | tokenId: tokenId, 159 | messageFeeAsset: messageFeeAsset, 160 | messageFeeAmount: messageFeeAmount 161 | }) 162 | ); 163 | } 164 | 165 | /** 166 | * @dev See {IERC721Bridge-submitCreateBridgeNFT}. 167 | * 168 | * We allow for `submitCreateBridgeNFT` to be called multiple times with the same bridge and token 169 | * information because a previous message may have been dropped or otherwise selectively not delivered. 170 | * If the bridge token already exists on the destination, we are sending a message that will 171 | * simply have no effect on the destination. 172 | * 173 | * Emits a {SubmitCreateBridgeToken} event. 174 | */ 175 | function submitCreateBridgeNFT( 176 | bytes32 destinationBlockchainID, 177 | address destinationBridgeAddress, 178 | ExampleERC721 nativeContract, 179 | address messageFeeAsset, 180 | uint256 messageFeeAmount 181 | ) external nonReentrant { 182 | if (destinationBridgeAddress == address(0)) { 183 | revert ZeroDestinationBridgeAddress(); 184 | } 185 | 186 | ITeleporterMessenger teleporterMessenger = _getTeleporterMessenger(); 187 | 188 | _manageFee(teleporterMessenger, messageFeeAsset, messageFeeAmount); 189 | 190 | // Create the calldata to create an instance of BridgeNFT contract on the destination chain. 191 | bytes memory messageData = encodeCreateBridgeNFTData( 192 | CreateBridgeNFTData({ 193 | nativeContractAddress: address(nativeContract), 194 | nativeName: nativeContract.name(), 195 | nativeSymbol: nativeContract.symbol(), 196 | nativeTokenURI: nativeContract.baseUri() 197 | }) 198 | ); 199 | 200 | // Send Teleporter message. 201 | bytes32 messageID = teleporterMessenger.sendCrossChainMessage( 202 | TeleporterMessageInput({ 203 | destinationBlockchainID: destinationBlockchainID, 204 | destinationAddress: destinationBridgeAddress, 205 | feeInfo: TeleporterFeeInfo({feeTokenAddress: messageFeeAsset, amount: messageFeeAmount}), 206 | requiredGasLimit: CREATE_BRIDGE_TOKENS_REQUIRED_GAS, 207 | allowedRelayerAddresses: new address[](0), 208 | message: messageData 209 | }) 210 | ); 211 | 212 | // Update the mapping to keep track of submitted create bridge token requests 213 | submittedBridgeNFTCreations[destinationBlockchainID][destinationBridgeAddress][address(nativeContract)] = true; 214 | 215 | emit SubmitCreateBridgeNFT( 216 | destinationBlockchainID, destinationBridgeAddress, address(nativeContract), messageID 217 | ); 218 | } 219 | 220 | function _manageFee(ITeleporterMessenger teleporterMessenger, address messageFeeAsset, uint256 messageFeeAmount) 221 | private 222 | returns (uint256 adjustedFeeAmount) 223 | { 224 | // For non-zero fee amounts, first transfer the fee to this contract, and then 225 | // allow the Teleporter contract to spend it. 226 | if (messageFeeAmount > 0) { 227 | if (messageFeeAsset == address(0)) { 228 | revert ZeroFeeAssetAddress(); 229 | } 230 | 231 | adjustedFeeAmount = SafeERC20TransferFrom.safeTransferFrom(IERC20(messageFeeAsset), messageFeeAmount); 232 | IERC20(messageFeeAsset).safeIncreaseAllowance(address(teleporterMessenger), adjustedFeeAmount); 233 | } 234 | } 235 | 236 | /** 237 | * @dev prepares the calldata and sends teleporter message to mint the equivalent NFT on the destination chain. 238 | * 239 | * Emits a {BridgeToken} event. 240 | */ 241 | function _processNativeTokenTransfer(NativeTokenTransferInfo memory transferInfo) private { 242 | bytes memory messageData = encodeMintBridgeNFTData( 243 | MintBridgeNFTData({ 244 | nativeContractAddress: transferInfo.nativeContractAddress, 245 | recipient: transferInfo.recipient, 246 | tokenId: transferInfo.tokenId 247 | }) 248 | ); 249 | 250 | bytes32 messageID = transferInfo.teleporterMessenger.sendCrossChainMessage( 251 | TeleporterMessageInput({ 252 | destinationBlockchainID: transferInfo.destinationBlockchainID, 253 | destinationAddress: transferInfo.destinationBridgeAddress, 254 | feeInfo: TeleporterFeeInfo({ 255 | feeTokenAddress: transferInfo.messageFeeAsset, 256 | amount: transferInfo.messageFeeAmount 257 | }), 258 | requiredGasLimit: MINT_BRIDGE_TOKENS_REQUIRED_GAS, 259 | allowedRelayerAddresses: new address[](0), 260 | message: messageData 261 | }) 262 | ); 263 | 264 | emit BridgeToken( 265 | transferInfo.nativeContractAddress, 266 | transferInfo.destinationBlockchainID, 267 | messageID, 268 | transferInfo.destinationBridgeAddress, 269 | transferInfo.recipient, 270 | transferInfo.tokenId 271 | ); 272 | } 273 | 274 | /** 275 | * @dev Encodes the parameters for the Mint action to be decoded and executed on the destination. 276 | */ 277 | function encodeMintBridgeNFTData(MintBridgeNFTData memory mintData) public pure returns (bytes memory) { 278 | bytes memory paramsData = abi.encode(mintData); 279 | 280 | return abi.encode(BridgeAction.Mint, paramsData); 281 | } 282 | 283 | /** 284 | * @dev Encodes the parameters for creating a BridgeNFT instance on the destination chain. 285 | */ 286 | function encodeCreateBridgeNFTData(CreateBridgeNFTData memory createData) public pure returns (bytes memory) { 287 | bytes memory paramsData = abi.encode(createData); 288 | 289 | return abi.encode(BridgeAction.Create, paramsData); 290 | } 291 | 292 | /** 293 | * @dev Process transfer of a bridged NFT to the native chain. 294 | * 295 | * It is the caller's responsibility to ensure that the wrapped token contract is supported by this bridge instance. 296 | * Emits a {BridgeTokens} event. 297 | */ 298 | function _processBridgedTokenTransfer(BridgedTokenTransferInfo memory transferInfo) private { 299 | // Check that the token ID is bridged 300 | if (!bridgedTokens[transferInfo.bridgedNFTContractAddress][transferInfo.tokenId]) { 301 | revert InvalidTokenId(); 302 | } 303 | 304 | // Burn the bridged tokenId to be transfered back to the native chain 305 | BridgeNFT bridgeNTF = BridgeNFT(transferInfo.bridgedNFTContractAddress); 306 | bridgeNTF.burn(transferInfo.tokenId); 307 | delete bridgedTokens[transferInfo.bridgedNFTContractAddress][ 308 | transferInfo.tokenId 309 | ]; 310 | 311 | // If the destination chain ID is the native chain ID for the wrapped token, the bridge address must also match. 312 | // This is because you are not allowed to bridge a token within its native chain. 313 | bytes32 nativeBlockchainID = bridgeNTF.nativeBlockchainID(); 314 | address nativeBridgeAddress = bridgeNTF.nativeBridge(); 315 | 316 | // Curently, we don't support hopping to a destination chain that is not the native chain of the wrapped token 317 | // until we figure out a better way to handle the fee. 318 | if (transferInfo.destinationBlockchainID != nativeBlockchainID) { 319 | revert InvalidDestinationBlockchainId(); 320 | } 321 | 322 | if (transferInfo.destinationBridgeAddress != nativeBridgeAddress) { 323 | revert InvalidDestinationBridgeAddress(); 324 | } 325 | 326 | // Send a message to the native chain and bridge of the wrapped asset that was burned. 327 | // The message includes the destination chain ID and bridge contract, which will differ from the native 328 | // ones in the event that the tokens are being bridge from one non-native chain to another with two hops. 329 | bytes memory messageData = encodeTransferBridgeNFTData( 330 | TransferBridgeNFTData({ 331 | destinationBlockchainID: transferInfo.destinationBlockchainID, 332 | destinationBridgeAddress: transferInfo.destinationBridgeAddress, 333 | nativeContractAddress: bridgeNTF.nativeAsset(), 334 | recipient: transferInfo.recipient, 335 | tokenId: transferInfo.tokenId 336 | }) 337 | ); 338 | 339 | bytes32 messageID = transferInfo.teleporterMessenger.sendCrossChainMessage( 340 | TeleporterMessageInput({ 341 | destinationBlockchainID: nativeBlockchainID, 342 | destinationAddress: nativeBridgeAddress, 343 | feeInfo: TeleporterFeeInfo({ 344 | feeTokenAddress: transferInfo.messageFeeAsset, 345 | amount: transferInfo.messageFeeAmount 346 | }), 347 | requiredGasLimit: TRANSFER_BRIDGE_TOKENS_REQUIRED_GAS, 348 | allowedRelayerAddresses: new address[](0), 349 | message: messageData 350 | }) 351 | ); 352 | 353 | emit BridgeToken( 354 | transferInfo.bridgedNFTContractAddress, 355 | transferInfo.destinationBlockchainID, 356 | messageID, 357 | transferInfo.destinationBridgeAddress, 358 | transferInfo.recipient, 359 | transferInfo.tokenId 360 | ); 361 | } 362 | 363 | /** 364 | * @dev Encodes the parameters for the Transfer action to be decoded and executed on the destination. 365 | */ 366 | function encodeTransferBridgeNFTData(TransferBridgeNFTData memory transferData) 367 | public 368 | pure 369 | returns (bytes memory) 370 | { 371 | // ABI encode the Transfer action and corresponding parameters for the transferBridgeToken 372 | // call to to be decoded and executed on the destination. 373 | bytes memory paramsData = abi.encode(transferData); 374 | 375 | return abi.encode(BridgeAction.Transfer, paramsData); 376 | } 377 | 378 | // TELEPORTER MESSAGE RECIEVER IMPLEMENTATION 379 | 380 | /** 381 | * @dev See {TeleporterUpgradeable-receiveTeleporterMessage}. 382 | * 383 | * Receives a Teleporter message and routes to the appropriate internal function call. 384 | */ 385 | function _receiveTeleporterMessage(bytes32 sourceBlockchainID, address originSenderAddress, bytes memory message) 386 | internal 387 | override 388 | { 389 | // Decode the payload to recover the action and corresponding function parameters 390 | (BridgeAction action, bytes memory actionData) = abi.decode(message, (BridgeAction, bytes)); 391 | 392 | // Route to the appropriate function. 393 | if (action == BridgeAction.Create) { 394 | CreateBridgeNFTData memory createData = abi.decode(actionData, (CreateBridgeNFTData)); 395 | _createBridgeNFTContract(sourceBlockchainID, originSenderAddress, createData); 396 | } else if (action == BridgeAction.Mint) { 397 | MintBridgeNFTData memory mintData = abi.decode(actionData, (MintBridgeNFTData)); 398 | _mintBridgeNFT(sourceBlockchainID, originSenderAddress, mintData); 399 | } else if (action == BridgeAction.Transfer) { 400 | TransferBridgeNFTData memory transferData = abi.decode(actionData, (TransferBridgeNFTData)); 401 | _transferBridgeNFT(transferData); 402 | } else { 403 | revert InvalidBridgeAction(); 404 | } 405 | } 406 | 407 | /** 408 | * @dev Teleporter message receiver for creating a new BridgeNFT contract on this chain. 409 | * 410 | * Emits a {CreateBridgeNFT} event. 411 | * 412 | * Note: This function is only called within `receiveTeleporterMessage`, which can only be 413 | * called by the Teleporter Messenger. 414 | */ 415 | function _createBridgeNFTContract( 416 | bytes32 nativeBlockchainID, 417 | address nativeBridgeAddress, 418 | CreateBridgeNFTData memory createData 419 | ) private { 420 | // Check that the contract is not already bridged 421 | if (nativeToBridgedNFT[nativeBlockchainID][nativeBridgeAddress][createData.nativeContractAddress] != address(0)) 422 | { 423 | revert TokenContractAlreadyBridged(); 424 | } 425 | 426 | address bridgeERC721Address = address( 427 | new BridgeNFT({ 428 | sourceBlockchainID: nativeBlockchainID, 429 | sourceBridge: nativeBridgeAddress, 430 | sourceAsset: createData.nativeContractAddress, 431 | tokenName: createData.nativeName, 432 | tokenSymbol: createData.nativeSymbol, 433 | tokenURI: createData.nativeTokenURI 434 | }) 435 | ); 436 | 437 | bridgedNFTContracts[bridgeERC721Address] = true; 438 | nativeToBridgedNFT[nativeBlockchainID][nativeBridgeAddress][createData.nativeContractAddress] = 439 | bridgeERC721Address; 440 | 441 | emit CreateBridgeNFT( 442 | nativeBlockchainID, nativeBridgeAddress, createData.nativeContractAddress, bridgeERC721Address 443 | ); 444 | } 445 | 446 | /** 447 | * @dev Teleporter message receiver for minting a tokenId from the specified instance of the BridgeNFT contract. 448 | * 449 | * Emits a {MintBridgeNFT} event. 450 | * 451 | * Note: This function is only called within `receiveTeleporterMessage`, which can only be 452 | * called by the Teleporter Messenger. 453 | */ 454 | function _mintBridgeNFT(bytes32 nativeBlockchainID, address nativeBridgeAddress, MintBridgeNFTData memory mintData) 455 | private 456 | { 457 | // The recipient cannot be the zero address. 458 | if (mintData.recipient == address(0)) { 459 | revert ZeroRecipientAddress(); 460 | } 461 | // Check that a bridge token exists for this native asset. 462 | // If not, one needs to be created by the delivery of a "createBridgeToken" message first 463 | // before this mint can be processed. Once the bridge token is create, this message 464 | // could then be retried to mint the tokens. 465 | address bridgeNFTAddress = 466 | nativeToBridgedNFT[nativeBlockchainID][nativeBridgeAddress][mintData.nativeContractAddress]; 467 | 468 | if (bridgeNFTAddress == address(0)) { 469 | revert TokenContractNotBridged(); 470 | } 471 | 472 | // Mint the bridged NFT. 473 | BridgeNFT(bridgeNFTAddress).mint(mintData.recipient, mintData.tokenId); 474 | 475 | bridgedTokens[bridgeNFTAddress][mintData.tokenId] = true; 476 | 477 | emit MintBridgeNFT(bridgeNFTAddress, mintData.recipient, mintData.tokenId); 478 | } 479 | 480 | /** 481 | * @dev Teleporter message receiver for handling transfering bridged tokenId back to the native chain. 482 | * 483 | * Note: This function is only called within `receiveTeleporterMessage`, which can only be 484 | * called by the Teleporter Messenger. 485 | */ 486 | function _transferBridgeNFT(TransferBridgeNFTData memory transferData) private { 487 | // Ensure that the destination blockchain ID is the current blockchain ID. No hops are supported at this time 488 | if (transferData.destinationBlockchainID != currentBlockchainID) { 489 | revert InvalidDestinationBlockchainId(); 490 | } 491 | 492 | // Neither the recipient, nor the NFT contract, nor the Remote Bridge can be the zero address. 493 | if (transferData.nativeContractAddress == address(0)) { 494 | revert ZeroTokenContractAddress(); 495 | } 496 | if (transferData.recipient == address(0)) { 497 | revert ZeroRecipientAddress(); 498 | } 499 | if (transferData.destinationBridgeAddress == address(0)) { 500 | revert ZeroDestinationBridgeAddress(); 501 | } 502 | 503 | // Transfer tokens to the recipient. 504 | IERC721(transferData.nativeContractAddress).safeTransferFrom( 505 | address(this), transferData.recipient, transferData.tokenId 506 | ); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /contracts/misc/erc721-bridge/ERC721BridgeTests.t.sol: -------------------------------------------------------------------------------- 1 | // (c) 2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // SPDX-License-Identifier: Ecosystem 5 | 6 | pragma solidity ^0.8.18; 7 | 8 | import {Test} from "forge-std/Test.sol"; 9 | import { 10 | BridgeNFT, 11 | ERC721Bridge, 12 | ExampleERC721, 13 | IERC721, 14 | IERC721Bridge, 15 | TeleporterMessageInput, 16 | TeleporterFeeInfo, 17 | IWarpMessenger, 18 | ITeleporterMessenger 19 | } from "./ERC721Bridge.sol"; 20 | import {TeleporterRegistry} from "@teleporter/upgrades/TeleporterRegistry.sol"; 21 | import {ExampleERC721} from "./ExampleERC721.sol"; 22 | import {Strings} from "@openzeppelin/contracts@4.8.1/utils/Strings.sol"; 23 | 24 | contract ERC721BridgeTest is Test { 25 | address public constant MOCK_TELEPORTER_MESSENGER_ADDRESS = 0x644E5b7c5D4Bc8073732CEa72c66e0BB90dFC00f; 26 | address public constant MOCK_TELEPORTER_REGISTRY_ADDRESS = 0xf9FA4a0c696b659328DDaaBCB46Ae4eBFC9e68e4; 27 | address public constant WARP_PRECOMPILE_ADDRESS = address(0x0200000000000000000000000000000000000005); 28 | bytes32 internal constant _MOCK_MESSAGE_ID = 29 | bytes32(hex"1111111111111111111111111111111111111111111111111111111111111111"); 30 | bytes32 private constant _MOCK_BLOCKCHAIN_ID = bytes32(uint256(123456)); 31 | bytes32 private constant _DEFAULT_OTHER_CHAIN_ID = 32 | bytes32(hex"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"); 33 | address private constant _DEFAULT_OTHER_BRIDGE_ADDRESS = 0xd54e3E251b9b0EEd3ed70A858e927bbC2659587d; 34 | string private constant _DEFAULT_TOKEN_NAME = "Test Token"; 35 | string private constant _DEFAULT_SYMBOL = "TSTTK"; 36 | string private constant _DEFAULT_TOKEN_URI = "https://example.com/ipfs/"; 37 | address private constant _DEFAULT_RECIPIENT = 0xa4CEE7d1aF6aDdDD33E3b1cC680AB84fdf1b6d1d; 38 | 39 | ERC721Bridge public erc721Bridge; 40 | ExampleERC721 public mockERC721; 41 | 42 | event BridgeToken( 43 | address indexed tokenContractAddress, 44 | bytes32 indexed destinationBlockchainID, 45 | bytes32 indexed teleporterMessageID, 46 | address destinationBridgeAddress, 47 | address recipient, 48 | uint256 tokenId 49 | ); 50 | 51 | event SubmitCreateBridgeNFT( 52 | bytes32 indexed destinationBlockchainID, 53 | address indexed destinationBridgeAddress, 54 | address indexed nativeContractAddress, 55 | bytes32 teleporterMessageID 56 | ); 57 | 58 | event CreateBridgeNFT( 59 | bytes32 indexed nativeBlockchainID, 60 | address indexed nativeBridgeAddress, 61 | address indexed nativeContractAddress, 62 | address bridgeNFTAddress 63 | ); 64 | 65 | event MintBridgeNFT(address indexed contractAddress, address recipient, uint256 tokenId); 66 | 67 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 68 | 69 | function setUp() public virtual { 70 | vm.mockCall( 71 | WARP_PRECOMPILE_ADDRESS, 72 | abi.encodeWithSelector(IWarpMessenger.getBlockchainID.selector), 73 | abi.encode(_MOCK_BLOCKCHAIN_ID) 74 | ); 75 | vm.expectCall(WARP_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IWarpMessenger.getBlockchainID.selector)); 76 | 77 | _initMockTeleporterRegistry(); 78 | 79 | vm.expectCall( 80 | MOCK_TELEPORTER_REGISTRY_ADDRESS, 81 | abi.encodeWithSelector(TeleporterRegistry(MOCK_TELEPORTER_REGISTRY_ADDRESS).latestVersion.selector) 82 | ); 83 | 84 | erc721Bridge = new ERC721Bridge(MOCK_TELEPORTER_REGISTRY_ADDRESS); 85 | mockERC721 = new ExampleERC721(); 86 | } 87 | 88 | function testSameBlockchainID() public { 89 | vm.expectRevert(IERC721Bridge.InvalidDestinationBlockchainId.selector); 90 | erc721Bridge.bridgeToken({ 91 | destinationBlockchainID: _MOCK_BLOCKCHAIN_ID, 92 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 93 | tokenContractAddress: address(mockERC721), 94 | recipient: _DEFAULT_RECIPIENT, 95 | tokenId: 1, 96 | messageFeeAsset: address(0), 97 | messageFeeAmount: 0 98 | }); 99 | } 100 | 101 | function testZeroDestinationBridgeAddress() public { 102 | vm.expectRevert(IERC721Bridge.ZeroDestinationBridgeAddress.selector); 103 | erc721Bridge.bridgeToken({ 104 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 105 | destinationBridgeAddress: address(0), 106 | tokenContractAddress: address(mockERC721), 107 | recipient: _DEFAULT_RECIPIENT, 108 | tokenId: 1, 109 | messageFeeAsset: address(0), 110 | messageFeeAmount: 0 111 | }); 112 | } 113 | 114 | function testZeroNFTContractAddress() public { 115 | vm.expectRevert(IERC721Bridge.ZeroTokenContractAddress.selector); 116 | erc721Bridge.bridgeToken({ 117 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 118 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 119 | tokenContractAddress: address(0), 120 | recipient: _DEFAULT_RECIPIENT, 121 | tokenId: 1, 122 | messageFeeAsset: address(0), 123 | messageFeeAmount: 0 124 | }); 125 | } 126 | 127 | function testZeroRecipient() public { 128 | vm.expectRevert(IERC721Bridge.ZeroRecipientAddress.selector); 129 | erc721Bridge.bridgeToken({ 130 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 131 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 132 | tokenContractAddress: address(mockERC721), 133 | recipient: address(0), 134 | tokenId: 1, 135 | messageFeeAsset: address(0), 136 | messageFeeAmount: 0 137 | }); 138 | } 139 | 140 | function testInvalidBridgeNFTContract() public { 141 | vm.expectRevert(IERC721Bridge.TokenContractNotBridged.selector); 142 | erc721Bridge.bridgeToken({ 143 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 144 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 145 | tokenContractAddress: address(mockERC721), 146 | recipient: _DEFAULT_RECIPIENT, 147 | tokenId: 1, 148 | messageFeeAsset: address(0), 149 | messageFeeAmount: 0 150 | }); 151 | } 152 | 153 | function testAlreadyBridgedTokenId() public { 154 | uint256 tokenId = 1; 155 | 156 | // For the test purposes we will mint the NFT to the bridge contract directly to simulate the NFT being already bridged. 157 | // Otherwise we would need to mint it to the user, then allow the bridge contract to transfer it, bridge it, and then 158 | // try to bridge it again. 159 | vm.prank(address(erc721Bridge)); 160 | mockERC721.mint(tokenId); 161 | 162 | // Bridge the NFT. 163 | _setUpMockERC721ContractValues(address(mockERC721)); 164 | _submitCreateBridgeERC721(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721)); 165 | 166 | vm.expectRevert(IERC721Bridge.InvalidTokenId.selector); 167 | vm.prank(_DEFAULT_RECIPIENT); 168 | erc721Bridge.bridgeToken({ 169 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 170 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 171 | tokenContractAddress: address(mockERC721), 172 | recipient: _DEFAULT_RECIPIENT, 173 | tokenId: tokenId, 174 | messageFeeAsset: address(0), 175 | messageFeeAmount: 0 176 | }); 177 | } 178 | 179 | function testNotOwnerOfTokenID() public { 180 | uint256 tokenId = 1; 181 | vm.prank(address(1)); 182 | mockERC721.mint(tokenId); 183 | 184 | // Bridge the NFT. 185 | _setUpMockERC721ContractValues(address(mockERC721)); 186 | _submitCreateBridgeERC721(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721)); 187 | 188 | vm.expectRevert(IERC721Bridge.InvalidTokenId.selector); 189 | vm.prank(_DEFAULT_RECIPIENT); 190 | erc721Bridge.bridgeToken({ 191 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 192 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 193 | tokenContractAddress: address(mockERC721), 194 | recipient: _DEFAULT_RECIPIENT, 195 | tokenId: tokenId, 196 | messageFeeAsset: address(0), 197 | messageFeeAmount: 0 198 | }); 199 | } 200 | 201 | function testLockNativeNFT() public { 202 | uint256 tokenId = 1; 203 | 204 | // Mint the token to the default recipient. 205 | // Approve the bridge contract to manage the token. 206 | 207 | vm.startPrank(_DEFAULT_RECIPIENT); 208 | mockERC721.mint(tokenId); 209 | mockERC721.approve(address(erc721Bridge), tokenId); 210 | vm.stopPrank(); 211 | 212 | // Bridge the NFT. 213 | _setUpMockERC721ContractValues(address(mockERC721)); 214 | _submitCreateBridgeERC721(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721)); 215 | _setUpExpectedTransferFromCall(address(mockERC721), _DEFAULT_RECIPIENT, address(erc721Bridge), tokenId); 216 | _submitBridgeERC721( 217 | _DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721), _DEFAULT_RECIPIENT, tokenId 218 | ); 219 | 220 | // Check that the NFT was locked in the bridge contract. 221 | assertEq(address(erc721Bridge), mockERC721.ownerOf(tokenId)); 222 | } 223 | 224 | function testNewBridgeNFTMint() public { 225 | uint256 tokenId = 1; 226 | 227 | address bridgeNFTAddress = _setupBridgeNFT({ 228 | nativeBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 229 | nativeBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 230 | nativeContractAddress: address(mockERC721), 231 | nativeName: _DEFAULT_TOKEN_NAME, 232 | nativeSymbol: _DEFAULT_SYMBOL, 233 | nativeTokenURI: _DEFAULT_TOKEN_URI, 234 | contractNonce: 1 235 | }); 236 | 237 | bytes memory message = erc721Bridge.encodeMintBridgeNFTData( 238 | IERC721Bridge.MintBridgeNFTData(address(mockERC721), _DEFAULT_RECIPIENT, tokenId) 239 | ); 240 | 241 | vm.prank(MOCK_TELEPORTER_MESSENGER_ADDRESS); 242 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 243 | emit MintBridgeNFT(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 244 | _setUpExpectedMintCall(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 245 | erc721Bridge.receiveTeleporterMessage(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, message); 246 | 247 | // Check the values of the newly created ERC721. 248 | BridgeNFT newBridgeNFT = BridgeNFT(bridgeNFTAddress); 249 | assertEq(_DEFAULT_TOKEN_NAME, newBridgeNFT.name()); 250 | assertEq(_DEFAULT_SYMBOL, newBridgeNFT.symbol()); 251 | assertEq(_DEFAULT_OTHER_CHAIN_ID, newBridgeNFT.nativeBlockchainID()); 252 | assertEq(_DEFAULT_OTHER_BRIDGE_ADDRESS, newBridgeNFT.nativeBridge()); 253 | assertEq(address(mockERC721), newBridgeNFT.nativeAsset()); 254 | 255 | // Check that the NFT was minted to the recipient on the bridge chain. 256 | assertEq(_DEFAULT_RECIPIENT, newBridgeNFT.ownerOf(tokenId)); 257 | } 258 | 259 | function testNewBridgeNFTBurnNotApproved() public { 260 | uint256 tokenId = 1; 261 | 262 | // Setup BridgeNFT contract and mint the token to the default recipient on the bridged chain. 263 | address bridgeNFTAddress = _setupBridgeNFT({ 264 | nativeBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 265 | nativeBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 266 | nativeContractAddress: address(mockERC721), 267 | nativeName: _DEFAULT_TOKEN_NAME, 268 | nativeSymbol: _DEFAULT_SYMBOL, 269 | nativeTokenURI: _DEFAULT_TOKEN_URI, 270 | contractNonce: 1 271 | }); 272 | 273 | bytes memory message = erc721Bridge.encodeMintBridgeNFTData( 274 | IERC721Bridge.MintBridgeNFTData(address(mockERC721), _DEFAULT_RECIPIENT, tokenId) 275 | ); 276 | 277 | vm.prank(MOCK_TELEPORTER_MESSENGER_ADDRESS); 278 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 279 | emit MintBridgeNFT(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 280 | _setUpExpectedMintCall(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 281 | erc721Bridge.receiveTeleporterMessage(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, message); 282 | 283 | assertEq(_DEFAULT_RECIPIENT, BridgeNFT(bridgeNFTAddress).ownerOf(tokenId)); 284 | 285 | vm.expectRevert("BridgeNFT: caller is not token owner or approved"); 286 | // Call bridgeToken on the ERC721Bridge contract to burn and transfer the token without approving the bridge contract to manage the token. 287 | vm.prank(_DEFAULT_RECIPIENT); 288 | erc721Bridge.bridgeToken( 289 | _DEFAULT_OTHER_CHAIN_ID, 290 | _DEFAULT_OTHER_BRIDGE_ADDRESS, 291 | bridgeNFTAddress, 292 | _DEFAULT_RECIPIENT, 293 | tokenId, 294 | address(0), 295 | 0 296 | ); 297 | } 298 | 299 | function testNewBridgeNFTBurnInvalidTokenID() public { 300 | // Setup BridgeNFT contract and mint the token to the default recipient on the bridged chain. 301 | address bridgeNFTAddress = _setupBridgeNFT({ 302 | nativeBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 303 | nativeBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 304 | nativeContractAddress: address(mockERC721), 305 | nativeName: _DEFAULT_TOKEN_NAME, 306 | nativeSymbol: _DEFAULT_SYMBOL, 307 | nativeTokenURI: _DEFAULT_TOKEN_URI, 308 | contractNonce: 1 309 | }); 310 | 311 | vm.expectRevert(IERC721Bridge.InvalidTokenId.selector); 312 | // Call bridgeToken on the ERC721Bridge contract to burn and transfer a token that does not exist on the bridged chain. 313 | vm.prank(_DEFAULT_RECIPIENT); 314 | erc721Bridge.bridgeToken( 315 | _DEFAULT_OTHER_CHAIN_ID, 316 | _DEFAULT_OTHER_BRIDGE_ADDRESS, 317 | bridgeNFTAddress, 318 | _DEFAULT_RECIPIENT, 319 | 1234, 320 | address(0), 321 | 0 322 | ); 323 | } 324 | 325 | function testNewBridgeNFTBurnOnTransfer() public { 326 | uint256 tokenId = 1; 327 | 328 | // Setup BridgeNFT contract and mint the token to the default recipient on the bridged chain. 329 | address bridgeNFTAddress = _setupBridgeNFT({ 330 | nativeBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 331 | nativeBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 332 | nativeContractAddress: address(mockERC721), 333 | nativeName: _DEFAULT_TOKEN_NAME, 334 | nativeSymbol: _DEFAULT_SYMBOL, 335 | nativeTokenURI: _DEFAULT_TOKEN_URI, 336 | contractNonce: 1 337 | }); 338 | 339 | bytes memory message = erc721Bridge.encodeMintBridgeNFTData( 340 | IERC721Bridge.MintBridgeNFTData(address(mockERC721), _DEFAULT_RECIPIENT, tokenId) 341 | ); 342 | 343 | vm.prank(MOCK_TELEPORTER_MESSENGER_ADDRESS); 344 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 345 | emit MintBridgeNFT(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 346 | _setUpExpectedMintCall(bridgeNFTAddress, _DEFAULT_RECIPIENT, tokenId); 347 | erc721Bridge.receiveTeleporterMessage(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, message); 348 | 349 | assertEq(_DEFAULT_RECIPIENT, BridgeNFT(bridgeNFTAddress).ownerOf(tokenId)); 350 | 351 | // Approve OTHER BRIDGE ADDRESS to manage the bridged token. 352 | vm.prank(_DEFAULT_RECIPIENT); 353 | BridgeNFT(bridgeNFTAddress).approve(address(erc721Bridge), tokenId); 354 | 355 | // Call bridgeToken on the ERC721Bridge contract to burn and transfer the token on the bridged chain. 356 | vm.expectCall(bridgeNFTAddress, abi.encodeWithSignature("burn(uint256)", tokenId)); 357 | _setupMockTransferBridgeNFTCall( 358 | _DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721), _DEFAULT_RECIPIENT, tokenId 359 | ); 360 | // Expect a Transfer event to be emitted from the BridgeNFT contract. 361 | vm.expectEmit(true, true, true, true, bridgeNFTAddress); 362 | emit Transfer(_DEFAULT_RECIPIENT, address(0), tokenId); 363 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 364 | emit BridgeToken( 365 | bridgeNFTAddress, 366 | _DEFAULT_OTHER_CHAIN_ID, 367 | _MOCK_MESSAGE_ID, 368 | _DEFAULT_OTHER_BRIDGE_ADDRESS, 369 | _DEFAULT_RECIPIENT, 370 | tokenId 371 | ); 372 | 373 | vm.prank(_DEFAULT_RECIPIENT); 374 | erc721Bridge.bridgeToken( 375 | _DEFAULT_OTHER_CHAIN_ID, 376 | _DEFAULT_OTHER_BRIDGE_ADDRESS, 377 | bridgeNFTAddress, 378 | _DEFAULT_RECIPIENT, 379 | tokenId, 380 | address(0), 381 | 0 382 | ); 383 | 384 | // Check the NFT was transferred to the zero address on the bridged chain. 385 | // ownerOf will revert if the token does not exist. 386 | vm.expectRevert("ERC721: invalid token ID"); 387 | BridgeNFT(bridgeNFTAddress).ownerOf(tokenId); 388 | } 389 | 390 | function testUnlockTokenOnTransfer() public { 391 | uint256 tokenId = 1; 392 | 393 | // Mint the token to the default recipient. 394 | // Approve the bridge contract to manage the token. 395 | vm.startPrank(_DEFAULT_RECIPIENT); 396 | mockERC721.mint(tokenId); 397 | mockERC721.approve(address(erc721Bridge), tokenId); 398 | vm.stopPrank(); 399 | 400 | // Bridge the NFT. 401 | _setUpMockERC721ContractValues(address(mockERC721)); 402 | _submitCreateBridgeERC721(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721)); 403 | _setUpExpectedTransferFromCall(address(mockERC721), _DEFAULT_RECIPIENT, address(erc721Bridge), tokenId); 404 | _submitBridgeERC721( 405 | _DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, address(mockERC721), _DEFAULT_RECIPIENT, tokenId 406 | ); 407 | 408 | // Check that the NFT was transferred to the recipient on the bridge chain. 409 | assertEq(address(erc721Bridge), mockERC721.ownerOf(tokenId)); 410 | 411 | // Recieve Transfer teleporter message and unlock the NFT 412 | bytes memory transferMessage = erc721Bridge.encodeTransferBridgeNFTData( 413 | IERC721Bridge.TransferBridgeNFTData( 414 | _MOCK_BLOCKCHAIN_ID, address(erc721Bridge), address(mockERC721), _DEFAULT_RECIPIENT, tokenId 415 | ) 416 | ); 417 | 418 | vm.prank(MOCK_TELEPORTER_MESSENGER_ADDRESS); 419 | _setUpExpectedTransferFromCall(address(mockERC721), address(erc721Bridge), _DEFAULT_RECIPIENT, tokenId); 420 | erc721Bridge.receiveTeleporterMessage(_DEFAULT_OTHER_CHAIN_ID, _DEFAULT_OTHER_BRIDGE_ADDRESS, transferMessage); 421 | 422 | // Check that the NFT was transferred to the recipient on the bridge chain. 423 | assertEq(_DEFAULT_RECIPIENT, mockERC721.ownerOf(tokenId)); 424 | } 425 | 426 | function testZeroTeleporterRegistryAddress() public { 427 | vm.expectRevert("TeleporterUpgradeable: zero teleporter registry address"); 428 | new ERC721Bridge(address(0)); 429 | } 430 | 431 | function _initMockTeleporterRegistry() internal { 432 | vm.mockCall( 433 | MOCK_TELEPORTER_REGISTRY_ADDRESS, 434 | abi.encodeWithSelector(TeleporterRegistry(MOCK_TELEPORTER_REGISTRY_ADDRESS).latestVersion.selector), 435 | abi.encode(1) 436 | ); 437 | 438 | vm.mockCall( 439 | MOCK_TELEPORTER_REGISTRY_ADDRESS, 440 | abi.encodeWithSelector( 441 | TeleporterRegistry.getVersionFromAddress.selector, (MOCK_TELEPORTER_MESSENGER_ADDRESS) 442 | ), 443 | abi.encode(1) 444 | ); 445 | 446 | vm.mockCall( 447 | MOCK_TELEPORTER_REGISTRY_ADDRESS, 448 | abi.encodeWithSelector(TeleporterRegistry.getAddressFromVersion.selector, (1)), 449 | abi.encode(MOCK_TELEPORTER_MESSENGER_ADDRESS) 450 | ); 451 | 452 | vm.mockCall( 453 | MOCK_TELEPORTER_REGISTRY_ADDRESS, 454 | abi.encodeWithSelector(TeleporterRegistry.getLatestTeleporter.selector), 455 | abi.encode(ITeleporterMessenger(MOCK_TELEPORTER_MESSENGER_ADDRESS)) 456 | ); 457 | } 458 | 459 | function _setUpExpectedMintCall(address nftContract, address recipient, uint256 tokenId) private { 460 | vm.expectCall(nftContract, abi.encodeCall(BridgeNFT.mint, (recipient, tokenId))); 461 | } 462 | 463 | function _setUpExpectedTransferFromCall(address nftContract, address from, address to, uint256 tokenId) private { 464 | vm.expectCall( 465 | nftContract, abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", from, to, tokenId) 466 | ); 467 | } 468 | 469 | // Calls submitCreateBridgeERC721 of the test's ERC721Bridge instance to add the specified 470 | // token to be able to be sent to the specified remote bridge. Checks that the expected 471 | // call to the Teleporter contract is made and that the expected event is emitted. This is 472 | // required before attempting to call bridgeTokens for the given token and bridge. 473 | function _submitCreateBridgeERC721( 474 | bytes32 destinationBlockchainID, 475 | address destinationBridgeAddress, 476 | address nativeContractAddress 477 | ) private { 478 | ExampleERC721 nativeToken = ExampleERC721(nativeContractAddress); 479 | 480 | TeleporterMessageInput memory expectedMessageInput = TeleporterMessageInput({ 481 | destinationBlockchainID: destinationBlockchainID, 482 | destinationAddress: destinationBridgeAddress, 483 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 484 | requiredGasLimit: erc721Bridge.CREATE_BRIDGE_TOKENS_REQUIRED_GAS(), 485 | allowedRelayerAddresses: new address[](0), 486 | message: erc721Bridge.encodeCreateBridgeNFTData( 487 | IERC721Bridge.CreateBridgeNFTData( 488 | nativeContractAddress, nativeToken.name(), nativeToken.symbol(), nativeToken.baseUri() 489 | ) 490 | ) 491 | }); 492 | 493 | vm.mockCall( 494 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 495 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)), 496 | abi.encode(_MOCK_MESSAGE_ID) 497 | ); 498 | vm.expectCall( 499 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 500 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)) 501 | ); 502 | 503 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 504 | emit SubmitCreateBridgeNFT( 505 | destinationBlockchainID, destinationBridgeAddress, address(nativeToken), _MOCK_MESSAGE_ID 506 | ); 507 | 508 | erc721Bridge.submitCreateBridgeNFT({ 509 | destinationBlockchainID: destinationBlockchainID, 510 | destinationBridgeAddress: destinationBridgeAddress, 511 | nativeContract: nativeToken, 512 | messageFeeAsset: address(0), 513 | messageFeeAmount: 0 514 | }); 515 | } 516 | 517 | // Calls bridgeERC721 of the test's ERC721Bridge instance to bridge the specified token to the 518 | // specified remote bridge. Checks that the expected call to the Teleporter contract is 519 | // made and that the expected event is emitted. 520 | function _submitBridgeERC721( 521 | bytes32 destinationBlockchainID, 522 | address destinationBridgeAddress, 523 | address nativeContractAddress, 524 | address recipient, 525 | uint256 tokenId 526 | ) private { 527 | _setupMockMintBridgeNFTCall( 528 | destinationBlockchainID, destinationBridgeAddress, nativeContractAddress, recipient, tokenId 529 | ); 530 | 531 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 532 | emit BridgeToken( 533 | nativeContractAddress, 534 | destinationBlockchainID, 535 | _MOCK_MESSAGE_ID, 536 | destinationBridgeAddress, 537 | recipient, 538 | tokenId 539 | ); 540 | 541 | vm.prank(_DEFAULT_RECIPIENT); 542 | erc721Bridge.bridgeToken({ 543 | destinationBlockchainID: _DEFAULT_OTHER_CHAIN_ID, 544 | destinationBridgeAddress: _DEFAULT_OTHER_BRIDGE_ADDRESS, 545 | tokenContractAddress: nativeContractAddress, 546 | recipient: _DEFAULT_RECIPIENT, 547 | tokenId: tokenId, 548 | messageFeeAsset: address(0), 549 | messageFeeAmount: 0 550 | }); 551 | } 552 | 553 | function _setupMockMintBridgeNFTCall( 554 | bytes32 destinationBlockchainID, 555 | address destinationBridgeAddress, 556 | address nativeContractAddress, 557 | address recipient, 558 | uint256 tokenId 559 | ) private { 560 | TeleporterMessageInput memory expectedMessageInput = TeleporterMessageInput({ 561 | destinationBlockchainID: destinationBlockchainID, 562 | destinationAddress: destinationBridgeAddress, 563 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 564 | requiredGasLimit: erc721Bridge.MINT_BRIDGE_TOKENS_REQUIRED_GAS(), 565 | allowedRelayerAddresses: new address[](0), 566 | message: erc721Bridge.encodeMintBridgeNFTData( 567 | IERC721Bridge.MintBridgeNFTData(nativeContractAddress, recipient, tokenId) 568 | ) 569 | }); 570 | 571 | vm.mockCall( 572 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 573 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)), 574 | abi.encode(_MOCK_MESSAGE_ID) 575 | ); 576 | vm.expectCall( 577 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 578 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)) 579 | ); 580 | } 581 | 582 | function _setupMockTransferBridgeNFTCall( 583 | bytes32 destinationBlockchainID, 584 | address destinationBridgeAddress, 585 | address nativeContractAddress, 586 | address recipient, 587 | uint256 tokenId 588 | ) private { 589 | TeleporterMessageInput memory expectedMessageInput = TeleporterMessageInput({ 590 | destinationBlockchainID: destinationBlockchainID, 591 | destinationAddress: destinationBridgeAddress, 592 | feeInfo: TeleporterFeeInfo({feeTokenAddress: address(0), amount: 0}), 593 | requiredGasLimit: erc721Bridge.TRANSFER_BRIDGE_TOKENS_REQUIRED_GAS(), 594 | allowedRelayerAddresses: new address[](0), 595 | message: erc721Bridge.encodeTransferBridgeNFTData( 596 | IERC721Bridge.TransferBridgeNFTData( 597 | destinationBlockchainID, destinationBridgeAddress, nativeContractAddress, recipient, tokenId 598 | ) 599 | ) 600 | }); 601 | 602 | vm.mockCall( 603 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 604 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)), 605 | abi.encode(_MOCK_MESSAGE_ID) 606 | ); 607 | vm.expectCall( 608 | MOCK_TELEPORTER_MESSENGER_ADDRESS, 609 | abi.encodeCall(ITeleporterMessenger.sendCrossChainMessage, (expectedMessageInput)) 610 | ); 611 | } 612 | 613 | function _setupBridgeNFT( 614 | bytes32 nativeBlockchainID, 615 | address nativeBridgeAddress, 616 | address nativeContractAddress, 617 | string memory nativeName, 618 | string memory nativeSymbol, 619 | string memory nativeTokenURI, 620 | uint8 contractNonce 621 | ) private returns (address) { 622 | address expectedBridgeNFTAddress = _deriveExpectedContractAddress(address(erc721Bridge), contractNonce); 623 | bytes memory message = erc721Bridge.encodeCreateBridgeNFTData( 624 | IERC721Bridge.CreateBridgeNFTData(nativeContractAddress, nativeName, nativeSymbol, nativeTokenURI) 625 | ); 626 | vm.prank(MOCK_TELEPORTER_MESSENGER_ADDRESS); 627 | vm.expectEmit(true, true, true, true, address(erc721Bridge)); 628 | emit CreateBridgeNFT(nativeBlockchainID, nativeBridgeAddress, nativeContractAddress, expectedBridgeNFTAddress); 629 | erc721Bridge.receiveTeleporterMessage(nativeBlockchainID, nativeBridgeAddress, message); 630 | 631 | return expectedBridgeNFTAddress; 632 | } 633 | 634 | function _setUpMockERC721ContractValues(address nftContract) private { 635 | ExampleERC721 token = ExampleERC721(nftContract); 636 | vm.mockCall(nftContract, abi.encodeCall(token.name, ()), abi.encode(_DEFAULT_TOKEN_NAME)); 637 | vm.expectCall(nftContract, abi.encodeCall(token.name, ())); 638 | vm.mockCall(nftContract, abi.encodeCall(token.symbol, ()), abi.encode(_DEFAULT_SYMBOL)); 639 | vm.expectCall(nftContract, abi.encodeCall(token.symbol, ())); 640 | } 641 | 642 | function _deriveExpectedContractAddress(address creator, uint8 nonce) private pure returns (address) { 643 | // Taken from https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed 644 | // We must use encodePacked rather than encode so that the parameters are not padded with extra zeros. 645 | return 646 | address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), creator, bytes1(nonce)))))); 647 | } 648 | 649 | function _formatERC721BridgeErrorMessage(string memory errorMessage) private pure returns (bytes memory) { 650 | return bytes(string.concat("ERC721Bridge: ", errorMessage)); 651 | } 652 | } 653 | --------------------------------------------------------------------------------