From a43683dcd8c71cba60ca85a30b08de946bb676fc Mon Sep 17 00:00:00 2001 From: Bruno Campos Date: Thu, 30 May 2024 10:40:03 -0300 Subject: [PATCH] added exchange factory --- contracts/orderbook/ExchangeFactory.sol | 90 +++++++++++++++++++ .../orderbook/hts/ExchangeFactoryHTS.sol | 90 +++++++++++++++++++ test/orderbook/exchange-factory-hts.test.ts | 74 +++++++++++++++ test/orderbook/exchange-factory.test.ts | 72 +++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 contracts/orderbook/ExchangeFactory.sol create mode 100644 contracts/orderbook/hts/ExchangeFactoryHTS.sol create mode 100644 test/orderbook/exchange-factory-hts.test.ts create mode 100644 test/orderbook/exchange-factory.test.ts diff --git a/contracts/orderbook/ExchangeFactory.sol b/contracts/orderbook/ExchangeFactory.sol new file mode 100644 index 0000000..a7e41ef --- /dev/null +++ b/contracts/orderbook/ExchangeFactory.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Exchange} from "./Exchange.sol"; + +/** + * @title Exchange Factory + * + * The contract which allows to deploy Exchanges with different token pairs + * and track contract addresses. + */ +contract ExchangeFactory { + // Available Exchanges + mapping(address exchange => bool) public availableExchanges; + + // Used salt => deployed Exchange + mapping(bytes32 => address) public exchangeDeployed; + + // emited when an exchagne is deployed + event ExchangeDeployed(address exchange, address tokenA, address tokenB, address deployer); + + /** + * @dev Deploys an Exchange using CREATE2 opcode. + * + * @param tokenA address of source token. + * @param tokenB address of target token + * @return exchange address of the deployed Exchange. + */ + function deployExchange( + address tokenA, + address tokenB + ) external returns (address exchange) { + bytes32 salt = bytes32(keccak256(abi.encodePacked(msg.sender, tokenA, tokenB))); + require(exchangeDeployed[salt] == address(0), "Exchange already deployed"); + + exchange = _deployExchange(salt, tokenA, tokenB); + + exchangeDeployed[salt] = exchange; + availableExchanges[exchange] = true; + + emit ExchangeDeployed(exchange, tokenA, tokenB, msg.sender); + } + + /** + * @dev Creates deployment data for the CREATE2 opcode. + * + * @return The the address of the contract created. + */ + function _deployExchange( + bytes32 salt, + address tokenA, + address tokenB + ) private returns (address) { + bytes memory _code = type(Exchange).creationCode; + bytes memory _constructData = abi.encode( + tokenA, + tokenB + ); + bytes memory deploymentData = abi.encodePacked(_code, _constructData); + return _deploy(salt, deploymentData); + } + + /** + * @dev Deploy function with create2 opcode call. + * + * @return The the address of the contract created. + */ + function _deploy(bytes32 salt, bytes memory bytecode) private returns (address) { + address addr; + // solhint-disable-next-line no-inline-assembly + assembly { + let encoded_data := add(0x20, bytecode) // load initialization code. + let encoded_size := mload(bytecode) // load init code's length. + addr := create2(callvalue(), encoded_data, encoded_size, salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + return addr; + } + + /** + * @dev Checks if Exchange is available. + * + * @return The bool flag of exchanges's availability. + */ + function isExchangeAvailable(address exchange) external view returns (bool) { + return availableExchanges[exchange]; + } +} diff --git a/contracts/orderbook/hts/ExchangeFactoryHTS.sol b/contracts/orderbook/hts/ExchangeFactoryHTS.sol new file mode 100644 index 0000000..0606eb6 --- /dev/null +++ b/contracts/orderbook/hts/ExchangeFactoryHTS.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ExchangeHTS} from "./ExchangeHTS.sol"; + +/** + * @title Exchange Factory + * + * The contract which allows to deploy Exchanges with different token pairs + * and track contract addresses. + */ +contract ExchangeFactoryHTS { + // Available Exchanges + mapping(address exchange => bool) public availableExchanges; + + // Used salt => deployed Exchange + mapping(bytes32 => address) public exchangeDeployed; + + // emited when an exchagne is deployed + event ExchangeDeployed(address exchange, address tokenA, address tokenB, address deployer); + + /** + * @dev Deploys an Exchange using CREATE2 opcode. + * + * @param tokenA address of source token. + * @param tokenB address of target token + * @return exchange address of the deployed Exchange. + */ + function deployExchange( + address tokenA, + address tokenB + ) external returns (address exchange) { + bytes32 salt = bytes32(keccak256(abi.encodePacked(msg.sender, tokenA, tokenB))); + require(exchangeDeployed[salt] == address(0), "Exchange already deployed"); + + exchange = _deployExchange(salt, tokenA, tokenB); + + exchangeDeployed[salt] = exchange; + availableExchanges[exchange] = true; + + emit ExchangeDeployed(exchange, tokenA, tokenB, msg.sender); + } + + /** + * @dev Creates deployment data for the CREATE2 opcode. + * + * @return The the address of the contract created. + */ + function _deployExchange( + bytes32 salt, + address tokenA, + address tokenB + ) private returns (address) { + bytes memory _code = type(ExchangeHTS).creationCode; + bytes memory _constructData = abi.encode( + tokenA, + tokenB + ); + bytes memory deploymentData = abi.encodePacked(_code, _constructData); + return _deploy(salt, deploymentData); + } + + /** + * @dev Deploy function with create2 opcode call. + * + * @return The the address of the contract created. + */ + function _deploy(bytes32 salt, bytes memory bytecode) private returns (address) { + address addr; + // solhint-disable-next-line no-inline-assembly + assembly { + let encoded_data := add(0x20, bytecode) // load initialization code. + let encoded_size := mload(bytecode) // load init code's length. + addr := create2(callvalue(), encoded_data, encoded_size, salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + return addr; + } + + /** + * @dev Checks if Exchange is available. + * + * @return The bool flag of exchanges's availability. + */ + function isExchangeAvailable(address exchange) external view returns (bool) { + return availableExchanges[exchange]; + } +} diff --git a/test/orderbook/exchange-factory-hts.test.ts b/test/orderbook/exchange-factory-hts.test.ts new file mode 100644 index 0000000..56cce12 --- /dev/null +++ b/test/orderbook/exchange-factory-hts.test.ts @@ -0,0 +1,74 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { PrivateKey, Client, AccountId } from "@hashgraph/sdk"; + +// Tests +describe("ExchangeFactory", function () { + async function deployFixture() { + const [owner] = await ethers.getSigners(); + + let client = Client.forTestnet(); + + const operatorPrKey = PrivateKey.fromStringECDSA(process.env.PRIVATE_KEY || ''); + const operatorAccountId = AccountId.fromString(process.env.ACCOUNT_ID || ''); + + client.setOperator( + operatorAccountId, + operatorPrKey + ); + + const exchangeFactoryFactory = await ethers.getContractFactory("ExchangeFactoryHTS", owner); + const exchangeFactory = await exchangeFactoryFactory.deploy(); + await exchangeFactory.waitForDeployment(); + + return { + exchangeFactory, + client, + owner, + }; + } + + describe("deployExchange", function () { + describe("when there is no exchange created for the pair", () => { + it("should deploy exchange", async function () { + const { exchangeFactory, owner } = await deployFixture(); + const exchangeDetails = { + tokenA: "0x000000000000000000000000000000000042cf0f", + tokenB: "0x000000000000000000000000000000000042cf11", + } + + const tx = await exchangeFactory.deployExchange( + exchangeDetails.tokenA, + exchangeDetails.tokenB, + { from: owner.address, gasLimit: 3000000 } + ); + + await expect(tx).to.emit(exchangeFactory, "ExchangeDeployed"); + }); + }); + + describe("when there is already an exchange created", () => { + it("should revert", async function () { + // @notice: revertedWith feature is not working with hedera + + // const { exchangeFactory, owner } = await deployFixture(); + // const exchangeDetails = { + // tokenA: "0x000000000000000000000000000000000042cf0f", + // tokenB: "0x000000000000000000000000000000000042cf11", + // } + + // await exchangeFactory.deployExchange( + // exchangeDetails.tokenA, + // exchangeDetails.tokenB, + // { from: owner.address, gasLimit: 3000000 } + // ); + + // await expect(exchangeFactory.deployExchange( + // exchangeDetails.tokenA, + // exchangeDetails.tokenB, + // { from: owner.address, gasLimit: 3000000 })).to.be.revertedWith('Exchange already deployed'); + // }); + }); + }); + }); +}); diff --git a/test/orderbook/exchange-factory.test.ts b/test/orderbook/exchange-factory.test.ts new file mode 100644 index 0000000..f121378 --- /dev/null +++ b/test/orderbook/exchange-factory.test.ts @@ -0,0 +1,72 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { PrivateKey, Client, AccountId } from "@hashgraph/sdk"; + +// Tests +describe("ExchangeFactory", function () { + async function deployFixture() { + const [owner] = await ethers.getSigners(); + + let client = Client.forTestnet(); + + const operatorPrKey = PrivateKey.fromStringECDSA(process.env.PRIVATE_KEY || ''); + const operatorAccountId = AccountId.fromString(process.env.ACCOUNT_ID || ''); + + client.setOperator( + operatorAccountId, + operatorPrKey + ); + + const exchangeFactoryFactory = await ethers.getContractFactory("ExchangeFactory", owner); + const exchangeFactory = await exchangeFactoryFactory.deploy(); + await exchangeFactory.waitForDeployment(); + + return { + exchangeFactory, + client, + owner, + }; + } + + describe("deployExchange", function () { + describe("when there is no exchange created for the pair", () => { + it("should deploy exchange", async function () { + const { exchangeFactory, owner } = await deployFixture(); + const exchangeDetails = { + tokenA: "0x000000000000000000000000000000000042cf0f", + tokenB: "0x000000000000000000000000000000000042cf11", + } + + const tx = await exchangeFactory.deployExchange( + exchangeDetails.tokenA, + exchangeDetails.tokenB, + { from: owner.address, gasLimit: 3000000 } + ); + + await expect(tx).to.emit(exchangeFactory, "ExchangeDeployed"); + }); + }); + + describe("when there is already an exchange created", () => { + it("should revert", async function () { + const { exchangeFactory, owner } = await deployFixture(); + const exchangeDetails = { + tokenA: "0x000000000000000000000000000000000042cf0f", + tokenB: "0x000000000000000000000000000000000042cf11", + } + + await exchangeFactory.deployExchange( + exchangeDetails.tokenA, + exchangeDetails.tokenB, + { from: owner.address, gasLimit: 3000000 } + ) + + await expect(exchangeFactory.deployExchange( + exchangeDetails.tokenA, + exchangeDetails.tokenB, + { from: owner.address, gasLimit: 3000000 } + )).to.be.revertedWith('Exchange already deployed'); + }); + }); + }); +});