diff --git a/.gitignore b/.gitignore index 0285a63..c44e2aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ .vscode/ .env + +.history/* \ No newline at end of file diff --git a/overridden_contracts/src/Gateway.sol b/overridden_contracts/src/Gateway.sol index 40274a1..7c914f4 100644 --- a/overridden_contracts/src/Gateway.sol +++ b/overridden_contracts/src/Gateway.sol @@ -267,6 +267,15 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { emit UnableToProcessSlashMessageB(err); success = false; } + } else if (message.command == Command.ReportRewards) { + try Gateway(this).sendRewards{gas: maxDispatchGas}(message.params) {} + catch Error(string memory err) { + emit UnableToProcessRewardsMessageS(err); + success = false; + } catch (bytes memory err) { + emit UnableToProcessRewardsMessageB(err); + success = false; + } } // Calculate a gas refund, capped to protect against huge spikes in `tx.gasprice` @@ -492,6 +501,40 @@ contract Gateway is IOGateway, IInitializable, IUpgradable { } } + function sendRewards(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(); + } + + // Probably need to send me the token to be minted? + ( + uint256 epoch, + uint256 eraIndex, + uint256 totalPointsToken, + uint256 totalTokensInflated, + bytes32 rewardsRoot, + bytes32 foreignTokenId + ) = abi.decode(data, (uint256, uint256, uint256, uint256, bytes32, bytes32)); + + Assets.mintForeignToken(foreignTokenId, middlewareAddress, totalTokensInflated); + + address tokenAddress = Assets.tokenAddressOf(foreignTokenId); + try IMiddlewareBasic(middlewareAddress).distributeRewards( + epoch, eraIndex, totalPointsToken, totalTokensInflated, rewardsRoot, tokenAddress + ) {} catch Error(string memory err) { + emit UnableToProcessRewardsS( + epoch, eraIndex, tokenAddress, totalPointsToken, totalTokensInflated, rewardsRoot, err + ); + } catch (bytes memory err) { + emit UnableToProcessRewardsB( + epoch, eraIndex, tokenAddress, totalPointsToken, totalTokensInflated, rewardsRoot, err + ); + } + } + function isTokenRegistered(address token) external view returns (bool) { return Assets.isTokenRegistered(token); } diff --git a/overridden_contracts/src/interfaces/IMiddlewareBasic.sol b/overridden_contracts/src/interfaces/IMiddlewareBasic.sol index 4fd2908..46f418a 100644 --- a/overridden_contracts/src/interfaces/IMiddlewareBasic.sol +++ b/overridden_contracts/src/interfaces/IMiddlewareBasic.sol @@ -16,21 +16,25 @@ 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 + * @notice Distribute rewards for a specific era contained in an epoch by providing a Merkle root, total points, total amount of tokens and the token address of the rewards. + * @param epoch network epoch of the middleware + * @param eraIndex era index of Starlight's rewards distribution + * @param totalPointsToken total amount of points for the reward distribution + * @param amount amount of tokens to distribute + * @param root Merkle root of the reward distribution + * @param tokenAddress The token address of the rewards * @dev This function is called by the gateway only + * @dev Emit DistributeRewards event. */ function distributeRewards( uint256 epoch, uint256 eraIndex, uint256 totalPointsToken, - uint256 tokensInflatedToken, - bytes32 rewardsRoot + uint256 amount, + bytes32 root, + address tokenAddress ) external; + /** * @notice Slashes an operator's stake * @dev Only the owner can call this function diff --git a/overridden_contracts/src/interfaces/IOGateway.sol b/overridden_contracts/src/interfaces/IOGateway.sol index 9f21d17..055a2ed 100644 --- a/overridden_contracts/src/interfaces/IOGateway.sol +++ b/overridden_contracts/src/interfaces/IOGateway.sol @@ -43,6 +43,34 @@ interface IOGateway is IGateway { // Emitted when the middleware fails to apply the slash message event UnableToProcessSlashMessageS(string error); + // Emitted when the middleware fails to process rewards + event UnableToProcessRewardsB( + uint256 indexed epoch, + uint256 indexed eraIndex, + address indexed tokenAddress, + uint256 totalPointsToken, + uint256 totalTokensInflated, + bytes32 rewardsRoot, + bytes error + ); + + // Emitted when the middleware fails to process rewards + event UnableToProcessRewardsS( + uint256 indexed epoch, + uint256 indexed eraIndex, + address indexed tokenAddress, + uint256 totalPointsToken, + uint256 totalTokensInflated, + bytes32 rewardsRoot, + string error + ); + + // Emitted when the middleware fails to apply the slash message + event UnableToProcessRewardsMessageB(bytes error); + + // Emitted when the middleware fails to apply the slash message + event UnableToProcessRewardsMessageS(string error); + // Slash struct, used to decode slashes, which are identified by // operatorKey to be slashed // slashFraction to be applied as parts per billion @@ -60,6 +88,10 @@ interface IOGateway is IGateway { function s_middleware() external view returns (address); + function reportSlashes(bytes calldata data) external; + + function sendRewards(bytes calldata data) external; + function sendOperatorsData(bytes32[] calldata data, uint48 epoch) external; function setMiddleware(address middleware) external; diff --git a/overridden_contracts/test/Gateway.t.sol b/overridden_contracts/test/Gateway.t.sol index 3d823f7..f6aa41f 100644 --- a/overridden_contracts/test/Gateway.t.sol +++ b/overridden_contracts/test/Gateway.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.25; -import {Test, Vm} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {console} from "forge-std/console.sol"; @@ -11,7 +11,6 @@ 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"; @@ -163,13 +162,6 @@ 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({ @@ -1017,119 +1009,4 @@ 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/override_test/Gateway.t.sol b/overridden_contracts/test/override_test/Gateway.t.sol index 9f98492..61b6e97 100644 --- a/overridden_contracts/test/override_test/Gateway.t.sol +++ b/overridden_contracts/test/override_test/Gateway.t.sol @@ -14,7 +14,7 @@ // along with Tanssi. If not, see pragma solidity 0.8.25; -import {Test, console2} from "forge-std/Test.sol"; +import {Test, console2, Vm} from "forge-std/Test.sol"; import { AgentExecuteCommand, InboundMessage, @@ -26,21 +26,28 @@ import { multiAddressFromBytes20 } from "../../src/Types.sol"; import {IGateway} from "../../src/interfaces/IGateway.sol"; +import {IMiddlewareBasic} from "../../src/interfaces/IMiddlewareBasic.sol"; import {MockGateway} from "../mocks/MockGateway.sol"; -import {CreateAgentParams, CreateChannelParams} from "../../src/Params.sol"; +import { + CreateAgentParams, + CreateChannelParams, + SetOperatingModeParams, + RegisterForeignTokenParams +} from "../../src/Params.sol"; import {OperatingMode, ParaID, Command} from "../../src/Types.sol"; import {GatewayProxy} from "../../src/GatewayProxy.sol"; import {MultiAddress} from "../../src/MultiAddress.sol"; import {AgentExecutor} from "../../src/AgentExecutor.sol"; -import {SetOperatingModeParams} from "../../src/Params.sol"; +import {Verification} from "../../src/Verification.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {Gateway} from "../../src/Gateway.sol"; import {IOGateway} from "../../src/interfaces/IOGateway.sol"; import {Operators} from "../../src/Operators.sol"; +import {Assets} from "../../src/Assets.sol"; import {MockOGateway} from "../mocks/MockOGateway.sol"; - +import {Token} from "../../src/Token.sol"; //NEW import {WETH9} from "canonical-weth/WETH9.sol"; import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol"; @@ -69,7 +76,7 @@ contract GatewayTest is Test { address public account1; address public account2; - address public middleware; + address public middleware = makeAddr("middleware"); uint64 public maxDispatchGas = 500_000; uint256 public maxRefund = 1 ether; @@ -96,9 +103,28 @@ contract GatewayTest is Test { // tokenID for DOT bytes32 public dotTokenID; + uint256 public constant SLASH_FRACTION = 500_000; + ChannelID internal constant PRIMARY_GOVERNANCE_CHANNEL_ID = ChannelID.wrap(bytes32(uint256(1))); ChannelID internal constant SECONDARY_GOVERNANCE_CHANNEL_ID = ChannelID.wrap(bytes32(uint256(2))); + // It's generated using VALIDATORS_DATA using scaleCodec. See OSubstrateTypes.EncodedOperatorsData in OSubstrateTypes.sol + bytes private constant FINAL_VALIDATORS_PAYLOAD = + hex"7015003800000cd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe228eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480100000000000000"; + + bytes32[] private VALIDATORS_DATA = [ + bytes32(0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d), + bytes32(0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22), + 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"; + + bytes private constant TEST_VECTOR_REWARDS_DATA = + hex"00000000000000000000000000000000000000000000000000000000075BCD15000000000000000000000000000000000000000000000000000000000000002A00000000000000000000000000000000000000000000000000007048860DDF79000000000000000000000000000000000000000000000000000000E5F4C8F3CAb6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e40101010101010101010101010101010101010101010101010101010101010101"; + function setUp() public { AgentExecutor executor = new AgentExecutor(); gatewayLogic = new MockOGateway( @@ -134,7 +160,6 @@ contract GatewayTest is Test { account1 = makeAddr("account1"); account2 = makeAddr("account2"); - middleware = makeAddr("middleware"); // create tokens for account 1 hoax(account1); @@ -147,23 +172,54 @@ contract GatewayTest is Test { recipientAddress20 = multiAddressFromBytes20(bytes20(keccak256("recipient"))); dotTokenID = bytes32(uint256(1)); + } - // set middleware - IOGateway(address(gateway)).setMiddleware(middleware); + function _makeReportSlashesCommand(uint256 slashFraction) public pure returns (Command, bytes memory) { + IOGateway.Slash[] memory slashes = new IOGateway.Slash[](1); + slashes[0] = IOGateway.Slash({operatorKey: bytes32(uint256(1)), slashFraction: slashFraction, timestamp: 1}); + uint256 eraIndex = 1; + return (Command.ReportSlashes, abi.encode(IOGateway.SlashParams({eraIndex: eraIndex, slashes: slashes}))); } - bytes private constant FINAL_VALIDATORS_PAYLOAD = - hex"7015003800000cd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe228eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480100000000000000"; + function _makeReportRewardsCommand() public returns (Command, bytes memory, address) { + uint256 epoch = 0; + uint256 eraIndex = 1; + uint256 totalPointsToken = 1 ether; + uint256 tokensInflatedToken = 1 ether; + bytes32 rewardsRoot = bytes32(uint256(1)); + bytes32 foreignTokenId = bytes32(uint256(1)); - bytes32[] private VALIDATORS_DATA = [ - bytes32(0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d), - bytes32(0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22), - bytes32(0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48) - ]; + RegisterForeignTokenParams memory params = + RegisterForeignTokenParams({foreignTokenID: foreignTokenId, name: "Test", symbol: "TST", decimals: 10}); - // 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"; + vm.expectEmit(true, true, false, false); + emit IGateway.ForeignTokenRegistered(foreignTokenId, address(0)); + MockGateway(address(gateway)).registerForeignTokenPublic(abi.encode(params)); + + address tokenAddress = MockGateway(address(gateway)).tokenAddressOf(foreignTokenId); + + return ( + Command.ReportRewards, + abi.encode(epoch, eraIndex, totalPointsToken, tokensInflatedToken, rewardsRoot, foreignTokenId), + tokenAddress + ); + } + + function makeMockProof() public pure returns (Verification.Proof memory) { + return Verification.Proof({ + leafPartial: Verification.MMRLeafPartial({ + version: 0, + parentNumber: 0, + parentHash: bytes32(0), + nextAuthoritySetID: 0, + nextAuthoritySetLen: 0, + nextAuthoritySetRoot: 0 + }), + leafProof: new bytes32[](0), + leafProofOrder: 0, + parachainHeadsRoot: bytes32(0) + }); + } function createLongOperatorsData() public view returns (bytes32[] memory) { bytes32[] memory result = new bytes32[](1001); @@ -188,10 +244,10 @@ contract GatewayTest is Test { return paraID; } - function testSendOperatorsDataX() public { + function testSendOperatorsData() public { // FINAL_VALIDATORS_PAYLOAD has been encoded with epoch 1. uint48 epoch = 1; - + IOGateway(address(gateway)).setMiddleware(middleware); // Create mock agent and paraID vm.prank(middleware); vm.expectEmit(true, false, false, true); @@ -203,7 +259,7 @@ contract GatewayTest is Test { function testShouldNotSendOperatorsDataBecauseOperatorsTooLong() public { bytes32[] memory longOperatorsData = createLongOperatorsData(); uint48 epoch = 42; - + IOGateway(address(gateway)).setMiddleware(middleware); vm.prank(middleware); vm.expectRevert(Operators.Operators__OperatorsLengthTooLong.selector); IOGateway(address(gateway)).sendOperatorsData(longOperatorsData, epoch); @@ -222,6 +278,8 @@ contract GatewayTest is Test { uint48 epoch = abi.decode(vm.parseJson(json, "$.epoch"), (uint48)); + IOGateway(address(gateway)).setMiddleware(middleware); + vm.prank(middleware); vm.expectEmit(true, false, false, true); emit IGateway.OutboundMessageAccepted(PRIMARY_GOVERNANCE_CHANNEL_ID, 1, messageID, final_payload); @@ -241,6 +299,8 @@ contract GatewayTest is Test { bytes32[] memory accounts = abi.decode(vm.parseJson(json, "$.accounts"), (bytes32[])); uint48 epoch = abi.decode(vm.parseJson(json, "$.epoch"), (uint48)); + IOGateway(address(gateway)).setMiddleware(middleware); + vm.prank(middleware); vm.expectEmit(true, false, false, true); emit IGateway.OutboundMessageAccepted(PRIMARY_GOVERNANCE_CHANNEL_ID, 1, messageID, final_payload); @@ -260,6 +320,8 @@ contract GatewayTest is Test { bytes32[] memory accounts = abi.decode(vm.parseJson(json, "$.accounts"), (bytes32[])); uint48 epoch = abi.decode(vm.parseJson(json, "$.epoch"), (uint48)); + IOGateway(address(gateway)).setMiddleware(middleware); + vm.prank(middleware); vm.expectEmit(true, false, false, true); emit IGateway.OutboundMessageAccepted(PRIMARY_GOVERNANCE_CHANNEL_ID, 1, messageID, final_payload); @@ -269,7 +331,7 @@ contract GatewayTest is Test { function testOwnerCanChangeMiddleware() public { vm.expectEmit(true, true, false, false); - emit IOGateway.MiddlewareChanged(address(middleware), 0x0123456789012345678901234567890123456789); + emit IOGateway.MiddlewareChanged(address(0), 0x0123456789012345678901234567890123456789); IOGateway(address(gateway)).setMiddleware(0x0123456789012345678901234567890123456789); @@ -296,4 +358,282 @@ contract GatewayTest is Test { assertEq(abi.encode(IOGateway.SlashParams({eraIndex: eraIndex, slashes: slashes})), TEST_VECTOR_SLASH_DATA); } + + // middleware not set, should not be able to process slash + function testSubmitSlashesWithoutMiddleware() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params) = _makeReportSlashesCommand(SLASH_FRACTION); + + 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(SLASH_FRACTION); + + 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(SLASH_FRACTION); + + 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: SLASH_FRACTION, 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(SLASH_FRACTION); + + // 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); + } + } + + function testDecodeRewards() public { + uint256 epoch = 123_456_789; + uint256 eraIndex = 42; + uint256 totalPointsToken = 123_456_789_012_345; + uint256 tokensInflatedToken = 987_654_321_098; + bytes32 rewardsRoot = 0xb6e16d27ac5ab427a7f68900ac5559ce272dc6c37c82b3e052246c82244c50e4; + bytes32 foreignTokenId = 0x0101010101010101010101010101010101010101010101010101010101010101; + + assertEq( + abi.encode(epoch, eraIndex, totalPointsToken, tokensInflatedToken, rewardsRoot, foreignTokenId), + TEST_VECTOR_REWARDS_DATA + ); + } + + function testSubmitRewards() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params, address tokenAddress) = _makeReportRewardsCommand(); + + // We mock the call so that it does not revert + vm.mockCall( + address(middleware), abi.encodeWithSelector(IMiddlewareBasic.distributeRewards.selector), abi.encode(true) + ); + + IOGateway(address(gateway)).setMiddleware(address(middleware)); + + // Expect the gateway to emit `InboundMessageDispatched` + 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() + ); + + assert(Token(tokenAddress).balanceOf(address(middleware)) > 0); + } + + function testSubmitRewardsWithoutMiddleware() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params, address tokenAddress) = _makeReportRewardsCommand(); + + vm.expectEmit(true, true, true, true); + emit IOGateway.UnableToProcessRewardsMessageB(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 rewards + function testSubmitRewardsWithMiddlewareNotComplyingInterface() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params, address tokenAddress) = _makeReportRewardsCommand(); + + 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.UnableToProcessRewardsMessageB(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 rewards reverts + function testSubmitRewardsWithMiddlewareComplyingInterfaceAndRewardsRevert() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params, address tokenAddress) = _makeReportRewardsCommand(); + + bytes memory expectedError = bytes("can't process rewards"); //This should actually come from IODefaultOperatorRewards + + // We mock the call so that it reverts + vm.mockCallRevert( + address(middleware), + abi.encodeWithSelector(IMiddlewareBasic.distributeRewards.selector), + "can't process rewards" + ); + + IOGateway(address(gateway)).setMiddleware(address(middleware)); + + uint256 expectedEpoch = 0; + uint256 expectedEraIndex = 1; + uint256 expectedTotalPointsToken = 1 ether; + uint256 expectedTotalTokensInflated = 1 ether; + bytes32 expectedRewardsRoot = bytes32(uint256(1)); + bytes32 expectedForeignTokenId = bytes32(uint256(1)); + + address expectedTokenAddress = MockGateway(address(gateway)).tokenAddressOf(expectedForeignTokenId); + + vm.expectEmit(true, true, true, true); + emit IOGateway.UnableToProcessRewardsB( + expectedEpoch, + expectedEraIndex, + expectedTokenAddress, + expectedTotalPointsToken, + expectedTotalTokensInflated, + expectedRewardsRoot, + 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 rewards processed + function testSubmitRewardsWithMiddlewareComplyingInterfaceAndRewardsProcessed() public { + deal(assetHubAgent, 50 ether); + + (Command command, bytes memory params, address tokenAddress) = _makeReportRewardsCommand(); + + // We mock the call so that it does not revert + vm.mockCall( + address(middleware), abi.encodeWithSelector(IMiddlewareBasic.distributeRewards.selector), abi.encode(true) + ); + + IOGateway(address(gateway)).setMiddleware(address(middleware)); + + 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 rewards error events has been emitted + for (uint256 i = 0; i < entries.length; i++) { + assertNotEq(entries[i].topics[0], IOGateway.UnableToProcessRewardsMessageB.selector); + assertNotEq(entries[i].topics[0], IOGateway.UnableToProcessRewardsMessageS.selector); + } + } }