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);
+ }
}