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