diff --git a/README.md b/README.md index 3ec53e48..b5b24d9e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ A bridge between Base and blockchains outside the Ethereum ecosystem. Currently has support for Solana. -> [!WARNING] -> -> The bridge is only live on testnet between Solana Devnet <> Base Sepolia. The code is a WIP and has not yet been audited. Do not use in production!! - [![GitHub contributors](https://img.shields.io/github/contributors/base/bridge)](https://github.com/base/bridge/graphs/contributors) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..9c7a3c83 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security + +## Bug bounty program + +In line with our strategy of being the safest way for users to access crypto: + ++ Coinbase extended our [best-in-industry](https://www.coinbase.com/blog/celebrating-10-years-of-our-bug-bounty-program) million-dollar [HackerOne bug bounty program](https://hackerone.com/coinbase?type=team) to cover the Base network and Base infrastructure. + ++ Coinbase has launched a 5 million-dollar [Cantina bug bounty program](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b) to cover all deployed smart contracts for Base, and those used as part of Coinbase products and services. + +## Reporting vulnerabilities + +All potential vulnerability reports can be submitted via the following platforms: + +1. [**HackerOne**](https://hackerone.com/coinbase): For offchain components and services. + For more information on reporting vulnerabilities and our HackerOne bug bounty program, view our [security program policies](https://hackerone.com/coinbase?view_policy=true). + +2. [**Cantina**](https://cantina.xyz/bounties/55316f42-3c5e-4746-9bd0-0f18dcbc344b): For deployed smart contracts. + For more information on what smart contracts are considered within the scope of the Cantina bug bounty program, view our [Tier 0](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b/overview?overviewTab=1&assetGroup=0) and [Tier 1](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b/overview?overviewTab=1&assetGroup=1) scope guides. + + +For all other security related inquiries, please reach out to [security@coinbase.com](mailto:security@coinbase.com). \ No newline at end of file diff --git a/audits/report-cantinacode-coinbase-bridge-add-on.pdf b/audits/report-cantinacode-coinbase-bridge-add-on.pdf new file mode 100644 index 00000000..fd545ab5 Binary files /dev/null and b/audits/report-cantinacode-coinbase-bridge-add-on.pdf differ diff --git a/audits/report-cantinacode-coinbase-bridge-pt2.pdf b/audits/report-cantinacode-coinbase-bridge-pt2.pdf new file mode 100644 index 00000000..e8c337e1 Binary files /dev/null and b/audits/report-cantinacode-coinbase-bridge-pt2.pdf differ diff --git a/audits/report-cantinacode-coinbase-bridge-pt3.pdf b/audits/report-cantinacode-coinbase-bridge-pt3.pdf new file mode 100644 index 00000000..5d0fb9e1 Binary files /dev/null and b/audits/report-cantinacode-coinbase-bridge-pt3.pdf differ diff --git a/audits/report-cantinacode-coinbase-bridge.pdf b/audits/report-cantinacode-coinbase-bridge.pdf new file mode 100644 index 00000000..3ec7aa18 Binary files /dev/null and b/audits/report-cantinacode-coinbase-bridge.pdf differ diff --git a/base/Makefile b/base/Makefile index 6656f487..6870b134 100644 --- a/base/Makefile +++ b/base/Makefile @@ -1,34 +1,93 @@ -include .env -BASE_RPC=https://base-sepolia.cbhq.net -# BASE_RPC=https://mainnet.base.org - -# Update values in this section to specify token recipients / token amount -#################################################################################################### -# alpha or prod -ENV_NAME=alpha - -SOLANA_SOL_RECEIVER = 0x9827993b7b317eb6c74279d49074a13b33a3495a30637c86a7513922014ba424 -USER_SPL_ATA = 0x6621d2d930ef7ada6260d3e0a5679a8fe81fde41262314f2efbf9c4c828550b1 -USER_REMOTE_ERC20_ATA = 0x4394d16902c022f8f87c7667429bc8b2860d033e316b9f9515534505fae314e9 -USER_REMOTE_ETH_ATA = 0x15e9307085f98b2e64eb3dd36b7fbafea201cc2a9fcb36c8c207e99bc7b97f30 - -AMOUNT = 1000000000 -#################################################################################################### - -BRIDGE = $(shell jq -r '.Bridge' deployments/base_sepolia_$(ENV_NAME).json) -LOCAL_SOL = $(shell jq -r '.WrappedSOL' deployments/base_sepolia_$(ENV_NAME).json) -REMOTE_SOL = $(shell jq -r '.RemoteSOL' deployments/base_sepolia_$(ENV_NAME).json) -LOCAL_SPL = $(shell jq -r '.WrappedSPL' deployments/base_sepolia_$(ENV_NAME).json) -REMOTE_SPL = $(shell jq -r '.RemoteSPL' deployments/base_sepolia_$(ENV_NAME).json) -REMOTE_ERC20 = $(shell jq -r '.RemoteERC20' deployments/base_sepolia_$(ENV_NAME).json) -REMOTE_ETH = $(shell jq -r '.RemoteETH' deployments/base_sepolia_$(ENV_NAME).json) - +# Environment mapping: ENV -> RPC URL, deployment file, BRIDGE_ENVIRONMENT +# Usage: make ENV={testnet-alpha|testnet-prod|mainnet} +VALID_ENVS := testnet-alpha testnet-prod mainnet + +ifeq ($(ENV),testnet-alpha) + BASE_RPC := https://base-sepolia.cbhq.net + DEPLOYMENT_FILE := deployments/base_sepolia_alpha.json + BRIDGE_ENV := alpha +else ifeq ($(ENV),testnet-prod) + BASE_RPC := https://base-sepolia.cbhq.net + DEPLOYMENT_FILE := deployments/base_sepolia_prod.json + BRIDGE_ENV := prod +else ifeq ($(ENV),mainnet) + BASE_RPC := https://mainnet.base.org + DEPLOYMENT_FILE := deployments/base_mainnet.json + BRIDGE_ENV := mainnet +endif + +ifndef LEDGER_ACCOUNT +override LEDGER_ACCOUNT = 0 +endif + +# Recipient addresses and amount (override via make args or env vars) +SOLANA_SOL_RECEIVER ?= 0x6e0019e37547b086395a9a6834f731bab5b631004ac22a6c9102047301e40c77 +USER_SPL_ATA ?= 0xfc6697ab185712eed628c12e499a3f4c2d4f73ced9dcc6eab8658bea86adb0e7 +USER_REMOTE_ERC20_ATA ?= 0x7227267f49cac157705e1a5f7ac9f9933c902ae5368cbcc2a12e06d0566f826c +USER_REMOTE_ETH_ATA ?= 0x3e506b04e88dab39a3687fc5eb822c18f8301fc09cfe945c3394fc6baf9aef25 + +AMOUNT ?= 1000000000 + +# Contract addresses (read from deployment file, only when ENV is set) LOCAL_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE -LOCAL_ERC20 = 0x62C1332822983B8412A6Ffda0fd77cd7d5733Ee9 -check: - @echo $(REMOTE_ERC20) +ifdef DEPLOYMENT_FILE +BRIDGE = $(shell jq -r '.Bridge' $(DEPLOYMENT_FILE)) +LOCAL_SOL = $(shell jq -r '.WrappedSOL' $(DEPLOYMENT_FILE)) +REMOTE_SOL = $(shell jq -r '.RemoteSOL' $(DEPLOYMENT_FILE)) +LOCAL_SPL = $(shell jq -r '.WrappedSPL' $(DEPLOYMENT_FILE)) +REMOTE_SPL = $(shell jq -r '.RemoteSPL' $(DEPLOYMENT_FILE)) +REMOTE_ERC20 = $(shell jq -r '.RemoteERC20' $(DEPLOYMENT_FILE)) +REMOTE_ETH = $(shell jq -r '.RemoteETH' $(DEPLOYMENT_FILE)) +LOCAL_ERC20 = $(shell jq -r '.LocalERC20' $(DEPLOYMENT_FILE)) +endif + +# Environment validation +.PHONY: validate-env +validate-env: +ifndef ENV + $(error ENV is required. Usage: make ENV={testnet-alpha|testnet-prod|mainnet}) +endif +ifeq ($(filter $(ENV),$(VALID_ENVS)),) + $(error Invalid ENV='$(ENV)'. Must be one of: $(VALID_ENVS)) +endif + +.PHONY: help +help: + @echo "Usage: make ENV={testnet-alpha|testnet-prod|mainnet}" + @echo "" + @echo "Environments:" + @echo " testnet-alpha Base Sepolia (alpha deployment)" + @echo " testnet-prod Base Sepolia (prod deployment)" + @echo " mainnet Base Mainnet" + @echo "" + @echo "Bridge targets (require ENV):" + @echo " bridge-sol-to-solana Bridge wrapped SOL to Solana" + @echo " bridge-spl-to-solana Bridge wrapped SPL to Solana" + @echo " bridge-erc20-to-solana Bridge ERC20 to Solana" + @echo " bridge-eth-to-solana Bridge ETH to Solana" + @echo "" + @echo "Deployment targets (require ENV):" + @echo " deploy Deploy bridge contracts" + @echo " upgrade Upgrade bridge contracts" + @echo " create-wrapped-spl Create wrapped SPL token" + @echo " create-mock-token Deploy a mock ERC20" + @echo "" + @echo "Query targets (require ENV):" + @echo " check-root Check the bridge root" + @echo " check-count Check the bridge message count" + @echo "" + @echo "Other targets:" + @echo " deps Install forge dependencies" + @echo " bindings Generate Go bindings" + @echo " coverage Run test coverage" + @echo "" + @echo "Overridable variables:" + @echo " AMOUNT Token amount (default: 1000000000)" + @echo " SOLANA_SOL_RECEIVER SOL recipient address" + @echo " LEDGER_ACCOUNT Ledger HD account index (default: 0)" .PHONY: deps deps: clean-lib forge-deps @@ -55,70 +114,63 @@ bindings: coverage: @ forge coverage --no-match-coverage "(script|test)" -.PHONY: dev-deploy -dev-deploy: deploy create-wrapped-sol create-wrapped-spl - .PHONY: deploy -deploy: - BRIDGE_ENVIRONMENT=$(ENV_NAME) forge script DeployScript --account testnet-admin --rpc-url $(BASE_RPC) --sender $(shell cast wallet address --account testnet-admin) --broadcast -vvvv +deploy: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) forge script DeployScript --account testnet-admin --rpc-url $(BASE_RPC) --sender $(shell cast wallet address --account testnet-admin) --broadcast -vvvv .PHONY: upgrade -upgrade: - BRIDGE_ENVIRONMENT=$(ENV_NAME) forge script UpgradeScript --account testnet-upgrade --rpc-url $(BASE_RPC) --broadcast -vvvv - -.PHONY: create-wrapped-sol -create-wrapped-sol: - BRIDGE_ENVIRONMENT=$(ENV_NAME) TOKEN_NAME=WrappedSOL TOKEN_SYMBOL=wSOL REMOTE_TOKEN=$(REMOTE_SOL) forge script CreateTokenScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv +upgrade: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) forge script UpgradeScript --account testnet-upgrade --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: create-wrapped-spl -create-wrapped-spl: - BRIDGE_ENVIRONMENT=$(ENV_NAME) TOKEN_NAME=WrappedSPL TOKEN_SYMBOL=wSPL REMOTE_TOKEN=$(REMOTE_SPL) forge script CreateTokenScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv +create-wrapped-spl: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) TOKEN_NAME=WrappedSPL TOKEN_SYMBOL=wSPL REMOTE_TOKEN=$(REMOTE_SPL) TOKEN_DECIMALS=9 forge script CreateTokenScript --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: create-mock-token -create-mock-token: - ADMIN=$(shell cast wallet address --account testnet-admin) forge script DeployERC20 --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv +create-mock-token: validate-env + ADMIN=0x644e3DedB0e4F83Bfcf8F9992964d240224B74dc forge script DeployERC20 --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: bridge-sol-to-solana -bridge-sol-to-solana: - BRIDGE_ENVIRONMENT=$(ENV_NAME) \ +bridge-sol-to-solana: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) \ LOCAL_TOKEN=$(LOCAL_SOL) \ REMOTE_TOKEN=$(REMOTE_SOL) \ TO=$(SOLANA_SOL_RECEIVER) \ AMOUNT=$(AMOUNT) \ - forge script BridgeTokensToSolanaScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv + forge script BridgeTokensToSolanaScript --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: bridge-spl-to-solana -bridge-spl-to-solana: - BRIDGE_ENVIRONMENT=$(ENV_NAME) \ +bridge-spl-to-solana: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) \ LOCAL_TOKEN=$(LOCAL_SPL) \ REMOTE_TOKEN=$(REMOTE_SPL) \ TO=$(USER_SPL_ATA) \ AMOUNT=$(AMOUNT) \ - forge script BridgeTokensToSolanaScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv + forge script BridgeTokensToSolanaScript --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: bridge-erc20-to-solana -bridge-erc20-to-solana: - BRIDGE_ENVIRONMENT=$(ENV_NAME) \ +bridge-erc20-to-solana: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) \ LOCAL_TOKEN=$(LOCAL_ERC20) \ REMOTE_TOKEN=$(REMOTE_ERC20) \ TO=$(USER_REMOTE_ERC20_ATA) \ AMOUNT=$(AMOUNT) \ NEEDS_APPROVAL=true \ - forge script BridgeTokensToSolanaScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv + forge script BridgeTokensToSolanaScript --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: bridge-eth-to-solana -bridge-eth-to-solana: - BRIDGE_ENVIRONMENT=$(ENV_NAME) \ +bridge-eth-to-solana: validate-env + BRIDGE_ENVIRONMENT=$(BRIDGE_ENV) \ LOCAL_TOKEN=$(LOCAL_ETH) \ REMOTE_TOKEN=$(REMOTE_ETH) \ TO=$(USER_REMOTE_ETH_ATA) \ AMOUNT=$(AMOUNT) \ - forge script BridgeTokensToSolanaScript --account testnet-admin --rpc-url $(BASE_RPC) --broadcast -vvvv + forge script BridgeTokensToSolanaScript --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --rpc-url $(BASE_RPC) --broadcast -vvvv .PHONY: check-root -check-root: - @ cast call $(BRIDGE) "getRoot()" --rpc-url https://base-sepolia-dev.cbhq.net:8545 +check-root: validate-env + @ cast call $(BRIDGE) "getRoot()" --rpc-url $(BASE_RPC) .PHONY: check-count -check-count: - @ cast call 0xCdfe10f911eD5039E031D6a7be3a0F97fA061C38 "count()" --rpc-url https://base-sepolia-dev.cbhq.net:8545 +check-count: validate-env + @ cast call $(BRIDGE) "count()" --rpc-url $(BASE_RPC) diff --git a/base/README.md b/base/README.md index ae032fc0..1be11504 100644 --- a/base/README.md +++ b/base/README.md @@ -4,6 +4,7 @@ A cross-chain bridge implementation that enables seamless message passing and to ## Contract Addresses +- **Base Mainnet Bridge**: `0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188` - **Base Sepolia Bridge**: `0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B` ## Overview diff --git a/base/deployments/base_mainnet.json b/base/deployments/base_mainnet.json new file mode 100644 index 00000000..0a614664 --- /dev/null +++ b/base/deployments/base_mainnet.json @@ -0,0 +1,14 @@ +{ + "Bridge": "0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188", + "BridgeValidator": "0xAF24c1c24Ff3BF1e6D882518120fC25442d6794B", + "CrossChainERC20Factory": "0xDD56781d0509650f8C2981231B6C917f2d5d7dF2", + "Twin": "0xb326c02150bb0De265Bb0eCeDA53531ab0163bf6", + "RelayerOrchestrator": "0x8Cfa6F29930E6310B6074baB0052c14a709B4741", + "RemoteSOL": "0x069be72ab836d4eacc02525b7350a78a395da2f1253a40ebafd6630000000000", + "RemoteSPL": "", + "RemoteERC20": "", + "RemoteETH": "", + "WrappedSOL": "0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82", + "WrappedSPL": "", + "LocalERC20": "0x4870D23984Dd663005EB8E2b616e4Ef62630183c" +} diff --git a/base/deployments/base_sepolia_alpha.json b/base/deployments/base_sepolia_alpha.json index f2a78b7b..8687378e 100644 --- a/base/deployments/base_sepolia_alpha.json +++ b/base/deployments/base_sepolia_alpha.json @@ -1,13 +1,14 @@ { - "Bridge": "0x91a5d5A71bC3Bd7a835050ED4A337B95De0Ae757", - "BridgeValidator": "0xBc540a534A6DE9Cb1B40b44bf3ABC6CdcE1C1eb1", + "Bridge": "0x64567a9147fa89B1edc987e36Eb6f4b6db71656b", + "BridgeValidator": "0xC05324843aca6C2b7446F15bdB17AF4599b761E6", "CrossChainERC20Factory": "0x8316DEBfdc3093DfFb10233B76F32481b7386950", "Twin": "0xc98727c438237BC7ABf0A1A9Ff25C74bAdcf9d11", - "RelayerOrchestrator": "0x5D988c61086839CeBd9eC714F3A6E23E0F05098F", + "RelayerOrchestrator": "0xfDa0B31c24316CA01DAa9d3E741729884C1e2ab8", "RemoteSOL": "0x069be72ab836d4eacc02525b7350a78a395da2f1253a40ebafd6630000000000", "RemoteSPL": "0x6ccf56ff18093bc61462cd67c8aa86216fbad049e41d9bffa0856e5f34af5498", "RemoteERC20": "0xba6a03ad9fe4921c8a5a4e8d3ef0a8d76ac6137ea1213879512abc24c6ab4376", "RemoteETH": "0x79f4f1fa82df5ae874eaff38c8288c15a0317eab263a6a2f4ed32ca9224ba62c", - "WrappedSOL": "0xC50EA8CAeDaE290FE4edA770b10aDEfc41CD698e", - "WrappedSPL": "0xCf8e666c57651670AA7266Aba3E334E3600B2306" -} \ No newline at end of file + "WrappedSOL": "0x003512146Fd54b71f926C7Fd4B7bd20Fc84E22c5", + "WrappedSPL": "0x80351342c4dd23C78c0837C640E041a239e67cD8", + "LocalERC20": "0x62C1332822983B8412A6Ffda0fd77cd7d5733Ee9" +} diff --git a/base/deployments/base_sepolia_prod.json b/base/deployments/base_sepolia_prod.json index 641b6c66..3b7fc206 100644 --- a/base/deployments/base_sepolia_prod.json +++ b/base/deployments/base_sepolia_prod.json @@ -9,5 +9,6 @@ "RemoteERC20": "0xc7cfe164b0b61064d6f23734f906c8141788be442d94495852d8af53b40e3212", "RemoteETH": "0xcb3d6f70c63eee106144cdf6cf934901b684be88171380e87a3f8d03f720a185", "WrappedSOL": "0xCace0c896714DaF7098FFD8CC54aFCFe0338b4BC", - "WrappedSPL": "0x955C7356776F9304feD38ed5AeC5699436C7C614" + "WrappedSPL": "0x955C7356776F9304feD38ed5AeC5699436C7C614", + "LocalERC20": "0x62C1332822983B8412A6Ffda0fd77cd7d5733Ee9" } diff --git a/base/deployments/template.json b/base/deployments/template.json index 80b87f9f..4b5f8610 100644 --- a/base/deployments/template.json +++ b/base/deployments/template.json @@ -9,5 +9,6 @@ "WrappedSOL": "", "WrappedSPL": "", "RemoteERC20": "", - "RemoteETH": "" + "RemoteETH": "", + "LocalERC20": "" } diff --git a/base/script/actions/CreateToken.s.sol b/base/script/actions/CreateToken.s.sol index 73954f69..b57e05a7 100644 --- a/base/script/actions/CreateToken.s.sol +++ b/base/script/actions/CreateToken.s.sol @@ -13,6 +13,7 @@ contract CreateTokenScript is DevOps { using LibString for string; bytes32 public immutable REMOTE_TOKEN = vm.envBytes32("REMOTE_TOKEN"); + uint8 public immutable TOKEN_DECIMALS = uint8(vm.envUint("TOKEN_DECIMALS")); string public tokenName = vm.envString("TOKEN_NAME"); string public tokenSymbol = vm.envString("TOKEN_SYMBOL"); @@ -25,7 +26,7 @@ contract CreateTokenScript is DevOps { function run() public { vm.startBroadcast(); address token = crossChainErc20Factory.deploy({ - remoteToken: REMOTE_TOKEN, name: tokenName, symbol: tokenSymbol, decimals: 9 + remoteToken: REMOTE_TOKEN, name: tokenName, symbol: tokenSymbol, decimals: TOKEN_DECIMALS }); console.log("Deployed Token at: %s", token); vm.stopBroadcast(); diff --git a/scripts/README.md b/scripts/README.md index ed795455..8750c8cf 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -2,6 +2,10 @@ Interactive command-line interface for managing the Base-Solana bridge operations. +> [!WARNING] +> +> The scripts in this directory are for example purposes only and have not been audited. Use at your own risk. + ## Setup ```bash diff --git a/scripts/src/commands/sol/bridge/base-to-solana/prove-message.handler.ts b/scripts/src/commands/sol/bridge/base-to-solana/prove-message.handler.ts index 98330189..dd8f03c6 100644 --- a/scripts/src/commands/sol/bridge/base-to-solana/prove-message.handler.ts +++ b/scripts/src/commands/sol/bridge/base-to-solana/prove-message.handler.ts @@ -13,7 +13,7 @@ import { type Address, type Hash, } from "viem"; -import { baseSepolia } from "viem/chains"; +import { base, baseSepolia } from "viem/chains"; import { decodeEventLog } from "viem/utils"; import { fetchBridge, getProveMessageInstruction } from "@base/bridge/bridge"; @@ -25,7 +25,7 @@ import { getKeypairSignerFromPath, getIdlConstant, } from "@internal/sol"; -import { CONFIGS, DEPLOY_ENVS } from "@internal/constants"; +import { CONFIGS, DEPLOY_ENVS, type Config } from "@internal/constants"; import { BRIDGE_ABI } from "@internal/base/abi"; export const argsSchema = z.object({ @@ -82,6 +82,7 @@ export async function handleProveMessage(args: Args) { logger.info(`Output Root: ${outputRootAddress}`); const { event, rawProof } = await generateProof( + config, args.transactionHash as Hash, baseBlockNumber, config.base.bridgeContract @@ -138,12 +139,13 @@ export async function handleProveMessage(args: Args) { } async function generateProof( + cfg: Config, transactionHash: Hash, bridgeBaseBlockNumber: bigint, baseBridgeAddress: Address ) { const publicClient = createPublicClient({ - chain: baseSepolia, + chain: cfg.base.chain, transport: http(), }); diff --git a/scripts/src/commands/sol/bridge/index.ts b/scripts/src/commands/sol/bridge/index.ts index afd89326..2aac26ae 100644 --- a/scripts/src/commands/sol/bridge/index.ts +++ b/scripts/src/commands/sol/bridge/index.ts @@ -5,6 +5,7 @@ import { solVaultCommand } from "./sol-vault.command"; import { bridgeCallCommand, bridgeSolCommand, + bridgeSolWithBcCommand, bridgeSplCommand, bridgeWrappedTokenCommand, wrapTokenCommand, @@ -20,6 +21,7 @@ bridgeCommand.addCommand(solVaultCommand); bridgeCommand.addCommand(bridgeCallCommand); bridgeCommand.addCommand(bridgeSolCommand); +bridgeCommand.addCommand(bridgeSolWithBcCommand); bridgeCommand.addCommand(bridgeSplCommand); bridgeCommand.addCommand(bridgeWrappedTokenCommand); bridgeCommand.addCommand(wrapTokenCommand); diff --git a/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.command.ts b/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.command.ts new file mode 100644 index 00000000..8fd3b6a4 --- /dev/null +++ b/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.command.ts @@ -0,0 +1,98 @@ +import { Command } from "commander"; + +import { + getInteractiveConfirm, + getOrPromptEvmAddress, + getOrPromptDecimal, + getOrPromptFilePath, + getOrPromptDeployEnv, + getOrPromptHex, + getOrPromptInteger, + validateAndExecute, + getOrPromptHash, +} from "@internal/utils/cli"; +import { + argsSchema, + handleBridgeSolWithBc, +} from "./bridge-sol-with-bc.handler"; + +type CommanderOptions = { + deployEnv?: string; + to?: string; + amount?: string; + builderCode?: string; + feeBps?: string; + payerKp?: string; + payForRelay?: boolean; +}; + +async function collectInteractiveOptions( + options: CommanderOptions +): Promise { + let opts = { ...options }; + + if (!opts.deployEnv) { + opts.deployEnv = await getOrPromptDeployEnv(); + } + + opts.to = await getOrPromptEvmAddress( + opts.to, + "Enter user address on Base (recipient for hookData)" + ); + + opts.amount = await getOrPromptDecimal( + opts.amount, + "Enter amount to bridge (in SOL)", + 0.001 + ); + + opts.builderCode = await getOrPromptHash( + opts.builderCode, + "Enter builder code (bytes32, 0x followed by 64 hex chars)" + ); + + opts.feeBps = await getOrPromptInteger( + opts.feeBps, + "Enter fee in basis points (e.g., 100 for 1%)", + 0, + 10000 + ); + + opts.payerKp = await getOrPromptFilePath( + opts.payerKp, + "Enter payer keypair path (or 'config' for Solana CLI config)", + ["config"] + ); + + if (opts.payForRelay === undefined) { + opts.payForRelay = await getInteractiveConfirm( + "Pay for relaying the message to Base?", + true + ); + } + + return opts; +} + +export const bridgeSolWithBcCommand = new Command("bridge-sol-with-bc") + .description("Bridge SOL from Solana to Base with Builder Code attribution") + .option( + "--deploy-env ", + "Target deploy environment (testnet-alpha | testnet-prod | mainnet)" + ) + .option("--to
", "User address on Base (for hookData)") + .option("--amount ", "Amount to bridge in SOL") + .option( + "--builder-code ", + "Builder code (bytes32, 0x followed by 64 hex chars)" + ) + .option("--fee-bps ", "Fee in basis points (e.g., 100 for 1%)") + .option( + "--payer-kp ", + "Payer keypair: 'config' or custom payer keypair path" + ) + .option("--pay-for-relay", "Pay for relaying the message to Base") + .action(async (options) => { + const opts = await collectInteractiveOptions(options); + await validateAndExecute(argsSchema, opts, handleBridgeSolWithBc); + }); diff --git a/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.handler.ts b/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.handler.ts new file mode 100644 index 00000000..7f5eee57 --- /dev/null +++ b/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol-with-bc.handler.ts @@ -0,0 +1,204 @@ +import { z } from "zod"; +import { + getProgramDerivedAddress, + type Instruction, + createSolanaRpc, +} from "@solana/kit"; +import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system"; +import { + toBytes, + isAddress as isEvmAddress, + encodeAbiParameters, + encodeFunctionData, +} from "viem"; + +import { + CallType, + fetchBridge, + getBridgeSolInstruction, +} from "@base/bridge/bridge"; + +import { logger } from "@internal/logger"; +import { FLYWHEEL_ABI } from "@internal/base/abi"; +import { + buildAndSendTransaction, + getSolanaCliConfigKeypairSigner, + getKeypairSignerFromPath, + getIdlConstant, + relayMessageToBase, + monitorMessageExecution, + buildPayForRelayInstruction, + outgoingMessagePubkey, + solVaultPubkey, +} from "@internal/sol"; +import { CONFIGS, DEPLOY_ENVS } from "@internal/constants"; + +export const argsSchema = z.object({ + deployEnv: z + .enum(DEPLOY_ENVS, { + message: + "Deploy environment must be 'testnet-alpha', 'testnet-prod', or 'mainnet'", + }) + .default("testnet-prod"), + to: z + .string() + .refine((value) => isEvmAddress(value), { + message: "Invalid Base/Ethereum address format", + }) + .brand<"baseAddress">(), + amount: z + .string() + .transform((val) => parseFloat(val)) + .refine((val) => !isNaN(val) && val > 0, { + message: "Amount must be a positive number", + }), + builderCode: z + .string() + .regex(/^0x[a-fA-F0-9]{64}$/, { + message: + "Builder code must be a valid bytes32 (0x followed by 64 hex characters)", + }) + .brand<"builderCode">(), + feeBps: z + .string() + .transform((val) => parseInt(val)) + .refine((val) => !isNaN(val) && val >= 0 && val <= 10000, { + message: "Fee BPS must be a number between 0 and 10000", + }), + payerKp: z + .union([z.literal("config"), z.string().brand<"payerKp">()]) + .default("config"), + payForRelay: z.boolean().default(true), +}); + +type Args = z.infer; +type PayerKpArg = Args["payerKp"]; + +export async function handleBridgeSolWithBc(args: Args): Promise { + try { + logger.info("--- Bridge SOL with Builder Code script ---"); + + const config = CONFIGS[args.deployEnv]; + const rpc = createSolanaRpc(config.solana.rpcUrl); + logger.info(`RPC URL: ${config.solana.rpcUrl}`); + + const payer = await resolvePayerKeypair(args.payerKp); + logger.info(`Payer: ${payer.address}`); + + const [bridgeAccountAddress] = await getProgramDerivedAddress({ + programAddress: config.solana.bridgeProgram, + seeds: [Buffer.from(getIdlConstant("BRIDGE_SEED"))], + }); + logger.info(`Bridge account: ${bridgeAccountAddress}`); + + const bridge = await fetchBridge(rpc, bridgeAccountAddress); + + const solVaultAddress = await solVaultPubkey(config.solana.bridgeProgram); + logger.info(`Sol Vault: ${solVaultAddress}`); + + // Calculate scaled amount (amount * 10^decimals) + const scaledAmount = BigInt(Math.floor(args.amount * Math.pow(10, 9))); + logger.info(`Amount: ${args.amount}`); + logger.info(`Scaled amount: ${scaledAmount}`); + + const { salt, pubkey: outgoingMessage } = await outgoingMessagePubkey( + config.solana.bridgeProgram + ); + logger.info(`Outgoing message: ${outgoingMessage}`); + + // Builder Code logic + logger.info(`User address (for hookData): ${args.to}`); + logger.info(`Builder code: ${args.builderCode}`); + logger.info(`Fee BPS: ${args.feeBps}`); + + // 1. Build hookData = abi.encode(user, code, feeBps) + const hookData = encodeAbiParameters( + [ + { type: "address", name: "user" }, + { type: "bytes32", name: "code" }, + { type: "uint16", name: "feeBps" }, + ], + [args.to as `0x${string}`, args.builderCode as `0x${string}`, args.feeBps] + ); + logger.info(`Hook data: ${hookData}`); + + // 2. Build call data for Flywheel.send(campaign, token, hookData) + const wSolAddress = config.base.wSol; + logger.info(`wSOL address: ${wSolAddress}`); + logger.info(`Flywheel address: ${config.base.flywheelContract}`); + logger.info(`Bridge campaign address: ${config.base.flywheelCampaign}`); + + const flywheelCallData = encodeFunctionData({ + abi: FLYWHEEL_ABI, + functionName: "send", + args: [config.base.flywheelCampaign, wSolAddress, hookData], + }); + logger.info(`Flywheel call data: ${flywheelCallData}`); + + // 3. Build the bridge instruction with call to Flywheel + const ixs: Instruction[] = [ + getBridgeSolInstruction( + { + // Accounts + payer, + from: payer, + gasFeeReceiver: bridge.data.gasConfig.gasFeeReceiver, + solVault: solVaultAddress, + bridge: bridgeAccountAddress, + outgoingMessage, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + + // Arguments + outgoingMessageSalt: salt, + to: toBytes(config.base.flywheelCampaign), // Send to campaign, not user + amount: scaledAmount, + call: { + ty: CallType.Call, + to: toBytes(config.base.flywheelContract), + value: 0n, + data: Buffer.from(flywheelCallData.slice(2), "hex"), + }, + }, + { programAddress: config.solana.bridgeProgram } + ), + ]; + + if (args.payForRelay) { + ixs.push( + await buildPayForRelayInstruction( + args.deployEnv, + outgoingMessage, + payer + ) + ); + } + + logger.info("Sending transaction..."); + const signature = await buildAndSendTransaction( + { type: "deploy-env", value: args.deployEnv }, + ixs, + payer + ); + logger.success("Bridge SOL with Builder Code operation completed!"); + logger.success(`Signature: ${signature}`); + + if (args.payForRelay) { + await monitorMessageExecution(args.deployEnv, outgoingMessage); + } else { + await relayMessageToBase(args.deployEnv, outgoingMessage); + } + } catch (error) { + logger.error("Bridge SOL with Builder Code operation failed:", error); + throw error; + } +} + +async function resolvePayerKeypair(payerKpArg: PayerKpArg) { + if (payerKpArg === "config") { + logger.info("Using Solana CLI config for payer keypair"); + return await getSolanaCliConfigKeypairSigner(); + } + + logger.info(`Using custom payer keypair: ${payerKpArg}`); + return await getKeypairSignerFromPath(payerKpArg); +} diff --git a/scripts/src/commands/sol/bridge/solana-to-base/bridge-wrapped-token.command.ts b/scripts/src/commands/sol/bridge/solana-to-base/bridge-wrapped-token.command.ts index 963ec811..08848062 100644 --- a/scripts/src/commands/sol/bridge/solana-to-base/bridge-wrapped-token.command.ts +++ b/scripts/src/commands/sol/bridge/solana-to-base/bridge-wrapped-token.command.ts @@ -84,7 +84,7 @@ async function collectInteractiveOptions( opts.amount = await getOrPromptDecimal( opts.amount, "Enter amount to bridge (in token units)", - 0.001 + 0.000000001 ); opts.payerKp = await getOrPromptFilePath( diff --git a/scripts/src/commands/sol/bridge/solana-to-base/index.ts b/scripts/src/commands/sol/bridge/solana-to-base/index.ts index bd07db79..ba087cff 100644 --- a/scripts/src/commands/sol/bridge/solana-to-base/index.ts +++ b/scripts/src/commands/sol/bridge/solana-to-base/index.ts @@ -1,5 +1,6 @@ export * from "./bridge-call.command"; export * from "./bridge-sol.command"; +export * from "./bridge-sol-with-bc.command"; export * from "./bridge-spl.command"; export * from "./bridge-wrapped-token.command"; export * from "./wrap-token.command"; diff --git a/scripts/src/commands/sol/deploy.handler.ts b/scripts/src/commands/sol/deploy.handler.ts index 462af048..4521faea 100644 --- a/scripts/src/commands/sol/deploy.handler.ts +++ b/scripts/src/commands/sol/deploy.handler.ts @@ -80,7 +80,7 @@ export async function handleDeploy(args: Args): Promise { // Deploy program logger.info("Deploying program..."); - await $`solana program deploy --url ${config.solana.cluster} --keypair ${deployerKpPath} --program-id ${programKpPath} ${programBinaryPath}`; + await $`solana program deploy --url ${config.solana.rpcUrl} --keypair ${deployerKpPath} --program-id ${programKpPath} ${programBinaryPath}`; logger.success("Program deployment completed!"); } catch (error) { diff --git a/scripts/src/internal/base/abi/flywheel.abi.ts b/scripts/src/internal/base/abi/flywheel.abi.ts new file mode 100644 index 00000000..e5bbde3c --- /dev/null +++ b/scripts/src/internal/base/abi/flywheel.abi.ts @@ -0,0 +1,689 @@ +export const FLYWHEEL_ABI = [ + { inputs: [], stateMutability: "nonpayable", type: "constructor" }, + { inputs: [], name: "CampaignDoesNotExist", type: "error" }, + { inputs: [], name: "FailedDeployment", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "balance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + ], + name: "InsufficientBalance", + type: "error", + }, + { inputs: [], name: "InsufficientCampaignFunds", type: "error" }, + { inputs: [], name: "InvalidCampaignStatus", type: "error" }, + { inputs: [], name: "Reentrancy", type: "error" }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "SendFailed", + type: "error", + }, + { inputs: [], name: "ZeroAddress", type: "error" }, + { inputs: [], name: "ZeroAmount", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "hooks", + type: "address", + }, + ], + name: "CampaignCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { indexed: false, internalType: "string", name: "uri", type: "string" }, + ], + name: "CampaignMetadataUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "enum Flywheel.CampaignStatus", + name: "oldStatus", + type: "uint8", + }, + { + indexed: false, + internalType: "enum Flywheel.CampaignStatus", + name: "newStatus", + type: "uint8", + }, + ], + name: "CampaignStatusUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "FeeAllocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "FeeDistributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "FeeSent", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "FeeTransferFailed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "FundsWithdrawn", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "PayoutAllocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "PayoutDeallocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { indexed: false, internalType: "bytes32", name: "key", type: "bytes32" }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "PayoutDistributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "campaign", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "extraData", + type: "bytes", + }, + ], + name: "PayoutSent", + type: "event", + }, + { + inputs: [], + name: "CAMPAIGN_IMPLEMENTATION", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "allocate", + outputs: [ + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Allocation[]", + name: "allocations", + type: "tuple[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + ], + name: "allocatedFee", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + ], + name: "allocatedPayout", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "campaign", type: "address" }], + name: "campaignExists", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "campaign", type: "address" }], + name: "campaignHooks", + outputs: [{ internalType: "address", name: "hooks", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "campaign", type: "address" }], + name: "campaignStatus", + outputs: [ + { + internalType: "enum Flywheel.CampaignStatus", + name: "status", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "campaign", type: "address" }], + name: "campaignURI", + outputs: [{ internalType: "string", name: "uri", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "hooks", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "createCampaign", + outputs: [{ internalType: "address", name: "campaign", type: "address" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "deallocate", + outputs: [ + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Allocation[]", + name: "allocations", + type: "tuple[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "distribute", + outputs: [ + { + components: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Distribution[]", + name: "distributions", + type: "tuple[]", + }, + { + components: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Distribution[]", + name: "fees", + type: "tuple[]", + }, + { internalType: "bool", name: "sendFeesNow", type: "bool" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "distributeFees", + outputs: [ + { + components: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Distribution[]", + name: "distributions", + type: "tuple[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "hooks", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "predictCampaignAddress", + outputs: [{ internalType: "address", name: "campaign", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "send", + outputs: [ + { + components: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Payout[]", + name: "payouts", + type: "tuple[]", + }, + { + components: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bytes", name: "extraData", type: "bytes" }, + ], + internalType: "struct Flywheel.Distribution[]", + name: "fees", + type: "tuple[]", + }, + { internalType: "bool", name: "sendFeesNow", type: "bool" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "totalAllocatedFees", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "totalAllocatedPayouts", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "updateMetadata", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { + internalType: "enum Flywheel.CampaignStatus", + name: "newStatus", + type: "uint8", + }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "updateStatus", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "campaign", type: "address" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "withdrawFunds", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/scripts/src/internal/base/abi/index.ts b/scripts/src/internal/base/abi/index.ts index 03945636..b093c27e 100644 --- a/scripts/src/internal/base/abi/index.ts +++ b/scripts/src/internal/base/abi/index.ts @@ -1,2 +1,3 @@ export * from "./bridge.abi"; export * from "./bridge-validator.abi"; +export * from "./flywheel.abi"; diff --git a/scripts/src/internal/constants.ts b/scripts/src/internal/constants.ts index a61902cc..4366fd43 100644 --- a/scripts/src/internal/constants.ts +++ b/scripts/src/internal/constants.ts @@ -45,6 +45,8 @@ export type Config = { // Contracts bridgeContract: EvmAddress; counterContract: EvmAddress; + flywheelContract: EvmAddress; + flywheelCampaign: EvmAddress; // ERC20s erc20: EvmAddress; @@ -85,6 +87,8 @@ export const CONFIGS = { // Contracts bridgeContract: "0x64567a9147fa89B1edc987e36Eb6f4b6db71656b", counterContract: "0x5d3eB988Daa06151b68369cf957e917B4371d35d", + flywheelContract: "0x00000f14ad09382841db481403d1775adee1179f", + flywheelCampaign: "0x7626f7F9A574f526066acE9073518DaB1Bee038C", // ERC20s erc20: "0x62C1332822983B8412A6Ffda0fd77cd7d5733Ee9", @@ -123,6 +127,8 @@ export const CONFIGS = { // Contracts bridgeContract: "0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B", counterContract: "0x5d3eB988Daa06151b68369cf957e917B4371d35d", + flywheelContract: "0x00000f14ad09382841db481403d1775adee1179f", + flywheelCampaign: "0x7626f7F9A574f526066acE9073518DaB1Bee038C", // ERC20s erc20: "0x62C1332822983B8412A6Ffda0fd77cd7d5733Ee9", @@ -141,29 +147,33 @@ export const CONFIGS = { baseRelayerKpPath: "keypairs/base-relayer.mainnet.json", // Base oracle signers - evmLocalKey: "0x", - evmKeychainKey: "0x", + evmLocalKey: "0x68fb9f14256fb52944c65f4afd207c2153ec18f1", + evmKeychainKey: "0x9B1494C2d4c0dedbd8DA203054ee7de08c138836", // Programs - bridgeProgram: "" as Address, - baseRelayerProgram: "" as Address, + bridgeProgram: address("HNCne2FkVaNghhjKXapxJzPaBvAKDG1Ge3gqhZyfVWLM"), + baseRelayerProgram: address( + "g1et5VenhfJHJwsdJsDbxWZuotD5H4iELNG61kS4fb9" + ), // SPLs - spl: "" as Address, - wEth: "" as Address, - wErc20: "" as Address, + spl: address("9YEGpFKedz7i8hMB7gDWQGuAfCRHUKBMCbTjnMi8vtUc"), + wEth: address("2ZCFyWM6WthDLBo41zJsMQmjJ4Kvb6yumvrbLpVh9LMX"), + wErc20: address("7qxnUBBmW8oiuz9skKkGQFvY1qRUP6zF3emA5bneyGaJ"), }, base: { chain: base, // Contracts - bridgeContract: "0x", + bridgeContract: "0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188", counterContract: "0x", + flywheelContract: "0x", + flywheelCampaign: "0x", // ERC20s - erc20: "0x", - wSol: "0x", - wSpl: "0x", + erc20: "0x4870D23984Dd663005EB8E2b616e4Ef62630183c", + wSol: "0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82", + wSpl: "0xcd9E97cf45BC53acC35A5aFb70458c47c214E7C7", }, }, } as const satisfies Record; diff --git a/scripts/src/internal/sol/base-relayer.ts b/scripts/src/internal/sol/base-relayer.ts index 22040957..af2d2d58 100644 --- a/scripts/src/internal/sol/base-relayer.ts +++ b/scripts/src/internal/sol/base-relayer.ts @@ -48,7 +48,7 @@ export async function buildPayForRelayInstruction( // Arguments outgoingMessage: outgoingMessage, - gasLimit: 200_000n, + gasLimit: 2_000_000n, }, { programAddress: config.solana.baseRelayerProgram } ); diff --git a/solana/README.md b/solana/README.md index 0defccbd..4c999c73 100644 --- a/solana/README.md +++ b/solana/README.md @@ -4,6 +4,9 @@ A cross-chain bridge program that enables seamless message passing between Solan ## Program IDs +- **Mainnet Bridge**: `HNCne2FkVaNghhjKXapxJzPaBvAKDG1Ge3gqhZyfVWLM` +- **Mainnet Base Relayer**: `g1et5VenhfJHJwsdJsDbxWZuotD5H4iELNG61kS4fb9` + - **Devnet Bridge**: `7c6mteAcTXaQ1MFBCrnuzoZVTTAEfZwa6wgy4bqX3KXC` - **Devnet Base Relayer**: `56MBBEYAtQAdjT4e1NzHD8XaoyRSTvfgbSVVcEcHj51H`