From ec27c1a98ace269f4055f95e907042c810639c23 Mon Sep 17 00:00:00 2001 From: Vincent Geddes <117534+vgeddes@users.noreply.github.com> Date: Fri, 24 May 2024 01:03:06 +0200 Subject: [PATCH] Deployment scripts for BeefyClient & Gateway (#1190) --- contracts/scripts/DeployBeefyClient.sol | 44 +++++++++ contracts/scripts/DeployLocal.sol | 3 +- ...yLogic.sol => DeployLocalGatewayLogic.sol} | 2 +- contracts/scripts/UpgradeShell.sol | 91 +++++++++++++++++++ contracts/scripts/beefy-checkpoint.js | 25 +++++ contracts/scripts/minsigs.py | 20 ---- contracts/src/Gateway.sol | 33 ++++++- contracts/src/storage/OperatorStorage.sol | 18 ++++ contracts/test/Gateway.t.sol | 62 +++++++++++-- web/package.json | 2 +- web/packages/test-helpers/package.json | 2 +- web/packages/test/scripts/deploy-contracts.sh | 2 +- 12 files changed, 269 insertions(+), 35 deletions(-) create mode 100644 contracts/scripts/DeployBeefyClient.sol rename contracts/scripts/{DeployGatewayLogic.sol => DeployLocalGatewayLogic.sol} (96%) create mode 100644 contracts/scripts/UpgradeShell.sol create mode 100644 contracts/scripts/beefy-checkpoint.js delete mode 100644 contracts/scripts/minsigs.py create mode 100644 contracts/src/storage/OperatorStorage.sol diff --git a/contracts/scripts/DeployBeefyClient.sol b/contracts/scripts/DeployBeefyClient.sol new file mode 100644 index 0000000000..6eb863bad1 --- /dev/null +++ b/contracts/scripts/DeployBeefyClient.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {BeefyClient} from "../src/BeefyClient.sol"; + +contract DeployBeefyClient is Script { + struct Config { + uint64 startBlock; + BeefyClient.ValidatorSet current; + BeefyClient.ValidatorSet next; + uint256 randaoCommitDelay; + uint256 randaoCommitExpiration; + uint256 minimumSignatures; + } + + function readConfig() internal pure returns (Config memory config) { + // Checkpoint generated at block 20733663 using the `./beefy-checkpoint.js` script in Polkadot-JS. + // Block 20733663 is significant as that was when our bridge was initialized on BridgeHub. + config = Config({ + startBlock: 20733663, + current: BeefyClient.ValidatorSet({id: 496, length: 297, root: 0xdd04a3a0a4a19180bdae78ecc0c089491d22f5b65b685199d877f20b7fc76434}), + next: BeefyClient.ValidatorSet({id: 497, length: 297, root: 0xdd04a3a0a4a19180bdae78ecc0c089491d22f5b65b685199d877f20b7fc76434}), + randaoCommitDelay: 128, + randaoCommitExpiration: 24, + minimumSignatures: 17 + }); + } + + function run() public { + vm.startBroadcast(); + Config memory config = readConfig(); + + new BeefyClient( + config.randaoCommitDelay, + config.randaoCommitExpiration, + config.minimumSignatures, + config.startBlock, + config.current, + config.next + ); + } +} diff --git a/contracts/scripts/DeployLocal.sol b/contracts/scripts/DeployLocal.sol index 4f943f6246..713085fcb1 100644 --- a/contracts/scripts/DeployLocal.sol +++ b/contracts/scripts/DeployLocal.sol @@ -87,7 +87,8 @@ contract DeployLocal is Script { assetHubCreateAssetFee: uint128(vm.envUint("CREATE_ASSET_FEE")), assetHubReserveTransferFee: uint128(vm.envUint("RESERVE_TRANSFER_FEE")), exchangeRate: ud60x18(vm.envUint("EXCHANGE_RATE")), - multiplier: ud60x18(vm.envUint("FEE_MULTIPLIER")) + multiplier: ud60x18(vm.envUint("FEE_MULTIPLIER")), + rescueOperator: address(0) }); GatewayProxy gateway = new GatewayProxy(address(gatewayLogic), abi.encode(config)); diff --git a/contracts/scripts/DeployGatewayLogic.sol b/contracts/scripts/DeployLocalGatewayLogic.sol similarity index 96% rename from contracts/scripts/DeployGatewayLogic.sol rename to contracts/scripts/DeployLocalGatewayLogic.sol index e8009d568f..05a9f45c73 100644 --- a/contracts/scripts/DeployGatewayLogic.sol +++ b/contracts/scripts/DeployLocalGatewayLogic.sol @@ -8,7 +8,7 @@ import {ParaID} from "../src//Types.sol"; import {Script} from "forge-std/Script.sol"; import {stdJson} from "forge-std/StdJson.sol"; -contract DeployGatewayLogic is Script { +contract DeployLocalGatewayLogic is Script { using stdJson for string; function setUp() public {} diff --git a/contracts/scripts/UpgradeShell.sol b/contracts/scripts/UpgradeShell.sol new file mode 100644 index 0000000000..34d6cfce4f --- /dev/null +++ b/contracts/scripts/UpgradeShell.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import {WETH9} from "canonical-weth/WETH9.sol"; +import {Script} from "forge-std/Script.sol"; +import {BeefyClient} from "../src/BeefyClient.sol"; + +import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IShell} from "../src/interfaces/IShell.sol"; +import {GatewayProxy} from "../src/GatewayProxy.sol"; +import {Gateway} from "../src/Gateway.sol"; +import {MockGatewayV2} from "../test/mocks/MockGatewayV2.sol"; +import {Agent} from "../src/Agent.sol"; +import {AgentExecutor} from "../src/AgentExecutor.sol"; +import {ChannelID, ParaID, OperatingMode} from "../src/Types.sol"; +import {SafeNativeTransfer} from "../src/utils/SafeTransfer.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {UD60x18, ud60x18} from "prb/math/src/UD60x18.sol"; + +function mDot(uint32 value) pure returns (uint128) { + // 1 mDOT = 0.001 DOT + return value * (10 ** 7); +} + +function dot(uint32 value) pure returns (uint128) { + return value * (10 ** 10); +} + +contract UpgradeShell is Script { + using SafeNativeTransfer for address payable; + using stdJson for string; + + struct Config { + address gatewayProxy; + address beefyClient; + ParaID bridgeHubParaID; + bytes32 bridgeHubAgentID; + uint8 foreignTokenDecimals; + uint128 maxDestinationFee; + Gateway.Config initializerParams; + } + + function readConfig() internal pure returns (Config memory config) { + config = Config({ + gatewayProxy: 0x27ca963C279c93801941e1eB8799c23f407d68e7, + beefyClient: address(0), + bridgeHubParaID: ParaID.wrap(1002), + bridgeHubAgentID: 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314, + foreignTokenDecimals: 10, + maxDestinationFee: dot(2), + initializerParams: Gateway.Config({ + mode: OperatingMode.Normal, + deliveryCost: mDot(100), // 0.1 DOT + registerTokenFee: 0.002 ether, + assetHubParaID: ParaID.wrap(1000), + assetHubAgentID: 0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79, + assetHubCreateAssetFee: mDot(100), // 0.1 DOT + assetHubReserveTransferFee: mDot(100), // 0.1 DOT + exchangeRate: ud60x18(0.0024e18), + multiplier: ud60x18(1.33e18), + rescueOperator: 0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967 + }) + }); + } + + function run() public { + vm.startBroadcast(); + + Config memory config = readConfig(); + + // AgentExecutor + AgentExecutor executor = new AgentExecutor(); + + // Gateway implementation + Gateway gatewayLogic = new Gateway( + config.beefyClient, + address(executor), + config.bridgeHubParaID, + config.bridgeHubAgentID, + config.foreignTokenDecimals, + config.maxDestinationFee + ); + + IShell shell = IShell(config.gatewayProxy); + + shell.upgrade(address(gatewayLogic), address(gatewayLogic).codehash, abi.encode(config.initializerParams)); + + vm.stopBroadcast(); + } +} diff --git a/contracts/scripts/beefy-checkpoint.js b/contracts/scripts/beefy-checkpoint.js new file mode 100644 index 0000000000..e57be20b03 --- /dev/null +++ b/contracts/scripts/beefy-checkpoint.js @@ -0,0 +1,25 @@ +// Polkadot-JS script to generate a BEEFY checkpoint + +let beefyBlock = 20733663; +let blockHash = await api.rpc.chain.getBlockHash(beefyBlock); +let apiAtBlock = await api.at(blockHash); + +let authorities = await apiAtBlock.query.beefyMmrLeaf.beefyAuthorities(); +let nextAuthorities = + await apiAtBlock.query.beefyMmrLeaf.beefyNextAuthorities(); + +let beefyCheckpoint = { + startBlock: beefyBlock, + current: { + id: authorities.id.toNumber(), + root: authorities.keysetCommitment.toHex(), + length: authorities.len.toNumber(), + }, + next: { + id: nextAuthorities.id.toNumber(), + root: nextAuthorities.keysetCommitment.toHex(), + length: nextAuthorities.len.toNumber(), + }, +}; + +console.log(JSON.stringify(beefyCheckpoint, null, 2)); diff --git a/contracts/scripts/minsigs.py b/contracts/scripts/minsigs.py deleted file mode 100644 index 7fd5f0ea3a..0000000000 --- a/contracts/scripts/minsigs.py +++ /dev/null @@ -1,20 +0,0 @@ -import math - -MAX_VALIDATOR_COUNT = 200000 -MIN_REQUIRED_SIGNATURES = 10 - -curve = [(x, math.ceil(math.log2(3 * x))) for x in range(1, MAX_VALIDATOR_COUNT + 1)] - -output = [] -cursor = curve[0][1] - -for (x, y) in curve: - if y < MIN_REQUIRED_SIGNATURES: - continue - if y > cursor: - cursor = y - output.append((x, y)) - -print("x\ty") -for (x, y) in output: - print(f"{x}\t{y}") diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 130fee2352..14e86038b1 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -45,6 +45,7 @@ import { import {CoreStorage} from "./storage/CoreStorage.sol"; import {PricingStorage} from "./storage/PricingStorage.sol"; import {AssetsStorage} from "./storage/AssetsStorage.sol"; +import {OperatorStorage} from "./storage/OperatorStorage.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; @@ -416,7 +417,9 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint128 amount ) external payable { _submitOutbound( - Assets.sendToken(token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount) + Assets.sendToken( + token, msg.sender, destinationChain, destinationAddress, destinationFee, MAX_DESTINATION_FEE, amount + ) ); } @@ -562,6 +565,8 @@ contract Gateway is IGateway, IInitializable, IUpgradable { uint256 registerTokenFee; /// @dev Fee multiplier UD60x18 multiplier; + /// @dev Optional rescueOperator + address rescueOperator; } /// @dev Initialize storage in the gateway @@ -612,5 +617,31 @@ contract Gateway is IGateway, IInitializable, IUpgradable { assets.registerTokenFee = config.registerTokenFee; assets.assetHubCreateAssetFee = config.assetHubCreateAssetFee; assets.assetHubReserveTransferFee = config.assetHubReserveTransferFee; + + // Initialize operator storage + OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); + operatorStorage.operator = config.rescueOperator; + } + + /// @dev Temporary rescue ability for the initial bootstrapping phase of the bridge + function rescue(address impl, bytes32 implCodeHash, bytes calldata initializerParams) external { + OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); + if (msg.sender != operatorStorage.operator) { + revert Unauthorized(); + } + Upgrade.upgrade(impl, implCodeHash, initializerParams); + } + + function dropRescueAbility() external { + OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); + if (msg.sender != operatorStorage.operator) { + revert Unauthorized(); + } + operatorStorage.operator = address(0); + } + + function rescueOperator() external view returns (address) { + OperatorStorage.Layout storage operatorStorage = OperatorStorage.layout(); + return operatorStorage.operator; } } diff --git a/contracts/src/storage/OperatorStorage.sol b/contracts/src/storage/OperatorStorage.sol new file mode 100644 index 0000000000..6cc0f68e1a --- /dev/null +++ b/contracts/src/storage/OperatorStorage.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +library OperatorStorage { + struct Layout { + address operator; + } + + bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.operator"); + + function layout() internal pure returns (Layout storage $) { + bytes32 slot = SLOT; + assembly { + $.slot := slot + } + } +} diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 74fb60a9c1..0bee10391f 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -23,7 +23,6 @@ import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {MultiAddress} from "../src/MultiAddress.sol"; import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, MultiAddress} from "../src/Types.sol"; - import {NativeTransferFailed} from "../src/utils/SafeTransfer.sol"; import {PricingStorage} from "../src/storage/PricingStorage.sol"; @@ -99,12 +98,7 @@ contract GatewayTest is Test { function setUp() public { AgentExecutor executor = new AgentExecutor(); gatewayLogic = new MockGateway( - address(0), - address(executor), - bridgeHubParaID, - bridgeHubAgentID, - foreignTokenDecimals, - maxDestinationFee + address(0), address(executor), bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals, maxDestinationFee ); Gateway.Config memory config = Gateway.Config({ mode: OperatingMode.Normal, @@ -115,7 +109,8 @@ contract GatewayTest is Test { assetHubCreateAssetFee: createTokenFee, assetHubReserveTransferFee: sendTokenFee, exchangeRate: exchangeRate, - multiplier: multiplier + multiplier: multiplier, + rescueOperator: 0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967 }); gateway = new GatewayProxy(address(gatewayLogic), abi.encode(config)); MockGateway(address(gateway)).setCommitmentsAreVerified(true); @@ -857,6 +852,55 @@ contract GatewayTest is Test { IGateway(address(gateway)).quoteSendTokenFee(address(token), destPara, maxDestinationFee + 1); vm.expectRevert(Assets.InvalidDestinationFee.selector); - IGateway(address(gateway)).sendToken{value: fee}(address(token), destPara, recipientAddress32, maxDestinationFee + 1, 1); + IGateway(address(gateway)).sendToken{value: fee}( + address(token), destPara, recipientAddress32, maxDestinationFee + 1, 1 + ); + } + + function testRescuebyTrustedOperator() public { + // Upgrade to this new logic contract + MockGatewayV2 newLogic = new MockGatewayV2(); + + address impl = address(newLogic); + bytes32 implCodeHash = address(newLogic).codehash; + bytes memory initParams = abi.encode(42); + + // Expect the gateway to emit `Upgraded` + vm.expectEmit(true, false, false, false); + emit IUpgradable.Upgraded(address(newLogic)); + + hoax(0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967); + Gateway(address(gateway)).rescue(impl, implCodeHash, initParams); + + // Verify that the MockGatewayV2.initialize was called + assertEq(MockGatewayV2(address(gateway)).getValue(), 42); + } + + function testRescuebyPublicFails() public { + // Upgrade to this new logic contract + MockGatewayV2 newLogic = new MockGatewayV2(); + + address impl = address(newLogic); + bytes32 implCodeHash = address(newLogic).codehash; + bytes memory initParams = abi.encode(42); + + vm.expectRevert(Gateway.Unauthorized.selector); + Gateway(address(gateway)).rescue(impl, implCodeHash, initParams); + } + + function testDropRescueAbility() public { + // Upgrade to this new logic contract + MockGatewayV2 newLogic = new MockGatewayV2(); + + address impl = address(newLogic); + bytes32 implCodeHash = address(newLogic).codehash; + bytes memory initParams = abi.encode(42); + + hoax(0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967); + Gateway(address(gateway)).dropRescueAbility(); + + vm.expectRevert(Gateway.Unauthorized.selector); + hoax(0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967); + Gateway(address(gateway)).rescue(impl, implCodeHash, initParams); } } diff --git a/web/package.json b/web/package.json index 96e11cefc1..da83f4bdd6 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,7 @@ "private": true, "engines": { "node": "^20 || ^18", - "pnpm": "^8" + "pnpm": ">=8" }, "scripts": { "preinstall": "npx only-allow pnpm", diff --git a/web/packages/test-helpers/package.json b/web/packages/test-helpers/package.json index 6d9f0f0fab..d4f82bcbe0 100644 --- a/web/packages/test-helpers/package.json +++ b/web/packages/test-helpers/package.json @@ -18,6 +18,7 @@ "format": "prettier src --write" }, "devDependencies": { + "@types/node": "^18.16.8", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", "eslint": "^8.26.0", @@ -38,7 +39,6 @@ "@typechain/ethers-v5": "^10.1.1", "@types/keccak": "^3.0.1", "@types/lodash": "^4.14.186", - "@types/node": "^18.13.0", "@types/secp256k1": "^4.0.3", "@types/seedrandom": "^3.0.2", "bitfield": "^4.1.0", diff --git a/web/packages/test/scripts/deploy-contracts.sh b/web/packages/test/scripts/deploy-contracts.sh index 46e0c12485..c623a56232 100755 --- a/web/packages/test/scripts/deploy-contracts.sh +++ b/web/packages/test/scripts/deploy-contracts.sh @@ -27,7 +27,7 @@ deploy_command() { deploy_gateway_logic() { - deploy_command scripts/DeployGatewayLogic.sol:DeployGatewayLogic + deploy_command scripts/DeployLocalGatewayLogic.sol:DeployLocalGatewayLogic pushd "$test_helpers_dir" pnpm generateContracts "$output_dir/contracts.json"