diff --git a/contracts/test/ExecutionLayer/SwapMock.sol b/contracts/test/ExecutionLayer/SwapMock.sol
new file mode 100644
index 000000000..98e01779c
--- /dev/null
+++ b/contracts/test/ExecutionLayer/SwapMock.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * SwapMock.sol - SKALE Interchain Messaging Agent
+ * Copyright (C) 2021-Present SKALE Labs
+ * @author Dmytro Stebaiev
+ *
+ * SKALE IMA is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SKALE IMA 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with SKALE IMA. If not, see .
+ */
+
+
+pragma solidity 0.8.27;
+
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+contract SwapMock {
+ IERC20 tokenA;
+ IERC20 tokenB;
+
+ error UnknownToken(IERC20 token);
+
+ function swap(IERC20 token, uint256 amount) external returns (uint256 resultAmount) {
+ IERC20 anotherToken = getAnotherToken(token);
+ address user = msg.sender;
+ token.transferFrom(user, address(this), amount);
+ anotherToken.transfer(user, amount);
+ return amount;
+ }
+
+ function setTokenA(IERC20 token) external {
+ tokenA = token;
+ }
+
+ function setTokenB(IERC20 token) external {
+ tokenB = token;
+ }
+
+ // public
+
+ function getAnotherToken(IERC20 token) public view returns (IERC20 anotherToken) {
+ require(tokenA == token || tokenB == token, UnknownToken(token));
+ if (token == tokenA) {
+ return tokenB;
+ } else if (token == tokenB) {
+ return tokenA;
+ } else {
+ revert UnknownToken(token);
+ }
+ }
+}
diff --git a/contracts/test/ExecutionLayer/executors/SwapMockSwap.sol b/contracts/test/ExecutionLayer/executors/SwapMockSwap.sol
new file mode 100644
index 000000000..03935115b
--- /dev/null
+++ b/contracts/test/ExecutionLayer/executors/SwapMockSwap.sol
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+/**
+ * Executor.sol - SKALE Interchain Messaging Agent
+ * Copyright (C) 2024-Present SKALE Labs
+ * @author Dmytro Stebaiev
+ *
+ * SKALE IMA is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SKALE IMA 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with SKALE IMA. If not, see .
+ */
+
+pragma solidity 0.8.27;
+
+import "hardhat/console.sol";
+
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {ExecutorId} from "@skalenetwork/ima-interfaces/schain/ExecutionLayer/IExecutor.sol";
+import {TokenInfo} from "@skalenetwork/ima-interfaces/schain/ExecutionLayer/IActionExecutor.sol";
+
+import {Executor} from "../../../schain/ExecutionLayer/Executor.sol";
+import {SwapMock} from "../SwapMock.sol";
+
+contract SwapMockSwap is Executor {
+ ExecutorId public constant ID = ExecutorId.wrap(keccak256("SwapMockSwap"));
+ SwapMock exchange;
+
+ function setExchange(SwapMock exchangeAddress) external {
+ exchange = exchangeAddress;
+ }
+
+ function execute(
+ TokenInfo[] memory inputTokens,
+ bytes memory
+ )
+ external
+ override
+ returns (TokenInfo[] memory outputTokens)
+ {
+ outputTokens = new TokenInfo[](inputTokens.length);
+ for (uint256 i = 0; i < inputTokens.length; ++i) {
+ IERC20 token = IERC20(getTokenAddress(inputTokens[i]));
+ IERC20 anotherToken = exchange.getAnotherToken(token);
+ uint256 anotherValue = exchange.swap(token, inputTokens[i].value);
+ outputTokens[i].token = address(anotherToken);
+ outputTokens[i].value = anotherValue;
+ }
+ }
+}
diff --git a/test/ExecutionManager.ts b/test/ExecutionManager.ts
index d7e530abc..a1582045c 100644
--- a/test/ExecutionManager.ts
+++ b/test/ExecutionManager.ts
@@ -1,5 +1,5 @@
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
-import { ethers } from "hardhat";
+import { ethers, upgrades } from "hardhat";
import { ExecutionManager, MessageProxyForSchain, Protocol, TokenManagerERC20, TokenManagerLinker } from "../typechain";
import { deployExecutionManager } from "./utils/deploy/schain/executionManager";
import { AgentMock } from "./utils/agent/AgentMock";
@@ -111,18 +111,6 @@ describe("ExecutionManager", () => {
assert(sourceExecutionManager);
assert(targetExecutionManager);
- // const metaAction = {
- // targetChainHash: ethers.id(targetSchainName),
- // actions: "0x",
- // nextMetaAction: await sourceExecutionManager.encodeMetaAction({
- // targetChainHash: ethers.id(targetSchainName),
- // actions: "0x",
- // nextMetaAction: "0x",
- // postActions: "0x"
- // }),
- // postActions: "0x"
- // }
-
const metaAction = (await sourceExecutionManager.createMetaAction(
targetSchainHash,
[]
@@ -235,4 +223,102 @@ describe("ExecutionManager", () => {
expect(await clone.balanceOf(user)).to.be.equal(0n);
expect(await token.balanceOf(user)).to.be.equal(value);
});
+
+ it.only("should transfer from Chain A to Chain B with execution of a swap on Chain B", async() => {
+ const schains = await setupMultipleSchains(2);
+ const agent = new AgentMock();
+ for (const [schainName, schainSetup] of schains) {
+ await agent.registerSchain(schainName, schainSetup.messageProxy);
+ }
+
+ const [sourceSchainName, targetSchainName] = [...schains.keys()];
+ const sourceSchainHash = ethers.id(sourceSchainName);
+ const targetSchainHash = ethers.id(targetSchainName);
+
+ const sourceExecutionManager = schains.get(sourceSchainName)?.executionManager;
+ const targetExecutionManager = schains.get(targetSchainName)?.executionManager;
+
+ assert(sourceExecutionManager);
+ assert(targetExecutionManager);
+
+ const sourceTokenManager = schains.get(sourceSchainName)?.tokenManager;
+ const targetTokenManager = schains.get(targetSchainName)?.tokenManager;
+
+ assert(sourceTokenManager);
+ assert(targetTokenManager);
+
+ // Create tokens on chain B
+
+ const token = await deployERC20OnChain("D2", "D2");
+ const value = ethers.parseEther("1");
+ await token.mint(user, value);
+
+ const token2 = await deployERC20OnChain("D2", "D2");
+ await token2.mint(user, value);
+
+ // Setup exchange
+
+ const exchange = await upgrades.deployProxy(await ethers.getContractFactory("SwapMock"));
+ await exchange.setTokenA(token);
+ await exchange.setTokenB(token2);
+ await token2.connect(user).transfer(exchange, value);
+
+ // Transfer the token to chain A
+
+ await token.connect(user).approve(targetTokenManager, value);
+ await targetTokenManager.connect(user).transferToSchainERC20(
+ sourceSchainName,
+ token, value
+ );
+
+ await agent.deliverMessages();
+
+ const cloneAddress = await sourceTokenManager.clonesErc20(targetSchainHash, token);
+ const clone = await ethers.getContractAt("ERC20OnChain", cloneAddress);
+
+ // Setup executors
+
+ const swapMockSwap = await ethers.deployContract("SwapMockSwap");
+ await swapMockSwap.setExchange(exchange);
+ await targetExecutionManager.setExecutor(await swapMockSwap.ID(), swapMockSwap);
+
+ const send = await ethers.getContractAt(
+ "Send",
+ await sourceExecutionManager.getExecutor(
+ ethers.id("Send")
+ )
+ );
+
+ // Send tokens and swap then
+
+ expect(await token.balanceOf(user)).to.be.equal(0n);
+ expect(await token2.balanceOf(user)).to.be.equal(0n);
+ expect(await clone.balanceOf(user)).to.be.equal(value);
+
+ const metaAction = (await sourceExecutionManager.createMetaAction(
+ targetSchainHash,
+ [
+ {
+ executor: ethers.id("SwapMockSwap"),
+ arguments: "0x"
+ },
+ {
+ executor: ethers.id("Send"),
+ arguments: await send.encodeArguments(user)
+ }
+ ]
+ )).toObject();
+
+ await clone.connect(user).approve(sourceExecutionManager, value);
+ await sourceExecutionManager.connect(user).execute(
+ metaAction,
+ [{token: clone, value: value, origin: token}]
+ );
+
+ await agent.deliverMessages();
+
+ expect(await token.balanceOf(user)).to.be.equal(0n);
+ expect(await token2.balanceOf(user)).to.be.equal(value);
+ expect(await clone.balanceOf(user)).to.be.equal(0n);
+ });
});