diff --git a/codecov.yml b/codecov.yml index 4159ca255e..aa9bfb581b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,20 +9,9 @@ ignore: - "contracts/src/utils" - "contracts/src/ScaleCodec.sol" - "contracts/src/SubstrateTypes.sol" - - "polkadot-sdk/bridges/snowbridge/pallets/ethereum-beacon-client/src/benchmarking" - - "polkadot-sdk/bridges/snowbridge/pallets/ethereum-beacon-client/src/weights.rs" - - "polkadot-sdk/bridges/snowbridge/pallets/inbound-queue/fixtures" - - "polkadot-sdk/bridges/snowbridge/pallets/ethereum-client/fixtures" - - "polkadot-sdk/bridges/snowbridge/pallets/ethereum-client/fuzz" - "contracts/src/DeployScript.sol" - - "parachain/pallets/inbound-queue/fixtures" - - "parachain/pallets/ethereum-client/fixtures" flags: solidity: paths: - contracts carryforward: true - rust: - paths: - - polkadot-sdk/bridges/snowbridge - carryforward: true diff --git a/contracts/src/DeployGatewayLogic.sol b/contracts/src/DeployGatewayLogic.sol new file mode 100644 index 0000000000..8c5cc70282 --- /dev/null +++ b/contracts/src/DeployGatewayLogic.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import {AgentExecutor} from "./AgentExecutor.sol"; +import {Gateway} from "./Gateway.sol"; +import {ParaID} from "./Types.sol"; +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +contract DeployGatewayLogic is Script { + using stdJson for string; + + function setUp() public {} + + function run() public { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.rememberKey(privateKey); + vm.startBroadcast(deployer); + + address beefyClient = vm.envAddress("BEEFY_CLIENT_CONTRACT_ADDRESS"); + + ParaID bridgeHubParaID = ParaID.wrap(uint32(vm.envUint("BRIDGE_HUB_PARAID"))); + bytes32 bridgeHubAgentID = vm.envBytes32("BRIDGE_HUB_AGENT_ID"); + + uint8 foreignTokenDecimals = uint8(vm.envUint("FOREIGN_TOKEN_DECIMALS")); + + AgentExecutor executor = new AgentExecutor(); + new Gateway(address(beefyClient), address(executor), bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 990b42807b..13614bfb9b 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -89,7 +89,7 @@ contract Gateway is IGateway, IInitializable { error InvalidAgentExecutionPayload(); error InvalidCodeHash(); error InvalidConstructorParams(); - error InvalidTransact(); + error AlreadyInitialized(); // handler functions are privileged modifier onlySelf() { @@ -585,6 +585,11 @@ contract Gateway is IGateway, IInitializable { Config memory config = abi.decode(data, (Config)); CoreStorage.Layout storage core = CoreStorage.layout(); + + if (core.channels[PRIMARY_GOVERNANCE_CHANNEL_ID].agent != address(0)) { + revert AlreadyInitialized(); + } + core.mode = config.mode; // Initialize agent for BridgeHub diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 9b7659232e..2494d88164 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -496,6 +496,65 @@ contract GatewayTest is Test { assertEq(GatewayV2(address(gateway)).getValue(), 42); } + function testUgradeInitializerRunsOnlyOnce() public { + // Upgrade to this current logic contract + AgentExecutor executor = new AgentExecutor(); + GatewayMock currentLogic = + new GatewayMock(address(0), address(executor), bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals); + + Gateway.Config memory config = Gateway.Config({ + mode: OperatingMode.Normal, + deliveryCost: outboundFee, + registerTokenFee: registerTokenFee, + assetHubParaID: assetHubParaID, + assetHubAgentID: assetHubAgentID, + assetHubCreateAssetFee: createTokenFee, + assetHubReserveTransferFee: sendTokenFee, + exchangeRate: exchangeRate + }); + + UpgradeParams memory params = UpgradeParams({ + impl: address(currentLogic), + implCodeHash: address(currentLogic).codehash, + initParams: abi.encode(config) + }); + + vm.expectRevert(Gateway.AlreadyInitialized.selector); + // Expect the gateway to emit `Upgraded` + GatewayMock(address(gateway)).upgradePublic(abi.encode(params)); + } + + function testUpgradeSkipsInitializerIfNoneProvided() public { + bytes32 agentID = keccak256("123"); + + testSetPricingParameters(); + uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); + assertEq(fee, 10000000000000000); + + testCreateAgent(); + assertNotEq(GatewayMock(address(gateway)).agentOf(agentID), address(0)); + + // Upgrade to this current logic contract + AgentExecutor executor = new AgentExecutor(); + GatewayMock currentLogic = + new GatewayMock(address(0), address(executor), bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals); + + bytes memory initParams; // empty + UpgradeParams memory params = UpgradeParams({ + impl: address(currentLogic), + implCodeHash: address(currentLogic).codehash, + initParams: initParams + }); + + // Expect the gateway to emit `Upgraded` + GatewayMock(address(gateway)).upgradePublic(abi.encode(params)); + + // Verify that storage was not overwritten + fee = IGateway(address(gateway)).quoteRegisterTokenFee(); + assertEq(fee, 10000000000000000); + assertNotEq(GatewayMock(address(gateway)).agentOf(agentID), address(0)); + } + function testUpgradeGatewayMock() public { GatewayUpgradeMock newLogic = new GatewayUpgradeMock(); uint256 d0 = 99; diff --git a/docs/.gitbook/assets/Screenshot 2024-02-16 at 11.21.53.png b/docs/.gitbook/assets/Screenshot 2024-02-16 at 11.21.53.png new file mode 100644 index 0000000000..c983f87565 Binary files /dev/null and b/docs/.gitbook/assets/Screenshot 2024-02-16 at 11.21.53.png differ diff --git a/docs/.gitbook/assets/image (2).png b/docs/.gitbook/assets/image (2).png new file mode 100644 index 0000000000..a65666b384 Binary files /dev/null and b/docs/.gitbook/assets/image (2).png differ diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index e3dbf32e04..b0c25839dc 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -37,3 +37,6 @@ ## Runbooks * [Updating Snowbridge Pallets, BridgeHub & AssetHub Runtimes](runbooks/updating-snowbridge-pallets-bridgehub-and-assethub-runtimes.md) +* [Initialize Ethereum light client with Forced Checkpoint](runbooks/initialize-ethereum-light-client-with-forced-checkpoint.md) +* [Initial Deployment of Gateway Contracts](runbooks/initial-deployment-of-gateway-contracts.md) +* [Halt Bridge in Case of Emergency](runbooks/halt-bridge-in-case-of-emergency.md) diff --git a/docs/rococo-testnet/rococo-sepolia-token-transfers.md b/docs/rococo-testnet/rococo-sepolia-token-transfers.md index cda1bb0364..e5146ca29a 100644 --- a/docs/rococo-testnet/rococo-sepolia-token-transfers.md +++ b/docs/rococo-testnet/rococo-sepolia-token-transfers.md @@ -14,7 +14,7 @@ Snowbridge has initially only activated support for the sending of ERC20 tokens If you already have [WETH](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14) you can skip this step. If not go to the [WETH](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14) contract and make a deposit. -Weth Contract Address: [0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14) +WETH Contract Address: [0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14) 1. Click on the Contracts tab. 2. Click on the Write Contract tab. @@ -28,11 +28,11 @@ Weth Contract Address: [0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14](https://sepo Snowbridge needs to be an approved spender for you to send tokens to Polkadot. -Gateway Contract Address: [0x5b4909ce6ca82d2ce23bd46738953c7959e710cd](https://sepolia.etherscan.io/address/0x5b4909ce6ca82d2ce23bd46738953c7959e710cd) +WETH Contract Address: [0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14](https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14) Navigate to the Contract tab, click on Write Contract, and connect your wallet. -1. Enter Snowbridge’s Gateway contract as the `guy` (spender) parameter. +1. Enter Snowbridge’s Gateway contract as the `guy` (spender) parameter. Gateway Contract Address: [0x5b4909ce6ca82d2ce23bd46738953c7959e710cd](https://sepolia.etherscan.io/address/0x5b4909ce6ca82d2ce23bd46738953c7959e710cd) 2. Enter the amount that Snowbridge can spend as the `wad` parameter. In this example, we have used 5 Gwei (5000000000 Wei).
diff --git a/docs/runbooks/halt-bridge-in-case-of-emergency.md b/docs/runbooks/halt-bridge-in-case-of-emergency.md new file mode 100644 index 0000000000..cb9e33560a --- /dev/null +++ b/docs/runbooks/halt-bridge-in-case-of-emergency.md @@ -0,0 +1,27 @@ +--- +description: >- + In a case of an emergency (e.g. a exploitable vulnerability found), the bridge + needs to be halted so that no messages can be processed. +--- + +# Halt Bridge in Case of Emergency + +### Halting Mechanism + +The bridge is halted through a storage item in each Snowbridge pallet called `OperatingMode` . + +Possible operating mode states are: + +* `Normal` +* `Halted` (`RejectingOutboundMessages` for the `system` pallet) + +If the operating mode is set to [`Halted`](https://github.com/Snowfork/polkadot-sdk/blob/2536e780bf6af052e1d9e85a8b2648aae91ec6d7/bridges/snowbridge/primitives/core/src/operating\_mode.rs#L12), no bridge messages will be processed. Each pallet needs to be disabled individually. Here are the pallets that need to be disabled, with the call hash to do so: + +* Ethereum client pallet: `0x520301` +* Inbound queue pallet: `0x500101` +* Outbound queue pallet: `0x510001` +* Ethereum system pallet: `0x530101` + +These extrinsics should be done from the relay chain, descending to the BridgeHub parachain origin, similar to the [force beacon checkpoint call](https://app.gitbook.com/o/bDGMcdShFBeGc3v6VzHf/s/tC80IPpnYgEJmgOYIpqZ/\~/changes/72/runbooks/initialize-ethereum-light-client-with-forced-checkpoint). + +If the bridge was halted, no messages will be processed. When the operating mode is changed to `normal` messages will be continue being processed. diff --git a/docs/runbooks/initial-deployment-of-gateway-contracts.md b/docs/runbooks/initial-deployment-of-gateway-contracts.md new file mode 100644 index 0000000000..ae5db71fa2 --- /dev/null +++ b/docs/runbooks/initial-deployment-of-gateway-contracts.md @@ -0,0 +1,85 @@ +--- +description: >- + How to upgrade the Ethereum gateway controlling the bridge on the Ethereum + side. +--- + +# Initial Deployment of Gateway Contracts + +### Build and Deploy Contracts + +To build and deploy the new contracts, follow these steps from inside the [Snowbridge repository](https://github.com/snowfork/snowbridge): + +#### Generate BEEFY State + +Set the following in .env file: + +```bash +export RELAYCHAIN_ENDPOINT= +export BEEFY_START_BLOCK= # default is 1 +``` + +The BEEFY start block can be selected from a recent block that has a BEEFY commitment. + +Run `scripts/generate-beefy-checkoutpoint.sh`. This will leave the beefy state in the contracts folder. + +#### Deploy Contracts + +Set the following in .env file: + +```bash +# Secret +export INFURA_PROJECT_ID= +export ETHERSCAN_API_KEY= +export DEPLOYER_ETH_KEY= + +# Chain +export ETH_NETWORK=< sepolia /mainnet > +export ETH_NETWORK_ID=< 11155111 /1 > + +# Endpoints +export RELAYCHAIN_ENDPOINT=wss://rococo-rpc.polkadot.io +export ETH_RPC_ENDPOINT=https://sepolia.infura.io/v3 +export ETH_WS_ENDPOINT=wss://sepolia.infura.io/ws/v3 +export BEACON_HTTP_ENDPOINT=https://lodestar-sepolia.chainsafe.io + +# Beefy +export BEEFY_START_BLOCK=8592280 # Default value +export MINIMUM_REQUIRED_SIGNATURES=16 +export ETH_RANDAO_DELAY=128 # 4 epochs=128 slots=25.6mins +export ETH_RANDAO_EXP=6 # 6 slots before expired + +# Channels and Agents +export BRIDGE_HUB_PARAID=1013 +export BRIDGE_HUB_AGENT_ID=0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314 +export ASSET_HUB_PARAID=1000 +export ASSET_HUB_AGENT_ID=0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79 +export REJECT_OUTBOUND_MESSAGES=false + +# Fees +export CREATE_ASSET_FEE=10000000000 +export DELIVERY_COST=10000000000 +export EXCHANGE_RATE=25000000000000 +export REGISTER_TOKEN_FEE=5000000000000000000 +export RESERVE_TRANSFER_FEE=10000000000 + +export FOREIGN_TOKEN_DECIMALS=12 + +# Initial agents deposits. Set low on purpose as they can be topped up manually +export ETH_BRIDGE_HUB_INITIAL_DEPOSIT=1000000 +``` + +Run `scripts/deploy-contracts.sh` + +Back up `contracts.json` and contract artifacts to s3: [https://s3.console.aws.amazon.com/s3/buckets/snowbridge-rococo-demo?region=eu-central-1\&bucketType=general\&tab=objects](https://s3.console.aws.amazon.com/s3/buckets/snowbridge-rococo-demo?region=eu-central-1\&bucketType=general\&tab=objects) + +Confirm settings with the team and get ETH for the deployment. + +Add contracts to tenderly + +### Update the EthereumGatewayAddress on BridgeHub + +To update the `EthereumGateway` [contract address](https://github.com/Snowfork/polkadot-sdk/blob/snowbridge/bridges/snowbridge/pallets/inbound-queue/src/lib.rs#L112), a set storage call is executed: + +* Example call hash: `0xff00630003000100d50f03082f000006020700c817a804824f1200a400040440aed97c7854d601808b98ae43079dafb3505b4909ce6ca82d2ce23bd46738953c7959e710cd` +* Link to pre-populate on Rococo: [https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frococo-rpc.polkadot.io#/extrinsics/decode/0xff00630003000100d50f03082f000006020700c817a804824f1200a400040440aed97c7854d601808b98ae43079dafb3505b4909ce6ca82d2ce23bd46738953c7959e710cd](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frococo-rpc.polkadot.io#/extrinsics/decode/0xff00630003000100d50f03082f000006020700c817a804824f1200a400040440aed97c7854d601808b98ae43079dafb3505b4909ce6ca82d2ce23bd46738953c7959e710cd\)) diff --git a/docs/runbooks/initialize-ethereum-light-client-with-forced-checkpoint.md b/docs/runbooks/initialize-ethereum-light-client-with-forced-checkpoint.md new file mode 100644 index 0000000000..5c6aec53ec --- /dev/null +++ b/docs/runbooks/initialize-ethereum-light-client-with-forced-checkpoint.md @@ -0,0 +1,33 @@ +--- +description: >- + Generate a beacon checkpoint to sync the Ethereum client from. This is done on + bridge initialization or forced sync reset. +--- + +# Initialize Ethereum light client with Forced Checkpoint + +## Generate Checkpoint Data + +On the server where the relayer is deployed, run the following command: + +``` +relayer/build/snowbridge-relay generate-beacon-checkpoint --url http://127.0.0.1:9596 +``` + +The command will output the beacon checkpoint data in hex form. Prepend `0x5200` to the resulting hex (the [Ethereum client pallet index](https://github.com/Snowfork/polkadot-sdk/blob/snowbridge/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs#L730) and the `force_checkpoint` [call index](https://github.com/Snowfork/polkadot-sdk/blob/snowbridge/bridges/snowbridge/pallets/ethereum-client/src/lib.rs#L216) combined). + +### Call Force Checkpoint from Relay Chain + +Use the resulting call data to make the Transact call to set the beacon checkpoint. + +* call hash: `0xff00630003000100d50f03082f00000602070008d6e82982ee3600a5025200821017d7c8b03a2a182824cfe569187a28faa718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000002101851a76c1adff357d59b36327d02cfb7f718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000000` +* link to pre-populate on Rococo: [https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frococo-rpc.polkadot.io#/extrinsics/decode/0xff00630003000100d50f03082f00000602070008d6e82982ee3600a5025200821017d7c8b03a2a182824cfe569187a28faa718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000002101851a76c1adff357d59b36327d02cfb7f718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000000](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frococo-rpc.polkadot.io#/extrinsics/decode/0xff00630003000100d50f03082f00000602070008d6e82982ee3600a5025200821017d7c8b03a2a182824cfe569187a28faa718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000002101851a76c1adff357d59b36327d02cfb7f718368a0ace36e2b1b8b6dbd7f8093c0594aa8a9c557dabac173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a05392000000000000000000) + +Replace the encoded call bytes highlighted in the screenshot below with the checkpoint data generated in the previous step: + +

Replace encoded call bytes with hex generated in the first step.

+ + + + + diff --git a/docs/runbooks/updating-snowbridge-pallets-bridgehub-and-assethub-runtimes.md b/docs/runbooks/updating-snowbridge-pallets-bridgehub-and-assethub-runtimes.md index 601dba41ae..b6a1b6171b 100644 --- a/docs/runbooks/updating-snowbridge-pallets-bridgehub-and-assethub-runtimes.md +++ b/docs/runbooks/updating-snowbridge-pallets-bridgehub-and-assethub-runtimes.md @@ -87,8 +87,8 @@ Configuration items to periodically configure for Snowbridge: These items would require a referenda item to be created on OpenGov. -TBH Items: +TBC Items: -* Which wallet will be used to create the referenda? Snowfork team members personal wallets? +* Which wallet will be used to create the referenda? * Exact steps to create referenda (e.g. extrinsic, renferenda on Polkassembly) -* Where is the voting and enactment periods set? +* Confirm where voting and enactment periods are set? diff --git a/web/packages/test/scripts/deploy-contracts.sh b/web/packages/test/scripts/deploy-contracts.sh index 5ac60bf26e..652720e1ed 100755 --- a/web/packages/test/scripts/deploy-contracts.sh +++ b/web/packages/test/scripts/deploy-contracts.sh @@ -3,8 +3,9 @@ set -eu source scripts/set-env.sh -deploy_contracts() -{ +deploy_command() { + local deploy_script=$1 + pushd "$contract_dir" if [ "$eth_network" != "localhost" ]; then forge script \ @@ -13,15 +14,31 @@ deploy_contracts() --verify \ --etherscan-api-key $etherscan_api_key \ -vvv \ - src/DeployScript.sol:DeployScript + $deploy_script else forge script \ --rpc-url $eth_endpoint_http \ --broadcast \ -vvv \ - src/DeployScript.sol:DeployScript + $deploy_script fi popd +} + +deploy_gateway_logic() +{ + deploy_command src/DeployGatewayLogic.sol:DeployGatewayLogic + + pushd "$test_helpers_dir" + pnpm generateContracts "$output_dir/contracts.json" + popd + + echo "Exported contract artifacts: $output_dir/contracts.json" +} + +deploy_contracts() +{ + deploy_command src/DeployScript.sol:DeployScript pushd "$test_helpers_dir" pnpm generateContracts "$output_dir/contracts.json"