diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml deleted file mode 100644 index 2c841b1e436..00000000000 --- a/.github/workflows/slither.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Slither Analysis - -on: - workflow_dispatch: - pull_request: - paths: - - "packages/protocol/**" - push: - branches: - - main - -jobs: - analyze: - runs-on: [taiko-runner] - steps: - - uses: actions/checkout@v4 - - - name: Install pnpm dependencies - uses: ./.github/actions/install-pnpm-dependencies - - # Workaround to prevent slither-action from trying to install deps with `package.json`, - # and since there is no lockfile it defaults `npm install` which fails because we use pnpm. - # See: https://github.com/crytic/slither-action/issues/44#issuecomment-1931104731 - - name: Remove package.json - run: rm packages/protocol/package.json - - - name: Run Slither - uses: crytic/slither-action@v0.3.2 - id: slither - with: - target: packages/protocol - sarif: results.sarif - fail-on: config - slither-config: packages/protocol/slither.config.json - slither-args: --triage-database packages/protocol/slither.db.json - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.slither.outputs.sarif }} diff --git a/Dockerfile b/Dockerfile index 67e3abdafea..a1d52db69d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ +ARG PACKAGE=eventindexer + FROM golang:1.21.0 as builder -ARG PACKAGE=eventindexer +ARG PACKAGE RUN apt install git curl @@ -12,16 +14,17 @@ COPY . . RUN go mod download -WORKDIR /taiko-mono/packages/$PACKAGE +WORKDIR /taiko-mono/packages/${PACKAGE} -RUN CGO_ENABLED=0 GOOS=linux go build -o /taiko-mono/packages/$PACKAGE/bin/${PACKAGE} /taiko-mono/packages/$PACKAGE/cmd/main.go +RUN CGO_ENABLED=0 GOOS=linux go build -o /taiko-mono/packages/${PACKAGE}/bin/${PACKAGE} /taiko-mono/packages/${PACKAGE}/cmd/main.go FROM alpine:latest ARG PACKAGE +ENV PACKAGE=${PACKAGE} RUN apk add --no-cache ca-certificates -COPY --from=builder /taiko-mono/packages/$PACKAGE/bin/$PACKAGE /usr/local/bin/ +COPY --from=builder /taiko-mono/packages/${PACKAGE}/bin/${PACKAGE} /usr/local/bin/ -ENTRYPOINT ["$PACKAGE"] \ No newline at end of file +ENTRYPOINT /usr/local/bin/${PACKAGE} \ No newline at end of file diff --git a/packages/bridge-ui/.env.example b/packages/bridge-ui/.env.example index e7105a3e289..a17db647151 100644 --- a/packages/bridge-ui/.env.example +++ b/packages/bridge-ui/.env.example @@ -38,3 +38,7 @@ export PUBLIC_SLOW_L1_BRIDGING_WARNING=false # Fees export PUBLIC_FEE_MULTIPLIER= + +# APIs +export MORALIS_PROJECT_ID="" +export MORALIS_API_KEY="" diff --git a/packages/bridge-ui/.eslintignore b/packages/bridge-ui/.eslintignore index c26e640c61f..1c8b757654e 100644 --- a/packages/bridge-ui/.eslintignore +++ b/packages/bridge-ui/.eslintignore @@ -18,3 +18,5 @@ package-lock.json yarn.lock src/generated/* + +.vercel/** diff --git a/packages/bridge-ui/.prettierignore b/packages/bridge-ui/.prettierignore index d31e3142a9d..7f5b8674294 100644 --- a/packages/bridge-ui/.prettierignore +++ b/packages/bridge-ui/.prettierignore @@ -16,3 +16,4 @@ CHANGELOG.md pnpm-lock.yaml package-lock.json yarn.lock +.vercel/** \ No newline at end of file diff --git a/packages/bridge-ui/package.json b/packages/bridge-ui/package.json index 646feb1efdc..50b984eab59 100644 --- a/packages/bridge-ui/package.json +++ b/packages/bridge-ui/package.json @@ -47,7 +47,7 @@ "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.3", "svelte": "^4.2.15", - "svelte-check": "^3.7.0", + "svelte-check": "^3.7.1", "tailwindcss": "^3.4.3", "ts-morph": "^19.0.0", "tslib": "^2.6.2", @@ -60,6 +60,7 @@ }, "type": "module", "dependencies": { + "@moralisweb3/common-evm-utils": "^2.26.1", "@wagmi/connectors": "^4.3.1", "@wagmi/core": "^2.8.1", "@walletconnect/ethereum-provider": "^2.12.2", @@ -70,6 +71,7 @@ "buffer": "^6.0.3", "debug": "^4.3.4", "events": "^3.3.0", + "moralis": "^2.26.1", "object-hash": "^3.0.0", "svelte-i18n": "^4.0.0", "viem": "^2.9.29" diff --git a/packages/bridge-ui/src/app.config.ts b/packages/bridge-ui/src/app.config.ts index 65fe1b198e7..364ba33e1ed 100644 --- a/packages/bridge-ui/src/app.config.ts +++ b/packages/bridge-ui/src/app.config.ts @@ -49,3 +49,10 @@ export const ipfsConfig = { gatewayTimeout: 200, overallTimeout: 5000, }; + +export const moralisApiConfig = { + limit: 10, + format: 'decimal', + excludeSpam: true, + mediaItems: false, +}; diff --git a/packages/bridge-ui/src/components/Bridge/FungibleBridgeComponents/ImportStep/TokenInput/TokenInput.svelte b/packages/bridge-ui/src/components/Bridge/FungibleBridgeComponents/ImportStep/TokenInput/TokenInput.svelte index 8f81bbb7da8..f70cea6767a 100644 --- a/packages/bridge-ui/src/components/Bridge/FungibleBridgeComponents/ImportStep/TokenInput/TokenInput.svelte +++ b/packages/bridge-ui/src/components/Bridge/FungibleBridgeComponents/ImportStep/TokenInput/TokenInput.svelte @@ -231,7 +231,7 @@ - +
@@ -242,9 +242,9 @@ >{$t('recipient.label')}
{:else if showInsufficientBalanceAlert} - + {:else if showInvalidTokenAlert} - + {:else} {/if} diff --git a/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ImportStep.svelte b/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ImportStep.svelte index 08d594af9aa..56a8fc13a7c 100644 --- a/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ImportStep.svelte +++ b/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ImportStep.svelte @@ -16,18 +16,26 @@ import { selectedImportMethod } from './state'; let foundNFTs: NFT[] = []; + + // States + let scanning = false; let canProceed = false; export let validating = false; - const scanForNFTs = async () => { + const nextPage = async () => { + await scanForNFTs(false); + }; + + const scanForNFTs = async (refresh: boolean) => { scanning = true; $selectedNFTs = []; const accountAddress = $account?.address; const srcChainId = $srcChain?.id; const destChainId = $destChain?.id; if (!accountAddress || !srcChainId || !destChainId) return; - const nftsFromAPIs = await fetchNFTs(accountAddress, srcChainId); + const nftsFromAPIs = await fetchNFTs({ address: accountAddress, chainId: srcChainId, refresh }); + foundNFTs = nftsFromAPIs.nfts; scanning = false; @@ -47,9 +55,6 @@ reset(); }; - // States - let scanning = false; - $: canImport = ($account?.isConnected && $srcChain?.id && $destChain && !scanning) || false; $: { @@ -74,9 +79,9 @@ {#if $selectedImportMethod === ImportMethod.MANUAL} {:else if $selectedImportMethod === ImportMethod.SCAN} - + scanForNFTs(true)} {nextPage} bind:foundNFTs bind:canProceed /> {:else} - + scanForNFTs(false)} /> {/if} diff --git a/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ScannedImport.svelte b/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ScannedImport.svelte index 3743a9a6c56..0c6e44c1490 100644 --- a/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ScannedImport.svelte +++ b/packages/bridge-ui/src/components/Bridge/NFTBridgeComponents/ImportStep/ScannedImport.svelte @@ -6,7 +6,7 @@ import { enteredAmount, selectedNFTs, tokenBalance } from '$components/Bridge/state'; import { ImportMethod } from '$components/Bridge/types'; import { ActionButton, Button } from '$components/Button'; - import { IconFlipper } from '$components/Icon'; + import { Icon, IconFlipper } from '$components/Icon'; import RotatingIcon from '$components/Icon/RotatingIcon.svelte'; import { NFTDisplay } from '$components/NFTs'; import { NFTView } from '$components/NFTs/types'; @@ -14,7 +14,8 @@ import { selectedImportMethod } from './state'; - export let scanForNFTs: () => Promise; + export let refresh: () => Promise; + export let nextPage: () => Promise; export let foundNFTs: NFT[] = []; @@ -22,12 +23,28 @@ let nftView: NFTView = NFTView.LIST; let scanning = false; + let hasMoreNFTs = true; let tokenAmountInput: TokenAmountInput; - function onScanClick() { + let previousNFTs: NFT[] = []; + const handleNextPage = () => { + previousNFTs = foundNFTs; scanning = true; - scanForNFTs().finally(() => { + + nextPage().finally(() => { + scanning = false; + }); + + if (previousNFTs.length === foundNFTs.length) { + hasMoreNFTs = false; + } + }; + + function onRefreshClick() { + scanning = true; + hasMoreNFTs = true; + refresh().finally(() => { scanning = false; }); } @@ -81,7 +98,7 @@ type="neutral" shape="circle" class="bg-neutral rounded-full w-[28px] h-[28px] border-none" - on:click={onScanClick}> + on:click={onRefreshClick}> @@ -97,6 +114,21 @@
+
+ +
{#if nftHasAmount} diff --git a/packages/bridge-ui/src/components/ChainSelectors/SelectorDialogs/ChainsDialog.svelte b/packages/bridge-ui/src/components/ChainSelectors/SelectorDialogs/ChainsDialog.svelte index 2e1843a53d2..6317de54fa3 100644 --- a/packages/bridge-ui/src/components/ChainSelectors/SelectorDialogs/ChainsDialog.svelte +++ b/packages/bridge-ui/src/components/ChainSelectors/SelectorDialogs/ChainsDialog.svelte @@ -62,10 +62,10 @@
  • -
    - - - -
    + + +
    diff --git a/packages/bridge-ui/src/i18n/en.json b/packages/bridge-ui/src/i18n/en.json index 1e7b0727106..7b76320727f 100644 --- a/packages/bridge-ui/src/i18n/en.json +++ b/packages/bridge-ui/src/i18n/en.json @@ -362,7 +362,9 @@ }, "paginator": { "of": "of", - "page": "Page" + "page": "Page", + "more": "Fetch more...", + "everything_loaded": "Everything loaded" }, "paused_modal": { "description": "The bridge is currently not available. Follow our official communication channels for more information. ", diff --git a/packages/bridge-ui/src/libs/bridge/fetchNFTs.ts b/packages/bridge-ui/src/libs/bridge/fetchNFTs.ts index af2a75504c9..781b89643ed 100644 --- a/packages/bridge-ui/src/libs/bridge/fetchNFTs.ts +++ b/packages/bridge-ui/src/libs/bridge/fetchNFTs.ts @@ -1,35 +1,86 @@ import type { Address } from 'viem'; +import { isL2Chain } from '$libs/chain'; import { eventIndexerApiServices } from '$libs/eventIndexer/initEventIndexer'; import { type NFT, TokenType } from '$libs/token'; -import { checkOwnershipOfNFTs } from '$libs/token/checkOwnership'; import { fetchNFTImageUrl } from '$libs/token/fetchNFTImageUrl'; import { getTokenWithInfoFromAddress } from '$libs/token/getTokenWithInfoFromAddress'; import { getLogger } from '$libs/util/logger'; +import type { FetchNftArgs } from '$nftAPI/infrastructure/types/common'; const log = getLogger('bridge:fetchNFTs'); -function deduplicateNFTs(nftArrays: NFT[][]): NFT[] { - const nftMap: Map = new Map(); - nftArrays.flat().forEach((nft) => { - Object.entries(nft.addresses).forEach(([chainID, address]) => { - const uniqueKey = `${address}-${chainID}-${nft.tokenId}`; - - if (!nftMap.has(uniqueKey)) { - nftMap.set(uniqueKey, nft); - } - }); +export const fetchNFTs = async ({ + address: userAddress, + chainId: srcChainId, + refresh, +}: FetchNftArgs): Promise<{ nfts: NFT[]; error: Error | null }> => { + let nfts: NFT[] = []; + try { + if (isL2Chain(srcChainId)) { + // Todo: replace with a third party service once available + // right now we have to use our own indexer for L2 + nfts = await fetchL2NFTs({ userAddress, srcChainId, refresh }); + } else { + nfts = await fetchL1NFTs({ userAddress, srcChainId, refresh }); + } + + const promises = Promise.all( + nfts.map(async (nft: NFT) => { + const nftWithImage = await fetchNFTImageUrl(nft); + return nftWithImage; + }), + ); + + const nftsWithImage = await promises; + nfts = nftsWithImage; + return { nfts, error: null }; + } catch (error) { + console.error('Fetch error:', error); + return { nfts: [], error: new Error('') }; + } +}; + +const fetchL1NFTs = async ({ + userAddress, + srcChainId, + refresh, +}: { + userAddress: Address; + srcChainId: number; + refresh: boolean; +}) => { + log('fetching L1 NFTs', { userAddress, srcChainId, refresh }); + const moralisResponse = await fetch('/api/nft', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ address: userAddress, chainId: srcChainId, refresh }), }); - return Array.from(nftMap.values()); -} -export async function fetchNFTs( - userAddress: Address, - srcChainId: number, -): Promise<{ nfts: NFT[]; error: Error | null }> { - let error: Error | null = null; + if (moralisResponse.ok) { + const responseData = await moralisResponse.json(); + log('cursor', responseData); + const { nfts } = responseData; - // Fetch from all indexers + return nfts; + } else { + console.error('HTTP error:', moralisResponse.statusText); + return { nfts: [], error: new Error(moralisResponse.statusText) }; + } +}; + +const fetchL2NFTs = async ({ + userAddress, + srcChainId, + refresh, +}: { + userAddress: Address; + srcChainId: number; + refresh: boolean; +}) => { + log('fetching L2 NFTs', { userAddress, srcChainId, refresh }); const indexerPromises: Promise[] = eventIndexerApiServices.map(async (eventIndexerApiService) => { const { items: result } = await eventIndexerApiService.getAllNftsByAddressFromAPI(userAddress, BigInt(srcChainId), { page: 0, @@ -60,36 +111,22 @@ export async function fetchNFTs( nftArrays = await Promise.all(indexerPromises); } catch (e) { log('error fetching nfts from indexer services', e); - error = e as Error; + throw e; } + return deduplicateNFTs(nftArrays); +}; - // Deduplicate based on address and chainID - const deduplicatedNfts = deduplicateNFTs(nftArrays); - - // Fetch image for each NFT - const promises = Promise.all( - deduplicatedNfts.map(async (nft) => { - const nftWithImage = await fetchNFTImageUrl(nft); - return nftWithImage; - }), - ); - const nftsWithImage = await promises; +// Deduplicate based on address and chainID +function deduplicateNFTs(nftArrays: NFT[][]): NFT[] { + const nftMap: Map = new Map(); + nftArrays.flat().forEach((nft) => { + Object.entries(nft.addresses).forEach(([chainID, address]) => { + const uniqueKey = `${address}-${chainID}-${nft.tokenId}`; - // Double check the ownership - const ownsAllNfts = await checkOwnershipOfNFTs(nftsWithImage, userAddress, srcChainId); - log(`user ${userAddress} owns all NFTs:`, ownsAllNfts); - // filter out the NFTs that the user doesn't own - const filteredNfts = nftsWithImage.filter((nft) => { - const isOwned = ownsAllNfts.successfulOwnershipChecks.find((result) => result.tokenId === nft.tokenId); - return isOwned; + if (!nftMap.has(uniqueKey)) { + nftMap.set(uniqueKey, nft); + } + }); }); - - if (filteredNfts.length !== nftsWithImage.length) { - //TODO: handle this case differently? maybe show a warning to the user? - log(`found ${nftsWithImage.length - filteredNfts.length} tokens that the user doesn't own`); - } - - log(`found ${filteredNfts.length} unique NFTs from all indexers`, filteredNfts); - - return { nfts: filteredNfts, error }; + return Array.from(nftMap.values()); } diff --git a/packages/bridge-ui/src/libs/nft/domain/interfaces/INFTRepository.ts b/packages/bridge-ui/src/libs/nft/domain/interfaces/INFTRepository.ts new file mode 100644 index 00000000000..89f833a0837 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/domain/interfaces/INFTRepository.ts @@ -0,0 +1,6 @@ +import type { NFT } from '$nftAPI/domain/models/NFT'; +import type { FetchNftArgs } from '$nftAPI/infrastructure/types/common'; + +export interface INFTRepository { + findByAddress({ address, chainId, refresh }: FetchNftArgs): Promise; +} diff --git a/packages/bridge-ui/src/libs/nft/domain/models/NFT.ts b/packages/bridge-ui/src/libs/nft/domain/models/NFT.ts new file mode 100644 index 00000000000..29347d961ec --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/domain/models/NFT.ts @@ -0,0 +1,17 @@ +import type { Address } from 'viem'; + +import type { NFTMetadata, TokenType } from '$libs/token'; + +export interface NFT { + type: TokenType; + name: string; + symbol: string; + addresses: Record; + owner: Address; + imported?: boolean; + mintable?: boolean; + balance: string | number; + tokenId: number | string; + uri?: string; + metadata?: NFTMetadata; +} diff --git a/packages/bridge-ui/src/libs/nft/domain/services/NFTService.ts b/packages/bridge-ui/src/libs/nft/domain/services/NFTService.ts new file mode 100644 index 00000000000..d4171cd1e94 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/domain/services/NFTService.ts @@ -0,0 +1,20 @@ +import type { Address } from 'viem'; + +import type { INFTRepository } from '../interfaces/INFTRepository'; +import type { NFT } from '../models/NFT'; + +export class NFTService { + constructor(private repository: INFTRepository) {} + + async fetchNFTsByAddress({ + address, + chainId, + refresh, + }: { + address: Address; + chainId: number; + refresh: boolean; + }): Promise { + return await this.repository.findByAddress({ address, chainId, refresh }); + } +} diff --git a/packages/bridge-ui/src/libs/nft/infrastructure/api/MoralisNFTRepository.server.ts b/packages/bridge-ui/src/libs/nft/infrastructure/api/MoralisNFTRepository.server.ts new file mode 100644 index 00000000000..442500a6b41 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/infrastructure/api/MoralisNFTRepository.server.ts @@ -0,0 +1,88 @@ +import Moralis from 'moralis'; +import { type Address, zeroAddress } from 'viem'; + +import { moralisApiConfig } from '$config'; +import { MORALIS_API_KEY } from '$env/static/private'; +import type { INFTRepository } from '$nftAPI/domain/interfaces/INFTRepository'; +import type { NFT } from '$nftAPI/domain/models/NFT'; +import { mapToNFTFromMoralis } from '$nftAPI/infrastructure/mappers/nft/MoralisNFTMapper'; +import type { NFTApiData } from '$nftAPI/infrastructure/types/moralis'; + +import type { FetchNftArgs } from '../types/common'; + +class MoralisNFTRepository implements INFTRepository { + private static instance: MoralisNFTRepository; + private static isInitialized = false; + + private cursor: string; + private lastFetchedAddress: Address; + private hasFetchedAll: boolean; + private nfts: NFT[] = []; + + private constructor() { + if (!MoralisNFTRepository.isInitialized) { + Moralis.start({ apiKey: MORALIS_API_KEY }) + .then(() => { + MoralisNFTRepository.isInitialized = true; + }) + .catch(console.error); + } + + this.cursor = ''; + this.lastFetchedAddress = zeroAddress; + this.hasFetchedAll = false; + } + + public static getInstance(): MoralisNFTRepository { + if (!MoralisNFTRepository.instance) { + MoralisNFTRepository.instance = new MoralisNFTRepository(); + } + return MoralisNFTRepository.instance; + } + + async findByAddress({ address, chainId, refresh = false }: FetchNftArgs): Promise { + this.lastFetchedAddress = address; + if (refresh) { + this.reset(); + } + if (this.hasFetchedAll) { + return this.nfts; + } + + try { + const response = await Moralis.EvmApi.nft.getWalletNFTs({ + cursor: this.getCursor(address, refresh), + chain: chainId, + excludeSpam: moralisApiConfig.excludeSpam, + mediaItems: moralisApiConfig.mediaItems, + address: address, + limit: moralisApiConfig.limit, + }); + + this.cursor = response.pagination.cursor || ''; + this.hasFetchedAll = !this.cursor; // If there is no cursor, we have fetched all NFTs + + const mappedData = response.result.map((nft) => mapToNFTFromMoralis(nft as unknown as NFTApiData, chainId)); + this.nfts = [...this.nfts, ...mappedData]; + return this.nfts; + } catch (e) { + console.error('Failed to fetch NFTs from Moralis:', e); + return []; + } + } + + private reset(): void { + this.cursor = ''; + this.hasFetchedAll = false; + this.nfts = []; + } + + private getCursor(address: Address, refresh: boolean): string { + if (this.lastFetchedAddress !== address || refresh) { + return ''; + } + return this.cursor || ''; + } +} + +export default MoralisNFTRepository.getInstance(); diff --git a/packages/bridge-ui/src/libs/nft/infrastructure/mappers/nft/MoralisNFTMapper.ts b/packages/bridge-ui/src/libs/nft/infrastructure/mappers/nft/MoralisNFTMapper.ts new file mode 100644 index 00000000000..15ebabafdb9 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/infrastructure/mappers/nft/MoralisNFTMapper.ts @@ -0,0 +1,20 @@ +import type { Address } from 'viem'; + +import type { TokenType } from '$libs/token'; +import type { NFT } from '$nftAPI/domain/models/NFT'; +import type { NFTApiData } from '$nftAPI/infrastructure/types/moralis'; + +export function mapToNFTFromMoralis(apiData: NFTApiData, chainId: number): NFT { + return { + tokenId: apiData.tokenId, + uri: apiData.tokenUri, + owner: apiData.ownerOf as Address, + name: apiData.name, + symbol: apiData.symbol, + type: apiData.contractType as TokenType, + balance: apiData.amount, + addresses: { + [chainId]: apiData.tokenAddress as Address, + }, + }; +} diff --git a/packages/bridge-ui/src/libs/nft/infrastructure/types/common.ts b/packages/bridge-ui/src/libs/nft/infrastructure/types/common.ts new file mode 100644 index 00000000000..d22d3bd57b1 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/infrastructure/types/common.ts @@ -0,0 +1,7 @@ +import type { Address } from 'viem'; + +export type FetchNftArgs = { + address: Address; + chainId: number; + refresh: boolean; +}; diff --git a/packages/bridge-ui/src/libs/nft/infrastructure/types/moralis.ts b/packages/bridge-ui/src/libs/nft/infrastructure/types/moralis.ts new file mode 100644 index 00000000000..848e078a6c0 --- /dev/null +++ b/packages/bridge-ui/src/libs/nft/infrastructure/types/moralis.ts @@ -0,0 +1,21 @@ +import type { LimitNumberError } from 'ajv/dist/vocabularies/validation/limitNumber'; +import type { Address } from 'viem'; + +export interface NFTApiData { + tokenId: string | number; + contractType: string; + chain: LimitNumberError; + tokenUri: string; + tokenAddress: Address; + tokenHash: string; + metadata: string; + name: string; + symbol: string; + ownerOf: Address; + blockNumberMinted: bigint; + blockNumber: bigint; + lastMetadataSync: Date; + lastTokenUriSync: Date; + amount: number | string; + possibleSpam: boolean; +} diff --git a/packages/bridge-ui/src/routes/api/nft/+server.ts b/packages/bridge-ui/src/routes/api/nft/+server.ts new file mode 100644 index 00000000000..5b750397bf3 --- /dev/null +++ b/packages/bridge-ui/src/routes/api/nft/+server.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from '@sveltejs/kit'; + +import { NFTService } from '$nftAPI/domain/services/NFTService'; +import moralisRepository from '$nftAPI/infrastructure/api/MoralisNFTRepository.server'; + +const nftService = new NFTService(moralisRepository); + +export const POST: RequestHandler = async ({ request }) => { + try { + const { address, chainId, refresh } = await request.json(); + + const nfts = await nftService.fetchNFTsByAddress({ address, chainId, refresh }); + + return new Response(JSON.stringify({ nfts }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + console.error('Failed to fetch NFTs:', error); + return new Response(JSON.stringify({ error: 'Failed to retrieve NFT data' }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }); + } +}; diff --git a/packages/bridge-ui/tsconfig.json b/packages/bridge-ui/tsconfig.json index fd05dc9b2f1..cda36531bce 100644 --- a/packages/bridge-ui/tsconfig.json +++ b/packages/bridge-ui/tsconfig.json @@ -23,6 +23,7 @@ "$chainConfig": ["./src/generated/chainConfig.ts"], "$relayerConfig": ["./src/generated/relayerConfig.ts"], "$customToken": ["./src/generated/customTokenConfig.ts"], + "$nftAPI/*": ["./src/libs/nft/*"], "$mocks": ["./src/tests/mocks/index.ts"] }, diff --git a/packages/bridge-ui/vercel.json b/packages/bridge-ui/vercel.json deleted file mode 100644 index 2124409429c..00000000000 --- a/packages/bridge-ui/vercel.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "routes": [ - { - "src": "/[^.]+", - "dest": "/", - "status": 200 - } - ] -} diff --git a/packages/eventindexer/indexer/filter.go b/packages/eventindexer/indexer/filter.go index fe3f861e666..d7849d3efee 100644 --- a/packages/eventindexer/indexer/filter.go +++ b/packages/eventindexer/indexer/filter.go @@ -36,7 +36,7 @@ func filterFunc( err = i.saveTransitionProvedEvents(ctx, chainID, transitionProvedEvents) if err != nil { - return errors.Wrap(err, "i.saveBlockProvenEvents") + return errors.Wrap(err, "i.saveTransitionProvedEvents") } return nil @@ -198,17 +198,14 @@ func (i *Indexer) filter( ctx context.Context, filter FilterFunc, ) error { - header, err := i.ethClient.HeaderByNumber(ctx, nil) + endBlockID, err := i.ethClient.BlockNumber(ctx) if err != nil { - return errors.Wrap(err, "i.ethClient.HeaderByNumber") + return errors.Wrap(err, "i.ethClient.BlockNumber") } - // the end block is the latest header. - endBlockID := header.Number.Uint64() - slog.Info("getting batch of events", "startBlock", i.latestIndexedBlockNumber, - "endBlock", header.Number.Int64(), + "endBlock", endBlockID, "batchSize", i.blockBatchSize, ) diff --git a/packages/eventindexer/indexer/index_nft_transfers.go b/packages/eventindexer/indexer/index_nft_transfers.go index 385d7441e55..d27a35fdce1 100644 --- a/packages/eventindexer/indexer/index_nft_transfers.go +++ b/packages/eventindexer/indexer/index_nft_transfers.go @@ -116,35 +116,34 @@ func (i *Indexer) saveERC721Transfer(ctx context.Context, chainID *big.Int, vLog ) // increment To address's balance - - _, err := i.nftBalanceRepo.IncreaseBalance(ctx, eventindexer.UpdateNFTBalanceOpts{ + // decrement From address's balance + increaseOpts := eventindexer.UpdateNFTBalanceOpts{ ChainID: chainID.Int64(), Address: to, TokenID: tokenID, ContractAddress: vLog.Address.Hex(), ContractType: "ERC721", Amount: 1, // ERC721 is always 1 - }) - if err != nil { - return err } + decreaseOpts := eventindexer.UpdateNFTBalanceOpts{} - // decrement From address's balance // ignore zero address since that is usually the "mint" if from != ZeroAddress.Hex() { - _, err = i.nftBalanceRepo.SubtractBalance(ctx, eventindexer.UpdateNFTBalanceOpts{ + decreaseOpts = eventindexer.UpdateNFTBalanceOpts{ ChainID: chainID.Int64(), Address: from, TokenID: tokenID, ContractAddress: vLog.Address.Hex(), ContractType: "ERC721", Amount: 1, // ERC721 is always 1 - }) - if err != nil { - return err } } + _, _, err := i.nftBalanceRepo.IncreaseAndDecreaseBalancesInTx(ctx, increaseOpts, decreaseOpts) + if err != nil { + return err + } + return nil } @@ -178,7 +177,7 @@ func (i *Indexer) saveERC1155Transfer(ctx context.Context, chainID *big.Int, vLo } transfers = append(transfers, t) - } else if vLog.Topics[0].Hex() != transferBatchSignatureHash.Hex() { + } else if vLog.Topics[0].Hex() == transferBatchSignatureHash.Hex() { var t []transfer err = erc1155ABI.UnpackIntoInterface(&t, "TransferBatch", []byte(vLog.Data)) @@ -200,32 +199,32 @@ func (i *Indexer) saveERC1155Transfer(ctx context.Context, chainID *big.Int, vLo // increment To address's balance for _, transfer := range transfers { - _, err = i.nftBalanceRepo.IncreaseBalance(ctx, eventindexer.UpdateNFTBalanceOpts{ + increaseOpts := eventindexer.UpdateNFTBalanceOpts{ ChainID: chainID.Int64(), Address: to, TokenID: transfer.ID.Int64(), ContractAddress: vLog.Address.Hex(), ContractType: "ERC1155", Amount: transfer.Amount.Int64(), - }) - if err != nil { - return err } + decreaseOpts := eventindexer.UpdateNFTBalanceOpts{} if from != ZeroAddress.Hex() { // decrement From address's balance - _, err = i.nftBalanceRepo.SubtractBalance(ctx, eventindexer.UpdateNFTBalanceOpts{ + decreaseOpts = eventindexer.UpdateNFTBalanceOpts{ ChainID: chainID.Int64(), Address: from, TokenID: transfer.ID.Int64(), ContractAddress: vLog.Address.Hex(), ContractType: "ERC1155", Amount: transfer.Amount.Int64(), - }) - if err != nil { - return err } } + + _, _, err = i.nftBalanceRepo.IncreaseAndDecreaseBalancesInTx(ctx, increaseOpts, decreaseOpts) + if err != nil { + return err + } } return nil diff --git a/packages/eventindexer/nft_balance.go b/packages/eventindexer/nft_balance.go index 9b1651a49b1..1497b32d6e3 100644 --- a/packages/eventindexer/nft_balance.go +++ b/packages/eventindexer/nft_balance.go @@ -30,8 +30,11 @@ type UpdateNFTBalanceOpts struct { // NFTBalanceRepository is used to interact with nft balances in the store type NFTBalanceRepository interface { - SubtractBalance(ctx context.Context, opts UpdateNFTBalanceOpts) (*NFTBalance, error) - IncreaseBalance(ctx context.Context, opts UpdateNFTBalanceOpts) (*NFTBalance, error) + IncreaseAndDecreaseBalancesInTx( + ctx context.Context, + increaseOpts UpdateNFTBalanceOpts, + decreaseOpts UpdateNFTBalanceOpts, + ) (increasedBalance *NFTBalance, decreasedBalance *NFTBalance, err error) FindByAddress(ctx context.Context, req *http.Request, address string, diff --git a/packages/eventindexer/pkg/mock/nft_balance_repository.go b/packages/eventindexer/pkg/mock/nft_balance_repository.go index 4b55cae4b70..a043371a5d3 100644 --- a/packages/eventindexer/pkg/mock/nft_balance_repository.go +++ b/packages/eventindexer/pkg/mock/nft_balance_repository.go @@ -16,18 +16,12 @@ func NewNFTBalanceRepository() *NFTBalanceRepository { return &NFTBalanceRepository{} } -func (r *NFTBalanceRepository) SubtractBalance( +func (r *NFTBalanceRepository) IncreaseAndDecreaseBalancesInTx( ctx context.Context, - opts eventindexer.UpdateNFTBalanceOpts, -) (*eventindexer.NFTBalance, error) { - return nil, nil -} - -func (r *NFTBalanceRepository) IncreaseBalance( - ctx context.Context, - opts eventindexer.UpdateNFTBalanceOpts, -) (*eventindexer.NFTBalance, error) { - return nil, nil + increaseOpts eventindexer.UpdateNFTBalanceOpts, + decreaseOpts eventindexer.UpdateNFTBalanceOpts, +) (increasedBalance *eventindexer.NFTBalance, decreasedBalance *eventindexer.NFTBalance, err error) { + return nil, nil, nil } func (r *NFTBalanceRepository) FindByAddress(ctx context.Context, diff --git a/packages/eventindexer/pkg/repo/nft_balance.go b/packages/eventindexer/pkg/repo/nft_balance.go index c3ec5e381d7..ccd6df691b8 100644 --- a/packages/eventindexer/pkg/repo/nft_balance.go +++ b/packages/eventindexer/pkg/repo/nft_balance.go @@ -24,8 +24,9 @@ func NewNFTBalanceRepository(db eventindexer.DB) (*NFTBalanceRepository, error) }, nil } -func (r *NFTBalanceRepository) IncreaseBalance( +func (r *NFTBalanceRepository) increaseBalanceInDB( ctx context.Context, + db *gorm.DB, opts eventindexer.UpdateNFTBalanceOpts, ) (*eventindexer.NFTBalance, error) { b := &eventindexer.NFTBalance{ @@ -34,11 +35,9 @@ func (r *NFTBalanceRepository) IncreaseBalance( Address: opts.Address, ContractType: opts.ContractType, ChainID: opts.ChainID, - Amount: 0, } - err := r.db. - GormDB(). + err := db. Where("contract_address = ?", opts.ContractAddress). Where("token_id = ?", opts.TokenID). Where("address = ?", opts.Address). @@ -55,15 +54,16 @@ func (r *NFTBalanceRepository) IncreaseBalance( b.Amount += opts.Amount // update the row to reflect new balance - if err := r.db.GormDB().Save(b).Error; err != nil { + if err := db.Save(b).Error; err != nil { return nil, errors.Wrap(err, "r.db.Save") } return b, nil } -func (r *NFTBalanceRepository) SubtractBalance( +func (r *NFTBalanceRepository) decreaseBalanceInDB( ctx context.Context, + db *gorm.DB, opts eventindexer.UpdateNFTBalanceOpts, ) (*eventindexer.NFTBalance, error) { b := &eventindexer.NFTBalance{ @@ -74,8 +74,7 @@ func (r *NFTBalanceRepository) SubtractBalance( ChainID: opts.ChainID, } - err := r.db. - GormDB(). + err := db. Where("contract_address = ?", opts.ContractAddress). Where("token_id = ?", opts.TokenID). Where("address = ?", opts.Address). @@ -86,7 +85,7 @@ func (r *NFTBalanceRepository) SubtractBalance( if err != gorm.ErrRecordNotFound { return nil, errors.Wrap(err, "r.db.gormDB.First") } else { - // cant subtract a balance if user never had this balance, indexing issue + // cant decrease a balance if user never had this balance, indexing issue return nil, nil } } @@ -95,12 +94,12 @@ func (r *NFTBalanceRepository) SubtractBalance( // we can just delete the row, this user has no more of this NFT if b.Amount == 0 { - if err := r.db.GormDB().Delete(b).Error; err != nil { + if err := db.Delete(b).Error; err != nil { return nil, errors.Wrap(err, "r.db.Delete") } } else { // update the row instead to reflect new balance - if err := r.db.GormDB().Save(b).Error; err != nil { + if err := db.Save(b).Error; err != nil { return nil, errors.Wrap(err, "r.db.Save") } } @@ -108,6 +107,30 @@ func (r *NFTBalanceRepository) SubtractBalance( return b, nil } +func (r *NFTBalanceRepository) IncreaseAndDecreaseBalancesInTx( + ctx context.Context, + increaseOpts eventindexer.UpdateNFTBalanceOpts, + decreaseOpts eventindexer.UpdateNFTBalanceOpts, +) (increasedBalance *eventindexer.NFTBalance, decreasedBalance *eventindexer.NFTBalance, err error) { + err = r.db.GormDB().Transaction(func(tx *gorm.DB) (err error) { + increasedBalance, err = r.increaseBalanceInDB(ctx, tx, increaseOpts) + if err != nil { + return err + } + + if decreaseOpts.Amount != 0 { + decreasedBalance, err = r.decreaseBalanceInDB(ctx, tx, decreaseOpts) + } + + return err + }) + if err != nil { + return nil, nil, errors.Wrap(err, "r.db.Transaction") + } + + return increasedBalance, decreasedBalance, nil +} + func (r *NFTBalanceRepository) FindByAddress(ctx context.Context, req *http.Request, address string, diff --git a/packages/eventindexer/pkg/repo/nft_balance_test.go b/packages/eventindexer/pkg/repo/nft_balance_test.go index 15416b29b05..86d04be8602 100644 --- a/packages/eventindexer/pkg/repo/nft_balance_test.go +++ b/packages/eventindexer/pkg/repo/nft_balance_test.go @@ -39,7 +39,7 @@ func Test_NewNFTBalanceRepo(t *testing.T) { } } -func TestIntegration_NFTBalance_Increase(t *testing.T) { +func TestIntegration_NFTBalance_Increase_And_Decrease(t *testing.T) { db, close, err := testMysql(t) assert.Equal(t, nil, err) @@ -48,43 +48,7 @@ func TestIntegration_NFTBalance_Increase(t *testing.T) { nftBalanceRepo, err := NewNFTBalanceRepository(db) assert.Equal(t, nil, err) - tests := []struct { - name string - opts eventindexer.UpdateNFTBalanceOpts - wantErr error - }{ - { - "success", - eventindexer.UpdateNFTBalanceOpts{ - ChainID: 1, - Address: "0x123", - TokenID: 1, - ContractAddress: "0x123", - ContractType: "ERC721", - Amount: 1, - }, - nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := nftBalanceRepo.IncreaseBalance(context.Background(), tt.opts) - assert.Equal(t, tt.wantErr, err) - }) - } -} - -func TestIntegration_NFTBalance_Decrease(t *testing.T) { - db, close, err := testMysql(t) - assert.Equal(t, nil, err) - - defer close() - - nftBalanceRepo, err := NewNFTBalanceRepository(db) - assert.Equal(t, nil, err) - - bal1, err := nftBalanceRepo.IncreaseBalance(context.Background(), + bal1, _, err := nftBalanceRepo.IncreaseAndDecreaseBalancesInTx(context.Background(), eventindexer.UpdateNFTBalanceOpts{ ChainID: 1, Address: "0x123", @@ -92,11 +56,11 @@ func TestIntegration_NFTBalance_Decrease(t *testing.T) { ContractAddress: "0x123", ContractType: "ERC721", Amount: 1, - }) + }, eventindexer.UpdateNFTBalanceOpts{}) assert.Equal(t, nil, err) assert.NotNil(t, bal1) - bal2, err := nftBalanceRepo.IncreaseBalance(context.Background(), + bal2, _, err := nftBalanceRepo.IncreaseAndDecreaseBalancesInTx(context.Background(), eventindexer.UpdateNFTBalanceOpts{ ChainID: 1, Address: "0x123", @@ -104,17 +68,26 @@ func TestIntegration_NFTBalance_Decrease(t *testing.T) { ContractAddress: "0x123456", ContractType: "ERC721", Amount: 2, - }) + }, eventindexer.UpdateNFTBalanceOpts{}) assert.Equal(t, nil, err) assert.NotNil(t, bal2) tests := []struct { - name string - opts eventindexer.UpdateNFTBalanceOpts - wantErr error + name string + increaseOpts eventindexer.UpdateNFTBalanceOpts + decreaseOpts eventindexer.UpdateNFTBalanceOpts + wantErr error }{ { "success", + eventindexer.UpdateNFTBalanceOpts{ + ChainID: 1, + Address: "0x123", + TokenID: 1, + ContractAddress: "0x123456789", + ContractType: "ERC721", + Amount: 1, + }, eventindexer.UpdateNFTBalanceOpts{ ChainID: 1, Address: "0x123", @@ -127,6 +100,14 @@ func TestIntegration_NFTBalance_Decrease(t *testing.T) { }, { "one left", + eventindexer.UpdateNFTBalanceOpts{ + ChainID: 1, + Address: "0x123", + TokenID: 1, + ContractAddress: "0x123456789", + ContractType: "ERC721", + Amount: 1, + }, eventindexer.UpdateNFTBalanceOpts{ ChainID: 1, Address: "0x123", @@ -141,7 +122,7 @@ func TestIntegration_NFTBalance_Decrease(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := nftBalanceRepo.SubtractBalance(context.Background(), tt.opts) + _, _, err := nftBalanceRepo.IncreaseAndDecreaseBalancesInTx(context.Background(), tt.increaseOpts, tt.decreaseOpts) assert.Equal(t, tt.wantErr, err) }) } diff --git a/packages/guardian-prover-health-check-ui/package.json b/packages/guardian-prover-health-check-ui/package.json index 36c63c1a386..f2a34ab0328 100644 --- a/packages/guardian-prover-health-check-ui/package.json +++ b/packages/guardian-prover-health-check-ui/package.json @@ -34,7 +34,7 @@ "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.2", "svelte": "^4.2.13", - "svelte-check": "^3.6.9", + "svelte-check": "^3.7.1", "tailwindcss": "^3.4.3", "tslib": "^2.6.2", "typescript": "^5.4.3", diff --git a/packages/protocol/contract_layout.md b/packages/protocol/contract_layout.md index 4e766210ee2..08d8a74d7a1 100644 --- a/packages/protocol/contract_layout.md +++ b/packages/protocol/contract_layout.md @@ -18,29 +18,29 @@ | __gap | uint256[50] | 301 | 0 | 1600 | contracts/L1/TaikoL1.sol:TaikoL1 | ## TaikoL2 -| Name | Type | Slot | Offset | Bytes | Contract | -|-------------------------|-----------------------------|------|--------|-------|----------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | -| _initializing | bool | 0 | 1 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[50] | 1 | 0 | 1600 | contracts/L2/TaikoL2.sol:TaikoL2 | -| _owner | address | 51 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[49] | 52 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | -| _pendingOwner | address | 101 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[49] | 102 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | -| addressManager | address | 151 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[49] | 152 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __reentry | uint8 | 201 | 0 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __paused | uint8 | 201 | 1 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | -| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[49] | 202 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | -| l2Hashes | mapping(uint256 => bytes32) | 251 | 0 | 32 | contracts/L2/TaikoL2.sol:TaikoL2 | -| publicInputHash | bytes32 | 252 | 0 | 32 | contracts/L2/TaikoL2.sol:TaikoL2 | -| gasExcess | uint64 | 253 | 0 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| lastSyncedBlock | uint64 | 253 | 8 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| parentTimestamp | uint64 | 253 | 16 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __currentBlockTimestamp | uint64 | 253 | 24 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| l1ChainId | uint64 | 254 | 0 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | -| __gap | uint256[46] | 255 | 0 | 1472 | contracts/L2/TaikoL2.sol:TaikoL2 | +| Name | Type | Slot | Offset | Bytes | Contract | +|-----------------|-----------------------------|------|--------|-------|----------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | +| _initializing | bool | 0 | 1 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[50] | 1 | 0 | 1600 | contracts/L2/TaikoL2.sol:TaikoL2 | +| _owner | address | 51 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[49] | 52 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | +| _pendingOwner | address | 101 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[49] | 102 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | +| addressManager | address | 151 | 0 | 20 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[49] | 152 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __reentry | uint8 | 201 | 0 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __paused | uint8 | 201 | 1 | 1 | contracts/L2/TaikoL2.sol:TaikoL2 | +| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[49] | 202 | 0 | 1568 | contracts/L2/TaikoL2.sol:TaikoL2 | +| l2Hashes | mapping(uint256 => bytes32) | 251 | 0 | 32 | contracts/L2/TaikoL2.sol:TaikoL2 | +| publicInputHash | bytes32 | 252 | 0 | 32 | contracts/L2/TaikoL2.sol:TaikoL2 | +| gasExcess | uint64 | 253 | 0 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| lastSyncedBlock | uint64 | 253 | 8 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __deprecated1 | uint64 | 253 | 16 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __deprecated2 | uint64 | 253 | 24 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| l1ChainId | uint64 | 254 | 0 | 8 | contracts/L2/TaikoL2.sol:TaikoL2 | +| __gap | uint256[46] | 255 | 0 | 1472 | contracts/L2/TaikoL2.sol:TaikoL2 | ## SignalService | Name | Type | Slot | Offset | Bytes | Contract | @@ -540,3 +540,23 @@ | isImageTrusted | mapping(bytes32 => bool) | 252 | 0 | 32 | contracts/verifiers/RiscZeroVerifier.sol:RiscZeroVerifier | | __gap | uint256[48] | 253 | 0 | 1536 | contracts/verifiers/RiscZeroVerifier.sol:RiscZeroVerifier | +## QuotaManager +| Name | Type | Slot | Offset | Bytes | Contract | +|----------------|-----------------------------------------------|------|--------|-------|------------------------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager | +| _initializing | bool | 0 | 1 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[50] | 1 | 0 | 1600 | contracts/bridge/QuotaManager.sol:QuotaManager | +| _owner | address | 51 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[49] | 52 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager | +| _pendingOwner | address | 101 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[49] | 102 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager | +| addressManager | address | 151 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[49] | 152 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __reentry | uint8 | 201 | 0 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __paused | uint8 | 201 | 1 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager | +| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[49] | 202 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager | +| tokenQuota | mapping(address => struct QuotaManager.Quota) | 251 | 0 | 32 | contracts/bridge/QuotaManager.sol:QuotaManager | +| quotaPeriod | uint24 | 252 | 0 | 3 | contracts/bridge/QuotaManager.sol:QuotaManager | +| __gap | uint256[48] | 253 | 0 | 1536 | contracts/bridge/QuotaManager.sol:QuotaManager | + diff --git a/packages/protocol/contracts/L2/Lib1559Math.sol b/packages/protocol/contracts/L2/Lib1559Math.sol index 8904ad8512c..4b5316c28f6 100644 --- a/packages/protocol/contracts/L2/Lib1559Math.sol +++ b/packages/protocol/contracts/L2/Lib1559Math.sol @@ -17,7 +17,6 @@ library Lib1559Math { function calc1559BaseFee( uint32 _gasTargetPerL1Block, uint8 _adjustmentQuotient, - uint64 _gasExcessMinValue, uint64 _gasExcess, uint64 _gasIssuance, uint32 _parentGasUsed @@ -30,7 +29,7 @@ library Lib1559Math { // value as this has already happened uint256 excess = uint256(_gasExcess) + _parentGasUsed; excess = excess > _gasIssuance ? excess - _gasIssuance : 1; - gasExcess_ = uint64(excess.min(type(uint64).max).max(_gasExcessMinValue)); + gasExcess_ = uint64(excess.min(type(uint64).max)); // The base fee per gas used by this block is the spot price at the // bonding curve, regardless the actual amount of gas used by this @@ -56,11 +55,7 @@ library Lib1559Math { if (_adjustmentFactor == 0) { revert EIP1559_INVALID_PARAMS(); } - - // Note that `/ _adjustmentFactor` can be removed which will make basefee increment faster. - // Alternatively, a larger initial value can be used for `_gasExcess`. - return _ethQty(_gasExcess, _adjustmentFactor) / LibFixedPointMath.SCALING_FACTOR - / _adjustmentFactor; + return _ethQty(_gasExcess, _adjustmentFactor) / LibFixedPointMath.SCALING_FACTOR; } /// @dev exp(gas_qty / TARGET / ADJUSTMENT_QUOTIENT) diff --git a/packages/protocol/contracts/L2/LibL2Config.sol b/packages/protocol/contracts/L2/LibL2Config.sol index b3309829cdb..70af37013b8 100644 --- a/packages/protocol/contracts/L2/LibL2Config.sol +++ b/packages/protocol/contracts/L2/LibL2Config.sol @@ -6,7 +6,6 @@ library LibL2Config { struct Config { uint32 gasTargetPerL1Block; uint8 basefeeAdjustmentQuotient; - uint64 gasExcessMinValue; } /// @notice Returns EIP1559 related configurations. @@ -17,8 +16,5 @@ library LibL2Config { // We need to monitor L2 state growth and lower this value when necessary. config_.gasTargetPerL1Block = 60_000_000; config_.basefeeAdjustmentQuotient = 8; - - // This value is picked to make the min base fee be 0.01gwei - config_.gasExcessMinValue = 17_331_548_325; } } diff --git a/packages/protocol/contracts/L2/TaikoL2.sol b/packages/protocol/contracts/L2/TaikoL2.sol index e0d36a4f97c..896376c1bdc 100644 --- a/packages/protocol/contracts/L2/TaikoL2.sol +++ b/packages/protocol/contracts/L2/TaikoL2.sol @@ -40,11 +40,8 @@ contract TaikoL2 is EssentialContract { /// @notice The last synced L1 block height. uint64 public lastSyncedBlock; - /// @notice The parent block's timestamp. - uint64 public parentTimestamp; - - /// @notice The current block's timestamp. - uint64 private __currentBlockTimestamp; + uint64 private __deprecated1; // was parentTimestamp + uint64 private __deprecated2; // was __currentBlockTimestamp /// @notice The L1's chain ID. uint64 public l1ChainId; @@ -100,7 +97,6 @@ contract TaikoL2 is EssentialContract { l1ChainId = _l1ChainId; gasExcess = _gasExcess; (publicInputHash,) = _calcPublicInputHash(block.number); - __currentBlockTimestamp = uint64(block.timestamp); } /// @notice Anchors the latest L1 block details to L2 for cross-layer @@ -163,9 +159,6 @@ contract TaikoL2 is EssentialContract { bytes32 _parentHash = blockhash(parentId); l2Hashes[parentId] = _parentHash; publicInputHash = publicInputHashNew; - - parentTimestamp = __currentBlockTimestamp; - __currentBlockTimestamp = uint64(block.timestamp); gasExcess = _gasExcess; emit Anchored(_parentHash, _gasExcess); @@ -211,7 +204,6 @@ contract TaikoL2 is EssentialContract { (basefee_, gasExcess_) = Lib1559Math.calc1559BaseFee( config.gasTargetPerL1Block, config.basefeeAdjustmentQuotient, - config.gasExcessMinValue, gasExcess, gasIssuance, _parentGasUsed diff --git a/packages/protocol/contracts/bridge/Bridge.sol b/packages/protocol/contracts/bridge/Bridge.sol index 30da04d4728..e5330d4db8b 100644 --- a/packages/protocol/contracts/bridge/Bridge.sol +++ b/packages/protocol/contracts/bridge/Bridge.sol @@ -7,6 +7,7 @@ import "../libs/LibAddress.sol"; import "../libs/LibMath.sol"; import "../signal/ISignalService.sol"; import "./IBridge.sol"; +import "./IQuotaManager.sol"; /// @title Bridge /// @notice See the documentation for {IBridge}. @@ -93,6 +94,16 @@ contract Bridge is EssentialContract, IBridge { _; } + modifier diffChain(uint64 _chainId) { + if (_chainId == 0 || _chainId == block.chainid) revert B_INVALID_CHAINID(); + _; + } + + modifier nonZeroAddr(address _addr) { + if (_addr == address(0)) revert B_INVALID_USER(); + _; + } + /// @notice Function to receive Ether. receive() external payable { } @@ -115,15 +126,13 @@ contract Bridge is EssentialContract, IBridge { external payable override + nonZeroAddr(_message.srcOwner) + nonZeroAddr(_message.destOwner) + diffChain(_message.destChainId) whenNotPaused nonReentrant returns (bytes32 msgHash_, Message memory message_) { - // Ensure the message owner is not null. - if (_message.srcOwner == address(0) || _message.destOwner == address(0)) { - revert B_INVALID_USER(); - } - if (_message.gasLimit == 0) { if (_message.fee != 0) revert B_INVALID_FEE(); } else if (_invocationGasLimit(_message, false) == 0) { @@ -135,11 +144,9 @@ contract Bridge is EssentialContract, IBridge { // Verify destination chain and to address. if (!destChainEnabled) revert B_INVALID_CHAINID(); - if (_message.destChainId == block.chainid) revert B_INVALID_CHAINID(); // Ensure the sent value matches the expected amount. - uint256 expectedAmount = _message.value + _message.fee; - if (expectedAmount != msg.value) revert B_INVALID_VALUE(); + if (_message.value + _message.fee != msg.value) revert B_INVALID_VALUE(); message_ = _message; @@ -160,8 +167,9 @@ contract Bridge is EssentialContract, IBridge { bytes calldata _proof ) external - whenNotPaused sameChain(_message.srcChainId) + diffChain(_message.destChainId) + whenNotPaused nonReentrant { bytes32 msgHash = hashMessage(_message); @@ -173,12 +181,12 @@ contract Bridge is EssentialContract, IBridge { revert B_MESSAGE_NOT_SENT(); } - (bool received,) = _proveSignalReceived( + _proveSignalReceived( signalService, signalForFailedMessage(msgHash), _message.destChainId, _proof ); - if (!received) revert B_SIGNAL_NOT_RECEIVED(); _updateMessageStatus(msgHash, Status.RECALLED); + _consumeEtherQuota(_message.value); // Execute the recall logic based on the contract's support for the // IRecallableSender interface @@ -206,8 +214,9 @@ contract Bridge is EssentialContract, IBridge { bytes calldata _proof ) external - whenNotPaused sameChain(_message.destChainId) + diffChain(_message.srcChainId) + whenNotPaused nonReentrant { uint256 gasStart = gasleft(); @@ -219,29 +228,27 @@ contract Bridge is EssentialContract, IBridge { bytes32 msgHash = hashMessage(_message); _checkStatus(msgHash, Status.NEW); + _consumeEtherQuota(_message.value + _message.fee); address signalService = resolve(LibStrings.B_SIGNAL_SERVICE, false); ProcessingStats memory stats; - bool received; - - (received, stats.numCacheOps) = + stats.numCacheOps = _proveSignalReceived(signalService, msgHash, _message.srcChainId, _proof); - if (!received) revert B_SIGNAL_NOT_RECEIVED(); uint256 refundAmount; - if ( - _message.to == address(0) || _message.to == address(this) - || _message.to == signalService - ) { + if (_unableToInvokeMessageCall(_message, signalService)) { // Handle special addresses that don't require actual invocation but // mark message as DONE refundAmount = _message.value; _updateMessageStatus(msgHash, Status.DONE); } else { - Status status = _invokeMessageCall( - _message, msgHash, _invocationGasLimit(_message, true) - ) ? Status.DONE : Status.RETRIABLE; + uint256 gasLimit = msg.sender == _message.destOwner + ? gasleft() // ignore _message.gasLimit + : _invocationGasLimit(_message, true); + + Status status = + _invokeMessageCall(_message, msgHash, gasLimit) ? Status.DONE : Status.RETRIABLE; _updateMessageStatus(msgHash, status); } @@ -276,12 +283,14 @@ contract Bridge is EssentialContract, IBridge { bool _isLastAttempt ) external - whenNotPaused sameChain(_message.destChainId) + diffChain(_message.srcChainId) + whenNotPaused nonReentrant { bytes32 msgHash = hashMessage(_message); _checkStatus(msgHash, Status.RETRIABLE); + _consumeEtherQuota(_message.value); uint256 invocationGasLimit; if (msg.sender != _message.destOwner) { @@ -309,8 +318,9 @@ contract Bridge is EssentialContract, IBridge { /// @inheritdoc IBridge function failMessage(Message calldata _message) external - whenNotPaused sameChain(_message.destChainId) + diffChain(_message.srcChainId) + whenNotPaused nonReentrant { if (msg.sender != _message.destOwner) revert B_PERMISSION_DENIED(); @@ -387,7 +397,7 @@ contract Bridge is EssentialContract, IBridge { view returns (bool enabled_, address destBridge_) { - destBridge_ = resolve(_chainId, "bridge", true); + destBridge_ = resolve(_chainId, LibStrings.B_BRIDGE, true); enabled_ = destBridge_ != address(0); } @@ -454,12 +464,6 @@ contract Bridge is EssentialContract, IBridge { if (_gasLimit == 0) return false; - if ( - _message.data.length >= 4 // msg can be empty - && bytes4(_message.data) != IMessageInvocable.onMessageInvocation.selector - && _message.to.isContract() - ) return false; - _storeContext(_msgHash, _message.from, _message.srcChainId); success_ = _message.to.sendEther(_message.value, _gasLimit, _message.data); _resetContext(); @@ -526,7 +530,6 @@ contract Bridge is EssentialContract, IBridge { /// @param _signal The signal. /// @param _chainId The ID of the chain the signal is stored on. /// @param _proof The merkle inclusion proof. - /// @return success_ true if the message was received. /// @return numCacheOps_ Num of cached items function _proveSignalReceived( address _signalService, @@ -535,15 +538,14 @@ contract Bridge is EssentialContract, IBridge { bytes calldata _proof ) private - returns (bool success_, uint32 numCacheOps_) + returns (uint32 numCacheOps_) { try ISignalService(_signalService).proveSignalReceived( - _chainId, resolve(_chainId, "bridge", false), _signal, _proof + _chainId, resolve(_chainId, LibStrings.B_BRIDGE, false), _signal, _proof ) returns (uint256 numCacheOps) { numCacheOps_ = uint32(numCacheOps); - success_ = true; } catch { - success_ = false; + revert B_SIGNAL_NOT_RECEIVED(); } } @@ -565,7 +567,7 @@ contract Bridge is EssentialContract, IBridge { returns (bool) { try ISignalService(_signalService).verifySignalReceived( - _chainId, resolve(_chainId, "bridge", false), _signal, _proof + _chainId, resolve(_chainId, LibStrings.B_BRIDGE, false), _signal, _proof ) { return true; } catch { @@ -594,4 +596,28 @@ contract Bridge is EssentialContract, IBridge { function _checkStatus(bytes32 _msgHash, Status _expectedStatus) private view { if (messageStatus[_msgHash] != _expectedStatus) revert B_INVALID_STATUS(); } + + function _consumeEtherQuota(uint256 _amount) private { + address quotaManager = resolve(LibStrings.B_QUOTA_MANAGER, true); + if (quotaManager != address(0)) { + IQuotaManager(quotaManager).consumeQuota(address(0), _amount); + } + } + + function _unableToInvokeMessageCall( + Message calldata _message, + address _signalService + ) + internal + view + returns (bool) + { + if (_message.to == address(0)) return true; + if (_message.to == address(this)) return true; + if (_message.to == _signalService) return true; + + return _message.data.length >= 4 + && bytes4(_message.data) != IMessageInvocable.onMessageInvocation.selector + && _message.to.isContract(); + } } diff --git a/packages/protocol/contracts/bridge/IQuotaManager.sol b/packages/protocol/contracts/bridge/IQuotaManager.sol new file mode 100644 index 00000000000..8b2381ae8e0 --- /dev/null +++ b/packages/protocol/contracts/bridge/IQuotaManager.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title IQuotaManager +/// @custom:security-contact security@taiko.xyz +interface IQuotaManager { + /// @notice Consumes a specific amount of quota for a given address. + /// This function must revert if available quota is smaller than the given amount of quota. + /// @param _token The token address. Ether is represented with address(0). + /// @param _amount The amount of quota to consume. + function consumeQuota(address _token, uint256 _amount) external; +} diff --git a/packages/protocol/contracts/bridge/QuotaManager.sol b/packages/protocol/contracts/bridge/QuotaManager.sol new file mode 100644 index 00000000000..133e5cdfb5e --- /dev/null +++ b/packages/protocol/contracts/bridge/QuotaManager.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../common/EssentialContract.sol"; +import "../common/LibStrings.sol"; +import "../libs/LibMath.sol"; +import "./IQuotaManager.sol"; + +/// @title QuotaManager +/// @dev An implementation of IQuotaManager for Ether and ERC20 tokens. +/// @custom:security-contact security@taiko.xyz +contract QuotaManager is EssentialContract, IQuotaManager { + using LibMath for uint256; + + struct Quota { + uint48 updatedAt; + uint104 quota; + uint104 available; + } + + mapping(address token => Quota tokenLimit) public tokenQuota; + uint24 public quotaPeriod; + + uint256[48] private __gap; + + event QuotaUpdated(address indexed token, uint256 oldQuota, uint256 newQuota); + + error QM_INVALID_PARAM(); + error QM_OUT_OF_QUOTA(); + + /// @notice Initializes the contract. + /// @param _owner The owner of this contract. msg.sender will be used if this value is zero. + /// @param _addressManager The address of the {AddressManager} contract. + /// @param _quotaPeriod The time required to restore all quota. + function init( + address _owner, + address _addressManager, + uint24 _quotaPeriod + ) + external + initializer + { + if (_quotaPeriod == 0) revert QM_INVALID_PARAM(); + + __Essential_init(_owner, _addressManager); + quotaPeriod = _quotaPeriod; + } + + /// @notice Updates the daily quota for a given address. + /// @param _token The token address with Ether represented by address(0). + /// @param _quota The new daily quota. + function updateQuota(address _token, uint104 _quota) external onlyOwner whenNotPaused { + if (_quota == tokenQuota[_token].quota) revert QM_INVALID_PARAM(); + + emit QuotaUpdated(_token, tokenQuota[_token].quota, _quota); + tokenQuota[_token].quota = _quota; + } + + /// @inheritdoc IQuotaManager + function consumeQuota( + address _token, + uint256 _amount + ) + external + whenNotPaused + onlyFromNamedEither(LibStrings.B_BRIDGE, LibStrings.B_ERC20_VAULT) + { + uint256 available = availableQuota(_token, 0); + if (available == type(uint256).max) return; + if (available < _amount) revert QM_OUT_OF_QUOTA(); + + unchecked { + available -= _amount; + } + tokenQuota[_token].available = uint104(available); + tokenQuota[_token].updatedAt = uint48(block.timestamp); + } + + /// @notice Returns the available quota for a given token. + /// @param _token The token address with Ether represented by address(0). + /// @param _leap Amount of seconds in the future. + /// @return The available quota. + function availableQuota(address _token, uint256 _leap) public view returns (uint256) { + Quota memory q = tokenQuota[_token]; + if (q.quota == 0) return type(uint256).max; + if (q.updatedAt == 0) return q.quota; + + uint256 issuance = q.quota * (block.timestamp + _leap - q.updatedAt) / quotaPeriod; + return (issuance + q.available).min(q.quota); + } +} diff --git a/packages/protocol/contracts/common/AddressResolver.sol b/packages/protocol/contracts/common/AddressResolver.sol index 4eb1b8513a0..27f88b2a221 100644 --- a/packages/protocol/contracts/common/AddressResolver.sol +++ b/packages/protocol/contracts/common/AddressResolver.sol @@ -26,6 +26,17 @@ abstract contract AddressResolver is IAddressResolver, Initializable { _; } + /// @dev Modifier that ensures the caller is a resolved address to either _name1 or _name2 + /// name. + /// @param _name1 The first name to check against. + /// @param _name2 The second name to check against. + modifier onlyFromNamedEither(bytes32 _name1, bytes32 _name2) { + if (msg.sender != resolve(_name1, true) && msg.sender != resolve(_name2, true)) { + revert RESOLVER_DENIED(); + } + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); diff --git a/packages/protocol/contracts/common/LibStrings.sol b/packages/protocol/contracts/common/LibStrings.sol index a5fb5b3b388..6d7e738d092 100644 --- a/packages/protocol/contracts/common/LibStrings.sol +++ b/packages/protocol/contracts/common/LibStrings.sol @@ -4,31 +4,31 @@ pragma solidity 0.8.24; /// @title LibStrings /// @custom:security-contact security@taiko.xyz library LibStrings { - bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog"); - bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer"); - bytes32 internal constant B_PROPOSER = bytes32("proposer"); - bytes32 internal constant B_PROPOSER_ONE = bytes32("proposer_one"); - bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service"); - bytes32 internal constant B_TAIKO_TOKEN = bytes32("taiko_token"); - bytes32 internal constant B_TAIKO = bytes32("taiko"); + bytes32 internal constant B_AUTOMATA_DCAP_ATTESTATION = bytes32("automata_dcap_attestation"); bytes32 internal constant B_BRIDGE = bytes32("bridge"); - bytes32 internal constant B_ERC20_VAULT = bytes32("erc20_vault"); + bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog"); + bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155"); bytes32 internal constant B_BRIDGED_ERC20 = bytes32("bridged_erc20"); + bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721"); + bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog"); bytes32 internal constant B_ERC1155_VAULT = bytes32("erc1155_vault"); - bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155"); + bytes32 internal constant B_ERC20_VAULT = bytes32("erc20_vault"); bytes32 internal constant B_ERC721_VAULT = bytes32("erc721_vault"); - bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721"); - bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog"); + bytes32 internal constant B_PROPOSER = bytes32("proposer"); + bytes32 internal constant B_PROPOSER_ONE = bytes32("proposer_one"); + bytes32 internal constant B_PROVER_ASSIGNMENT = bytes32("PROVER_ASSIGNMENT"); + bytes32 internal constant B_QUOTA_MANAGER = bytes32("quota_manager"); bytes32 internal constant B_SGX_WATCHDOG = bytes32("sgx_watchdog"); + bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service"); + bytes32 internal constant B_TAIKO = bytes32("taiko"); + bytes32 internal constant B_TAIKO_TOKEN = bytes32("taiko_token"); + bytes32 internal constant B_TIER_GUARDIAN = bytes32("tier_guardian"); + bytes32 internal constant B_TIER_GUARDIAN_MINORITY = bytes32("tier_guardian_minority"); bytes32 internal constant B_TIER_PROVIDER = bytes32("tier_provider"); bytes32 internal constant B_TIER_SGX = bytes32("tier_sgx"); bytes32 internal constant B_TIER_SGX_ZKVM = bytes32("tier_sgx_zkvm"); - bytes32 internal constant B_TIER_GUARDIAN_MINORITY = bytes32("tier_guardian_minority"); - bytes32 internal constant B_TIER_GUARDIAN = bytes32("tier_guardian"); - bytes32 internal constant B_AUTOMATA_DCAP_ATTESTATION = bytes32("automata_dcap_attestation"); - bytes32 internal constant B_PROVER_ASSIGNMENT = bytes32("PROVER_ASSIGNMENT"); + bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer"); bytes32 internal constant H_RETURN_LIVENESS_BOND = keccak256("RETURN_LIVENESS_BOND"); - bytes32 internal constant H_STATE_ROOT = keccak256("STATE_ROOT"); bytes32 internal constant H_SIGNAL_ROOT = keccak256("SIGNAL_ROOT"); - string internal constant S_SIGNAL = "SIGNAL"; + bytes32 internal constant H_STATE_ROOT = keccak256("STATE_ROOT"); } diff --git a/packages/protocol/contracts/libs/LibBytes.sol b/packages/protocol/contracts/libs/LibBytes.sol new file mode 100644 index 00000000000..283cdb6b52f --- /dev/null +++ b/packages/protocol/contracts/libs/LibBytes.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +library LibBytes { + // Taken from: + // https://github.com/0xPolygonHermez/zkevm-contracts/blob/main/contracts/PolygonZkEVMBridge.sol#L835-L860 + /// @notice Function to convert returned data to string + /// returns 'NOT_VALID_ENCODING' as fallback value. + function toString(bytes memory _data) internal pure returns (string memory) { + if (_data.length >= 64) { + return abi.decode(_data, (string)); + } else if (_data.length == 32) { + // Since the strings on bytes32 are encoded left-right, check the first zero in the data + uint256 nonZeroBytes; + while (nonZeroBytes < 32 && _data[nonZeroBytes] != 0) { + ++nonZeroBytes; + } + + // If the first one is 0, we do not handle the encoding + if (nonZeroBytes == 0) return ""; + + // Create a byte array with nonZeroBytes length + bytes memory bytesArray = new bytes(nonZeroBytes); + for (uint256 i; i < nonZeroBytes; ++i) { + bytesArray[i] = _data[i]; + } + return string(bytesArray); + } else { + return ""; + } + } +} diff --git a/packages/protocol/contracts/libs/LibNetwork.sol b/packages/protocol/contracts/libs/LibNetwork.sol index 46c3a18c9ee..b4c3f651ecc 100644 --- a/packages/protocol/contracts/libs/LibNetwork.sol +++ b/packages/protocol/contracts/libs/LibNetwork.sol @@ -41,7 +41,7 @@ library LibNetwork { /// @param _chainId The chain ID. /// @return true if the chain ID represents an internal Taiko devnet's base layer, false /// otherwise. - function isTaikoDevnetL1(uint256 _chainId) internal pure returns (bool) { + function isTaikoDevnet(uint256 _chainId) internal pure returns (bool) { return _chainId >= 32_300 && _chainId <= 32_400; } @@ -51,6 +51,6 @@ library LibNetwork { /// @return true if the chain supports Dencun hardfork, false otherwise. function isDencunSupported(uint256 _chainId) internal pure returns (bool) { return _chainId == LibNetwork.MAINNET || _chainId == LibNetwork.HOLESKY - || _chainId == LibNetwork.SEPOLIA || isTaikoDevnetL1(_chainId); + || _chainId == LibNetwork.SEPOLIA || isTaikoDevnet(_chainId); } } diff --git a/packages/protocol/contracts/signal/SignalService.sol b/packages/protocol/contracts/signal/SignalService.sol index df50b0ad2c4..19d01d09ba7 100644 --- a/packages/protocol/contracts/signal/SignalService.sol +++ b/packages/protocol/contracts/signal/SignalService.sol @@ -190,7 +190,7 @@ contract SignalService is EssentialContract, ISignalService { pure returns (bytes32) { - return keccak256(abi.encodePacked(LibStrings.S_SIGNAL, _chainId, _app, _signal)); + return keccak256(abi.encodePacked("SIGNAL", _chainId, _app, _signal)); } function _verifyHopProof( @@ -323,7 +323,8 @@ contract SignalService is EssentialContract, ISignalService { address app = _app; bytes32 signal = _signal; bytes32 value = _signal; - address signalService = resolve(chainId, "signal_service", false); + address signalService = resolve(chainId, LibStrings.B_SIGNAL_SERVICE, false); + if (signalService == address(this)) revert SS_INVALID_MID_HOP_CHAINID(); HopProof memory hop; bytes32 signalRoot; @@ -348,7 +349,8 @@ contract SignalService is EssentialContract, ISignalService { if (hop.chainId == 0 || hop.chainId == block.chainid) { revert SS_INVALID_MID_HOP_CHAINID(); } - signalService = resolve(hop.chainId, "signal_service", false); + signalService = resolve(hop.chainId, LibStrings.B_SIGNAL_SERVICE, false); + if (signalService == address(this)) revert SS_INVALID_MID_HOP_CHAINID(); } isFullProof = hop.accountProof.length != 0; diff --git a/packages/protocol/contracts/tokenvault/BaseVault.sol b/packages/protocol/contracts/tokenvault/BaseVault.sol index dd766698324..c37f0b8d468 100644 --- a/packages/protocol/contracts/tokenvault/BaseVault.sol +++ b/packages/protocol/contracts/tokenvault/BaseVault.sol @@ -6,6 +6,17 @@ import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../bridge/IBridge.sol"; import "../common/EssentialContract.sol"; import "../common/LibStrings.sol"; +import "../libs/LibBytes.sol"; + +/// @title INameSymbol +/// @notice Interface for contracts that provide name() and symbol() +/// functions. These functions may not be part of the official interface but are +/// used by some contracts. +/// @custom:security-contact security@taiko.xyz +interface INameSymbol { + function name() external view returns (string memory); + function symbol() external view returns (string memory); +} /// @title BaseVault /// @notice This abstract contract provides a base implementation for vaults. @@ -16,6 +27,8 @@ abstract contract BaseVault is IMessageInvocable, IERC165Upgradeable { + using LibBytes for bytes; + uint256[50] private __gap; error VAULT_INVALID_TO_ADDR(); @@ -58,4 +71,16 @@ abstract contract BaseVault is function checkToAddress(address _to) internal view { if (_to == address(0) || _to == address(this)) revert VAULT_INVALID_TO_ADDR(); } + + function safeSymbol(address _token) internal view returns (string memory symbol_) { + (bool success, bytes memory data) = + address(_token).staticcall(abi.encodeCall(INameSymbol.symbol, ())); + return success ? data.toString() : ""; + } + + function safeName(address _token) internal view returns (string memory) { + (bool success, bytes memory data) = + address(_token).staticcall(abi.encodeCall(INameSymbol.name, ())); + return success ? data.toString() : ""; + } } diff --git a/packages/protocol/contracts/tokenvault/ERC1155Vault.sol b/packages/protocol/contracts/tokenvault/ERC1155Vault.sol index fc17cdbf0a0..4a264c54a24 100644 --- a/packages/protocol/contracts/tokenvault/ERC1155Vault.sol +++ b/packages/protocol/contracts/tokenvault/ERC1155Vault.sol @@ -4,22 +4,10 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155ReceiverUpgradeable.sol"; import "../libs/LibAddress.sol"; +import "../common/LibStrings.sol"; import "./BaseNFTVault.sol"; import "./BridgedERC1155.sol"; -/// @title IERC1155NameAndSymbol -/// @notice Interface for ERC1155 contracts that provide name() and symbol() -/// functions. These functions may not be part of the official interface but are -/// used by some contracts. -/// @custom:security-contact security@taiko.xyz -interface IERC1155NameAndSymbol { - /// @notice Returns the name of the token. - function name() external view returns (string memory); - - /// @notice Returns the symbol of the token. - function symbol() external view returns (string memory); -} - /// @title ERC1155Vault /// @dev Labeled in AddressResolver as "erc1155_vault" /// @notice This vault holds all ERC1155 tokens that users have deposited. @@ -207,7 +195,7 @@ contract ERC1155Vault is BaseNFTVault, ERC1155ReceiverUpgradeable { /// @inheritdoc BaseVault function name() public pure override returns (bytes32) { - return "erc1155_vault"; + return LibStrings.B_ERC1155_VAULT; } /// @dev Transfers ERC1155 tokens to the `to` address. @@ -256,16 +244,9 @@ contract ERC1155Vault is BaseNFTVault, ERC1155ReceiverUpgradeable { ctoken_ = CanonicalNFT({ chainId: uint64(block.chainid), addr: _op.token, - symbol: "", - name: "" + symbol: safeSymbol(_op.token), + name: safeName(_op.token) }); - IERC1155NameAndSymbol t = IERC1155NameAndSymbol(_op.token); - try t.name() returns (string memory _name) { - ctoken_.name = _name; - } catch { } - try t.symbol() returns (string memory _symbol) { - ctoken_.symbol = _symbol; - } catch { } IERC1155(_op.token).safeBatchTransferFrom({ from: msg.sender, diff --git a/packages/protocol/contracts/tokenvault/ERC20Vault.sol b/packages/protocol/contracts/tokenvault/ERC20Vault.sol index 6149fb6482f..bdac051b2cf 100644 --- a/packages/protocol/contracts/tokenvault/ERC20Vault.sol +++ b/packages/protocol/contracts/tokenvault/ERC20Vault.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../bridge/IQuotaManager.sol"; +import "../common/LibStrings.sol"; import "../libs/LibAddress.sol"; import "./BridgedERC20.sol"; import "./BaseVault.sol"; @@ -316,7 +318,7 @@ contract ERC20Vault is BaseVault { /// @inheritdoc BaseVault function name() public pure override returns (bytes32) { - return "erc20_vault"; + return LibStrings.B_ERC20_VAULT; } function _transferTokens( @@ -336,6 +338,8 @@ contract ERC20Vault is BaseVault { // check. IBridgedERC20(token_).mint(_to, _amount); } + + _consumeTokenQuota(token_, _amount); } /// @dev Handles the message on the source chain and returns the encoded @@ -363,24 +367,11 @@ contract ERC20Vault is BaseVault { ctoken_ = CanonicalERC20({ chainId: uint64(block.chainid), addr: _op.token, - decimals: 0, - symbol: "", - name: "" + decimals: _safeDecimals(_op.token), + symbol: safeSymbol(_op.token), + name: safeName(_op.token) }); - // Try fill in the boilerplate values, but use try-catch because functions below are - // ERC20-optional only. - IERC20Metadata meta = IERC20Metadata(_op.token); - try meta.decimals() returns (uint8 _decimals) { - ctoken_.decimals = _decimals; - } catch { } - try meta.name() returns (string memory _name) { - ctoken_.name = _name; - } catch { } - try meta.symbol() returns (string memory _symbol) { - ctoken_.symbol = _symbol; - } catch { } - // Query the balance then query it again to get the actual amount of // token transferred into this address, this is more accurate than // simply using `amount` -- some contract may deduct a fee from the @@ -442,4 +433,17 @@ contract ERC20Vault is BaseVault { ctokenDecimal: ctoken.decimals }); } + + function _consumeTokenQuota(address _token, uint256 _amount) private { + address quotaManager = resolve(LibStrings.B_QUOTA_MANAGER, true); + if (quotaManager != address(0)) { + IQuotaManager(quotaManager).consumeQuota(_token, _amount); + } + } + + function _safeDecimals(address _token) private view returns (uint8) { + (bool success, bytes memory data) = + address(_token).staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); + return success && data.length == 32 ? abi.decode(data, (uint8)) : 18; + } } diff --git a/packages/protocol/contracts/tokenvault/ERC721Vault.sol b/packages/protocol/contracts/tokenvault/ERC721Vault.sol index 5ab96aab2c8..f5673b6801c 100644 --- a/packages/protocol/contracts/tokenvault/ERC721Vault.sol +++ b/packages/protocol/contracts/tokenvault/ERC721Vault.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "../libs/LibAddress.sol"; +import "../common/LibStrings.sol"; import "./BaseNFTVault.sol"; import "./BridgedERC721.sol"; @@ -158,7 +159,7 @@ contract ERC721Vault is BaseNFTVault, IERC721Receiver { /// @inheritdoc BaseVault function name() public pure override returns (bytes32) { - return "erc721_vault"; + return LibStrings.B_ERC721_VAULT; } function _transferTokens( @@ -199,26 +200,17 @@ contract ERC721Vault is BaseNFTVault, IERC721Receiver { BridgedERC721(_op.token).burn(msg.sender, _op.tokenIds[i]); } } else { - ERC721Upgradeable t = ERC721Upgradeable(_op.token); - ctoken_ = CanonicalNFT({ chainId: uint64(block.chainid), addr: _op.token, - symbol: "", - name: "" + symbol: safeSymbol(_op.token), + name: safeName(_op.token) }); - // Try fill in the boilerplate values, but use try-catch because functions below are - // ERC20-optional only. - try t.name() returns (string memory _name) { - ctoken_.name = _name; - } catch { } - try t.symbol() returns (string memory _symbol) { - ctoken_.symbol = _symbol; - } catch { } - for (uint256 i; i < _op.tokenIds.length; ++i) { - t.safeTransferFrom(msg.sender, address(this), _op.tokenIds[i]); + ERC721Upgradeable(_op.token).safeTransferFrom( + msg.sender, address(this), _op.tokenIds[i] + ); } } } diff --git a/packages/protocol/deployments/gen-layouts.sh b/packages/protocol/deployments/gen-layouts.sh index 1470ea8880b..fefa3e3370b 100755 --- a/packages/protocol/deployments/gen-layouts.sh +++ b/packages/protocol/deployments/gen-layouts.sh @@ -25,6 +25,7 @@ contracts=( "AutomataDcapV3Attestation" "SgxVerifier" "RiscZeroVerifier" + "QuotaManager" ) # Empty the output file initially diff --git a/packages/protocol/docs/how_taiko_proves_blocks.md b/packages/protocol/docs/how_taiko_proves_blocks.md index 0c5d1c3e3e9..1d1dd7dcf08 100644 --- a/packages/protocol/docs/how_taiko_proves_blocks.md +++ b/packages/protocol/docs/how_taiko_proves_blocks.md @@ -109,7 +109,7 @@ struct BlockMetadata { The following [**block level variables**](https://docs.soliditylang.org/en/latest/units-and-global-variables.html) are accessible to the EVM, but their values are not part of the MPT so we need a different way to verify their correctness. - `blockhash(uint blockNumber) returns (bytes32)`: hash of the given block when `blocknumber` is one of the 256 most recent blocks; otherwise returns zero -- `block.basefee` (`uint`): current block's base fee ([EIP-3198](https://eips.ethereum.org/EIPS/eip-3198) and [modified EIP-1559](./L2EIP1559.md)) +- `block.basefee` (`uint`): current block's base fee ([EIP-3198](https://eips.ethereum.org/EIPS/eip-3198) and [modified EIP-1559](eip1559_on_l2.md)) - `block.chainid` (`uint`): current chain id - `block.coinbase` (`address payable`): current block miner's address - `block.prevrandao` (`uint`): alias for `block.prevrandao` ([EIP-4399](https://eips.ethereum.org/EIPS/eip-4399)) diff --git a/packages/protocol/script/DeployL1QuotaManager.s.sol b/packages/protocol/script/DeployL1QuotaManager.s.sol new file mode 100644 index 00000000000..3fb752002a9 --- /dev/null +++ b/packages/protocol/script/DeployL1QuotaManager.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../test/DeployCapability.sol"; +import "../contracts/bridge/QuotaManager.sol"; + +contract DeployL1QuotaManager is DeployCapability { + uint256 public privateKey = vm.envUint("PRIVATE_KEY"); + // MAINNET_SECURITY_COUNCIL: council.taiko.eth (0x7C50d60743D3FCe5a39FdbF687AFbAe5acFF49Fd) + address public addressManager = vm.envAddress("L1_ROLLUP_ADDRESS_MANAGER"); + address public owner = vm.envAddress("OWNER"); + + modifier broadcast() { + require(privateKey != 0, "invalid private key"); + vm.startBroadcast(); + _; + vm.stopBroadcast(); + } + + function run() external broadcast { + // Deploy the QuotaManager contract with a 15 minute quota period + QuotaManager qm = QuotaManager( + deployProxy({ + name: "quota_manager", + impl: address(new QuotaManager()), + data: abi.encodeCall(QuotaManager.init, (owner, addressManager, 15 minutes)) + }) + ); + + // L2-to-L1 Ether per 15 minutes: 500 Ether + qm.updateQuota(address(0), 250 ether); + + // L2-to-L1 TKO per 15 minutes: 100_000 (0.01% total supply) + qm.updateQuota(0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800, 100_000 ether); + } +} diff --git a/packages/protocol/test/L2/Lib1559Math.t.sol b/packages/protocol/test/L2/Lib1559Math.t.sol index 84605c75060..a4d275d26be 100644 --- a/packages/protocol/test/L2/Lib1559Math.t.sol +++ b/packages/protocol/test/L2/Lib1559Math.t.sol @@ -6,42 +6,33 @@ import "../TaikoTest.sol"; contract TestLib1559Math is TaikoTest { using LibMath for uint256; - function test_eip1559_math() external { + function test_eip1559_math() external pure { LibL2Config.Config memory config = LibL2Config.get(); uint256 adjustmentFactor = config.gasTargetPerL1Block * config.basefeeAdjustmentQuotient; uint256 baseFee; uint256 i; - - baseFee = Lib1559Math.basefee(config.gasExcessMinValue, adjustmentFactor); - assertEq(baseFee, 0.01 gwei); // 0.01gwei - console2.log("gasExcessMinValue:", config.gasExcessMinValue, "min base fee:", baseFee); - - for (; baseFee < 0.1 gwei; ++i) { - baseFee = Lib1559Math.basefee(config.gasTargetPerL1Block * i, adjustmentFactor); - console2.log("base fee:", i, baseFee); - } - - // base fee will reach 1 gwei if gasExcess > 18540000000 - console2.log("base fee will reach 0.1 gwei if gasExcess >", config.gasTargetPerL1Block * i); - assertEq(i, 309); - - for (; baseFee < 1 gwei; ++i) { - baseFee = Lib1559Math.basefee(config.gasTargetPerL1Block * i, adjustmentFactor); - console2.log("base fee:", i, baseFee); + uint256 target = 0.01 gwei; + + for (uint256 k; k < 5; ++k) { + for (; baseFee < target; ++i) { + baseFee = Lib1559Math.basefee(config.gasTargetPerL1Block * i, adjustmentFactor); + } + console2.log("base fee:", baseFee); + console2.log(" gasExcess:", config.gasTargetPerL1Block * i); + console2.log(" i:", i); + target *= 10; } + } - // base fee will reach 10 gwei if gasExcess > 19620000000 - console2.log("base fee will reach 1 gwei if gasExcess >", config.gasTargetPerL1Block * i); - assertEq(i, 327); + function test_eip1559_math_max() external pure { + LibL2Config.Config memory config = LibL2Config.get(); + uint256 adjustmentFactor = config.gasTargetPerL1Block * config.basefeeAdjustmentQuotient; - for (; baseFee < 10 gwei; ++i) { - baseFee = Lib1559Math.basefee(config.gasTargetPerL1Block * i, adjustmentFactor); - console2.log("base fee:", i, baseFee); - } + uint256 gasExcess = type(uint64).max; + uint256 baseFee = Lib1559Math.basefee(gasExcess, adjustmentFactor); - // base fee will reach 10 gwei if gasExcess > 20760000000 - console2.log("base fee will reach 1 gwei if gasExcess >", config.gasTargetPerL1Block * i); - assertEq(i, 346); + console2.log("base fee (gwei):", baseFee / 1 gwei); + console2.log(" gasExcess:", gasExcess); } } diff --git a/packages/protocol/test/L2/TaikoL2.t.sol b/packages/protocol/test/L2/TaikoL2.t.sol index 7a3971ec92b..eadb14a6f38 100644 --- a/packages/protocol/test/L2/TaikoL2.t.sol +++ b/packages/protocol/test/L2/TaikoL2.t.sol @@ -55,9 +55,7 @@ contract TestTaikoL2 is TaikoTest { ) ); - L2.setConfigAndExcess( - LibL2Config.Config(gasTarget, quotient, uint64(gasTarget) * 300), gasExcess - ); + L2.setConfigAndExcess(LibL2Config.Config(gasTarget, quotient), gasExcess); ss.authorize(address(L2), true); @@ -71,7 +69,7 @@ contract TestTaikoL2 is TaikoTest { // calling anchor in the same block more than once should fail function test_L2_AnchorTx_revert_in_same_block() external { - vm.fee(40_253_331); + vm.fee(1); vm.prank(L2.GOLDEN_TOUCH_ADDRESS()); _anchor(BLOCK_GAS_LIMIT); diff --git a/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol b/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol index 9bdd5c3b74a..8b21fbaf56f 100644 --- a/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol +++ b/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol @@ -55,9 +55,7 @@ contract TestTaikoL2NoFeeCheck is TaikoTest { ) ); - L2.setConfigAndExcess( - LibL2Config.Config(gasTarget, quotient, uint64(gasTarget) * 300), gasExcess - ); + L2.setConfigAndExcess(LibL2Config.Config(gasTarget, quotient), gasExcess); ss.authorize(address(L2), true); diff --git a/packages/protocol/test/TaikoTest.sol b/packages/protocol/test/TaikoTest.sol index aa23271e687..9465758a269 100644 --- a/packages/protocol/test/TaikoTest.sol +++ b/packages/protocol/test/TaikoTest.sol @@ -17,6 +17,7 @@ import "../contracts/verifiers/RiscZeroVerifier.sol"; import "../contracts/L1/tiers/TierProviderV1.sol"; import "../contracts/L1/hooks/AssignmentHook.sol"; import "../contracts/L1/provers/GuardianProver.sol"; +import "../contracts/bridge/QuotaManager.sol"; import "../contracts/L2/DelegateOwner.sol"; diff --git a/packages/protocol/test/bridge/Bridge2.t.sol b/packages/protocol/test/bridge/Bridge2.t.sol new file mode 100644 index 00000000000..95d01427a80 --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../TaikoTest.sol"; + +contract BridgeTest2 is TaikoTest { + bytes public constant fakeProof = ""; + + address public owner; + uint64 public remoteChainId; + address public remoteBridge; + + AddressManager public addressManager; + SignalService public signalService; + Bridge public bridge; + + modifier transactedBy(address addr) { + vm.deal(addr, 100 ether); + vm.startPrank(addr); + + _; + vm.stopPrank(); + } + + modifier assertSameTotalBalance() { + uint256 totalBalance = getBalanceForAccounts(); + _; + uint256 totalBalance2 = getBalanceForAccounts(); + assertEq(totalBalance2, totalBalance); + assertEq(address(signalService).balance, 0); + } + + modifier dealEther(address addr) { + vm.deal(addr, 100 ether); + _; + } + + function setUp() public dealEther(owner) { + owner = vm.addr(0x1000); + remoteChainId = uint64(block.chainid + 1); + remoteBridge = vm.addr(0x2000); + + vm.startPrank(owner); + + addressManager = AddressManager( + deployProxy({ + name: "address_manager", + impl: address(new AddressManager()), + data: abi.encodeCall(AddressManager.init, (address(0))) + }) + ); + + signalService = SkipProofCheckSignal( + deployProxy({ + name: "signal_service", + impl: address(new SkipProofCheckSignal()), + data: abi.encodeCall(SignalService.init, (address(0), address(addressManager))), + registerTo: address(addressManager) + }) + ); + + bridge = Bridge( + payable( + deployProxy({ + name: "bridge", + impl: address(new Bridge()), + data: abi.encodeCall(Bridge.init, (address(0), address(addressManager))), + registerTo: address(addressManager) + }) + ) + ); + + vm.deal(address(bridge), 10_000 ether); + + addressManager.setAddress(remoteChainId, "bridge", remoteBridge); + vm.stopPrank(); + } + + function getBalanceForAccounts() public view returns (uint256) { + return Alice.balance + Bob.balance + Carol.balance + David.balance + address(bridge).balance + + owner.balance; + } +} diff --git a/packages/protocol/test/bridge/Bridge2_failMessage.t.sol b/packages/protocol/test/bridge/Bridge2_failMessage.t.sol new file mode 100644 index 00000000000..6ac17268f65 --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2_failMessage.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./Bridge2.t.sol"; + +contract BridgeTest2_failMessage is BridgeTest2 { + function test_bridge2_failMessage_not_by_destOwner() + public + transactedBy(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + message.gasLimit = 1_000_000; + message.fee = 1000; + message.value = 2 ether; + message.destOwner = Alice; + message.to = Bob; + + vm.expectRevert(Bridge.B_PERMISSION_DENIED.selector); + bridge.failMessage(message); + } + + function test_bridge2_failMessage_by_destOwner__message_retriable() + public + dealEther(Alice) + dealEther(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + message.gasLimit = bridge.getMessageMinGasLimit(0) - 1; + + vm.expectRevert(Bridge.B_INVALID_STATUS.selector); + vm.prank(Alice); + bridge.failMessage(message); + + vm.prank(Carol); + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.RETRIABLE); + + vm.prank(Alice); + bridge.failMessage(message); + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.FAILED); + + vm.expectRevert(Bridge.B_INVALID_STATUS.selector); + vm.prank(Alice); + bridge.failMessage(message); + } + + function test_bridge2_failMessage_by_destOwner__message_processed() + public + transactedBy(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 0; + message.fee = 1_000_000; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + vm.expectRevert(Bridge.B_INVALID_STATUS.selector); + bridge.failMessage(message); + } +} diff --git a/packages/protocol/test/bridge/Bridge2_processMessage.t.sol b/packages/protocol/test/bridge/Bridge2_processMessage.t.sol new file mode 100644 index 00000000000..a2ed2d62a61 --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2_processMessage.t.sol @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./Bridge2.t.sol"; + +contract Target is IMessageInvocable { + uint256 public receivedEther; + IBridge private bridge; + IBridge.Context public ctx; + + constructor(IBridge _bridge) { + bridge = _bridge; + } + + function onMessageInvocation(bytes calldata) external payable { + ctx = bridge.context(); + receivedEther += msg.value; + } + + function anotherFunc(bytes calldata) external payable { + receivedEther += msg.value; + } + + fallback() external payable { + ctx = bridge.context(); + receivedEther += msg.value; + } + + receive() external payable { } +} + +contract BridgeTest2_processMessage is BridgeTest2 { + function test_bridge2_processMessage_basic() public dealEther(Alice) assertSameTotalBalance { + vm.startPrank(Alice); + + IBridge.Message memory message; + + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.processMessage(message, fakeProof); + + message.destChainId = uint64(block.chainid); + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.processMessage(message, fakeProof); + + message.srcChainId = uint64(block.chainid); + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.processMessage(message, fakeProof); + + message.srcChainId = remoteChainId + 1; + vm.expectRevert(Bridge.B_PERMISSION_DENIED.selector); + bridge.processMessage(message, fakeProof); + + message.srcChainId = remoteChainId; + vm.expectRevert(); // RESOLVER_ZERO_ADDR src bridge not registered + bridge.processMessage(message, fakeProof); + + message.gasLimit = 1_000_000; + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.NEW); + + bridge.processMessage(message, fakeProof); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + vm.stopPrank(); + + vm.prank(owner); + addressManager.setAddress(message.srcChainId, "bridge", address(0)); + + vm.startPrank(Alice); + + message.id += 1; + vm.expectRevert(); // RESOLVER_ZERO_ADDR src bridge not registered + bridge.processMessage(message, fakeProof); + + vm.stopPrank(); + } + + function test_bridge2_processMessage__special_to_address__0_fee__nonezero_gaslimit() + public + transactedBy(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 1_000_000; + message.fee = 0; + message.value = 2 ether; + vm.expectRevert(LibAddress.ETH_TRANSFER_FAILED.selector); + bridge.processMessage(message, fakeProof); + + message.destOwner = Alice; + uint256 aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + message.to = address(bridge); + aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + message.to = address(signalService); + aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + } + + function test_bridge2_processMessage__special_to_address__0_fee__0_gaslimit() + public + dealEther(Alice) + dealEther(Bob) + assertSameTotalBalance + { + vm.startPrank(Alice); + + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 0; + message.fee = 0; + message.value = 2 ether; + vm.expectRevert(Bridge.B_PERMISSION_DENIED.selector); + bridge.processMessage(message, fakeProof); + + message.destOwner = Alice; + uint256 aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + message.to = address(bridge); + aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + message.to = address(signalService); + aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + assertEq(Alice.balance, aliceBalance + 2 ether); + + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + vm.stopPrank(); + + message.value = 3 ether; + + vm.prank(Bob); + vm.expectRevert(Bridge.B_PERMISSION_DENIED.selector); + bridge.processMessage(message, fakeProof); + + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.NEW); + } + + function test_bridge2_processMessage__special_to_address__nonezero_fee__nonezero_gaslimit() + public + transactedBy(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 1; + message.fee = 5_000_000; + message.value = 2 ether; + message.destOwner = Bob; + + uint256 bobBalance = Bob.balance; + uint256 aliceBalance = Alice.balance; + + bridge.processMessage(message, fakeProof); + + assertEq(Bob.balance, bobBalance + 2 ether); + assertEq(Alice.balance, aliceBalance + 5_000_000); + + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + message.gasLimit = 10_000_000; + bobBalance = Bob.balance; + aliceBalance = Alice.balance; + + bridge.processMessage(message, fakeProof); + assertTrue(Bob.balance > bobBalance + 2 ether); + assertTrue(Alice.balance < aliceBalance + 5_000_000); + } + + function test_bridge2_processMessage__special_to_address__nonezero_fee__0_gaslimit() + public + transactedBy(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 0; + message.fee = 5_000_000; + message.value = 2 ether; + message.destOwner = Alice; + + uint256 aliceBalance = Alice.balance; + + bridge.processMessage(message, fakeProof); + + assertEq(Alice.balance, aliceBalance + 2 ether + 5_000_000); + + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + } + + function test_bridge2_processMessage__eoa_address__0_fee__nonezero_gaslimit() + public + transactedBy(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 1_000_000; + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + + uint256 aliceBalance = Alice.balance; + uint256 davidBalance = David.balance; + + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + assertEq(Alice.balance, aliceBalance); + assertEq(David.balance, davidBalance + 2 ether); + } + + function test_bridge2_processMessage__eoa_to_address__0_fee__0_gaslimit() + public + transactedBy(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 0; + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + + uint256 davidBalance = David.balance; + + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + assertEq(David.balance, davidBalance + 2 ether); + } + + function test_bridge2_processMessage__eoa_to_address__nonezero_fee__nonezero_gaslimit() + public + transactedBy(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 1_000_000; + message.fee = 5_000_000; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + + uint256 aliceBalance = Alice.balance; + uint256 davidBalance = David.balance; + + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + assertEq(David.balance, davidBalance + 2 ether); + assertTrue(Alice.balance > aliceBalance); + assertTrue(Alice.balance < aliceBalance + 5_000_000); + } + + function test_bridge2_processMessage__eoa_to_address__nonezero_fee__0_gaslimit() + public + transactedBy(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 0; + message.fee = 1_000_000; + message.value = 2 ether; + message.destOwner = Alice; + message.to = David; + + uint256 davidBalance = David.balance; + + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + assertEq(David.balance, davidBalance + 2 ether); + } + + function test_bridge2_processMessage__special_invocation() public transactedBy(Carol) { + Target target = new Target(bridge); + + uint256 totalBalance = getBalanceForAccounts() + address(target).balance; + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.gasLimit = 1_000_000; + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = address(target); + message.data = abi.encodeCall(Target.anotherFunc, ("")); + + uint256 aliceBalance = Alice.balance; + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + assertEq(Alice.balance, aliceBalance + 2 ether); + assertEq(target.receivedEther(), 0 ether); + + message.data = "1"; + bridge.processMessage(message, fakeProof); + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + assertEq(target.receivedEther(), 2 ether); + + (bytes32 msgHash, address from, uint64 srcChainId) = target.ctx(); + assertEq(msgHash, hash); + assertEq(from, message.from); + assertEq(srcChainId, message.srcChainId); + + message.to = Bob; + message.data = "something else"; + + bridge.processMessage(message, fakeProof); + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + assertEq(Bob.balance, 2 ether); + + message.to = address(target); + message.data = abi.encodeCall(Target.onMessageInvocation, ("")); + bridge.processMessage(message, fakeProof); + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + assertEq(target.receivedEther(), 4 ether); + + (msgHash, from, srcChainId) = target.ctx(); + assertEq(msgHash, hash); + assertEq(from, message.from); + assertEq(srcChainId, message.srcChainId); + + uint256 totalBalance2 = getBalanceForAccounts() + address(target).balance; + assertEq(totalBalance2, totalBalance); + } +} diff --git a/packages/protocol/test/bridge/Bridge2_recallMessage.t.sol b/packages/protocol/test/bridge/Bridge2_recallMessage.t.sol new file mode 100644 index 00000000000..f23095f0826 --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2_recallMessage.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./Bridge2.t.sol"; + +contract TestRecallableSender is IRecallableSender, IERC165 { + IBridge private bridge; + IBridge.Context public ctx; + + constructor(IBridge _bridge) { + bridge = _bridge; + } + + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return _interfaceId == type(IRecallableSender).interfaceId + || _interfaceId == type(IERC165Upgradeable).interfaceId; + } + + function onMessageRecalled(IBridge.Message calldata, bytes32) external payable { + ctx = bridge.context(); + } +} + +contract BridgeTest2_recallMessage is BridgeTest2 { + function test_bridge2_recallMessage_basic() public transactedBy(Carol) assertSameTotalBalance { + IBridge.Message memory message; + message.srcOwner = Alice; + message.destOwner = Bob; + message.destChainId = remoteChainId; + message.value = 1 ether; + + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.recallMessage(message, fakeProof); + + message.srcChainId = uint64(block.chainid); + vm.expectRevert(Bridge.B_MESSAGE_NOT_SENT.selector); + bridge.recallMessage(message, fakeProof); + + uint256 aliceBalance = Alice.balance; + uint256 carolBalance = Carol.balance; + uint256 bridgeBalance = address(bridge).balance; + + (, IBridge.Message memory m) = bridge.sendMessage{ value: 1 ether }(message); + assertEq(Alice.balance, aliceBalance); + assertEq(Carol.balance, carolBalance - 1 ether); + assertEq(address(bridge).balance, bridgeBalance + 1 ether); + + bridge.recallMessage(m, fakeProof); + bytes32 hash = bridge.hashMessage(m); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.RECALLED); + + assertEq(Alice.balance, aliceBalance + 1 ether); + assertEq(Carol.balance, carolBalance - 1 ether); + assertEq(address(bridge).balance, bridgeBalance); + + // recall the same message again + vm.expectRevert(Bridge.B_INVALID_STATUS.selector); + bridge.recallMessage(m, fakeProof); + } + + function test_bridge2_recallMessage_missing_local_signal_service() + public + dealEther(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + message.srcOwner = Alice; + message.destOwner = Bob; + message.destChainId = remoteChainId; + message.value = 1 ether; + message.srcChainId = uint64(block.chainid); + + vm.prank(Carol); + (, IBridge.Message memory m) = bridge.sendMessage{ value: 1 ether }(message); + + vm.prank(owner); + addressManager.setAddress(uint64(block.chainid), "signal_service", address(0)); + + vm.prank(Carol); + vm.expectRevert(); + bridge.recallMessage(m, fakeProof); + } + + function test_bridge2_recallMessage_callable_sender() public dealEther(Carol) { + TestRecallableSender callableSender = new TestRecallableSender(bridge); + vm.deal(address(callableSender), 100 ether); + + uint256 totalBalance = getBalanceForAccounts() + address(callableSender).balance; + + IBridge.Message memory message; + message.srcOwner = Alice; + message.destOwner = Bob; + message.destChainId = remoteChainId; + message.value = 1 ether; + message.srcChainId = uint64(block.chainid); + + vm.prank(address(callableSender)); + (bytes32 mhash, IBridge.Message memory m) = bridge.sendMessage{ value: 1 ether }(message); + + vm.prank(address(callableSender)); + bridge.recallMessage(m, fakeProof); + bytes32 hash = bridge.hashMessage(m); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.RECALLED); + + (bytes32 msgHash, address from, uint64 srcChainId) = callableSender.ctx(); + assertEq(msgHash, mhash); + assertEq(from, address(bridge)); + assertEq(srcChainId, block.chainid); + + uint256 totalBalance2 = getBalanceForAccounts() + address(callableSender).balance; + assertEq(totalBalance2, totalBalance); + } +} diff --git a/packages/protocol/test/bridge/Bridge2_retryMessage.t.sol b/packages/protocol/test/bridge/Bridge2_retryMessage.t.sol new file mode 100644 index 00000000000..b450bc05655 --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2_retryMessage.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./Bridge2.t.sol"; + +contract Target is IMessageInvocable { + bool public toFail; + + function onMessageInvocation(bytes calldata) external payable { + if (toFail) revert("failed"); + } + + function setToFail(bool fail) external { + toFail = fail; + } +} + +contract BridgeTest2_retryMessage is BridgeTest2 { + function test_bridge2_retryMessage_1() + public + dealEther(Alice) + dealEther(Carol) + assertSameTotalBalance + { + Target target = new Target(); + target.setToFail(true); + + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = address(target); + message.data = abi.encodeCall(Target.onMessageInvocation, ("hello")); + message.gasLimit = 1_000_000; + + vm.prank(Carol); + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.RETRIABLE); + + vm.expectRevert(Bridge.B_PERMISSION_DENIED.selector); + vm.prank(Carol); + bridge.retryMessage(message, true); + + vm.expectRevert(Bridge.B_RETRY_FAILED.selector); + vm.prank(Carol); + bridge.retryMessage(message, false); + + vm.expectRevert(Bridge.B_RETRY_FAILED.selector); + vm.prank(Alice); + bridge.retryMessage(message, false); + + vm.prank(Alice); + bridge.retryMessage(message, true); + + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.FAILED); + } + + function test_bridge2_retryMessage_2() public dealEther(Alice) dealEther(Carol) { + Target target = new Target(); + target.setToFail(true); + + uint256 totalBalance = getBalanceForAccounts() + address(target).balance; + IBridge.Message memory message; + + message.destChainId = uint64(block.chainid); + message.srcChainId = remoteChainId; + + message.fee = 0; + message.value = 2 ether; + message.destOwner = Alice; + message.to = address(target); + message.data = abi.encodeCall(Target.onMessageInvocation, ("hello")); + message.gasLimit = 1_000_000; + + vm.prank(Carol); + bridge.processMessage(message, fakeProof); + bytes32 hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.RETRIABLE); + + target.setToFail(false); + + vm.prank(Alice); + bridge.retryMessage(message, false); + + hash = bridge.hashMessage(message); + assertTrue(bridge.messageStatus(hash) == IBridge.Status.DONE); + + uint256 totalBalance2 = getBalanceForAccounts() + address(target).balance; + assertEq(totalBalance2, totalBalance); + } +} diff --git a/packages/protocol/test/bridge/Bridge2_sendMessage.t.sol b/packages/protocol/test/bridge/Bridge2_sendMessage.t.sol new file mode 100644 index 00000000000..8e5f560738a --- /dev/null +++ b/packages/protocol/test/bridge/Bridge2_sendMessage.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./Bridge2.t.sol"; + +contract BridgeTest2_sendMessage is BridgeTest2 { + function test_bridge2_sendMessage_invalid_message() + public + transactedBy(Carol) + assertSameTotalBalance + { + // init an all-zero message + IBridge.Message memory message; + + vm.expectRevert(Bridge.B_INVALID_USER.selector); + bridge.sendMessage(message); + + message.srcOwner = Alice; + vm.expectRevert(Bridge.B_INVALID_USER.selector); + bridge.sendMessage(message); + + message.destOwner = Bob; + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.sendMessage(message); + + message.destChainId = remoteChainId + 1; + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.sendMessage(message); + + message.destChainId = uint64(block.chainid); + vm.expectRevert(Bridge.B_INVALID_CHAINID.selector); + bridge.sendMessage(message); + + // an bridge has been registered for remoteChainId + message.destChainId = remoteChainId; + bridge.sendMessage(message); // id = 0 + + message.value = 10_000_000; + message.gasLimit = 20_000_000; + message.fee = 30_000_000; + vm.expectRevert(Bridge.B_INVALID_VALUE.selector); + + message.data = "hello"; + bridge.sendMessage(message); + + (bytes32 mhash, IBridge.Message memory m) = bridge.sendMessage{ value: 40_000_000 }(message); + assertEq(m.id, 1); + assertEq(m.srcOwner, Alice); // Not Carol + assertEq(m.srcChainId, block.chainid); + assertEq(mhash, bridge.hashMessage(m)); + + m.id = 0; + m.from = address(0); + m.srcChainId = 0; + assertEq(keccak256(abi.encode(message)), keccak256(abi.encode(m))); + + (bytes32 mhash2, IBridge.Message memory m2) = + bridge.sendMessage{ value: 40_000_000 }(message); + + assertEq(m2.id, 2); + assertTrue(mhash2 != mhash); + } + + function test_bridge2_sendMessage_invocationGasLimit() + public + transactedBy(Carol) + assertSameTotalBalance + { + IBridge.Message memory message; + message.srcOwner = Alice; + message.destOwner = Bob; + message.destChainId = remoteChainId; + message.fee = 1; + vm.expectRevert(Bridge.B_INVALID_FEE.selector); + bridge.sendMessage(message); + + uint32 minGasLimit = bridge.getMessageMinGasLimit(message.data.length); + console2.log("minGasLimit:", minGasLimit); + + message.gasLimit = minGasLimit - 1; + vm.expectRevert(Bridge.B_INVALID_GAS_LIMIT.selector); + bridge.sendMessage(message); + + message.gasLimit = minGasLimit; + vm.expectRevert(Bridge.B_INVALID_GAS_LIMIT.selector); + bridge.sendMessage(message); + + message.gasLimit = minGasLimit + 1; + vm.expectRevert(Bridge.B_INVALID_VALUE.selector); + bridge.sendMessage(message); + + bridge.sendMessage{ value: message.fee }(message); + + message.fee = 0; + bridge.sendMessage(message); + } + + function test_bridge2_sendMessage_missing_local_signal_service() + public + dealEther(Alice) + assertSameTotalBalance + { + IBridge.Message memory message; + message.srcOwner = Alice; + message.destOwner = Bob; + message.destChainId = remoteChainId; + + vm.prank(Alice); + bridge.sendMessage(message); + + vm.prank(owner); + addressManager.setAddress(uint64(block.chainid), "signal_service", address(0)); + + vm.prank(Alice); + vm.expectRevert(); + bridge.sendMessage(message); + } +} diff --git a/packages/protocol/test/bridge/QuotaManager.t.sol b/packages/protocol/test/bridge/QuotaManager.t.sol new file mode 100644 index 00000000000..be308c6e09b --- /dev/null +++ b/packages/protocol/test/bridge/QuotaManager.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../TaikoTest.sol"; + +contract QuotaManagerTest is TaikoTest { + AddressManager public am; + QuotaManager public qm; + + address bridge = vm.addr(0x100); + + function setUp() public { + vm.startPrank(Alice); // The owner + vm.deal(Alice, 100 ether); + + am = AddressManager( + deployProxy({ + name: "address_manager", + impl: address(new AddressManager()), + data: abi.encodeCall(AddressManager.init, (address(0))) + }) + ); + + am.setAddress(uint64(block.chainid), LibStrings.B_BRIDGE, bridge); + + qm = QuotaManager( + payable( + deployProxy({ + name: "quota_manager", + impl: address(new QuotaManager()), + data: abi.encodeCall(QuotaManager.init, (address(0), address(am), 24 hours)) + }) + ) + ); + + vm.stopPrank(); + } + + function test_quota_manager_consume_configged() public { + address Ether = address(0); + assertEq(qm.availableQuota(Ether, 0), type(uint256).max); + + vm.expectRevert(); + qm.updateQuota(Ether, 10 ether); + + vm.prank(Alice); + qm.updateQuota(Ether, 10 ether); + assertEq(qm.availableQuota(address(0), 0), 10 ether); + + vm.expectRevert(AddressResolver.RESOLVER_DENIED.selector); + qm.consumeQuota(Ether, 5 ether); + + vm.prank(bridge); + qm.consumeQuota(Ether, 6 ether); + assertEq(qm.availableQuota(Ether, 0), 4 ether); + + assertEq(qm.availableQuota(Ether, 3 hours), 4 ether + 10 ether * 3 / 24); + + vm.warp(block.timestamp + 3 hours); + assertEq(qm.availableQuota(Ether, 0), 4 ether + 10 ether * 3 / 24); + + vm.warp(block.timestamp + 24 hours); + assertEq(qm.availableQuota(Ether, 0), 10 ether); + } + + function test_quota_manager_consume_unconfigged() public { + address token = address(999); + assertEq(qm.availableQuota(token, 0), type(uint256).max); + + vm.prank(bridge); + qm.consumeQuota(token, 6 ether); + assertEq(qm.availableQuota(token, 0), type(uint256).max); + } +} diff --git a/packages/relayer/pkg/http/get_recommended_processing_fees.go b/packages/relayer/pkg/http/get_recommended_processing_fees.go index 0fd733f7e1b..63887d8766d 100644 --- a/packages/relayer/pkg/http/get_recommended_processing_fees.go +++ b/packages/relayer/pkg/http/get_recommended_processing_fees.go @@ -76,7 +76,7 @@ const ( Layer2 layer = iota ) -// getBlockInfoResponse +// GetRecommendedProcessingFees // // returns block info for the chains // diff --git a/packages/relayer/pkg/proof/types.go b/packages/relayer/pkg/proof/types.go index 457e55d5843..0a90df91ab1 100644 --- a/packages/relayer/pkg/proof/types.go +++ b/packages/relayer/pkg/proof/types.go @@ -35,7 +35,7 @@ func (q *Bytes) UnmarshalText(input []byte) error { type Slice [][]byte -// MarshalText implements encoding.TextMarshaler +// MarshalJSON implements encoding.TextMarshaler func (s Slice) MarshalJSON() ([]byte, error) { bs := make([]hexutil.Bytes, len(s)) for i, b := range s { @@ -45,7 +45,7 @@ func (s Slice) MarshalJSON() ([]byte, error) { return json.Marshal(bs) } -// UnmarshalText implements encoding.TextUnmarshaler. +// UnmarshalJSON implements encoding.TextUnmarshaler. func (s *Slice) UnmarshalJSON(data []byte) error { var bs []hexutil.Bytes if err := json.Unmarshal(data, &bs); err != nil { diff --git a/packages/relayer/processor/process_message.go b/packages/relayer/processor/process_message.go index bc609f8a019..0ce7b430b61 100644 --- a/packages/relayer/processor/process_message.go +++ b/packages/relayer/processor/process_message.go @@ -98,15 +98,11 @@ func (p *Processor) processMessage( return false, msgBody.TimesRetried, err } - receipt, err := p.sendProcessMessageCall(ctx, msgBody.Event, encodedSignalProof) + _, err = p.sendProcessMessageCall(ctx, msgBody.Event, encodedSignalProof) if err != nil { return false, msgBody.TimesRetried, err } - if receipt.Status != types.ReceiptStatusSuccessful { - return false, msgBody.TimesRetried, err - } - messageStatus, err := p.destBridge.MessageStatus(&bind.CallOpts{ Context: ctx, }, msgBody.Event.MsgHash) @@ -412,6 +408,9 @@ func (p *Processor) sendProcessMessageCall( if receipt.Status != types.ReceiptStatusSuccessful { relayer.MessageSentEventsProcessedReverted.Inc() + slog.Warn("Transaction reverted", "txHash", hex.EncodeToString(receipt.TxHash.Bytes()), + "srcTxHash", event.Raw.TxHash.Hex(), + "status", receipt.Status) return nil, errTxReverted } diff --git a/packages/taikoon-ui/package.json b/packages/taikoon-ui/package.json index ddba8f43c3e..abbb4b70cfa 100644 --- a/packages/taikoon-ui/package.json +++ b/packages/taikoon-ui/package.json @@ -37,7 +37,7 @@ "prettier-plugin-svelte": "^3.1.2", "raw-body": "^2.5.2", "svelte": "^4.2.7", - "svelte-check": "^3.6.0", + "svelte-check": "^3.7.1", "svelte-copy": "^1.4.2", "svelte-i18n": "^4.0.0", "svelte-scrolling": "^1.4.0",