diff --git a/contracts/.gitignore b/contracts/.gitignore index e8048ec912..0804e83fc8 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -17,3 +17,5 @@ tsconfig.tsbuildinfo lcov.info tenderly.yaml docs/ + +.history/ \ No newline at end of file diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 07efb9f04c..a18854a6e8 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -6,6 +6,7 @@ import {MerkleProof} from "openzeppelin/utils/cryptography/MerkleProof.sol"; import {Verification} from "./Verification.sol"; import {Assets} from "./Assets.sol"; +import {Operators} from "./Operators.sol"; import {AgentExecutor} from "./AgentExecutor.sol"; import {Agent} from "./Agent.sol"; import { @@ -473,6 +474,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable { _submitOutbound(ticket); } + function sendOperatorsData(bytes calldata data, ParaID destinationChain) external payable { + Ticket memory ticket = Operators.encodeOperatorsData(data, destinationChain); + _submitOutbound(ticket); + } + // @dev Get token address by tokenID function tokenAddressOf(bytes32 tokenID) external view returns (address) { return Assets.tokenAddressOf(tokenID); diff --git a/contracts/src/Operators.sol b/contracts/src/Operators.sol new file mode 100644 index 0000000000..1ee31093cf --- /dev/null +++ b/contracts/src/Operators.sol @@ -0,0 +1,51 @@ +//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.25; + +import {BeefyClient} from "./BeefyClient.sol"; +import {ScaleCodec} from "./utils/ScaleCodec.sol"; +import {SubstrateTypes} from "./SubstrateTypes.sol"; +import {MultiAddress, Ticket, Costs, ParaID} from "./Types.sol"; +import {IGateway} from "./interfaces/IGateway.sol"; + +library Operators { + error Operators__UnsupportedOperatorsLength(); + error Operators__OperatorsLengthTooLong(); + error Operators__OperatorsKeysCannotBeEmpty(); + + uint8 private constant VALIDATOR_KEY_HEX_LENGTH = 32; + uint16 private constant MAX_OPERATORS = 1000; + + function encodeOperatorsData(bytes calldata operatorsKeys, ParaID dest) internal returns (Ticket memory ticket) { + if (operatorsKeys.length == 0) { + revert Operators__OperatorsKeysCannotBeEmpty(); + } + + if (operatorsKeys.length % VALIDATOR_KEY_HEX_LENGTH != 0) { + revert Operators__UnsupportedOperatorsLength(); + } + uint256 validatorsKeysLength = operatorsKeys.length / VALIDATOR_KEY_HEX_LENGTH; + if (validatorsKeysLength > MAX_OPERATORS) { + revert Operators__OperatorsLengthTooLong(); + } + + ticket.dest = dest; + //TODO For now mock it to 0 + ticket.costs = Costs(0, 0); + + ticket.payload = SubstrateTypes.EncodedOperatorsData(operatorsKeys, uint32(validatorsKeysLength)); + emit IGateway.OperatorsDataCreated(validatorsKeysLength, ticket.payload); + } +} diff --git a/contracts/src/SubstrateTypes.sol b/contracts/src/SubstrateTypes.sol index 296f32ce57..3c0e5d46eb 100644 --- a/contracts/src/SubstrateTypes.sol +++ b/contracts/src/SubstrateTypes.sol @@ -9,7 +9,7 @@ import {ParaID} from "./Types.sol"; * @title SCALE encoders for common Substrate types */ library SubstrateTypes { - error UnsupportedCompactEncoding(); + error SubstrateTypes__UnsupportedCompactEncoding(); /** * @dev Encodes `MultiAddress::Id`: https://crates.parity.io/sp_runtime/enum.MultiAddress.html#variant.Id @@ -196,4 +196,26 @@ library SubstrateTypes { ScaleCodec.encodeU128(xcmFee) ); } + + enum Message { + V0 + } + + enum OutboundCommandV1 { + ReceiveValidators + } + + function EncodedOperatorsData(bytes calldata operatorsKeys, uint32 operatorsCount) + internal + pure + returns (bytes memory) + { + return bytes.concat( + bytes4(0x70150038), + bytes1(uint8(Message.V0)), + bytes1(uint8(OutboundCommandV1.ReceiveValidators)), + ScaleCodec.encodeCompactU32(operatorsCount), + operatorsKeys + ); + } } diff --git a/contracts/src/interfaces/IGateway.sol b/contracts/src/interfaces/IGateway.sol index 37e725727a..6f0b149c04 100644 --- a/contracts/src/interfaces/IGateway.sol +++ b/contracts/src/interfaces/IGateway.sol @@ -110,4 +110,8 @@ interface IGateway { uint128 destinationFee, uint128 amount ) external payable; + + event OperatorsDataCreated(uint256 indexed validatorsCount, bytes payload); + + function sendOperatorsData(bytes calldata data, ParaID destinationChain) external payable; } diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index e17c7df1a9..2e457dbd22 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -19,6 +19,7 @@ import {AgentExecutor} from "../src/AgentExecutor.sol"; import {Agent} from "../src/Agent.sol"; import {Verification} from "../src/Verification.sol"; import {Assets} from "../src/Assets.sol"; +import {Operators} from "../src/Operators.sol"; import {SubstrateTypes} from "./../src/SubstrateTypes.sol"; import {MultiAddress} from "../src/MultiAddress.sol"; import {Channel, InboundMessage, OperatingMode, ParaID, Command, ChannelID, MultiAddress} from "../src/Types.sol"; @@ -1014,4 +1015,74 @@ contract GatewayTest is Test { bytes memory encodedParams = abi.encode(params); MockGateway(address(gateway)).agentExecutePublic(encodedParams); } + + bytes private constant FINAL_VALIDATORS_PAYLOAD = + hex"7015003800000cd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe228eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"; + bytes private constant VALIDATORS_DATA = + hex"d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe228eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"; + + bytes private constant WRONG_LENGTH_VALIDATORS_DATA = + hex"d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe228eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a"; + + function createLongOperatorsData() public pure returns (bytes memory) { + bytes memory result = new bytes(VALIDATORS_DATA.length * 1000); + + for (uint256 i = 0; i < 33; i++) { + for (uint256 j = 0; j < VALIDATORS_DATA.length; j++) { + result[i * VALIDATORS_DATA.length + j] = VALIDATORS_DATA[j]; + } + } + + return result; + } + + function testSendOperatorsData() public { + // Create mock agent and paraID + ParaID paraID = ParaID.wrap(1); + bytes32 agentID = keccak256("1"); + + MockGateway(address(gateway)).createAgentPublic(abi.encode(CreateAgentParams({agentID: agentID}))); + + CreateChannelParams memory params = + CreateChannelParams({channelID: paraID.into(), agentID: agentID, mode: OperatingMode.Normal}); + + MockGateway(address(gateway)).createChannelPublic(abi.encode(params)); + + vm.expectEmit(true, false, false, true); + emit IGateway.OutboundMessageAccepted(paraID.into(), 1, messageID, FINAL_VALIDATORS_PAYLOAD); + + IGateway(address(gateway)).sendOperatorsData{value: 1 ether}(VALIDATORS_DATA, paraID); + } + + function testShouldNotSendOperatorsDataBecauseOperatorsNotMultipleOf32() public { + // Create mock agent and paraID + ParaID paraID = ParaID.wrap(1); + bytes32 agentID = keccak256("1"); + + MockGateway(address(gateway)).createAgentPublic(abi.encode(CreateAgentParams({agentID: agentID}))); + + CreateChannelParams memory params = + CreateChannelParams({channelID: paraID.into(), agentID: agentID, mode: OperatingMode.Normal}); + + MockGateway(address(gateway)).createChannelPublic(abi.encode(params)); + vm.expectRevert(Operators.Operators__UnsupportedOperatorsLength.selector); + IGateway(address(gateway)).sendOperatorsData{value: 1 ether}(WRONG_LENGTH_VALIDATORS_DATA, paraID); + } + + function testShouldNotSendOperatorsDataBecauseOperatorsTooLong() public { + // Create mock agent and paraID + ParaID paraID = ParaID.wrap(1); + bytes32 agentID = keccak256("1"); + + MockGateway(address(gateway)).createAgentPublic(abi.encode(CreateAgentParams({agentID: agentID}))); + + CreateChannelParams memory params = + CreateChannelParams({channelID: paraID.into(), agentID: agentID, mode: OperatingMode.Normal}); + + MockGateway(address(gateway)).createChannelPublic(abi.encode(params)); + bytes memory longOperatorsData = createLongOperatorsData(); + + vm.expectRevert(Operators.Operators__OperatorsLengthTooLong.selector); + IGateway(address(gateway)).sendOperatorsData{value: 1 ether}(longOperatorsData, paraID); + } }