diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml new file mode 100644 index 0000000..e30e7ca --- /dev/null +++ b/.github/workflows/cancel.yml @@ -0,0 +1,14 @@ +name: Cancel +on: [push] +jobs: + cancel: + name: "Cancel Previous Build" + if: github.ref != 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: styfle/cancel-workflow-action@0.9.1 + with: + workflow_id: ".github/workflows/test.yml" + all_but_latest: true + access_token: ${{ github.token }} \ No newline at end of file diff --git a/.github/workflows/relayer.yml b/.github/workflows/test.yml similarity index 83% rename from .github/workflows/relayer.yml rename to .github/workflows/test.yml index 8ffa3b2..cfaf5f8 100644 --- a/.github/workflows/relayer.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ # This workflow will test relayer -name: Relayer +name: Test on: push: @@ -51,12 +51,23 @@ jobs: - name: Check mage run: mage --version + - name: Run Forge fmt + run: | + forge fmt overridden_contracts/ --check + id: fmt + - name: Build run: mage build - name: Check if go contract bindings are up-to-date run: git diff --exit-code ./relays/contracts || (echo "The contract bindings are not up-to-date against contracts." && exit 1) - - name: Test + - name: Test Relayer run: mage test + - name: Run Forge tests + run: | + cd snowbridge/contracts + forge test -vvv + id: test + diff --git a/overridden_contracts/src/Gateway.sol b/overridden_contracts/src/Gateway.sol index fa8515f..40274a1 100644 --- a/overridden_contracts/src/Gateway.sol +++ b/overridden_contracts/src/Gateway.sol @@ -56,6 +56,7 @@ import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; import {Operators} from "./Operators.sol"; import {IOGateway} from "./interfaces/IOGateway.sol"; +import {IMiddlewareBasic} from "./interfaces/IMiddlewareBasic.sol"; contract Gateway is IOGateway, IInitializable, IUpgradable { using Address for address; @@ -105,6 +106,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { error TokenNotRegistered(); error CantSetMiddlewareToZeroAddress(); error CantSetMiddlewareToSameAddress(); + error MiddlewareNotSet(); // Message handlers can only be dispatched by the gateway itself modifier onlySelf() { @@ -255,6 +257,16 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { catch { success = false; } + } else if (message.command == Command.ReportSlashes) { + // We need to put all this inside a generic try-catch, since we dont want to revert decoding nor anything + try Gateway(this).reportSlashes{gas: maxDispatchGas}(message.params) {} + catch Error(string memory err) { + emit UnableToProcessSlashMessageS(err); + success = false; + } catch (bytes memory err) { + emit UnableToProcessSlashMessageB(err); + success = false; + } } // Calculate a gas refund, capped to protect against huge spikes in `tx.gasprice` @@ -281,23 +293,17 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { return CoreStorage.layout().mode; } - function channelOperatingModeOf( - ChannelID channelID - ) external view returns (OperatingMode) { + function channelOperatingModeOf(ChannelID channelID) external view returns (OperatingMode) { Channel storage ch = _ensureChannel(channelID); return ch.mode; } - function channelNoncesOf( - ChannelID channelID - ) external view returns (uint64, uint64) { + function channelNoncesOf(ChannelID channelID) external view returns (uint64, uint64) { Channel storage ch = _ensureChannel(channelID); return (ch.inboundNonce, ch.outboundNonce); } - function agentOf( - bytes32 agentID - ) external view returns (address) { + function agentOf(bytes32 agentID) external view returns (address) { return _ensureAgent(agentID); } @@ -315,9 +321,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { */ // Execute code within an agent - function agentExecute( - bytes calldata data - ) external onlySelf { + function agentExecute(bytes calldata data) external onlySelf { AgentExecuteParams memory params = abi.decode(data, (AgentExecuteParams)); address agent = _ensureAgent(params.agentID); @@ -335,9 +339,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Create an agent for a consensus system on Polkadot - function createAgent( - bytes calldata data - ) external onlySelf { + function createAgent(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); CreateAgentParams memory params = abi.decode(data, (CreateAgentParams)); @@ -354,9 +356,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Create a messaging channel for a Polkadot parachain - function createChannel( - bytes calldata data - ) external onlySelf { + function createChannel(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); CreateChannelParams memory params = abi.decode(data, (CreateChannelParams)); @@ -379,9 +379,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Update the configuration for a channel - function updateChannel( - bytes calldata data - ) external onlySelf { + function updateChannel(bytes calldata data) external onlySelf { UpdateChannelParams memory params = abi.decode(data, (UpdateChannelParams)); Channel storage ch = _ensureChannel(params.channelID); @@ -396,17 +394,13 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Perform an upgrade of the gateway - function upgrade( - bytes calldata data - ) external onlySelf { + function upgrade(bytes calldata data) external onlySelf { UpgradeParams memory params = abi.decode(data, (UpgradeParams)); Upgrade.upgrade(params.impl, params.implCodeHash, params.initParams); } // @dev Set the operating mode of the gateway - function setOperatingMode( - bytes calldata data - ) external onlySelf { + function setOperatingMode(bytes calldata data) external onlySelf { CoreStorage.Layout storage $ = CoreStorage.layout(); SetOperatingModeParams memory params = abi.decode(data, (SetOperatingModeParams)); $.mode = params.mode; @@ -414,9 +408,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // @dev Transfer funds from an agent to a recipient account - function transferNativeFromAgent( - bytes calldata data - ) external onlySelf { + function transferNativeFromAgent(bytes calldata data) external onlySelf { TransferNativeFromAgentParams memory params = abi.decode(data, (TransferNativeFromAgentParams)); address agent = _ensureAgent(params.agentID); @@ -426,9 +418,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // @dev Set token fees of the gateway - function setTokenTransferFees( - bytes calldata data - ) external onlySelf { + function setTokenTransferFees(bytes calldata data) external onlySelf { AssetsStorage.Layout storage $ = AssetsStorage.layout(); SetTokenTransferFeesParams memory params = abi.decode(data, (SetTokenTransferFeesParams)); $.assetHubCreateAssetFee = params.assetHubCreateAssetFee; @@ -438,9 +428,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // @dev Set pricing params of the gateway - function setPricingParameters( - bytes calldata data - ) external onlySelf { + function setPricingParameters(bytes calldata data) external onlySelf { PricingStorage.Layout storage pricing = PricingStorage.layout(); SetPricingParametersParams memory params = abi.decode(data, (SetPricingParametersParams)); pricing.exchangeRate = params.exchangeRate; @@ -453,33 +441,58 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { * Assets */ // @dev Register a new fungible Polkadot token for an agent - function registerForeignToken( - bytes calldata data - ) external onlySelf { + function registerForeignToken(bytes calldata data) external onlySelf { RegisterForeignTokenParams memory params = abi.decode(data, (RegisterForeignTokenParams)); Assets.registerForeignToken(params.foreignTokenID, params.name, params.symbol, params.decimals); } // @dev Mint foreign token from polkadot - function mintForeignToken( - bytes calldata data - ) external onlySelf { + function mintForeignToken(bytes calldata data) external onlySelf { MintForeignTokenParams memory params = abi.decode(data, (MintForeignTokenParams)); Assets.mintForeignToken(params.foreignTokenID, params.recipient, params.amount); } // @dev Transfer Ethereum native token back from polkadot - function transferNativeToken( - bytes calldata data - ) external onlySelf { + function transferNativeToken(bytes calldata data) external onlySelf { TransferNativeTokenParams memory params = abi.decode(data, (TransferNativeTokenParams)); address agent = _ensureAgent(params.agentID); Assets.transferNativeToken(AGENT_EXECUTOR, agent, params.token, params.recipient, params.amount); } - function isTokenRegistered( - address token - ) external view returns (bool) { + // @dev Mint foreign token from polkadot + function reportSlashes(bytes calldata data) external onlySelf { + GatewayCoreStorage.Layout storage layout = GatewayCoreStorage.layout(); + address middlewareAddress = layout.middleware; + // Dont process message if we dont have a middleware set + if (middlewareAddress == address(0)) { + revert MiddlewareNotSet(); + } + + // Decode + (IOGateway.SlashParams memory slashes) = abi.decode(data, (IOGateway.SlashParams)); + IMiddlewareBasic middleware = IMiddlewareBasic(middlewareAddress); + + // At most it will be 10, defined by + // https://github.com/moondance-labs/tanssi/blob/88e59e6e5afb198947690487f286b9ad7cd4cde6/chains/orchestrator-relays/runtime/dancelight/src/lib.rs#L1446 + for (uint256 i = 0; i < slashes.slashes.length; ++i) { + uint48 epoch = middleware.getEpochAtTs(uint48(slashes.slashes[i].timestamp)); + //TODO maxDispatchGas should be probably be defined for all slashes, not only for one + try middleware.slash(epoch, slashes.slashes[i].operatorKey, slashes.slashes[i].slashFraction) {} + catch Error(string memory err) { + emit UnableToProcessIndividualSlashS( + slashes.slashes[i].operatorKey, slashes.slashes[i].slashFraction, slashes.slashes[i].timestamp, err + ); + continue; + } catch (bytes memory err) { + emit UnableToProcessIndividualSlashB( + slashes.slashes[i].operatorKey, slashes.slashes[i].slashFraction, slashes.slashes[i].timestamp, err + ); + continue; + } + } + } + + function isTokenRegistered(address token) external view returns (bool) { return Assets.isTokenRegistered(token); } @@ -493,18 +506,16 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // Register an Ethereum-native token in the gateway and on AssetHub - function registerToken( - address token - ) external payable { + function registerToken(address token) external payable { _submitOutbound(Assets.registerToken(token)); } // Total fee for sending a token - function quoteSendTokenFee( - address token, - ParaID destinationChain, - uint128 destinationFee - ) external view returns (uint256) { + function quoteSendTokenFee(address token, ParaID destinationChain, uint128 destinationFee) + external + view + returns (uint256) + { return _calculateFee(Assets.sendTokenCosts(token, destinationChain, destinationFee, MAX_DESTINATION_FEE)); } @@ -524,16 +535,11 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // @dev Get token address by tokenID - function tokenAddressOf( - bytes32 tokenID - ) external view returns (address) { + function tokenAddressOf(bytes32 tokenID) external view returns (address) { return Assets.tokenAddressOf(tokenID); } - function sendOperatorsData( - bytes32[] calldata data, - uint48 epoch - ) external onlyMiddleware { + function sendOperatorsData(bytes32[] calldata data, uint48 epoch) external onlyMiddleware { Ticket memory ticket = Operators.encodeOperatorsData(data, epoch); _submitOutboundToChannel(PRIMARY_GOVERNANCE_CHANNEL_ID, ticket.payload); } @@ -558,19 +564,21 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // Verify that a message commitment is considered finalized by our BEEFY light client. - function _verifyCommitment( - bytes32 commitment, - Verification.Proof calldata proof - ) internal view virtual returns (bool) { + function _verifyCommitment(bytes32 commitment, Verification.Proof calldata proof) + internal + view + virtual + returns (bool) + { return Verification.verifyCommitment(BEEFY_CLIENT, BRIDGE_HUB_PARA_ID_ENCODED, commitment, proof); } // Convert foreign currency to native currency (ROC/KSM/DOT -> ETH) - function _convertToNative( - UD60x18 exchangeRate, - UD60x18 multiplier, - UD60x18 amount - ) internal view returns (uint256) { + function _convertToNative(UD60x18 exchangeRate, UD60x18 multiplier, UD60x18 amount) + internal + view + returns (uint256) + { UD60x18 ethDecimals = convert(1e18); UD60x18 foreignDecimals = convert(10).pow(convert(uint256(FOREIGN_TOKEN_DECIMALS))); UD60x18 nativeAmount = multiplier.mul(amount).mul(exchangeRate).div(foreignDecimals).mul(ethDecimals); @@ -578,18 +586,14 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } // Calculate the fee for accepting an outbound message - function _calculateFee( - Costs memory costs - ) internal view returns (uint256) { + function _calculateFee(Costs memory costs) internal view returns (uint256) { PricingStorage.Layout storage pricing = PricingStorage.layout(); UD60x18 amount = convert(pricing.deliveryCost + costs.foreign); return costs.native + _convertToNative(pricing.exchangeRate, pricing.multiplier, amount); } // Submit an outbound message to Polkadot, after taking fees - function _submitOutbound( - Ticket memory ticket - ) internal { + function _submitOutbound(Ticket memory ticket) internal { ChannelID channelID = ticket.dest.into(); Channel storage channel = _ensureChannel(channelID); @@ -639,9 +643,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Outbound message can be disabled globally or on a per-channel basis. - function _ensureOutboundMessagingEnabled( - Channel storage ch - ) internal view { + function _ensureOutboundMessagingEnabled(Channel storage ch) internal view { CoreStorage.Layout storage $ = CoreStorage.layout(); if ($.mode != OperatingMode.Normal || ch.mode != OperatingMode.Normal) { revert Disabled(); @@ -649,9 +651,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Ensure that the specified parachain has a channel allocated - function _ensureChannel( - ChannelID channelID - ) internal view returns (Channel storage ch) { + function _ensureChannel(ChannelID channelID) internal view returns (Channel storage ch) { ch = CoreStorage.layout().channels[channelID]; // A channel always has an agent specified. if (ch.agent == address(0)) { @@ -660,9 +660,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } /// @dev Ensure that the specified agentID has a corresponding contract - function _ensureAgent( - bytes32 agentID - ) internal view returns (address agent) { + function _ensureAgent(bytes32 agentID) internal view returns (address agent) { agent = CoreStorage.layout().agents[agentID]; if (agent == address(0)) { revert AgentDoesNotExist(); @@ -736,9 +734,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { /// } /// ``` /// - function initialize( - bytes calldata data - ) external virtual { + function initialize(bytes calldata data) external virtual { // Ensure that arbitrary users cannot initialize storage in this logic contract. if (ERC1967.load() == address(0)) { revert Unauthorized(); @@ -794,9 +790,7 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { operatorStorage.operator = config.rescueOperator; } - function _transferOwnership( - address newOwner - ) internal { + function _transferOwnership(address newOwner) internal { GatewayCoreStorage.Layout storage layout = GatewayCoreStorage.layout(); address oldOwner = layout.owner; @@ -805,32 +799,28 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { emit OwnershipTransferred(oldOwner, newOwner); } - function transferOwnership( - address newOwner - ) external onlyOwner { + function transferOwnership(address newOwner) external onlyOwner { _transferOwnership(newOwner); } /// Changes the middleware address. - function setMiddleware( - address middleware - ) external onlyOwner { + function setMiddleware(address middleware) external onlyOwner { GatewayCoreStorage.Layout storage layout = GatewayCoreStorage.layout(); address oldMiddleware = layout.middleware; - if(middleware == address(0)) { + if (middleware == address(0)) { revert CantSetMiddlewareToZeroAddress(); } - if(middleware == oldMiddleware) { + if (middleware == oldMiddleware) { revert CantSetMiddlewareToSameAddress(); } - + layout.middleware = middleware; emit MiddlewareChanged(oldMiddleware, middleware); } - function s_middleware() external view returns(address) { + function s_middleware() external view returns (address) { GatewayCoreStorage.Layout storage layout = GatewayCoreStorage.layout(); return layout.middleware; } diff --git a/overridden_contracts/src/Operators.sol b/overridden_contracts/src/Operators.sol index 8c68674..c665013 100644 --- a/overridden_contracts/src/Operators.sol +++ b/overridden_contracts/src/Operators.sol @@ -28,10 +28,10 @@ library Operators { uint16 private constant MAX_OPERATORS = 1000; - function encodeOperatorsData( - bytes32[] calldata operatorsKeys, - uint48 epoch - ) internal returns (Ticket memory ticket) { + function encodeOperatorsData(bytes32[] calldata operatorsKeys, uint48 epoch) + internal + returns (Ticket memory ticket) + { if (operatorsKeys.length == 0) { revert Operators__OperatorsKeysCannotBeEmpty(); } diff --git a/overridden_contracts/src/Types.sol b/overridden_contracts/src/Types.sol index 3131ea9..00f113c 100644 --- a/overridden_contracts/src/Types.sol +++ b/overridden_contracts/src/Types.sol @@ -110,7 +110,8 @@ enum Command { Reserved30, Reserved31, Test, - ReportRewards + ReportRewards, + ReportSlashes } /// @dev DEPRECATED diff --git a/overridden_contracts/src/Verification.sol b/overridden_contracts/src/Verification.sol index eaaba4b..443001b 100644 --- a/overridden_contracts/src/Verification.sol +++ b/overridden_contracts/src/Verification.sol @@ -98,11 +98,12 @@ library Verification { /// @param messageCommitment The message commitment root expected to be contained within the /// digest of BridgeHub parachain header. /// @param proof The chain of proofs described above - function verifyCommitment(address beefyClient, bytes4 encodedParaID, bytes32 messageCommitment, Proof calldata proof) - external - view - returns (bool) - { + function verifyCommitment( + address beefyClient, + bytes4 encodedParaID, + bytes32 messageCommitment, + Proof calldata proof + ) external view returns (bool) { bytes32 leafHash = createMMRLeaf(proof.leafPartial, proof.parachainHeadsRoot, messageCommitment); // Verify that the MMR leaf is part of the MMR maintained by the BEEFY light client @@ -202,7 +203,11 @@ library Verification { // SCALE-encode: MMRLeaf // Reference: https://github.com/paritytech/substrate/blob/14e0a0b628f9154c5a2c870062c3aac7df8983ed/primitives/consensus/beefy/src/mmr.rs#L52 - function createMMRLeaf(MMRLeafPartial memory leaf, bytes32 parachainHeadsRoot, bytes32 messageCommitment) internal pure returns (bytes32) { + function createMMRLeaf(MMRLeafPartial memory leaf, bytes32 parachainHeadsRoot, bytes32 messageCommitment) + internal + pure + returns (bytes32) + { bytes memory encodedLeaf = bytes.concat( ScaleCodec.encodeU8(leaf.version), ScaleCodec.encodeU32(leaf.parentNumber), diff --git a/overridden_contracts/src/interfaces/IMiddlewareBasic.sol b/overridden_contracts/src/interfaces/IMiddlewareBasic.sol new file mode 100644 index 0000000..4fd2908 --- /dev/null +++ b/overridden_contracts/src/interfaces/IMiddlewareBasic.sol @@ -0,0 +1,49 @@ +//SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see +pragma solidity ^0.8.0; + +interface IMiddlewareBasic { + /** + * @notice Distributes rewards + * @param epoch The epoch of the rewards distribution + * @param eraIndex The era index of the rewards distribution + * @param totalPointsToken The total points token + * @param tokensInflatedToken The total tokens inflated token + * @param rewardsRoot The rewards root + * @dev This function is called by the gateway only + */ + function distributeRewards( + uint256 epoch, + uint256 eraIndex, + uint256 totalPointsToken, + uint256 tokensInflatedToken, + bytes32 rewardsRoot + ) external; + /** + * @notice Slashes an operator's stake + * @dev Only the owner can call this function + * @dev This function first updates the stake cache for the target epoch + * @param epoch The epoch number + * @param operatorKey The operator key to slash + * @param percentage Percentage to slash, represented as parts per billion. + */ + function slash(uint48 epoch, bytes32 operatorKey, uint256 percentage) external; + /** + * @notice Determines which epoch a timestamp belongs to + * @param timestamp The timestamp to check + * @return epoch The corresponding epoch number + */ + function getEpochAtTs(uint48 timestamp) external view returns (uint48 epoch); +} diff --git a/overridden_contracts/src/interfaces/IOGateway.sol b/overridden_contracts/src/interfaces/IOGateway.sol index b93a457..9f21d17 100644 --- a/overridden_contracts/src/interfaces/IOGateway.sol +++ b/overridden_contracts/src/interfaces/IOGateway.sol @@ -12,7 +12,7 @@ // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Tanssi. If not, see -pragma solidity 0.8.25; +pragma solidity ^0.8.0; import {ParaID} from "../Types.sol"; import {IGateway} from "./IGateway.sol"; @@ -27,14 +27,40 @@ interface IOGateway is IGateway { // Emitted when the middleware contract address is changed by the owner. event MiddlewareChanged(address indexed previousMiddleware, address indexed newMiddleware); - function s_middleware() external view returns(address); + // Emitted when the middleware fails to apply an individual slash + event UnableToProcessIndividualSlashB( + bytes32 indexed operatorKey, uint256 slashFranction, uint256 indexed timestamp, bytes error + ); - function sendOperatorsData( - bytes32[] calldata data, - uint48 epoch - ) external; + // Emitted when the middleware fails to apply an individual slash + event UnableToProcessIndividualSlashS( + bytes32 indexed operatorKey, uint256 slashFranction, uint256 indexed timestamp, string error + ); - function setMiddleware( - address middleware - ) external; + // Emitted when the middleware fails to apply the slash message + event UnableToProcessSlashMessageB(bytes error); + + // Emitted when the middleware fails to apply the slash message + event UnableToProcessSlashMessageS(string error); + + // Slash struct, used to decode slashes, which are identified by + // operatorKey to be slashed + // slashFraction to be applied as parts per billion + // timestamp identifying when the slash happened + struct Slash { + bytes32 operatorKey; + uint256 slashFraction; + uint256 timestamp; + } + + struct SlashParams { + uint256 eraIndex; + Slash[] slashes; + } + + function s_middleware() external view returns (address); + + function sendOperatorsData(bytes32[] calldata data, uint48 epoch) external; + + function setMiddleware(address middleware) external; } diff --git a/overridden_contracts/src/libraries/OSubstrateTypes.sol b/overridden_contracts/src/libraries/OSubstrateTypes.sol index f5f5d2d..980f032 100644 --- a/overridden_contracts/src/libraries/OSubstrateTypes.sol +++ b/overridden_contracts/src/libraries/OSubstrateTypes.sol @@ -26,11 +26,11 @@ library OSubstrateTypes { ReceiveValidators } - function EncodedOperatorsData( - bytes32[] calldata operatorsKeys, - uint32 operatorsCount, - uint48 epoch - ) internal view returns (bytes memory) { + function EncodedOperatorsData(bytes32[] calldata operatorsKeys, uint32 operatorsCount, uint48 epoch) + internal + view + returns (bytes memory) + { bytes memory operatorsFlattened = new bytes(operatorsCount * 32); for (uint32 i = 0; i < operatorsCount; i++) { for (uint32 j = 0; j < 32; j++) { diff --git a/overridden_contracts/src/storage/GatewayCoreStorage.sol b/overridden_contracts/src/storage/GatewayCoreStorage.sol index f00ca88..d161c96 100644 --- a/overridden_contracts/src/storage/GatewayCoreStorage.sol +++ b/overridden_contracts/src/storage/GatewayCoreStorage.sol @@ -30,4 +30,4 @@ library GatewayCoreStorage { ptr.slot := slot } } -} \ No newline at end of file +} diff --git a/overridden_contracts/test/BeefyClient.t.sol b/overridden_contracts/test/BeefyClient.t.sol index 895744a..8731e4f 100644 --- a/overridden_contracts/test/BeefyClient.t.sol +++ b/overridden_contracts/test/BeefyClient.t.sol @@ -155,7 +155,7 @@ contract BeefyClientTest is Test { uint32 nextAuthoritySetLen = uint32(beefyCommitmentRaw.readUint(".params.leaf.nextAuthoritySetLen")); bytes32 nextAuthoritySetRoot = beefyCommitmentRaw.readBytes32(".params.leaf.nextAuthoritySetRoot"); bytes32 parachainHeadsRoot = beefyCommitmentRaw.readBytes32(".params.leaf.parachainHeadsRoot"); - bytes32 messageCommitment = beefyCommitmentRaw.readBytes32(".params.leaf.messageCommitment"); + bytes32 messageCommitment = beefyCommitmentRaw.readBytes32(".params.leaf.messageCommitment"); mmrLeaf = BeefyClient.MMRLeaf( version, parentNumber, @@ -165,7 +165,6 @@ contract BeefyClientTest is Test { nextAuthoritySetRoot, parachainHeadsRoot, messageCommitment - ); } diff --git a/overridden_contracts/test/Gateway.t.sol b/overridden_contracts/test/Gateway.t.sol index cafe474..3d823f7 100644 --- a/overridden_contracts/test/Gateway.t.sol +++ b/overridden_contracts/test/Gateway.t.sol @@ -1,17 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.25; -import {Test} from "forge-std/Test.sol"; +import {Test, Vm} from "forge-std/Test.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {console} from "forge-std/console.sol"; import {BeefyClient} from "../src/BeefyClient.sol"; import {IGateway} from "../src/interfaces/IGateway.sol"; +import {IOGateway} from "../src/interfaces/IOGateway.sol"; import {IInitializable} from "../src/interfaces/IInitializable.sol"; import {IUpgradable} from "../src/interfaces/IUpgradable.sol"; +import {IMiddlewareBasic} from "../src/interfaces/IMiddlewareBasic.sol"; import {Gateway} from "../src/Gateway.sol"; import {MockGateway} from "./mocks/MockGateway.sol"; + import {MockGatewayV2} from "./mocks/MockGatewayV2.sol"; import {GatewayProxy} from "../src/GatewayProxy.sol"; @@ -160,6 +163,13 @@ contract GatewayTest is Test { return (Command.CreateAgent, abi.encode((keccak256("6666")))); } + function _makeReportSlashesCommand() public pure returns (Command, bytes memory) { + IOGateway.Slash[] memory slashes = new IOGateway.Slash[](1); + slashes[0] = IOGateway.Slash({operatorKey: bytes32(uint256(1)), slashFraction: 500_000, timestamp: 1}); + uint256 eraIndex = 1; + return (Command.ReportSlashes, abi.encode(IOGateway.SlashParams({eraIndex: eraIndex, slashes: slashes}))); + } + function makeMockProof() public pure returns (Verification.Proof memory) { return Verification.Proof({ leafPartial: Verification.MMRLeafPartial({ @@ -1007,4 +1017,119 @@ contract GatewayTest is Test { bytes memory encodedParams = abi.encode(params); MockGateway(address(gateway)).agentExecutePublic(encodedParams); } + + // middleware not set, should not be able to process slash + function testSubmitSlashesWithoutMiddleware() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params) = _makeReportSlashesCommand(); + + vm.expectEmit(true, true, true, true); + emit IOGateway.UnableToProcessSlashMessageB(abi.encodeWithSelector(Gateway.MiddlewareNotSet.selector)); + // Expect the gateway to emit `InboundMessageDispatched` + vm.expectEmit(true, true, true, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, false); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + } + + // middleware set, but not complying with the interface, should not process slash + function testSubmitSlashesWithMiddlewareNotComplyingInterface() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params) = _makeReportSlashesCommand(); + + IOGateway(address(gateway)).setMiddleware(0x0123456789012345678901234567890123456789); + + bytes memory empty; + // Expect the gateway to emit `InboundMessageDispatched` + // For some reason when you are loading an address not complying an interface, you get an empty message + // It still serves us to know that this is the reason + vm.expectEmit(true, true, true, true); + emit IOGateway.UnableToProcessSlashMessageB(empty); + vm.expectEmit(true, true, true, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, false); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + } + + // middleware set, complying interface but slash reverts + function testSubmitSlashesWithMiddlewareComplyingInterfaceAndSlashRevert() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params) = _makeReportSlashesCommand(); + + bytes memory expectedError = bytes("no process slash"); + + // We mock the call so that it reverts + vm.mockCallRevert(address(1), abi.encodeWithSelector(IMiddlewareBasic.slash.selector), "no process slash"); + + // We mock the call so that it does not revert, but it will revert in the previous one + vm.mockCall(address(1), abi.encodeWithSelector(IMiddlewareBasic.getEpochAtTs.selector), abi.encode(10)); + + IOGateway(address(gateway)).setMiddleware(address(1)); + + IOGateway.Slash memory expectedSlash = + IOGateway.Slash({operatorKey: bytes32(uint256(1)), slashFraction: 500_000, timestamp: 1}); + + vm.expectEmit(true, true, true, true); + emit IOGateway.UnableToProcessIndividualSlashB( + expectedSlash.operatorKey, expectedSlash.slashFraction, expectedSlash.timestamp, expectedError + ); + vm.expectEmit(true, true, true, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + } + + // middleware set, complying interface and slash processed + function testSubmitSlashesWithMiddlewareComplyingInterfaceAndSlashProcessed() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params) = _makeReportSlashesCommand(); + + // We mock the call so that it does not revert + vm.mockCall(address(1), abi.encodeWithSelector(IMiddlewareBasic.slash.selector), abi.encode(10)); + + // We mock the call so that it does not revert + vm.mockCall(address(1), abi.encodeWithSelector(IMiddlewareBasic.getEpochAtTs.selector), abi.encode(10)); + + IOGateway(address(gateway)).setMiddleware(address(1)); + + // Since we are asserting all fields, the last one is a true, therefore meaning + // that the dispatch went through correctly + + vm.expectEmit(true, true, true, true); + emit IGateway.InboundMessageDispatched(assetHubParaID.into(), 1, messageID, true); + + hoax(relayer, 1 ether); + vm.recordLogs(); + IGateway(address(gateway)).submitV1( + InboundMessage(assetHubParaID.into(), 1, command, params, maxDispatchGas, maxRefund, reward, messageID), + proof, + makeMockProof() + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + // We assert none of the slash error events has been emitted + for (uint256 i = 0; i < entries.length; i++) { + assertNotEq(entries[i].topics[0], IOGateway.UnableToProcessIndividualSlashB.selector); + assertNotEq(entries[i].topics[0], IOGateway.UnableToProcessIndividualSlashS.selector); + } + } } diff --git a/overridden_contracts/test/mocks/MockOGateway.sol b/overridden_contracts/test/mocks/MockOGateway.sol index 4e2a881..e4299c6 100644 --- a/overridden_contracts/test/mocks/MockOGateway.sol +++ b/overridden_contracts/test/mocks/MockOGateway.sol @@ -23,58 +23,44 @@ contract MockOGateway is Gateway { Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubHubAgentID, foreignTokenDecimals, maxDestinationFee) {} - function agentExecutePublic( - bytes calldata params - ) external { + function agentExecutePublic(bytes calldata params) external { this.agentExecute(params); } - function createAgentPublic( - bytes calldata params - ) external { + function createAgentPublic(bytes calldata params) external { this.createAgent(params); } - function upgradePublic( - bytes calldata params - ) external { + function upgradePublic(bytes calldata params) external { this.upgrade(params); } - function createChannelPublic( - bytes calldata params - ) external { + function createChannelPublic(bytes calldata params) external { this.createChannel(params); } - function updateChannelPublic( - bytes calldata params - ) external { + function updateChannelPublic(bytes calldata params) external { this.updateChannel(params); } - function setOperatingModePublic( - bytes calldata params - ) external { + function setOperatingModePublic(bytes calldata params) external { this.setOperatingMode(params); } - function transferNativeFromAgentPublic( - bytes calldata params - ) external { + function transferNativeFromAgentPublic(bytes calldata params) external { this.transferNativeFromAgent(params); } - function setCommitmentsAreVerified( - bool value - ) external { + function setCommitmentsAreVerified(bool value) external { commitmentsAreVerified = value; } - function _verifyCommitment( - bytes32 commitment, - Verification.Proof calldata proof - ) internal view override returns (bool) { + function _verifyCommitment(bytes32 commitment, Verification.Proof calldata proof) + internal + view + override + returns (bool) + { if (BEEFY_CLIENT != address(0)) { return super._verifyCommitment(commitment, proof); } else { @@ -83,33 +69,23 @@ contract MockOGateway is Gateway { } } - function setTokenTransferFeesPublic( - bytes calldata params - ) external { + function setTokenTransferFeesPublic(bytes calldata params) external { this.setTokenTransferFees(params); } - function setPricingParametersPublic( - bytes calldata params - ) external { + function setPricingParametersPublic(bytes calldata params) external { this.setPricingParameters(params); } - function registerForeignTokenPublic( - bytes calldata params - ) external { + function registerForeignTokenPublic(bytes calldata params) external { this.registerForeignToken(params); } - function mintForeignTokenPublic( - bytes calldata params - ) external { + function mintForeignTokenPublic(bytes calldata params) external { this.mintForeignToken(params); } - function transferNativeTokenPublic( - bytes calldata params - ) external { + function transferNativeTokenPublic(bytes calldata params) external { this.transferNativeToken(params); } -} \ No newline at end of file +} diff --git a/overridden_contracts/test/override_test/Gateway.t.sol b/overridden_contracts/test/override_test/Gateway.t.sol index 0622532..9f98492 100644 --- a/overridden_contracts/test/override_test/Gateway.t.sol +++ b/overridden_contracts/test/override_test/Gateway.t.sol @@ -161,6 +161,10 @@ contract GatewayTest is Test { bytes32(0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48) ]; + // Test vector generated by: https://github.com/moondance-labs/tanssi/blob/242196324a37ac0020a7c7955bffe09670f63751/primitives/bridge/src/tests.rs#L84 + bytes private constant TEST_VECTOR_SLASH_DATA = + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002A000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030404040404040404040404040404040404040404040404040404040404040404000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000001F405050505050505050505050505050505050505050505050505050505050505050000000000000000000000000000000000000000000000000000000000000FA0000000000000000000000000000000000000000000000000000000000000019006060606060606060606060606060606060606060606060606060606060606060000000000000000000000000000000000000000000000000000000000000BB8000000000000000000000000000000000000000000000000000000000000012C"; + function createLongOperatorsData() public view returns (bytes32[] memory) { bytes32[] memory result = new bytes32[](1001); @@ -187,7 +191,7 @@ contract GatewayTest is Test { function testSendOperatorsDataX() public { // FINAL_VALIDATORS_PAYLOAD has been encoded with epoch 1. uint48 epoch = 1; - + // Create mock agent and paraID vm.prank(middleware); vm.expectEmit(true, false, false, true); @@ -278,4 +282,18 @@ contract GatewayTest is Test { vm.expectRevert(abi.encodeWithSelector(Gateway.Unauthorized.selector)); IOGateway(address(gateway)).setMiddleware(0x9876543210987654321098765432109876543210); } + + function testDecodeSlashes() public { + uint256 eraIndex = 42; + IOGateway.Slash[] memory slashes = new IOGateway.Slash[](3); + bytes32 alice = 0x0404040404040404040404040404040404040404040404040404040404040404; + bytes32 bob = 0x0505050505050505050505050505050505050505050505050505050505050505; + bytes32 charlie = 0x0606060606060606060606060606060606060606060606060606060606060606; + + slashes[0] = IOGateway.Slash({operatorKey: alice, slashFraction: 5_000, timestamp: 500}); + slashes[1] = IOGateway.Slash({operatorKey: bob, slashFraction: 4_000, timestamp: 400}); + slashes[2] = IOGateway.Slash({operatorKey: charlie, slashFraction: 3_000, timestamp: 300}); + + assertEq(abi.encode(IOGateway.SlashParams({eraIndex: eraIndex, slashes: slashes})), TEST_VECTOR_SLASH_DATA); + } }