├── .prettierrc ├── docs └── audit_report.pdf ├── .gitignore ├── contracts ├── interfaces │ ├── IPCVPolicy.sol │ ├── uniswapV2 │ │ ├── IUniswapV2Factory.sol │ │ └── IUniswapV2Pair.sol │ ├── IWETH.sol │ ├── uniswapV3 │ │ ├── IUniswapV3Factory.sol │ │ ├── ISwapRouter.sol │ │ └── IUniswapV3Pool.sol │ ├── IL2StandardERC20.sol │ ├── ILiquidityERC20.sol │ ├── IPairFactory.sol │ ├── IVault.sol │ ├── IMarginFactory.sol │ ├── IAmmFactory.sol │ ├── IERC20.sol │ ├── IRouterForKeeper.sol │ ├── IPCVTreasury.sol │ ├── IPriceOracle.sol │ ├── IOrderBook.sol │ ├── IAmm.sol │ ├── IRouter.sol │ ├── IConfig.sol │ └── IMargin.sol ├── mocks │ ├── EoaReplace.sol │ ├── MockArbSys.sol │ ├── MockQuoteToken.sol │ ├── MockSwapRouter.sol │ ├── MockUniswapV3Factory.sol │ ├── MockFactory.sol │ ├── Faucet.sol │ ├── MockBaseToken.sol │ ├── MockToken.sol │ ├── MockPriceOracle.sol │ ├── MockRouter.sol │ ├── MockPriceOracleM.sol │ ├── MockMargin.sol │ ├── MockWETH.sol │ ├── MockFlashAttacker.sol │ ├── MockAmm.sol │ ├── MyAmm.sol │ ├── MockAmmM.sol │ ├── PriceOracleForTest.sol │ └── MockConfig.sol ├── utils │ ├── Reentrant.sol │ ├── Initializable.sol │ ├── Ownable.sol │ └── Whitelist.sol ├── libraries │ ├── FixedPoint96.sol │ ├── ChainAdapter.sol │ ├── UQ112x112.sol │ ├── Math.sol │ ├── SignedMath.sol │ ├── TransferHelper.sol │ ├── UniswapV3TwapGetter.sol │ ├── FullMath.sol │ ├── FixedPoint.sol │ └── V2Oracle.sol ├── core │ ├── Migrator.sol │ ├── PairFactory.sol │ ├── MarginFactory.sol │ ├── AmmFactory.sol │ ├── Multicall2.sol │ ├── LiquidityERC20.sol │ ├── PCVTreasury.sol │ ├── Config.sol │ ├── RouterForKeeper.sol │ ├── OrderBook.sol │ └── FeeTreasury.sol └── token │ ├── ApeXTokenMantle.sol │ └── ApeXToken.sol ├── test ├── 1.json ├── shared │ ├── fixtures.js │ └── utilities.js ├── core │ ├── price_oracle_for_test.js │ ├── config.js │ └── fee-treasury.js ├── balance-tree.js ├── merkle-tree.js ├── ERC20MerkleDrop.js ├── migrator.js ├── erc20.json └── limitorder │ └── routerForKeeper.js ├── README.md ├── .env.example ├── scripts ├── verify_apex.js ├── deploy_apex.js ├── verify_apex_mantle.js ├── deploy_apex_mantle.js ├── deploy_fee_treasury.js ├── deploy_migrator.js └── deploy_limit_order.js ├── package.json └── hardhat.config.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "printWidth": 120 5 | } -------------------------------------------------------------------------------- /docs/audit_report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeX-Protocol/core/HEAD/docs/audit_report.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | artifacts 4 | cache 5 | package-lock.json 6 | .deps 7 | .env 8 | bin 9 | .openzeppelin 10 | .DS_Store 11 | .history 12 | .VSCodeCounter 13 | contracts/NFT/MultiMint2.sol 14 | -------------------------------------------------------------------------------- /contracts/interfaces/IPCVPolicy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IPCVPolicy { 5 | function execute(address lpToken, uint256 amount, bytes calldata data) external; 6 | } 7 | -------------------------------------------------------------------------------- /contracts/interfaces/uniswapV2/IUniswapV2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | interface IUniswapV2Factory { 4 | function getPair(address tokenA, address tokenB) external view returns (address pair); 5 | } -------------------------------------------------------------------------------- /contracts/mocks/EoaReplace.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | contract EoaReplace { 5 | address public feeTo; 6 | 7 | function setFeeTo(address _feeTo) external { 8 | feeTo = _feeTo; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "APEX", 4 | "external_url": "ipfs", 5 | "image": "ipfs/1.jpg", 6 | "attributes": [ 7 | { "trait_type": "ATK", "value": "100" }, 8 | { "trait_type": "Nickname", "value": "NO1" } 9 | ] 10 | } -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint256 value) external returns (bool); 8 | 9 | function withdraw(uint256) external; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/uniswapV3/IUniswapV3Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapV3Factory { 5 | function getPool( 6 | address tokenA, 7 | address tokenB, 8 | uint24 fee 9 | ) external view returns (address pool); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/utils/Reentrant.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract Reentrant { 5 | bool private entered; 6 | 7 | modifier nonReentrant() { 8 | require(entered == false, "Reentrant: reentrant call"); 9 | entered = true; 10 | _; 11 | entered = false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApeX Protocol 2 | 3 | Contracts for DEX. 4 | 5 | ## Install Dependencies 6 | npm install 7 | 8 | ## Compile Contracts 9 | npm run compile 10 | 11 | ## Run Tests 12 | npm run test 13 | npx hardhat test ./test/amm.js 14 | 15 | 16 | ## Run Deploy 17 | > set .env 18 | 19 | npm run deploy_arb 20 | 21 | 22 | reference deploy: npx hardhat run scripts/deploy_reference.js --network l2rinkeby -------------------------------------------------------------------------------- /contracts/libraries/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title FixedPoint96 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | /// @dev Used in SqrtPriceMath.sol 7 | library FixedPoint96 { 8 | uint8 internal constant RESOLUTION = 96; 9 | uint256 internal constant Q96 = 0x1000000000000000000000000; 10 | } 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DEVNET_PRIVKEY=xxxxxxxxxxxxxxxx 2 | ETHERSCAN_API_KEY=yyyyyyyy 3 | ARBISCAN_API_KEY=yyyyyyyy 4 | MAINNET_RPC="https://mainnet.infura.io/v3/" 5 | RINKEBY_RPC="https://rinkeby.infura.io/v3/" 6 | ARBITRUM_ONE_RPC="https://arb1.arbitrum.io/rpc" 7 | ARBITRUM_TESTNET_RPC="https://rinkeby.arbitrum.io/rpc" 8 | BSC_MAINNET_PRC="https://bsc-dataseed1.binance.org" 9 | BSC_TESTNET_PRC="https://data-seed-prebsc-1-s1.binance.org:8545/" -------------------------------------------------------------------------------- /contracts/mocks/MockArbSys.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract MockArbSys { 4 | function arbBlockNumber() external view returns (uint256) { 5 | return block.number; 6 | } 7 | 8 | function blockTimestamp() external view returns (uint256) { 9 | return block.timestamp; 10 | } 11 | 12 | function getBlock() external view returns (uint256 number, uint256 timestamp) { 13 | number = block.number; 14 | timestamp = block.timestamp; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/shared/fixtures.js: -------------------------------------------------------------------------------- 1 | const { expandDecimals } = require("./utilities") 2 | 3 | async function deployContract(name, args) { 4 | const contractFactory = await ethers.getContractFactory(name) 5 | return await contractFactory.deploy(...args) 6 | } 7 | 8 | async function contractAt(name, address) { 9 | const contractFactory = await ethers.getContractFactory(name) 10 | return await contractFactory.attach(address) 11 | } 12 | 13 | module.exports = { 14 | deployContract, 15 | contractAt 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mocks/MockQuoteToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockQuoteToken is ERC20 { 7 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 8 | 9 | function decimals() public pure override returns (uint8) { 10 | return 6; 11 | } 12 | 13 | function mint(address account, uint256 amount) external { 14 | _mint(account, amount); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/verify_apex.js: -------------------------------------------------------------------------------- 1 | const { 2 | ethers 3 | } = require("hardhat"); 4 | 5 | const apexToken = "0x6fDd9fa8237883de7c2ebC6908Ab9e243600567E"; 6 | 7 | const main = async () => { 8 | await hre.run("verify:verify", { 9 | address: apexToken, 10 | contract: "contracts/token/ApeXToken.sol:ApeXToken", 11 | constructorArguments: [ 12 | ], 13 | }); 14 | }; 15 | 16 | main() 17 | .then(() => process.exit(0)) 18 | .catch((error) => { 19 | console.error(error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/deploy_apex.js: -------------------------------------------------------------------------------- 1 | const { 2 | ethers 3 | } = require("hardhat"); 4 | const verifyStr = "npx hardhat verify --network"; 5 | 6 | const main = async () => { 7 | const ApexToken = await ethers.getContractFactory("ApeXToken"); 8 | let apexToken = await ApexToken.deploy(); 9 | console.log("ApexToken:", apexToken.address); 10 | console.log(verifyStr, process.env.HARDHAT_NETWORK); 11 | }; 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch((error) => { 16 | console.error(error); 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /contracts/mocks/MockSwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/uniswapV3/ISwapRouter.sol"; 5 | 6 | contract MockSwapRouter is ISwapRouter { 7 | address public override WETH9; 8 | address public override factory; 9 | 10 | constructor(address WETH9_, address factory_) { 11 | WETH9 = WETH9_; 12 | factory = factory_; 13 | } 14 | 15 | function exactInputSingle(ExactInputSingleParams calldata params) external payable override returns (uint256 amountOut) { 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /contracts/interfaces/IL2StandardERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 6 | interface IL2StandardERC20 is IERC20, IERC165 { 7 | function l1Token() external returns (address); 8 | function mint(address _to, uint256 _amount) external; 9 | function burn(address _from, uint256 _amount) external; 10 | event Mint(address indexed _account, uint256 _amount); 11 | event Burn(address indexed _account, uint256 _amount); 12 | } -------------------------------------------------------------------------------- /contracts/mocks/MockUniswapV3Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/uniswapV3/IUniswapV3Factory.sol"; 5 | 6 | contract MockUniswapV3Factory is IUniswapV3Factory { 7 | mapping(address => mapping(address => mapping(uint24 => address))) public override getPool; 8 | 9 | function setPool( 10 | address tokenA, 11 | address tokenB, 12 | uint24 fee, 13 | address pool 14 | ) external { 15 | getPool[tokenA][tokenB][fee] = pool; 16 | getPool[tokenB][tokenA][fee] = pool; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/mocks/MockFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../core/Margin.sol"; 6 | 7 | contract MockFactory { 8 | address public config; 9 | Margin public margin; 10 | 11 | constructor(address _config) { 12 | config = _config; 13 | } 14 | 15 | function createPair() public { 16 | margin = new Margin(); 17 | } 18 | 19 | function initialize( 20 | address baseToken_, 21 | address quoteToken_, 22 | address amm_ 23 | ) public { 24 | margin.initialize(baseToken_, quoteToken_, amm_); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/ILiquidityERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IERC20.sol"; 5 | 6 | interface ILiquidityERC20 is IERC20 { 7 | function permit( 8 | address owner, 9 | address spender, 10 | uint256 value, 11 | uint256 deadline, 12 | uint8 v, 13 | bytes32 r, 14 | bytes32 s 15 | ) external; 16 | 17 | function nonces(address owner) external view returns (uint256); 18 | 19 | function DOMAIN_SEPARATOR() external view returns (bytes32); 20 | 21 | function PERMIT_TYPEHASH() external pure returns (bytes32); 22 | } 23 | -------------------------------------------------------------------------------- /contracts/utils/Initializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract Initializable { 5 | bool private _initialized; 6 | 7 | bool private _initializing; 8 | 9 | modifier initializer() { 10 | require(_initializing || !_initialized, "Initializable: contract is already initialized"); 11 | 12 | bool isTopLevelCall = !_initializing; 13 | if (isTopLevelCall) { 14 | _initializing = true; 15 | _initialized = true; 16 | } 17 | 18 | _; 19 | 20 | if (isTopLevelCall) { 21 | _initializing = false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/libraries/ChainAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IArbSys { 5 | function arbBlockNumber() external view returns (uint256); 6 | } 7 | 8 | library ChainAdapter { 9 | address constant arbSys = address(100); 10 | 11 | function blockNumber() internal view returns (uint256) { 12 | uint256 chainId; 13 | assembly { 14 | chainId := chainid() 15 | } 16 | if (chainId == 421611 || chainId == 42161) { // Arbitrum Testnet || Arbitrum Mainnet 17 | return IArbSys(arbSys).arbBlockNumber(); 18 | } else { 19 | return block.number; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /contracts/interfaces/IPairFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IPairFactory { 5 | event NewPair(address indexed baseToken, address indexed quoteToken, address amm, address margin); 6 | 7 | function createPair(address baseToken, address quotoToken) external returns (address amm, address margin); 8 | 9 | function ammFactory() external view returns (address); 10 | 11 | function marginFactory() external view returns (address); 12 | 13 | function getAmm(address baseToken, address quoteToken) external view returns (address); 14 | 15 | function getMargin(address baseToken, address quoteToken) external view returns (address); 16 | } 17 | -------------------------------------------------------------------------------- /contracts/interfaces/uniswapV2/IUniswapV2Pair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapV2Pair { 5 | function token0() external view returns (address); 6 | 7 | function token1() external view returns (address); 8 | 9 | function totalSupply() external view returns (uint256); 10 | 11 | function getReserves() 12 | external 13 | view 14 | returns ( 15 | uint112 reserve0, 16 | uint112 reserve1, 17 | uint32 blockTimestampLast 18 | ); 19 | 20 | function price0CumulativeLast() external view returns (uint256); 21 | 22 | function price1CumulativeLast() external view returns (uint256); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/libraries/UQ112x112.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 5 | 6 | // range: [0, 2**112 - 1] 7 | // resolution: 1 / 2**112 8 | 9 | library UQ112x112 { 10 | uint224 constant Q112 = 2**112; 11 | 12 | // encode a uint112 as a UQ112x112 13 | function encode(uint112 y) internal pure returns (uint224 z) { 14 | z = uint224(y) * Q112; // never overflows 15 | } 16 | 17 | // divide a UQ112x112 by a uint112, returning a UQ112x112 18 | function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { 19 | z = x / uint224(y); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/interfaces/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IVault { 5 | event Deposit(address indexed user, uint256 amount); 6 | event Withdraw(address indexed user, address indexed receiver, uint256 amount); 7 | 8 | /// @notice deposit baseToken to user 9 | function deposit(address user, uint256 amount) external; 10 | 11 | /// @notice withdraw user's baseToken from margin contract to receiver 12 | function withdraw( 13 | address user, 14 | address receiver, 15 | uint256 amount 16 | ) external; 17 | 18 | /// @notice get baseToken amount in margin 19 | function reserve() external view returns (uint256); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/interfaces/IMarginFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IMarginFactory { 5 | event MarginCreated(address indexed baseToken, address indexed quoteToken, address margin); 6 | 7 | function createMargin(address baseToken, address quoteToken) external returns (address margin); 8 | 9 | function initMargin( 10 | address baseToken, 11 | address quoteToken, 12 | address amm 13 | ) external; 14 | 15 | function upperFactory() external view returns (address); 16 | 17 | function config() external view returns (address); 18 | 19 | function getMargin(address baseToken, address quoteToken) external view returns (address margin); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/libraries/Math.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library Math { 5 | function min(uint256 x, uint256 y) internal pure returns (uint256) { 6 | if (x > y) { 7 | return y; 8 | } 9 | return x; 10 | } 11 | 12 | // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) 13 | function sqrt(uint256 y) internal pure returns (uint256 z) { 14 | if (y > 3) { 15 | z = y; 16 | uint256 x = y / 2 + 1; 17 | while (x < z) { 18 | z = x; 19 | x = (y / x + x) / 2; 20 | } 21 | } else if (y != 0) { 22 | z = 1; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/mocks/Faucet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IERC20 { 6 | function transfer(address to, uint256 value) external returns (bool); 7 | function decimals() external pure returns (uint8); 8 | } 9 | 10 | contract Faucet { 11 | // userAddress => token => timestamp 12 | mapping(address => mapping(address => uint256)) public lastWithdrawTime; 13 | 14 | function withdraw(address token) external { 15 | uint256 withdrawableTime = lastWithdrawTime[msg.sender][token] + 24*60*60; 16 | require(block.timestamp >= withdrawableTime, "time limit"); 17 | uint256 amount = 10 * (10**uint256(IERC20(token).decimals())); 18 | IERC20(token).transfer(msg.sender, amount); 19 | lastWithdrawTime[msg.sender][token] = block.timestamp; 20 | } 21 | } -------------------------------------------------------------------------------- /contracts/mocks/MockBaseToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockBaseToken is ERC20 { 7 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 8 | 9 | receive() external payable {} 10 | 11 | function deposit() external payable { 12 | _mint(msg.sender, msg.value); 13 | } 14 | 15 | function withdraw(uint256 amount) external payable { 16 | _burn(msg.sender, msg.value); 17 | payable(msg.sender).transfer(amount); 18 | } 19 | 20 | function mint(address account, uint256 amount) external { 21 | _mint(account, amount); 22 | } 23 | 24 | function ethBalance() external view returns (uint256) { 25 | return address(this).balance; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/interfaces/uniswapV3/ISwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ISwapRouter { 5 | struct ExactInputSingleParams { 6 | address tokenIn; 7 | address tokenOut; 8 | uint24 fee; 9 | address recipient; 10 | uint256 amountIn; 11 | uint256 amountOutMinimum; 12 | uint160 sqrtPriceLimitX96; 13 | } 14 | 15 | /// @notice Swaps `amountIn` of one token for as much as possible of another token 16 | /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata 17 | /// @return amountOut The amount of the received token 18 | function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); 19 | 20 | function factory() external view returns (address); 21 | 22 | function WETH9() external view returns (address); 23 | } -------------------------------------------------------------------------------- /scripts/verify_apex_mantle.js: -------------------------------------------------------------------------------- 1 | const { 2 | ethers 3 | } = require("hardhat"); 4 | 5 | const apexMantleToken = "0x96630b0D78d29E7E8d87f8703dE7c14b2d5AE413"; 6 | 7 | const l2Bridge = "0x4200000000000000000000000000000000000010"; 8 | const l1Address = "0x52A8845DF664D76C69d2EEa607CD793565aF42B8"; 9 | const name = "ApeX Token"; 10 | const symbol = "APEX"; 11 | const decimals = 18; 12 | 13 | const main = async () => { 14 | await hre.run("verify:verify", { 15 | address: apexMantleToken, 16 | contract: "contracts/token/ApeXTokenMantle.sol:ApeXTokenMantle", 17 | constructorArguments: [ 18 | l2Bridge, 19 | l1Address, 20 | name, 21 | symbol, 22 | decimals 23 | ], 24 | }); 25 | }; 26 | 27 | main() 28 | .then(() => process.exit(0)) 29 | .catch((error) => { 30 | console.error(error); 31 | process.exit(1); 32 | }); 33 | -------------------------------------------------------------------------------- /contracts/interfaces/IAmmFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IAmmFactory { 5 | event AmmCreated(address indexed baseToken, address indexed quoteToken, address amm); 6 | 7 | function createAmm(address baseToken, address quoteToken) external returns (address amm); 8 | 9 | function initAmm( 10 | address baseToken, 11 | address quoteToken, 12 | address margin 13 | ) external; 14 | 15 | function setFeeTo(address) external; 16 | 17 | function setFeeToSetter(address) external; 18 | 19 | function upperFactory() external view returns (address); 20 | 21 | function config() external view returns (address); 22 | 23 | function feeTo() external view returns (address); 24 | 25 | function feeToSetter() external view returns (address); 26 | 27 | function getAmm(address baseToken, address quoteToken) external view returns (address amm); 28 | } 29 | -------------------------------------------------------------------------------- /scripts/deploy_apex_mantle.js: -------------------------------------------------------------------------------- 1 | const { 2 | ethers,hre 3 | } = require("hardhat"); 4 | const verifyStr = "npx hardhat verify --network"; 5 | 6 | const l2Bridge = "0x4200000000000000000000000000000000000010"; 7 | const l1Address = "0x52A8845DF664D76C69d2EEa607CD793565aF42B8"; 8 | const name = "ApeX Token"; 9 | const symbol = "APEX"; 10 | const decimals = 18; 11 | 12 | const main = async () => { 13 | const [owner] = await ethers.getSigners(); 14 | console.log("owner:", owner.address); 15 | 16 | const ApexToken = await ethers.getContractFactory("ApeXTokenMantle"); 17 | let apexToken = await ApexToken.deploy( 18 | l2Bridge, 19 | l1Address, 20 | name, 21 | symbol, 22 | decimals 23 | ); 24 | console.log("ApexToken:", apexToken.address); 25 | console.log(verifyStr, process.env.HARDHAT_NETWORK); 26 | }; 27 | 28 | main() 29 | .then(() => process.exit(0)) 30 | .catch((error) => { 31 | console.error(error); 32 | process.exit(1); 33 | }); 34 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC20 { 5 | event Approval(address indexed owner, address indexed spender, uint256 value); 6 | event Transfer(address indexed from, address indexed to, uint256 value); 7 | 8 | function allowance(address owner, address spender) external view returns (uint256); 9 | 10 | function approve(address spender, uint256 value) external returns (bool); 11 | 12 | function transfer(address to, uint256 value) external returns (bool); 13 | 14 | function transferFrom( 15 | address from, 16 | address to, 17 | uint256 value 18 | ) external returns (bool); 19 | 20 | function totalSupply() external view returns (uint256); 21 | 22 | function balanceOf(address owner) external view returns (uint256); 23 | 24 | function name() external view returns (string memory); 25 | 26 | function symbol() external view returns (string memory); 27 | 28 | function decimals() external pure returns (uint8); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/libraries/SignedMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library SignedMath { 5 | function abs(int256 x) internal pure returns (uint256) { 6 | if (x < 0) { 7 | return uint256(0 - x); 8 | } 9 | return uint256(x); 10 | } 11 | 12 | function addU(int256 x, uint256 y) internal pure returns (int256) { 13 | require(y <= uint256(type(int256).max), "overflow"); 14 | return x + int256(y); 15 | } 16 | 17 | function subU(int256 x, uint256 y) internal pure returns (int256) { 18 | require(y <= uint256(type(int256).max), "overflow"); 19 | return x - int256(y); 20 | } 21 | 22 | function mulU(int256 x, uint256 y) internal pure returns (int256) { 23 | require(y <= uint256(type(int256).max), "overflow"); 24 | return x * int256(y); 25 | } 26 | 27 | function divU(int256 x, uint256 y) internal pure returns (int256) { 28 | require(y <= uint256(type(int256).max), "overflow"); 29 | return x / int256(y); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/core/price_oracle_for_test.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("@ethersproject/bignumber"); 2 | const { expect } = require("chai"); 3 | const { ethers } = require("hardhat"); 4 | const base = "0xD4c652999084ef502Cbe6b0a2bD7277b7dab092E"; 5 | const quote = "0xAd4215344396F4B53AaF7B494Cc3580E8CF14104"; 6 | 7 | describe("PriceOracleForTest contract", function () { 8 | let poft; 9 | beforeEach(async function () { 10 | [owner, addr1, liquidator, ...addrs] = await ethers.getSigners(); 11 | 12 | const PriceOracleForTest = await ethers.getContractFactory("PriceOracleForTest"); 13 | poft = await PriceOracleForTest.deploy(); 14 | 15 | await poft.setReserve(base, quote, 10000, 20000); 16 | }); 17 | describe("query", async function () { 18 | it("get reserves", async function () { 19 | let result = await poft.getReserves(base, quote); 20 | expect(result[0]).to.equal(10000); 21 | expect(result[1]).to.equal(20000); 22 | }); 23 | 24 | it("query quote", async function () { 25 | let result = await poft.quote(base, quote, 10); 26 | expect(result[0]).to.equal(20); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /contracts/mocks/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol"; 6 | 7 | contract MockToken is ERC20, ERC20FlashMint { 8 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 9 | 10 | function flashFee(address token, uint256 amount) public view override returns (uint256) { 11 | require(token == address(this), "ERC20FlashMint: wrong token"); 12 | return amount / 10; 13 | } 14 | 15 | receive() external payable {} 16 | 17 | function deposit() external payable { 18 | _mint(msg.sender, msg.value); 19 | } 20 | 21 | function withdraw(uint256 amount) external payable { 22 | _burn(msg.sender, msg.value); 23 | payable(msg.sender).transfer(amount); 24 | } 25 | 26 | function mint(address account, uint256 amount) external { 27 | _mint(account, amount); 28 | } 29 | 30 | function ethBalance() external view returns (uint256) { 31 | return address(this).balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/utils/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract Ownable { 5 | address public owner; 6 | address public pendingOwner; 7 | 8 | event NewOwner(address indexed oldOwner, address indexed newOwner); 9 | event NewPendingOwner(address indexed oldPendingOwner, address indexed newPendingOwner); 10 | 11 | modifier onlyOwner() { 12 | require(msg.sender == owner, "Ownable: REQUIRE_OWNER"); 13 | _; 14 | } 15 | 16 | function setPendingOwner(address newPendingOwner) external onlyOwner { 17 | require(pendingOwner != newPendingOwner, "Ownable: ALREADY_SET"); 18 | emit NewPendingOwner(pendingOwner, newPendingOwner); 19 | pendingOwner = newPendingOwner; 20 | } 21 | 22 | function acceptOwner() external { 23 | require(msg.sender == pendingOwner, "Ownable: REQUIRE_PENDING_OWNER"); 24 | address oldOwner = owner; 25 | address oldPendingOwner = pendingOwner; 26 | owner = pendingOwner; 27 | pendingOwner = address(0); 28 | emit NewOwner(oldOwner, owner); 29 | emit NewPendingOwner(oldPendingOwner, pendingOwner); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/interfaces/IRouterForKeeper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | import "./IOrderBook.sol"; 4 | 5 | interface IRouterForKeeper { 6 | event CollectFee(address indexed trader, address indexed margin, uint256 fee); 7 | 8 | function pairFactory() external view returns (address); 9 | 10 | function WETH() external view returns (address); 11 | 12 | function openPositionWithWallet(IOrderBook.OpenPositionOrder memory order) 13 | external 14 | returns (uint256 baseAmount); 15 | 16 | function openPositionWithMargin(IOrderBook.OpenPositionOrder memory order) 17 | external 18 | returns (uint256 baseAmount); 19 | 20 | function closePosition(IOrderBook.ClosePositionOrder memory order) 21 | external 22 | returns (uint256 baseAmount, uint256 withdrawAmount); 23 | 24 | function getSpotPriceWithMultiplier(address baseToken, address quoteToken) 25 | external 26 | view 27 | returns ( 28 | uint256 spotPriceWithMultiplier, 29 | uint256 baseDecimal, 30 | uint256 quoteDecimal 31 | ); 32 | 33 | function setOrderBook(address newOrderBook) external; 34 | } 35 | -------------------------------------------------------------------------------- /contracts/interfaces/IPCVTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IPCVTreasury { 5 | event NewLiquidityToken(address indexed lpToken); 6 | event NewBondPool(address indexed pool); 7 | event Deposit(address indexed pool, address indexed lpToken, uint256 amountIn, uint256 payout); 8 | event Withdraw(address indexed lpToken, address indexed policy, uint256 amount); 9 | event ApeXGranted(address indexed to, uint256 amount); 10 | 11 | function apeXToken() external view returns (address); 12 | 13 | function isLiquidityToken(address) external view returns (bool); 14 | 15 | function isBondPool(address) external view returns (bool); 16 | 17 | function addLiquidityToken(address lpToken) external; 18 | 19 | function addBondPool(address pool) external; 20 | 21 | function deposit( 22 | address lpToken, 23 | uint256 amountIn, 24 | uint256 payout 25 | ) external; 26 | 27 | function withdraw( 28 | address lpToken, 29 | address policy, 30 | uint256 amount, 31 | bytes calldata data 32 | ) external; 33 | 34 | function grantApeX(address to, uint256 amount) external; 35 | } 36 | -------------------------------------------------------------------------------- /test/balance-tree.js: -------------------------------------------------------------------------------- 1 | const MerkleTree = require('./merkle-tree') 2 | const { BigNumber, utils } = require('ethers') 3 | 4 | class BalanceTree { 5 | 6 | constructor(balances) { 7 | this.tree = new MerkleTree( 8 | balances.map(({ account, amount }, index) => { 9 | return this.toNode(index, account, amount) 10 | }) 11 | ) 12 | } 13 | 14 | verifyProof( 15 | index, 16 | account, 17 | amount, 18 | proof, 19 | root 20 | ) { 21 | let pair = this.toNode(index, account, amount) 22 | for (const item of proof) { 23 | pair = MerkleTree.combinedHash(pair, item) 24 | } 25 | 26 | return pair.equals(root) 27 | } 28 | 29 | // keccak256(abi.encode(index, account, amount)) 30 | toNode(index, account, amount) { 31 | return Buffer.from( 32 | utils.solidityKeccak256(['uint256', 'address', 'uint256'], [index, account, amount]).substr(2), 33 | 'hex' 34 | ) 35 | } 36 | 37 | getHexRoot() { 38 | return this.tree.getHexRoot() 39 | } 40 | 41 | // returns the hex bytes32 values of the proof 42 | getProof(index, account, amount) { 43 | return this.tree.getHexProof(this.toNode(index, account, amount)) 44 | } 45 | 46 | } 47 | module.exports = BalanceTree; -------------------------------------------------------------------------------- /contracts/mocks/MockPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract MockPriceOracle { 6 | int256 public pf = 0; 7 | uint256 public p; 8 | 9 | constructor() { 10 | p = 1; 11 | } 12 | 13 | //premiumFraction is (markPrice - indexPrice) * fundingRatePrecision / 8h / indexPrice 14 | function getPremiumFraction(address amm) external view returns (int256) { 15 | return pf; 16 | } 17 | 18 | function setPf(int256 _pf) external { 19 | pf = _pf; 20 | } 21 | 22 | function getMarkPriceAcc( 23 | address amm, 24 | uint8 beta, 25 | uint256 quoteAmount, 26 | bool negative 27 | ) public view returns (uint256 price) { 28 | return quoteAmount / p; 29 | } 30 | 31 | function setMarkPrice(uint256 _p) external { 32 | p = _p; 33 | } 34 | 35 | function quote( 36 | address baseToken, 37 | address quoteToken, 38 | uint256 baseAmount 39 | ) external view returns (uint256 quoteAmount, uint8 source) { 40 | quoteAmount = 100000 * 10**6; 41 | source = 0; 42 | } 43 | 44 | function updateAmmTwap(address pair) external {} 45 | 46 | function setupTwap(address amm) external {} 47 | } 48 | -------------------------------------------------------------------------------- /scripts/deploy_fee_treasury.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | const verifyStr = "npx hardhat verify --network"; 3 | 4 | const main = async () => { 5 | //// ArbitrumOne 6 | // const v3Router = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; 7 | // const usdc = "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"; 8 | // const operator = "0x1956b2c4C511FDDd9443f50b36C4597D10cD9985"; 9 | // const nextSettleTime = 1649051940; 10 | //// Testnet 11 | const v3Router = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; 12 | const usdc = "0x79dCF515aA18399CF8fAda58720FAfBB1043c526"; // mockUSDC in testnet 13 | const operator = "0x63bbc06bec247942bfc3ac11b4dd4d9bae434144"; 14 | const nextSettleTime = Math.round(new Date().getTime() / 1000) + 60; 15 | 16 | const FeeTreasury = await ethers.getContractFactory("FeeTreasury"); 17 | let feeTreasury = await FeeTreasury.deploy(v3Router, usdc, operator, nextSettleTime); 18 | console.log("FeeTreasury:", feeTreasury.address); 19 | console.log(verifyStr, process.env.HARDHAT_NETWORK, feeTreasury.address, v3Router, usdc, operator, nextSettleTime); 20 | 21 | let Config = await ethers.getContractFactory("Config"); 22 | let config = await Config.attach("0xCF4f7E54Cbd24C062Eda93A6f2aE30E039109dd6"); 23 | await config.registerRouter(feeTreasury.address); 24 | }; 25 | 26 | main() 27 | .then(() => process.exit(0)) 28 | .catch((error) => { 29 | console.error(error); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /scripts/deploy_migrator.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("ethers"); 2 | const { ethers } = require("hardhat"); 3 | const verifyStr = "npx hardhat verify --network"; 4 | 5 | const main = async () => { 6 | //// ArbitrumOne 7 | const oldRouter = "0x4e101bC1Eb3Ebb22276a7D94Bd8B5adf2da8d793"; 8 | const newRouter = "0x146c57aBB43a5B457cd8E109D35Ac27057a672e2"; 9 | const oldConfigAddress = "0xC69d007331957808B215e7f42d645FF439f16b47"; 10 | const newConfigAddress = "0x38a71796bC0291Bc09f4D890B45A9A93d49eDf70"; 11 | //// Testnet 12 | // const oldRouter = "0xcbccda0Df16Ba36AfEde7bc6d676E261098f3a9E"; 13 | // const newRouter = "0x363fE608166b204ea70017F095949295374fd371"; 14 | // const oldConfigAddress = "0xF74F984F78CEBC8734A98F6C8aFf4c13F274EA6B"; 15 | // const newConfigAddress = "0x37a74ECe864f40b156eA7e0b2b8EAB8097cb2512"; 16 | 17 | const Migrator = await ethers.getContractFactory("Migrator"); 18 | let migrator = await Migrator.deploy(oldRouter, newRouter); 19 | console.log("Migrator:", migrator.address); 20 | console.log(verifyStr, process.env.HARDHAT_NETWORK, migrator.address, oldRouter, newRouter); 21 | 22 | const Config = await await ethers.getContractFactory("Config"); 23 | let oldConfig = await Config.attach(oldConfigAddress); 24 | await oldConfig.registerRouter(migrator.address); 25 | let newConfig = await Config.attach(newConfigAddress); 26 | await newConfig.registerRouter(migrator.address); 27 | }; 28 | 29 | main() 30 | .then(() => process.exit(0)) 31 | .catch((error) => { 32 | console.error(error); 33 | process.exit(1); 34 | }); 35 | -------------------------------------------------------------------------------- /contracts/utils/Whitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "./Ownable.sol"; 5 | 6 | abstract contract Whitelist is Ownable { 7 | mapping(address => bool) public whitelist; 8 | mapping(address => bool) public operator; //have access to mint/burn 9 | 10 | function addManyWhitelist(address[] calldata accounts) external onlyOwner { 11 | for (uint256 i = 0; i < accounts.length; i++) { 12 | whitelist[accounts[i]] = true; 13 | } 14 | } 15 | 16 | function removeManyWhitelist(address[] calldata accounts) external onlyOwner { 17 | for (uint256 i = 0; i < accounts.length; i++) { 18 | whitelist[accounts[i]] = false; 19 | } 20 | } 21 | 22 | function addOperator(address account) external onlyOwner { 23 | _addOperator(account); 24 | } 25 | 26 | function removeOperator(address account) external onlyOwner { 27 | require(operator[account], "whitelist.removeOperator: NOT_OPERATOR"); 28 | operator[account] = false; 29 | } 30 | 31 | function _addOperator(address account) internal { 32 | require(!operator[account], "whitelist.addOperator: ALREADY_OPERATOR"); 33 | operator[account] = true; 34 | } 35 | 36 | modifier onlyOperator() { 37 | require(operator[msg.sender], "whitelist: NOT_IN_OPERATOR"); 38 | _; 39 | } 40 | 41 | modifier operatorOrWhitelist() { 42 | require(operator[msg.sender] || whitelist[msg.sender], "whitelist: NOT_IN_OPERATOR_OR_WHITELIST"); 43 | _; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/interfaces/uniswapV3/IUniswapV3Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapV3Pool { 5 | function swap( 6 | address recipient, 7 | bool zeroForOne, 8 | int256 amountSpecified, 9 | uint160 sqrtPriceLimitX96, 10 | bytes calldata data 11 | ) external returns (int256 amount0, int256 amount1); 12 | 13 | function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; 14 | 15 | function slot0() 16 | external 17 | view 18 | returns ( 19 | uint160 sqrtPriceX96, 20 | int24 tick, 21 | uint16 observationIndex, 22 | uint16 observationCardinality, 23 | uint16 observationCardinalityNext, 24 | uint8 feeProtocol, 25 | bool unlocked 26 | ); 27 | 28 | function observations(uint256 index) 29 | external 30 | view 31 | returns ( 32 | uint32 blockTimestamp, 33 | int56 tickCumulative, 34 | uint160 secondsPerLiquidityCumulativeX128, 35 | bool initialized 36 | ); 37 | 38 | function liquidity() external view returns (uint128); 39 | 40 | function token0() external view returns (address); 41 | 42 | function token1() external view returns (address); 43 | 44 | function fee() external view returns (uint24); 45 | 46 | function observe(uint32[] calldata secondsAgos) 47 | external 48 | view 49 | returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); 50 | } 51 | -------------------------------------------------------------------------------- /contracts/core/Migrator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IERC20.sol"; 5 | import "../interfaces/IAmm.sol"; 6 | import "../interfaces/IRouter.sol"; 7 | import "../interfaces/IPairFactory.sol"; 8 | import "../libraries/TransferHelper.sol"; 9 | import "../utils/Reentrant.sol"; 10 | 11 | contract Migrator is Reentrant { 12 | event Migrate(address indexed user, uint256 oldLiquidity, uint256 newLiquidity, uint256 baseAmount); 13 | 14 | IRouter public oldRouter; 15 | IRouter public newRouter; 16 | IPairFactory public oldFactory; 17 | IPairFactory public newFactory; 18 | 19 | constructor(IRouter oldRouter_, IRouter newRouter_) { 20 | oldRouter = oldRouter_; 21 | newRouter = newRouter_; 22 | oldFactory = IPairFactory(oldRouter.pairFactory()); 23 | newFactory = IPairFactory(newRouter.pairFactory()); 24 | } 25 | 26 | function migrate(address baseToken, address quoteToken) external nonReentrant { 27 | address oldAmm = oldFactory.getAmm(baseToken, quoteToken); 28 | uint256 oldLiquidity = IERC20(oldAmm).balanceOf(msg.sender); 29 | require(oldLiquidity > 0, "ZERO_LIQUIDITY"); 30 | TransferHelper.safeTransferFrom(oldAmm, msg.sender, oldAmm, oldLiquidity); 31 | (uint256 baseAmount, , ) = IAmm(oldAmm).burn(address(this)); 32 | require(baseAmount > 0, "ZERO_BASE_AMOUNT"); 33 | 34 | address newAmm = newFactory.getAmm(baseToken, quoteToken); 35 | TransferHelper.safeTransfer(baseToken, newAmm, baseAmount); 36 | ( , , uint256 newLiquidity) = IAmm(newAmm).mint(msg.sender); 37 | emit Migrate(msg.sender, oldLiquidity, newLiquidity, baseAmount); 38 | } 39 | } -------------------------------------------------------------------------------- /scripts/deploy_limit_order.js: -------------------------------------------------------------------------------- 1 | const { upgrades } = require("hardhat"); 2 | const { BigNumber } = require("@ethersproject/bignumber"); 3 | const verifyStr = "npx hardhat verify --network"; 4 | 5 | // prod 6 | const pairFactoryAddr = "0xA7B799832B46B51b2b6a156FDCE58525dE24Ac0f"; 7 | const wethAddress = "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"; 8 | const botAddr = "0xa41e805010d338Cf909138DFE4dbB7Ee356d20A5"; 9 | //test 10 | // const pairFactoryAddr = "0xCE09a98C734ffB8e209b907FB0657193796FE3fD"; // dev 11 | // const wethAddress = "0x655e2b2244934Aea3457E3C56a7438C271778D44"; // mockWETH 12 | // const botAddr = "0xbc6e4e0bc15293b5b9f0173c1c4a56525768d36c"; 13 | 14 | let signer; 15 | let routerForKeeper; 16 | let orderBook; 17 | 18 | const main = async () => { 19 | const accounts = await hre.ethers.getSigners(); 20 | signer = accounts[0].address; 21 | await createOrderBook(); 22 | }; 23 | 24 | async function createOrderBook() { 25 | const RouterForKeeper = await ethers.getContractFactory("RouterForKeeper"); 26 | routerForKeeper = await RouterForKeeper.deploy(pairFactoryAddr, wethAddress); 27 | console.log("RouterForKeeper:", routerForKeeper.address); 28 | 29 | const OrderBook = await ethers.getContractFactory("OrderBook"); 30 | orderBook = await OrderBook.deploy(routerForKeeper.address, botAddr); 31 | console.log("OrderBook:", orderBook.address); 32 | await routerForKeeper.setOrderBook(orderBook.address); 33 | 34 | console.log(verifyStr, process.env.HARDHAT_NETWORK, routerForKeeper.address, pairFactoryAddr, wethAddress); 35 | console.log(verifyStr, process.env.HARDHAT_NETWORK, orderBook.address, routerForKeeper.address, botAddr); 36 | } 37 | 38 | main() 39 | .then(() => process.exit(0)) 40 | .catch((error) => { 41 | console.error(error); 42 | process.exit(1); 43 | }); 44 | -------------------------------------------------------------------------------- /contracts/interfaces/IPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IPriceOracle { 5 | function setupTwap(address amm) external; 6 | 7 | function quoteFromAmmTwap(address amm, uint256 baseAmount) external view returns (uint256 quoteAmount); 8 | 9 | function updateAmmTwap(address pair) external; 10 | 11 | // index price maybe get from different oracle, like UniswapV3 TWAP,Chainklink, or others 12 | // source represents which oracle used. 0 = UniswapV3 TWAP 13 | function quote( 14 | address baseToken, 15 | address quoteToken, 16 | uint256 baseAmount 17 | ) external view returns (uint256 quoteAmount, uint8 source); 18 | 19 | function getIndexPrice(address amm) external view returns (uint256); 20 | 21 | function getMarketPrice(address amm) external view returns (uint256); 22 | 23 | function getMarkPrice(address amm) external view returns (uint256 price, bool isIndexPrice); 24 | 25 | function getMarkPriceAfterSwap( 26 | address amm, 27 | uint256 quoteAmount, 28 | uint256 baseAmount 29 | ) external view returns (uint256 price, bool isIndexPrice); 30 | 31 | function getMarkPriceInRatio( 32 | address amm, 33 | uint256 quoteAmount, 34 | uint256 baseAmount 35 | ) 36 | external 37 | view 38 | returns ( 39 | uint256 resultBaseAmount, 40 | uint256 resultQuoteAmount, 41 | bool isIndexPrice 42 | ); 43 | 44 | function getMarkPriceAcc( 45 | address amm, 46 | uint8 beta, 47 | uint256 quoteAmount, 48 | bool negative 49 | ) external view returns (uint256 baseAmount); 50 | 51 | function getPremiumFraction(address amm) external view returns (int256); 52 | } 53 | -------------------------------------------------------------------------------- /contracts/core/PairFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IPairFactory.sol"; 5 | import "../interfaces/IAmmFactory.sol"; 6 | import "../interfaces/IMarginFactory.sol"; 7 | import "../interfaces/IAmm.sol"; 8 | import "../interfaces/IMargin.sol"; 9 | import "../utils/Ownable.sol"; 10 | 11 | contract PairFactory is IPairFactory, Ownable { 12 | address public override ammFactory; 13 | address public override marginFactory; 14 | 15 | constructor() { 16 | owner = msg.sender; 17 | } 18 | 19 | function init(address ammFactory_, address marginFactory_) external onlyOwner { 20 | require(ammFactory == address(0) && marginFactory == address(0), "PairFactory: ALREADY_INITED"); 21 | require(ammFactory_ != address(0) && marginFactory_ != address(0), "PairFactory: ZERO_ADDRESS"); 22 | ammFactory = ammFactory_; 23 | marginFactory = marginFactory_; 24 | } 25 | 26 | function createPair(address baseToken, address quoteToken) external override returns (address amm, address margin) { 27 | amm = IAmmFactory(ammFactory).createAmm(baseToken, quoteToken); 28 | margin = IMarginFactory(marginFactory).createMargin(baseToken, quoteToken); 29 | IAmmFactory(ammFactory).initAmm(baseToken, quoteToken, margin); 30 | IMarginFactory(marginFactory).initMargin(baseToken, quoteToken, amm); 31 | emit NewPair(baseToken, quoteToken, amm, margin); 32 | } 33 | 34 | function getAmm(address baseToken, address quoteToken) external view override returns (address) { 35 | return IAmmFactory(ammFactory).getAmm(baseToken, quoteToken); 36 | } 37 | 38 | function getMargin(address baseToken, address quoteToken) external view override returns (address) { 39 | return IMarginFactory(marginFactory).getMargin(baseToken, quoteToken); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/mocks/MockRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "../interfaces/IMargin.sol"; 6 | import "../interfaces/IWETH.sol"; 7 | 8 | contract MockRouter { 9 | IMargin public margin; 10 | IERC20 public baseToken; 11 | IWETH public WETH; 12 | 13 | constructor(address _baseToken, address _weth) { 14 | baseToken = IERC20(_baseToken); 15 | WETH = IWETH(_weth); 16 | } 17 | 18 | receive() external payable { 19 | assert(msg.sender == address(WETH)); // only accept ETH via fallback from the WETH contract 20 | } 21 | 22 | function setMarginContract(address _marginContract) external { 23 | margin = IMargin(_marginContract); 24 | } 25 | 26 | function addMargin(address _receiver, uint256 _amount) external { 27 | baseToken.transferFrom(msg.sender, address(margin), _amount); 28 | margin.addMargin(_receiver, _amount); 29 | } 30 | 31 | function removeMargin(uint256 _amount) external { 32 | margin.removeMargin(msg.sender, msg.sender, _amount); 33 | } 34 | 35 | function withdrawETH(address quoteToken, uint256 amount) external { 36 | margin.removeMargin(msg.sender, msg.sender, amount); 37 | IWETH(WETH).withdraw(amount); 38 | } 39 | 40 | function closePositionETH( 41 | address quoteToken, 42 | uint256 quoteAmount, 43 | uint256 deadline, 44 | bool autoWithdraw 45 | ) external returns (uint256 baseAmount, uint256 withdrawAmount) { 46 | baseAmount = margin.closePosition(msg.sender, quoteAmount); 47 | if (autoWithdraw) { 48 | withdrawAmount = margin.getWithdrawable(msg.sender); 49 | if (withdrawAmount > 0) { 50 | margin.removeMargin(msg.sender, msg.sender, withdrawAmount); 51 | IWETH(WETH).withdraw(withdrawAmount); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/libraries/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false 5 | library TransferHelper { 6 | function safeApprove( 7 | address token, 8 | address to, 9 | uint256 value 10 | ) internal { 11 | // bytes4(keccak256(bytes('approve(address,uint256)'))); 12 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); 13 | require( 14 | success && (data.length == 0 || abi.decode(data, (bool))), 15 | "TransferHelper::safeApprove: approve failed" 16 | ); 17 | } 18 | 19 | function safeTransfer( 20 | address token, 21 | address to, 22 | uint256 value 23 | ) internal { 24 | // bytes4(keccak256(bytes('transfer(address,uint256)'))); 25 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); 26 | require( 27 | success && (data.length == 0 || abi.decode(data, (bool))), 28 | "TransferHelper::safeTransfer: transfer failed" 29 | ); 30 | } 31 | 32 | function safeTransferFrom( 33 | address token, 34 | address from, 35 | address to, 36 | uint256 value 37 | ) internal { 38 | // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); 39 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); 40 | require( 41 | success && (data.length == 0 || abi.decode(data, (bool))), 42 | "TransferHelper::transferFrom: transferFrom failed" 43 | ); 44 | } 45 | 46 | function safeTransferETH(address to, uint256 value) internal { 47 | (bool success, ) = to.call{value: value}(new bytes(0)); 48 | require(success, "TransferHelper::safeTransferETH: ETH transfer failed"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/libraries/UniswapV3TwapGetter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/uniswapV3/IUniswapV3Pool.sol"; 5 | import "./FixedPoint96.sol"; 6 | import "./TickMath.sol"; 7 | import "./FullMath.sol"; 8 | 9 | library UniswapV3TwapGetter { 10 | function getSqrtTwapX96(address uniswapV3Pool, uint32 twapInterval) internal view returns (uint160 sqrtPriceX96) { 11 | IUniswapV3Pool pool = IUniswapV3Pool(uniswapV3Pool); 12 | if (twapInterval == 0) { 13 | // return the current price if twapInterval == 0 14 | (sqrtPriceX96, , , , , , ) = pool.slot0(); 15 | } else { 16 | (, , uint16 index, uint16 cardinality, , , ) = pool.slot0(); 17 | (uint32 targetElementTime, , , bool initialized) = pool.observations((index + 1) % cardinality); 18 | if (!initialized) { 19 | (targetElementTime, , , ) = pool.observations(0); 20 | } 21 | uint32 delta = uint32(block.timestamp) - targetElementTime; 22 | if (delta == 0) { 23 | (sqrtPriceX96, , , , , , ) = pool.slot0(); 24 | } else { 25 | if (delta < twapInterval) twapInterval = delta; 26 | uint32[] memory secondsAgos = new uint32[](2); 27 | secondsAgos[0] = twapInterval; // from (before) 28 | secondsAgos[1] = 0; // to (now) 29 | (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos); 30 | // tick(imprecise as it's an integer) to price 31 | sqrtPriceX96 = TickMath.getSqrtRatioAtTick( 32 | int24((tickCumulatives[1] - tickCumulatives[0]) / int56(uint56(twapInterval))) 33 | ); 34 | } 35 | } 36 | } 37 | 38 | function getPriceX96FromSqrtPriceX96(uint160 sqrtPriceX96) internal pure returns (uint256 priceX96) { 39 | return FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/token/ApeXTokenMantle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "../interfaces/IL2StandardERC20.sol"; 6 | 7 | contract ApeXTokenMantle is IL2StandardERC20,ERC20 { 8 | address public override l1Token; 9 | address public l2Bridge; 10 | uint8 public decimal; 11 | 12 | /** 13 | * @param _l2Bridge Address of the L2 standard bridge. 14 | * @param _l1Token Address of the corresponding L1 token. 15 | * @param _name ERC20 name. 16 | * @param _symbol ERC20 symbol. 17 | */ 18 | constructor( 19 | address _l2Bridge, 20 | address _l1Token, 21 | string memory _name, 22 | string memory _symbol, 23 | uint8 _decimal 24 | ) ERC20(_name, _symbol) { 25 | l1Token = _l1Token; 26 | l2Bridge = _l2Bridge; 27 | decimal = _decimal; 28 | } 29 | 30 | modifier onlyL2Bridge() { 31 | require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); 32 | _; 33 | } 34 | 35 | function supportsInterface(bytes4 _interfaceId) public pure override returns (bool) { 36 | bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 37 | bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^ 38 | IL2StandardERC20.mint.selector ^ 39 | IL2StandardERC20.burn.selector; 40 | return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface; 41 | } 42 | 43 | function mint(address _to, uint256 _amount) public virtual override onlyL2Bridge { 44 | _mint(_to, _amount); 45 | emit Mint(_to, _amount); 46 | } 47 | 48 | function burn(address _from, uint256 _amount) public virtual override onlyL2Bridge { 49 | _burn(_from, _amount); 50 | emit Burn(_from, _amount); 51 | } 52 | 53 | function decimals() public view virtual override returns (uint8) { 54 | return decimal; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/mocks/MockPriceOracleM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract MockPriceOracleOfMargin { 6 | int256 public pf = 0; 7 | uint256 public p; 8 | uint256 public markPriceInRatio; 9 | bool public isIndex; 10 | 11 | constructor() { 12 | p = 2e9; 13 | } 14 | 15 | //premiumFraction is (markPrice - indexPrice) * fundingRatePrecision / 8h / indexPrice 16 | function getPremiumFraction(address amm) external view returns (int256) { 17 | return pf; 18 | } 19 | 20 | function setPf(int256 _pf) external { 21 | pf = _pf; 22 | } 23 | 24 | //2000usdc = 2000*(1e-12)*1e18 25 | function setMarkPriceInRatio(uint256 _markPriceInRatio) external { 26 | markPriceInRatio = _markPriceInRatio; 27 | } 28 | 29 | function setIsIndex(bool value) external { 30 | isIndex = value; 31 | } 32 | 33 | function getMarkPriceAcc( 34 | address amm, 35 | uint8 beta, 36 | uint256 quoteAmount, 37 | bool negative 38 | ) public view returns (uint256 price) { 39 | return (quoteAmount * 1e18) / p; 40 | } 41 | 42 | function setMarkPrice(uint256 _p) external { 43 | p = _p; 44 | } 45 | 46 | function quote( 47 | address baseToken, 48 | address quoteToken, 49 | uint256 baseAmount 50 | ) external view returns (uint256 quoteAmount, uint8 source) { 51 | quoteAmount = 100000 * 10**6; 52 | source = 0; 53 | } 54 | 55 | function updateAmmTwap(address pair) external {} 56 | 57 | function setupTwap(address amm) external {} 58 | 59 | function getMarkPriceInRatio( 60 | address amm, 61 | uint256 quoteAmount, 62 | uint256 baseAmount 63 | ) 64 | external 65 | view 66 | returns ( 67 | uint256, 68 | uint256, 69 | bool 70 | ) 71 | { 72 | return ((quoteAmount * 1e18) / markPriceInRatio, (baseAmount * markPriceInRatio) / 1e18, isIndex); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/token/ApeXToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 7 | 8 | contract ApeXToken is ERC20Votes, Ownable { 9 | event AddMinter(address minter); 10 | event RemoveMinter(address minter); 11 | 12 | using EnumerableSet for EnumerableSet.AddressSet; 13 | EnumerableSet.AddressSet private minters; 14 | uint256 public constant initTotalSupply = 1_000_000_000e18; // 1 billion 15 | 16 | // modifier for mint function 17 | modifier onlyMinter() { 18 | require(isMinter(msg.sender), "ApeXToken: CALLER_IS_NOT_THE_MINTER"); 19 | _; 20 | } 21 | 22 | constructor() ERC20Permit("") ERC20("ApeX Token", "APEX") { 23 | _mint(msg.sender, initTotalSupply); 24 | } 25 | 26 | function mint(address to, uint256 amount) external onlyMinter returns (bool) { 27 | _mint(to, amount); 28 | return true; 29 | } 30 | 31 | function addMinter(address minter) external onlyOwner returns (bool) { 32 | require(minter != address(0), "ApeXToken.addMinter: ZERO_ADDRESS"); 33 | emit AddMinter(minter); 34 | return EnumerableSet.add(minters, minter); 35 | } 36 | 37 | function removeMinter(address minter) external onlyOwner returns (bool) { 38 | require(minter != address(0), "ApeXToken.delMinter: ZERO_ADDRESS"); 39 | emit RemoveMinter(minter); 40 | return EnumerableSet.remove(minters, minter); 41 | } 42 | 43 | function isMinter(address account) public view returns (bool) { 44 | return EnumerableSet.contains(minters, account); 45 | } 46 | 47 | function getMinterLength() external view returns (uint256) { 48 | return EnumerableSet.length(minters); 49 | } 50 | 51 | function getMinter(uint256 index) external view onlyOwner returns (address) { 52 | require(index <= EnumerableSet.length(minters) - 1, "ApeXToken.getMinter: OUT_OF_BOUNDS"); 53 | return EnumerableSet.at(minters, index); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/core/MarginFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "./Margin.sol"; 5 | import "../interfaces/IMarginFactory.sol"; 6 | 7 | //factory of margin, called by pairFactory 8 | contract MarginFactory is IMarginFactory { 9 | address public immutable override upperFactory; // PairFactory 10 | address public immutable override config; 11 | 12 | // baseToken => quoteToken => margin 13 | mapping(address => mapping(address => address)) public override getMargin; 14 | 15 | modifier onlyUpper() { 16 | require(msg.sender == upperFactory, "AmmFactory: FORBIDDEN"); 17 | _; 18 | } 19 | 20 | constructor(address upperFactory_, address config_) { 21 | require(upperFactory_ != address(0), "MarginFactory: ZERO_UPPER"); 22 | require(config_ != address(0), "MarginFactory: ZERO_CONFIG"); 23 | upperFactory = upperFactory_; 24 | config = config_; 25 | } 26 | 27 | function createMargin(address baseToken, address quoteToken) external override onlyUpper returns (address margin) { 28 | require(baseToken != quoteToken, "MarginFactory.createMargin: IDENTICAL_ADDRESSES"); 29 | require(baseToken != address(0) && quoteToken != address(0), "MarginFactory.createMargin: ZERO_ADDRESS"); 30 | require(getMargin[baseToken][quoteToken] == address(0), "MarginFactory.createMargin: MARGIN_EXIST"); 31 | bytes32 salt = keccak256(abi.encodePacked(baseToken, quoteToken)); 32 | bytes memory marginBytecode = type(Margin).creationCode; 33 | assembly { 34 | margin := create2(0, add(marginBytecode, 32), mload(marginBytecode), salt) 35 | } 36 | getMargin[baseToken][quoteToken] = margin; 37 | emit MarginCreated(baseToken, quoteToken, margin); 38 | } 39 | 40 | function initMargin( 41 | address baseToken, 42 | address quoteToken, 43 | address amm 44 | ) external override onlyUpper { 45 | require(amm != address(0), "MarginFactory.initMargin: ZERO_AMM"); 46 | address margin = getMargin[baseToken][quoteToken]; 47 | require(margin != address(0), "MarginFactory.initMargin: ZERO_MARGIN"); 48 | IMargin(margin).initialize(baseToken, quoteToken, amm); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/mocks/MockMargin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "../interfaces/IAmm.sol"; 7 | import "../libraries/SignedMath.sol"; 8 | 9 | contract MockMargin { 10 | 11 | using SignedMath for int256; 12 | address public config; 13 | address public amm; 14 | address public baseToken; 15 | address public quoteToken; 16 | uint256 public reserve; 17 | int256 public netpostion ; 18 | uint256 public totalpostion ; 19 | constructor() { 20 | } 21 | 22 | function initialize( 23 | address baseToken_, 24 | address quoteToken_, 25 | address amm_, 26 | address config_ 27 | ) external { 28 | baseToken = baseToken_; 29 | quoteToken = quoteToken_; 30 | amm = amm_; 31 | config = config_ ; 32 | } 33 | 34 | 35 | function totalPosition() external view returns (uint256 ){ 36 | return netpostion.abs()*7; 37 | } 38 | function netPosition() external view returns (int256 ){ 39 | return netpostion; 40 | } 41 | function setNetPosition(int256 newNetpositon ) external returns (int256 ){ 42 | 43 | netpostion = newNetpositon; 44 | return netpostion; 45 | } 46 | 47 | 48 | function deposit(address user, uint256 amount) external { 49 | require(msg.sender == amm, "Margin.deposit: REQUIRE_AMM"); 50 | require(amount > 0, "Margin.deposit: AMOUNT_IS_ZERO"); 51 | uint256 balance = IERC20(baseToken).balanceOf(address(this)); 52 | require(amount <= balance - reserve, "Margin.deposit: INSUFFICIENT_AMOUNT"); 53 | 54 | reserve = reserve + amount; 55 | } 56 | 57 | // need for testing 58 | function swapProxy( 59 | address trader, 60 | address inputToken, 61 | address outputToken, 62 | uint256 inputAmount, 63 | uint256 outputAmount 64 | ) external returns (uint256[2] memory amounts) { 65 | IAmm(amm).swap(trader, inputToken, outputToken, inputAmount, outputAmount); 66 | } 67 | 68 | function withdraw( 69 | address user, 70 | address receiver, 71 | uint256 amount 72 | ) external { 73 | require(msg.sender == amm, "Margin.withdraw: REQUIRE_AMM"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/mocks/MockWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | contract MockWETH { 5 | string public name = "Mock Wrapped Ether"; 6 | string public symbol = "mWETH"; 7 | uint8 public decimals = 18; 8 | 9 | event Approval(address indexed src, address indexed guy, uint256 wad); 10 | event Transfer(address indexed src, address indexed dst, uint256 wad); 11 | event Deposit(address indexed dst, uint256 wad); 12 | event Withdrawal(address indexed src, uint256 wad); 13 | 14 | mapping(address => uint256) public balanceOf; 15 | mapping(address => mapping(address => uint256)) public allowance; 16 | 17 | constructor() { 18 | balanceOf[msg.sender] = 1_000_000_000e18; 19 | } 20 | 21 | receive() external payable { 22 | deposit(); 23 | } 24 | 25 | fallback() external payable { 26 | deposit(); 27 | } 28 | 29 | function deposit() public payable { 30 | balanceOf[msg.sender] += msg.value; 31 | emit Deposit(msg.sender, msg.value); 32 | } 33 | 34 | function withdraw(uint256 wad) public { 35 | require(balanceOf[msg.sender] >= wad, "invalid balance"); 36 | balanceOf[msg.sender] -= wad; 37 | payable(msg.sender).transfer(wad); 38 | emit Withdrawal(msg.sender, wad); 39 | } 40 | 41 | function totalSupply() public view returns (uint256) { 42 | return address(this).balance; 43 | } 44 | 45 | function approve(address guy, uint256 wad) public returns (bool) { 46 | allowance[msg.sender][guy] = wad; 47 | emit Approval(msg.sender, guy, wad); 48 | return true; 49 | } 50 | 51 | function transfer(address dst, uint256 wad) public returns (bool) { 52 | return transferFrom(msg.sender, dst, wad); 53 | } 54 | 55 | function transferFrom( 56 | address src, 57 | address dst, 58 | uint256 wad 59 | ) public returns (bool) { 60 | require(balanceOf[src] >= wad); 61 | 62 | if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { 63 | require(allowance[src][msg.sender] >= wad); 64 | allowance[src][msg.sender] -= wad; 65 | } 66 | 67 | balanceOf[src] -= wad; 68 | balanceOf[dst] += wad; 69 | 70 | emit Transfer(src, dst, wad); 71 | 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/interfaces/IOrderBook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IOrderBook { 5 | struct RespData { 6 | bool success; 7 | bytes result; 8 | } 9 | 10 | struct OpenPositionOrder { 11 | address routerToExecute; 12 | address trader; 13 | address baseToken; 14 | address quoteToken; 15 | uint8 side; 16 | uint256 baseAmount; 17 | uint256 quoteAmount; 18 | uint256 slippage; 19 | uint256 limitPrice; 20 | uint256 deadline; 21 | bool withWallet; 22 | bytes nonce; 23 | } 24 | 25 | struct ClosePositionOrder { 26 | address routerToExecute; 27 | address trader; 28 | address baseToken; 29 | address quoteToken; 30 | uint8 side; 31 | uint256 quoteAmount; 32 | uint256 limitPrice; 33 | uint256 deadline; 34 | bool autoWithdraw; 35 | bytes nonce; 36 | } 37 | 38 | event BatchExecuteOpen(OpenPositionOrder[] orders, bytes[] signatures, bool requireSuccess); 39 | 40 | event BatchExecuteClose(ClosePositionOrder[] orders, bytes[] signatures, bool requireSuccess); 41 | 42 | event SetRouterForKeeper(address newRouterForKeeper); 43 | 44 | event ExecuteOpen(OpenPositionOrder order, bytes signature); 45 | 46 | event ExecuteClose(ClosePositionOrder order, bytes signature); 47 | 48 | event ExecuteLog(bytes orderId, bool success); 49 | 50 | function batchExecuteOpen( 51 | OpenPositionOrder[] memory orders, 52 | bytes[] memory signatures, 53 | bool requireSuccess 54 | ) external returns (RespData[] memory respData); 55 | 56 | function batchExecuteClose( 57 | ClosePositionOrder[] memory orders, 58 | bytes[] memory signatures, 59 | bool requireSuccess 60 | ) external returns (RespData[] memory respData); 61 | 62 | function executeOpen(OpenPositionOrder memory order, bytes memory signature) external; 63 | 64 | function executeClose(ClosePositionOrder memory order, bytes memory signature) external; 65 | 66 | function verifyOpen(OpenPositionOrder memory order, bytes memory signature) external view returns (bool); 67 | 68 | function verifyClose(ClosePositionOrder memory order, bytes memory signature) external view returns (bool); 69 | 70 | function setRouterForKeeper(address routerForKeeper) external; 71 | 72 | function addBot(address newBot) external; 73 | 74 | function reverseBotState(address bot) external; 75 | } 76 | -------------------------------------------------------------------------------- /contracts/core/AmmFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "./Amm.sol"; 5 | import "../interfaces/IAmmFactory.sol"; 6 | import "../interfaces/IPriceOracle.sol"; 7 | 8 | contract AmmFactory is IAmmFactory { 9 | address public immutable override upperFactory; // PairFactory 10 | address public immutable override config; 11 | address public override feeTo; 12 | address public override feeToSetter; 13 | 14 | // baseToken => quoteToken => amm 15 | mapping(address => mapping(address => address)) public override getAmm; 16 | 17 | modifier onlyUpper() { 18 | require(msg.sender == upperFactory, "AmmFactory: FORBIDDEN"); 19 | _; 20 | } 21 | 22 | constructor( 23 | address upperFactory_, 24 | address config_, 25 | address feeToSetter_ 26 | ) { 27 | require(config_ != address(0) && feeToSetter_ != address(0), "AmmFactory: ZERO_ADDRESS"); 28 | upperFactory = upperFactory_; 29 | config = config_; 30 | feeToSetter = feeToSetter_; 31 | } 32 | 33 | function createAmm(address baseToken, address quoteToken) external override onlyUpper returns (address amm) { 34 | require(baseToken != quoteToken, "AmmFactory.createAmm: IDENTICAL_ADDRESSES"); 35 | require(baseToken != address(0) && quoteToken != address(0), "AmmFactory.createAmm: ZERO_ADDRESS"); 36 | require(getAmm[baseToken][quoteToken] == address(0), "AmmFactory.createAmm: AMM_EXIST"); 37 | bytes32 salt = keccak256(abi.encodePacked(baseToken, quoteToken)); 38 | bytes memory ammBytecode = type(Amm).creationCode; 39 | assembly { 40 | amm := create2(0, add(ammBytecode, 32), mload(ammBytecode), salt) 41 | } 42 | getAmm[baseToken][quoteToken] = amm; 43 | emit AmmCreated(baseToken, quoteToken, amm); 44 | } 45 | 46 | function initAmm( 47 | address baseToken, 48 | address quoteToken, 49 | address margin 50 | ) external override onlyUpper { 51 | address amm = getAmm[baseToken][quoteToken]; 52 | Amm(amm).initialize(baseToken, quoteToken, margin); 53 | IPriceOracle(IConfig(config).priceOracle()).setupTwap(amm); 54 | } 55 | 56 | function setFeeTo(address feeTo_) external override { 57 | require(msg.sender == feeToSetter, "AmmFactory.setFeeTo: FORBIDDEN"); 58 | feeTo = feeTo_; 59 | } 60 | 61 | function setFeeToSetter(address feeToSetter_) external override { 62 | require(msg.sender == feeToSetter, "AmmFactory.setFeeToSetter: FORBIDDEN"); 63 | feeToSetter = feeToSetter_; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apex-protocol/core", 3 | "version": "0.1.3", 4 | "description": "Core Smart Contracts of ApeX Protocol", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/ApeX-Protocol/core" 11 | }, 12 | "keywords": [ 13 | "apex" 14 | ], 15 | "author": "", 16 | "license": "GPL-3.0", 17 | "bugs": { 18 | "url": "https://github.com/ApeX-Protocol/core/issues" 19 | }, 20 | "homepage": "https://apex.exchange", 21 | "files": [ 22 | "contracts/", 23 | "artifacts/contracts/**/*.json", 24 | "!artifacts/contracts/**/*.dbg.json" 25 | ], 26 | "engines": { 27 | "node": ">=10" 28 | }, 29 | "scripts": { 30 | "compile": "npx hardhat compile", 31 | "compile_watch": "npx hardhat watch compilation", 32 | "test": "npx hardhat test", 33 | "test_watch": "npx hardhat watch test", 34 | "deploy_core_arbi": "hardhat run scripts/deploy_core.js --network arbitrumOne", 35 | "deploy_fee_treasury_arbi": "hardhat run scripts/deploy_fee_treasury.js --network arbitrumOne", 36 | "deploy_limit_order_arbi": "hardhat run scripts/deploy_limit_order.js --network arbitrumOne", 37 | "deploy_core_test": "hardhat run scripts/deploy_core.js --network arbitrumTestnet", 38 | "deploy_fee_treasury_test": "hardhat run scripts/deploy_fee_treasury.js --network arbitrumTestnet", 39 | "deploy_limit_order_test": "hardhat run scripts/deploy_limit_order.js --network arbitrumTestnet", 40 | "deploy_apex": "npx hardhat run scripts/deploy_apex.js --network ", 41 | "verify_apex": "npx hardhat run scripts/verify_apex.js --network ", 42 | "deploy_apex_mantle": "npx hardhat run scripts/deploy_apex_mantle.js --network ", 43 | "verify_apex_mantle": "npx hardhat run scripts/verify_apex_mantle.js --network " 44 | }, 45 | "devDependencies": { 46 | "@nomiclabs/hardhat-ethers": "^2.0.2", 47 | "@nomiclabs/hardhat-etherscan": "^3.1.7", 48 | "@nomiclabs/hardhat-waffle": "^2.0.1", 49 | "@openzeppelin/hardhat-upgrades": "^1.12.0", 50 | "@openzeppelin/test-helpers": "^0.5.15", 51 | "arb-ts": "^1.0.0-beta.4", 52 | "chai": "^4.3.4", 53 | "ethereum-waffle": "^3.4.0", 54 | "ethers": "^5.4.7", 55 | "hardhat": "^2.8.0", 56 | "hardhat-deploy": "^0.9.4", 57 | "hardhat-gas-reporter": "^1.0.4", 58 | "keccak256": "^1.0.2", 59 | "merkletreejs": "^0.2.13", 60 | "solidity-coverage": "^0.7.17" 61 | }, 62 | "dependencies": { 63 | "@openzeppelin/contracts": "^4.3.2", 64 | "@uniswap/lib": "^4.0.1-alpha", 65 | "@uniswap/v3-core": "^1.0.0", 66 | "@uniswap/v2-core": "1.0.1", 67 | "dotenv": "^10.0.0", 68 | "hardhat-watcher": "^2.1.1" 69 | } 70 | } -------------------------------------------------------------------------------- /test/core/config.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("@ethersproject/bignumber"); 2 | const { expect } = require("chai"); 3 | const { ethers } = require("hardhat"); 4 | 5 | describe("Config contract", function () { 6 | let config; 7 | beforeEach(async function () { 8 | [owner, addr1, liquidator, ...addrs] = await ethers.getSigners(); 9 | 10 | const Config = await ethers.getContractFactory("Config"); 11 | config = await Config.deploy(); 12 | await config.setBeta(100); 13 | await config.setInitMarginRatio(909); 14 | await config.setLiquidateThreshold(10000); 15 | await config.setLiquidateFeeRatio(2000); 16 | }); 17 | 18 | describe("set initMarginRatio", async function () { 19 | it("set correct ratio", async function () { 20 | await config.setInitMarginRatio(1000); 21 | expect(await config.initMarginRatio()).to.equal(1000); 22 | }); 23 | 24 | it("revert when set wrong ratio", async function () { 25 | await expect(config.setInitMarginRatio(9)).to.be.revertedWith("Config: INVALID_MARGIN_RATIO"); 26 | }); 27 | 28 | it("reverted if addr1 set", async function () { 29 | await expect(config.connect(addr1).setInitMarginRatio(9)).to.be.revertedWith("Ownable: REQUIRE_OWNER"); 30 | }); 31 | }); 32 | 33 | describe("set liquidateThreshold", async function () { 34 | it("set correct threshold", async function () { 35 | await config.setLiquidateThreshold(10000); 36 | expect(await config.liquidateThreshold()).to.equal(10000); 37 | }); 38 | 39 | it("revert when set wrong threshold", async function () { 40 | await expect(config.setLiquidateThreshold(80)).to.be.revertedWith("Config: INVALID_LIQUIDATE_THRESHOLD"); 41 | }); 42 | }); 43 | 44 | describe("set liquidateFeeRatio", async function () { 45 | it("set correct fee ratio", async function () { 46 | await config.setLiquidateFeeRatio(1000); 47 | expect(await config.liquidateFeeRatio()).to.equal(1000); 48 | }); 49 | 50 | it("revert when set wrong fee ratio", async function () { 51 | await expect(config.setLiquidateFeeRatio(3000)).to.be.revertedWith("Config: INVALID_LIQUIDATE_FEE_RATIO"); 52 | }); 53 | }); 54 | 55 | describe("registerRouter", async function () { 56 | it("revert when register a registered router", async function () { 57 | await config.registerRouter(addr1.address); 58 | await expect(config.registerRouter(addr1.address)).to.be.revertedWith("Config: REGISTERED"); 59 | }); 60 | }); 61 | 62 | describe("unregisterRouter", async function () { 63 | it("revert when unregister an unregister router", async function () { 64 | await expect(config.unregisterRouter(addr1.address)).to.be.revertedWith("Config: UNREGISTERED"); 65 | }); 66 | 67 | it("unregister an registered router", async function () { 68 | await config.registerRouter(addr1.address); 69 | await config.unregisterRouter(addr1.address); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /contracts/mocks/MockFlashAttacker.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol"; 7 | import "../interfaces/IMargin.sol"; 8 | 9 | interface IERC20Mint { 10 | function mint(address spender, uint256 amount) external; 11 | } 12 | 13 | contract MockFlashAttacker is IERC3156FlashBorrower { 14 | ERC20FlashMint public baseToken; 15 | IMargin public margin; 16 | address public quoteToken; 17 | 18 | enum Action { 19 | action1, 20 | action2 21 | } 22 | 23 | struct FlashData { 24 | uint256 baseAmount; 25 | Action action; 26 | } 27 | 28 | constructor( 29 | address _token, 30 | address _margin, 31 | address _quoteToken 32 | ) { 33 | baseToken = ERC20FlashMint(_token); 34 | margin = IMargin(_margin); 35 | quoteToken = _quoteToken; 36 | } 37 | 38 | function onFlashLoan( 39 | address _initiator, 40 | address _token, 41 | uint256 _amount, 42 | uint256 _fee, 43 | bytes calldata data 44 | ) external override returns (bytes32) { 45 | require(_initiator == address(this), ""); 46 | uint8 long = 0; 47 | FlashData memory flashData = abi.decode(data, (FlashData)); 48 | if (flashData.action == Action.action1) { 49 | IERC20(_token).transfer(address(margin), flashData.baseAmount); 50 | margin.addMargin(address(this), flashData.baseAmount); 51 | margin.openPosition(address(this), long, flashData.baseAmount * 2); 52 | margin.closePosition(address(this), flashData.baseAmount * 2); 53 | } else { 54 | IERC20(_token).transfer(address(margin), flashData.baseAmount); 55 | margin.addMargin(address(this), flashData.baseAmount); 56 | margin.openPosition(address(this), long, flashData.baseAmount * 2); 57 | margin.removeMargin(address(this), address(this), 1); 58 | } 59 | 60 | IERC20(_token).approve(address(_token), _amount + _fee); 61 | return keccak256("ERC3156FlashBorrower.onFlashLoan"); 62 | } 63 | 64 | function attack1(uint256 borrow, uint256 baseAmount) public { 65 | baseToken.flashLoan( 66 | IERC3156FlashBorrower(this), 67 | address(baseToken), 68 | borrow, 69 | abi.encode(FlashData(baseAmount, Action.action1)) 70 | ); 71 | } 72 | 73 | function attack2(uint256 borrow, uint256 baseAmount) public { 74 | IERC20Mint(address(baseToken)).mint(address(this), 1000); 75 | baseToken.flashLoan( 76 | IERC3156FlashBorrower(this), 77 | address(baseToken), 78 | borrow, 79 | abi.encode(FlashData(baseAmount, Action.action2)) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/merkle-tree.js: -------------------------------------------------------------------------------- 1 | const { bufferToHex, keccak256 } = require('ethereumjs-util'); 2 | 3 | class MerkleTree { 4 | constructor(elements) { 5 | this.elements = [...elements] 6 | // Sort elements 7 | this.elements.sort(Buffer.compare) 8 | // Deduplicate elements 9 | this.elements = MerkleTree.bufDedup(this.elements) 10 | 11 | this.bufferElementPositionIndex = this.elements.reduce((memo, el, index) => { 12 | memo[bufferToHex(el)] = index 13 | return memo 14 | }, {}) 15 | 16 | // Create layers 17 | this.layers = this.getLayers(this.elements) 18 | } 19 | 20 | getLayers(elements) { 21 | if (elements.length === 0) { 22 | throw new Error('empty tree') 23 | } 24 | 25 | const layers = [] 26 | layers.push(elements) 27 | 28 | // Get next layer until we reach the root 29 | while (layers[layers.length - 1].length > 1) { 30 | layers.push(this.getNextLayer(layers[layers.length - 1])) 31 | } 32 | 33 | return layers 34 | } 35 | 36 | getNextLayer(elements) { 37 | return elements.reduce((layer, el, idx, arr) => { 38 | if (idx % 2 === 0) { 39 | // Hash the current element with its pair element 40 | layer.push(MerkleTree.combinedHash(el, arr[idx + 1])) 41 | } 42 | 43 | return layer 44 | }, []) 45 | } 46 | 47 | static combinedHash(first, second) { 48 | if (!first) { 49 | return second 50 | } 51 | if (!second) { 52 | return first 53 | } 54 | 55 | return keccak256(MerkleTree.sortAndConcat(first, second)) 56 | } 57 | 58 | getRoot() { 59 | return this.layers[this.layers.length - 1][0] 60 | } 61 | 62 | getHexRoot() { 63 | return bufferToHex(this.getRoot()) 64 | } 65 | 66 | getProof(el) { 67 | let idx = this.bufferElementPositionIndex[bufferToHex(el)] 68 | 69 | if (typeof idx !== 'number') { 70 | throw new Error('Element does not exist in Merkle tree') 71 | } 72 | 73 | return this.layers.reduce((proof, layer) => { 74 | const pairElement = MerkleTree.getPairElement(idx, layer) 75 | 76 | if (pairElement) { 77 | proof.push(pairElement) 78 | } 79 | 80 | idx = Math.floor(idx / 2) 81 | 82 | return proof 83 | }, []) 84 | } 85 | 86 | getHexProof(el) { 87 | const proof = this.getProof(el) 88 | 89 | return MerkleTree.bufArrToHexArr(proof) 90 | } 91 | 92 | static getPairElement(idx, layer) { 93 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1 94 | 95 | if (pairIdx < layer.length) { 96 | return layer[pairIdx] 97 | } else { 98 | return null 99 | } 100 | } 101 | 102 | static bufDedup(elements) { 103 | return elements.filter((el, idx) => { 104 | return idx === 0 || !elements[idx - 1].equals(el) 105 | }) 106 | } 107 | 108 | static bufArrToHexArr(arr) { 109 | if (arr.some((el) => !Buffer.isBuffer(el))) { 110 | throw new Error('Array is not an array of buffers') 111 | } 112 | 113 | return arr.map((el) => '0x' + el.toString('hex')) 114 | } 115 | 116 | static sortAndConcat(...args) { 117 | return Buffer.concat([...args].sort(Buffer.compare)) 118 | } 119 | } 120 | module.exports = MerkleTree; -------------------------------------------------------------------------------- /contracts/core/Multicall2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../libraries/ChainAdapter.sol"; 5 | 6 | /// @title Multicall - Aggregate results from multiple read-only function calls 7 | contract Multicall2 { 8 | struct Call { 9 | address target; 10 | bytes callData; 11 | } 12 | struct Result { 13 | bool success; 14 | bytes returnData; 15 | } 16 | 17 | function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { 18 | blockNumber = ChainAdapter.blockNumber(); 19 | returnData = new bytes[](calls.length); 20 | for (uint256 i = 0; i < calls.length; i++) { 21 | (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); 22 | require(success, "Multicall aggregate: call failed"); 23 | returnData[i] = ret; 24 | } 25 | } 26 | 27 | function blockAndAggregate(Call[] memory calls) 28 | public 29 | returns ( 30 | uint256 blockNumber, 31 | bytes32 blockHash, 32 | Result[] memory returnData 33 | ) 34 | { 35 | (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); 36 | } 37 | 38 | function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { 39 | blockHash = blockhash(blockNumber); 40 | } 41 | 42 | function getBlockNumber() public view returns (uint256 blockNumber) { 43 | blockNumber = ChainAdapter.blockNumber(); 44 | } 45 | 46 | function getCurrentBlockCoinbase() public view returns (address coinbase) { 47 | coinbase = block.coinbase; 48 | } 49 | 50 | function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { 51 | difficulty = block.difficulty; 52 | } 53 | 54 | function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { 55 | gaslimit = block.gaslimit; 56 | } 57 | 58 | function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { 59 | timestamp = block.timestamp; 60 | } 61 | 62 | function getEthBalance(address addr) public view returns (uint256 balance) { 63 | balance = addr.balance; 64 | } 65 | 66 | function getLastBlockHash() public view returns (bytes32 blockHash) { 67 | blockHash = blockhash(ChainAdapter.blockNumber() - 1); 68 | } 69 | 70 | function tryAggregate(bool requireSuccess, Call[] memory calls) public returns (Result[] memory returnData) { 71 | returnData = new Result[](calls.length); 72 | for (uint256 i = 0; i < calls.length; i++) { 73 | (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); 74 | 75 | if (requireSuccess) { 76 | require(success, "Multicall2 aggregate: call failed"); 77 | } 78 | 79 | returnData[i] = Result(success, ret); 80 | } 81 | } 82 | 83 | function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) 84 | public 85 | returns ( 86 | uint256 blockNumber, 87 | bytes32 blockHash, 88 | Result[] memory returnData 89 | ) 90 | { 91 | blockNumber = ChainAdapter.blockNumber(); 92 | blockHash = blockhash(blockNumber); 93 | returnData = tryAggregate(requireSuccess, calls); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/interfaces/IAmm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IAmm { 5 | event Mint(address indexed sender, address indexed to, uint256 baseAmount, uint256 quoteAmount, uint256 liquidity); 6 | event Burn(address indexed sender, address indexed to, uint256 baseAmount, uint256 quoteAmount, uint256 liquidity); 7 | event Swap(address indexed trader, address indexed inputToken, address indexed outputToken, uint256 inputAmount, uint256 outputAmount); 8 | event ForceSwap(address indexed trader, address indexed inputToken, address indexed outputToken, uint256 inputAmount, uint256 outputAmount); 9 | event Rebase(uint256 quoteReserveBefore, uint256 quoteReserveAfter, uint256 _baseReserve , uint256 quoteReserveFromInternal, uint256 quoteReserveFromExternal ); 10 | event Sync(uint112 reserveBase, uint112 reserveQuote); 11 | 12 | // only factory can call this function 13 | function initialize( 14 | address baseToken_, 15 | address quoteToken_, 16 | address margin_ 17 | ) external; 18 | 19 | function mint(address to) 20 | external 21 | returns ( 22 | uint256 baseAmount, 23 | uint256 quoteAmount, 24 | uint256 liquidity 25 | ); 26 | 27 | function burn(address to) 28 | external 29 | returns ( 30 | uint256 baseAmount, 31 | uint256 quoteAmount, 32 | uint256 liquidity 33 | ); 34 | 35 | // only binding margin can call this function 36 | function swap( 37 | address trader, 38 | address inputToken, 39 | address outputToken, 40 | uint256 inputAmount, 41 | uint256 outputAmount 42 | ) external returns (uint256[2] memory amounts); 43 | 44 | // only binding margin can call this function 45 | function forceSwap( 46 | address trader, 47 | address inputToken, 48 | address outputToken, 49 | uint256 inputAmount, 50 | uint256 outputAmount 51 | ) external; 52 | 53 | function rebase() external returns (uint256 quoteReserveAfter); 54 | 55 | function collectFee() external returns (bool feeOn); 56 | 57 | function factory() external view returns (address); 58 | 59 | function config() external view returns (address); 60 | 61 | function baseToken() external view returns (address); 62 | 63 | function quoteToken() external view returns (address); 64 | 65 | function price0CumulativeLast() external view returns (uint256); 66 | 67 | function price1CumulativeLast() external view returns (uint256); 68 | 69 | function margin() external view returns (address); 70 | 71 | function lastPrice() external view returns (uint256); 72 | 73 | function getReserves() 74 | external 75 | view 76 | returns ( 77 | uint112 reserveBase, 78 | uint112 reserveQuote, 79 | uint32 blockTimestamp 80 | ); 81 | 82 | function estimateSwap( 83 | address inputToken, 84 | address outputToken, 85 | uint256 inputAmount, 86 | uint256 outputAmount 87 | ) external view returns (uint256[2] memory amounts); 88 | 89 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 90 | 91 | function getFeeLiquidity() external view returns (uint256); 92 | 93 | function getTheMaxBurnLiquidity() external view returns (uint256 maxLiquidity); 94 | } 95 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("hardhat-deploy"); 3 | require("@nomiclabs/hardhat-waffle"); 4 | require("hardhat-watcher"); 5 | require("@nomiclabs/hardhat-ethers"); 6 | require("@nomiclabs/hardhat-etherscan"); 7 | require("hardhat-gas-reporter"); 8 | require("solidity-coverage"); 9 | require("@openzeppelin/hardhat-upgrades"); 10 | 11 | module.exports = { 12 | solidity: { 13 | compilers: [ 14 | { 15 | version: "0.8.2", 16 | settings: { 17 | optimizer: { 18 | enabled: true, 19 | runs: 200, 20 | }, 21 | }, 22 | }, 23 | ], 24 | }, 25 | mocha: { 26 | timeout: 600000, 27 | }, 28 | 29 | networks: { 30 | hardhat: { 31 | allowUnlimitedContractSize: true, 32 | }, 33 | localhost: { 34 | url: "http://localhost:8545", 35 | }, 36 | mainnet: { 37 | url: process.env.MAINNET_RPC, 38 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 39 | }, 40 | goerli: { 41 | url: process.env.GOERLI_RPC, 42 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 43 | }, 44 | rinkeby: { 45 | url: process.env.RINKEBY_RPC, 46 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 47 | }, 48 | arbitrumOne: { 49 | url: process.env.ARBITRUM_ONE_RPC, 50 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 51 | }, 52 | arbitrumTestnet: { 53 | url: process.env.ARBITRUM_TESTNET_RPC, 54 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 55 | }, 56 | bscMainnet: { 57 | url: process.env.BSC_MAINNET_PRC, 58 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 59 | }, 60 | bscTestnet: { 61 | url: process.env.BSC_TESTNET_PRC, 62 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 63 | }, 64 | mantleTestnet:{ 65 | chainId: 5001, 66 | url: process.env.MANTLE_TESTNET_RPC_URL, 67 | accounts: process.env.DEVNET_PRIVKEY !== undefined ? [process.env.DEVNET_PRIVKEY] : [], 68 | }, 69 | mantle: { 70 | chainId: 5000, 71 | url: process.env.MANTLE_MAINNET_RPC_URL, 72 | accounts: process.env.MAINNET_PRIVKEY !== undefined ? [process.env.MAINNET_PRIVKEY] : [], 73 | } 74 | }, 75 | etherscan: { 76 | apiKey: process.env.ETHERSCAN_API_KEY, 77 | customChains: [{ 78 | network: "mantle", 79 | chainId: Number(process.env.MANTLE_MAINNET_CHAIN_ID), 80 | urls: { 81 | apiURL: `${process.env.MANTLE_MAINNET_EXPLORER}api`, 82 | browserURL: process.env.MANTLE_MAINNET_EXPLORER, 83 | }, 84 | }, 85 | { 86 | network: "mantleTestnet", 87 | chainId: Number(process.env.MANTLE_TESTNET_CHAIN_ID), 88 | urls: { 89 | apiURL: `${process.env.MANTLE_TESTNET_EXPLORER}api`, 90 | browserURL: process.env.MANTLE_TESTNET_EXPLORER, 91 | }, 92 | }, 93 | ], 94 | }, 95 | watcher: { 96 | compilation: { 97 | tasks: ["compile"], 98 | }, 99 | test: { 100 | tasks: ["test"], 101 | files: ["./test/*"], 102 | }, 103 | }, 104 | }; 105 | -------------------------------------------------------------------------------- /test/ERC20MerkleDrop.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | const MerkleTree = require("./merkle-tree.js"); 3 | const BalanceTree = require("./balance-tree.js"); 4 | const keccak256 = require("keccak256"); 5 | const { expect } = require("chai"); 6 | const fs = require("fs"); 7 | 8 | async function deploy(name, ...params) { 9 | const Contract = await ethers.getContractFactory(name); 10 | return await Contract.deploy(...params).then((f) => f.deployed()); 11 | } 12 | 13 | function hashToken(index, account, amount) { 14 | return Buffer.from( 15 | ethers.utils.solidityKeccak256(["uint256", "address", "uint256"], [index, account, amount]).slice(2), 16 | "hex" 17 | ); 18 | } 19 | 20 | describe("ERC20MerkleDrop", function () { 21 | let erc20; 22 | let owner; 23 | let alice; 24 | let bob; 25 | let tree; 26 | let distributor; 27 | let distributorFile; 28 | let fileTree; 29 | 30 | // merkleTree = new MerkleTree(Object.entries(tokens).map(token => hashToken(...token)), keccak256, { sortPairs: true }); 31 | 32 | describe("Mint all elements", function () { 33 | before(async function () { 34 | [owner, alice, bob] = await ethers.getSigners(); 35 | erc20 = await deploy("MyToken", "AAA token", "AAA", 0, 100000000); 36 | tree = new BalanceTree([ 37 | { account: alice.address, amount: ethers.BigNumber.from(100) }, 38 | { account: bob.address, amount: ethers.BigNumber.from(101) }, 39 | ]); 40 | 41 | let json = JSON.parse(fs.readFileSync("./test/erc20.json", { encoding: "utf8" })); 42 | 43 | if (typeof json !== "object") throw new Error("Invalid JSON"); 44 | 45 | //console.log(JSON.stringify(json)); 46 | 47 | //--------------- 48 | 49 | let balances = new Array(); 50 | let valid = true; 51 | for (const [key, value] of Object.entries(json)) { 52 | balances.push({ account: key, amount: value }); 53 | } 54 | fileTree = new BalanceTree(balances); 55 | //console.log(balances); 56 | // }) 57 | 58 | // Root 59 | const root = fileTree.getHexRoot().toString("hex"); 60 | console.log("Reconstructed merkle root", root); 61 | console.log("root", root); 62 | distributor = await deploy("MerkleDistributor", erc20.address, tree.getHexRoot()); 63 | distributorFile = await deploy("MerkleDistributor", erc20.address, fileTree.getHexRoot()); 64 | 65 | await erc20.transfer(distributor.address, 201); 66 | await erc20.transfer(distributorFile.address, 1000); 67 | }); 68 | 69 | it("successful claim", async () => { 70 | const proof0 = tree.getProof(0, alice.address, ethers.BigNumber.from(100)); 71 | await expect(distributor.claim(0, alice.address, 100, proof0)) 72 | .to.emit(distributor, "Claimed") 73 | .withArgs(0, alice.address, 100); 74 | const proof1 = tree.getProof(1, bob.address, ethers.BigNumber.from(101)); 75 | await expect(distributor.claim(1, bob.address, 101, proof1)) 76 | .to.emit(distributor, "Claimed") 77 | .withArgs(1, bob.address, 101); 78 | }); 79 | 80 | it("file tree claim", async () => { 81 | const proof0 = fileTree.getProof(0, "0xF3c6F5F265F503f53EAD8aae90FC257A5aa49AC1", ethers.BigNumber.from(1)); 82 | await expect(distributorFile.claim(0, "0xF3c6F5F265F503f53EAD8aae90FC257A5aa49AC1", 1, proof0)) 83 | .to.emit(distributorFile, "Claimed") 84 | .withArgs(0, "0xF3c6F5F265F503f53EAD8aae90FC257A5aa49AC1", 1); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /contracts/mocks/MockAmm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockAmm is ERC20 { 8 | address public baseToken; 9 | address public quoteToken; 10 | uint112 private baseReserve; 11 | uint112 private quoteReserve; 12 | uint32 private blockTimestampLast; 13 | uint256 public price = 1; 14 | 15 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 16 | 17 | function initialize(address baseToken_, address quoteToken_) external { 18 | baseToken = baseToken_; 19 | quoteToken = quoteToken_; 20 | } 21 | 22 | function setPrice(uint256 _price) external { 23 | price = _price; 24 | } 25 | 26 | function setReserves(uint112 reserveBase, uint112 reserveQuote) external { 27 | baseReserve = reserveBase; 28 | quoteReserve = reserveQuote; 29 | } 30 | 31 | function getReserves() 32 | public 33 | view 34 | returns ( 35 | uint112 reserveBase, 36 | uint112 reserveQuote, 37 | uint32 blockTimestamp 38 | ) 39 | { 40 | reserveBase = baseReserve; 41 | reserveQuote = quoteReserve; 42 | blockTimestamp = blockTimestampLast; 43 | } 44 | 45 | function mint(address to) 46 | external 47 | returns ( 48 | uint256 baseAmount, 49 | uint256 quoteAmount, 50 | uint256 liquidity 51 | ) 52 | { 53 | baseAmount = 1000; 54 | quoteAmount = 1000; 55 | liquidity = 1000; 56 | _mint(to, liquidity); 57 | } 58 | 59 | function estimateSwap( 60 | address inputToken, 61 | address outputToken, 62 | uint256 inputAmount, 63 | uint256 outputAmount 64 | ) external view returns (uint256[2] memory amounts) { 65 | inputToken = inputToken; 66 | outputToken = outputToken; 67 | if (inputToken == baseToken) { 68 | if (inputAmount != 0) { 69 | amounts = [0, inputAmount * price]; 70 | } else { 71 | amounts = [outputAmount / price, 0]; 72 | } 73 | } else { 74 | if (inputAmount != 0) { 75 | amounts = [0, inputAmount / price]; 76 | } else { 77 | amounts = [outputAmount * price, 0]; 78 | } 79 | } 80 | } 81 | 82 | function swap( 83 | address trader, 84 | address inputToken, 85 | address outputToken, 86 | uint256 inputAmount, 87 | uint256 outputAmount 88 | ) external view returns (uint256[2] memory amounts) { 89 | inputToken = inputToken; 90 | outputToken = outputToken; 91 | 92 | if (inputToken == baseToken) { 93 | if (inputAmount != 0) { 94 | amounts = [0, inputAmount * price]; 95 | } else { 96 | amounts = [outputAmount / price, 0]; 97 | } 98 | } else { 99 | if (inputAmount != 0) { 100 | amounts = [0, inputAmount / price]; 101 | } else { 102 | amounts = [outputAmount * price, 0]; 103 | } 104 | } 105 | } 106 | 107 | function forceSwap( 108 | address trader, 109 | address inputToken, 110 | address outputToken, 111 | uint256 inputAmount, 112 | uint256 outputAmount 113 | ) external {} 114 | } 115 | -------------------------------------------------------------------------------- /contracts/mocks/MyAmm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IAmm.sol"; 5 | import "../core/LiquidityERC20.sol"; 6 | import "../interfaces/IERC20.sol"; 7 | 8 | contract MyAmm is IAmm, LiquidityERC20 { 9 | uint256 public constant override MINIMUM_LIQUIDITY = 10**3; 10 | 11 | address public override factory; 12 | address public override config; 13 | address public override baseToken; 14 | address public override quoteToken; 15 | address public override margin; 16 | 17 | uint256 public override price0CumulativeLast; 18 | uint256 public override price1CumulativeLast; 19 | 20 | uint256 public override lastPrice; 21 | 22 | uint112 private baseReserve; 23 | uint112 private quoteReserve; 24 | uint32 private blockTimestampLast; 25 | 26 | constructor() { 27 | factory = msg.sender; 28 | } 29 | 30 | // only factory can call this function 31 | function initialize( 32 | address baseToken_, 33 | address quoteToken_, 34 | address margin_ 35 | ) external override { 36 | baseToken = baseToken_; 37 | quoteToken = quoteToken_; 38 | margin = margin_; 39 | } 40 | 41 | function setReserves(uint112 reserveBase, uint112 reserveQuote) external { 42 | baseReserve = reserveBase; 43 | quoteReserve = reserveQuote; 44 | } 45 | 46 | function mint(address to) 47 | external override 48 | returns ( 49 | uint256 baseAmount, 50 | uint256 quoteAmount, 51 | uint256 liquidity 52 | ) { 53 | _mint(to, 10000); 54 | } 55 | 56 | function burn(address to) 57 | external override 58 | returns ( 59 | uint256 baseAmount, 60 | uint256 quoteAmount, 61 | uint256 liquidity 62 | ) { 63 | IERC20(baseToken).transfer(to, 100); 64 | } 65 | 66 | // only binding margin can call this function 67 | function swap( 68 | address trader, 69 | address inputToken, 70 | address outputToken, 71 | uint256 inputAmount, 72 | uint256 outputAmount 73 | ) external override returns (uint256[2] memory amounts) { 74 | 75 | } 76 | 77 | // only binding margin can call this function 78 | function forceSwap( 79 | address trader, 80 | address inputToken, 81 | address outputToken, 82 | uint256 inputAmount, 83 | uint256 outputAmount 84 | ) external override { 85 | 86 | } 87 | 88 | function rebase() external override returns (uint256 quoteReserveAfter) { 89 | 90 | } 91 | 92 | function collectFee() external override returns (bool) { 93 | 94 | } 95 | 96 | function getReserves() 97 | external 98 | view override 99 | returns ( 100 | uint112 reserveBase, 101 | uint112 reserveQuote, 102 | uint32 blockTimestamp 103 | ) 104 | { 105 | reserveBase = baseReserve; 106 | reserveQuote = quoteReserve; 107 | blockTimestamp = blockTimestampLast; 108 | } 109 | 110 | function estimateSwap( 111 | address inputToken, 112 | address outputToken, 113 | uint256 inputAmount, 114 | uint256 outputAmount 115 | ) external view override returns (uint256[2] memory amounts) { 116 | 117 | } 118 | 119 | function getFeeLiquidity() external override view returns (uint256) { 120 | 121 | } 122 | 123 | function getTheMaxBurnLiquidity() external override view returns (uint256 maxLiquidity) { 124 | 125 | } 126 | } -------------------------------------------------------------------------------- /test/migrator.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { BigNumber } = require("@ethersproject/bignumber"); 3 | const { ethers } = require("hardhat"); 4 | 5 | describe("Migrator contract", function () { 6 | let owner; 7 | let treasury; 8 | let weth; 9 | let oldRouter; 10 | let newRouter; 11 | let oldPairFactory; 12 | let priceOracle; 13 | let apeXToken; 14 | let baseToken; 15 | let quoteToken; 16 | let migrator; 17 | 18 | beforeEach(async function () { 19 | [owner, treasury] = await ethers.getSigners(); 20 | 21 | const MockWETH = await ethers.getContractFactory("MockWETH"); 22 | weth = await MockWETH.deploy(); 23 | 24 | const MockToken = await ethers.getContractFactory("MockToken"); 25 | apeXToken = await MockToken.deploy("ApeX Token", "APEX"); 26 | baseToken = await MockToken.deploy("Base Token", "BT"); 27 | quoteToken = await MockToken.deploy("Quote Token", "QT"); 28 | 29 | const Config = await ethers.getContractFactory("Config"); 30 | let config = await Config.deploy(); 31 | 32 | const PriceOracle = await ethers.getContractFactory("PriceOracleForTest"); 33 | priceOracle = await PriceOracle.deploy(); 34 | await priceOracle.setReserve(baseToken.address, apeXToken.address, 10000000, 20000000); 35 | await priceOracle.setReserve(baseToken.address, quoteToken.address, 10000000, 20000000); 36 | await priceOracle.setReserve(weth.address, quoteToken.address, 10000000, 20000000); 37 | await config.setPriceOracle(priceOracle.address); 38 | 39 | const PairFactory = await ethers.getContractFactory("PairFactory"); 40 | const AmmFactory = await ethers.getContractFactory("AmmFactory"); 41 | const MarginFactory = await ethers.getContractFactory("MarginFactory"); 42 | oldPairFactory = await PairFactory.deploy(); 43 | let oldAmmFactory = await AmmFactory.deploy(oldPairFactory.address, config.address, owner.address); 44 | let oldMarginFactory = await MarginFactory.deploy(oldPairFactory.address, config.address); 45 | await oldPairFactory.init(oldAmmFactory.address, oldMarginFactory.address); 46 | 47 | let newPairFactory = await PairFactory.deploy(); 48 | let newAmmFactory = await AmmFactory.deploy(newPairFactory.address, config.address, owner.address); 49 | let newMarginFactory = await MarginFactory.deploy(newPairFactory.address, config.address); 50 | await newPairFactory.init(newAmmFactory.address, newMarginFactory.address); 51 | 52 | const Router = await ethers.getContractFactory("Router"); 53 | oldRouter = await Router.deploy(); 54 | await oldRouter.initialize(config.address, oldPairFactory.address, treasury.address, weth.address); 55 | await config.registerRouter(oldRouter.address); 56 | 57 | newRouter = await Router.deploy(); 58 | await newRouter.initialize(config.address, newPairFactory.address, treasury.address, weth.address); 59 | await config.registerRouter(newRouter.address); 60 | 61 | const Migrator = await ethers.getContractFactory("Migrator"); 62 | migrator = await Migrator.deploy(oldRouter.address, newRouter.address); 63 | await config.registerRouter(migrator.address); 64 | }); 65 | 66 | describe("migrate", function () { 67 | it("migrate", async function () { 68 | await baseToken.mint(owner.address, 200000000); 69 | await baseToken.approve(oldRouter.address, 100000000); 70 | await baseToken.approve(newRouter.address, 100000000); 71 | await oldRouter.addLiquidity(baseToken.address, quoteToken.address, 100000000, 1, 9999999999, false); 72 | await newRouter.addLiquidity(baseToken.address, quoteToken.address, 100000000, 1, 9999999999, false); 73 | let ammAddress = await oldPairFactory.getAmm(baseToken.address, quoteToken.address); 74 | const Amm = await ethers.getContractFactory("Amm"); 75 | let amm = await Amm.attach(ammAddress); 76 | await amm.approve(migrator.address, BigNumber.from("100000000000000000000000000000000000")); 77 | await migrator.migrate(baseToken.address, quoteToken.address); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /contracts/core/LiquidityERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/ILiquidityERC20.sol"; 5 | 6 | contract LiquidityERC20 is ILiquidityERC20 { 7 | string public constant override name = "APEX LP"; 8 | string public constant override symbol = "APEX-LP"; 9 | uint8 public constant override decimals = 18; 10 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 11 | bytes32 public constant override PERMIT_TYPEHASH = 12 | 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 13 | 14 | bytes32 public immutable override DOMAIN_SEPARATOR; 15 | 16 | uint256 public override totalSupply; 17 | mapping(address => uint256) public override balanceOf; 18 | mapping(address => mapping(address => uint256)) public override allowance; 19 | mapping(address => uint256) public override nonces; 20 | 21 | constructor() { 22 | uint256 chainId; 23 | assembly { 24 | chainId := chainid() 25 | } 26 | DOMAIN_SEPARATOR = keccak256( 27 | abi.encode( 28 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 29 | keccak256(bytes(name)), 30 | keccak256(bytes("1")), 31 | chainId, 32 | address(this) 33 | ) 34 | ); 35 | } 36 | 37 | function approve(address spender, uint256 value) external override returns (bool) { 38 | _approve(msg.sender, spender, value); 39 | return true; 40 | } 41 | 42 | function transfer(address to, uint256 value) external override returns (bool) { 43 | _transfer(msg.sender, to, value); 44 | return true; 45 | } 46 | 47 | function transferFrom( 48 | address from, 49 | address to, 50 | uint256 value 51 | ) external override returns (bool) { 52 | if (allowance[from][msg.sender] != type(uint256).max) { 53 | allowance[from][msg.sender] = allowance[from][msg.sender] - value; 54 | } 55 | _transfer(from, to, value); 56 | return true; 57 | } 58 | 59 | function permit( 60 | address owner, 61 | address spender, 62 | uint256 value, 63 | uint256 deadline, 64 | uint8 v, 65 | bytes32 r, 66 | bytes32 s 67 | ) external override { 68 | require(deadline >= block.timestamp, "LiquidityERC20: EXPIRED"); 69 | bytes32 digest = keccak256( 70 | abi.encodePacked( 71 | "\x19\x01", 72 | DOMAIN_SEPARATOR, 73 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 74 | ) 75 | ); 76 | address recoveredAddress = ecrecover(digest, v, r, s); 77 | require(recoveredAddress != address(0) && recoveredAddress == owner, "LiquidityERC20: INVALID_SIGNATURE"); 78 | _approve(owner, spender, value); 79 | } 80 | 81 | function _mint(address to, uint256 value) internal { 82 | totalSupply = totalSupply + value; 83 | balanceOf[to] = balanceOf[to] + value; 84 | emit Transfer(address(0), to, value); 85 | } 86 | 87 | function _burn(address from, uint256 value) internal { 88 | balanceOf[from] = balanceOf[from] - value; 89 | totalSupply = totalSupply - value; 90 | emit Transfer(from, address(0), value); 91 | } 92 | 93 | function _approve( 94 | address owner, 95 | address spender, 96 | uint256 value 97 | ) private { 98 | allowance[owner][spender] = value; 99 | emit Approval(owner, spender, value); 100 | } 101 | 102 | function _transfer( 103 | address from, 104 | address to, 105 | uint256 value 106 | ) private { 107 | balanceOf[from] = balanceOf[from] - value; 108 | balanceOf[to] = balanceOf[to] + value; 109 | emit Transfer(from, to, value); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /contracts/core/PCVTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IPCVTreasury.sol"; 5 | import "../interfaces/IPCVPolicy.sol"; 6 | import "../interfaces/IERC20.sol"; 7 | import "../libraries/TransferHelper.sol"; 8 | import "../utils/Ownable.sol"; 9 | 10 | ///@notice PCVTreasury contract manager all PCV(Protocol Controlled Value) assets 11 | ///@dev apeXToken for bonding is need to be transferred into this contract before bonding really setup 12 | contract PCVTreasury is IPCVTreasury, Ownable { 13 | address public immutable override apeXToken; 14 | mapping(address => bool) public override isLiquidityToken; 15 | mapping(address => bool) public override isBondPool; 16 | 17 | constructor(address apeXToken_) { 18 | owner = msg.sender; 19 | apeXToken = apeXToken_; 20 | } 21 | 22 | function addLiquidityToken(address lpToken) external override onlyOwner { 23 | require(lpToken != address(0), "PCVTreasury.addLiquidityToken: ZERO_ADDRESS"); 24 | require(!isLiquidityToken[lpToken], "PCVTreasury.addLiquidityToken: ALREADY_ADDED"); 25 | isLiquidityToken[lpToken] = true; 26 | emit NewLiquidityToken(lpToken); 27 | } 28 | 29 | function addBondPool(address pool) external override onlyOwner { 30 | require(pool != address(0), "PCVTreasury.addBondPool: ZERO_ADDRESS"); 31 | require(!isBondPool[pool], "PCVTreasury.addBondPool: ALREADY_ADDED"); 32 | isBondPool[pool] = true; 33 | emit NewBondPool(pool); 34 | } 35 | 36 | function deposit( 37 | address lpToken, 38 | uint256 amountIn, 39 | uint256 payout 40 | ) external override { 41 | require(isBondPool[msg.sender], "PCVTreasury.deposit: FORBIDDEN"); 42 | require(isLiquidityToken[lpToken], "PCVTreasury.deposit: NOT_LIQUIDITY_TOKEN"); 43 | require(amountIn > 0, "PCVTreasury.deposit: ZERO_AMOUNT_IN"); 44 | require(payout > 0, "PCVTreasury.deposit: ZERO_PAYOUT"); 45 | uint256 apeXBalance = IERC20(apeXToken).balanceOf(address(this)); 46 | require(payout <= apeXBalance, "PCVTreasury.deposit: NOT_ENOUGH_APEX"); 47 | TransferHelper.safeTransferFrom(lpToken, msg.sender, address(this), amountIn); 48 | TransferHelper.safeTransfer(apeXToken, msg.sender, payout); 49 | emit Deposit(msg.sender, lpToken, amountIn, payout); 50 | } 51 | 52 | /// @notice Call this function can withdraw specified lp token to a policy contract 53 | /// @param lpToken The lp token address want to be withdraw 54 | /// @param policy The policy contract address to receive the lp token 55 | /// @param amount Withdraw amount of lp token 56 | /// @param data Other data want to send to the policy 57 | function withdraw( 58 | address lpToken, 59 | address policy, 60 | uint256 amount, 61 | bytes calldata data 62 | ) external override onlyOwner { 63 | require(isLiquidityToken[lpToken], "PCVTreasury.deposit: NOT_LIQUIDITY_TOKEN"); 64 | require(policy != address(0), "PCVTreasury.deposit: ZERO_ADDRESS"); 65 | require(amount > 0, "PCVTreasury.deposit: ZERO_AMOUNT"); 66 | uint256 balance = IERC20(lpToken).balanceOf(address(this)); 67 | require(amount <= balance, "PCVTreasury.deposit: NOT_ENOUGH_BALANCE"); 68 | TransferHelper.safeTransfer(lpToken, policy, amount); 69 | IPCVPolicy(policy).execute(lpToken, amount, data); 70 | emit Withdraw(lpToken, policy, amount); 71 | } 72 | 73 | /// @notice left apeXToken in this contract can be granted out by owner 74 | /// @param to the address receive the apeXToken 75 | /// @param amount the amount want to be granted 76 | function grantApeX(address to, uint256 amount) external override onlyOwner { 77 | require(to != address(0), "PCVTreasury.grantApeX: ZERO_ADDRESS"); 78 | require(amount > 0, "PCVTreasury.grantApeX: ZERO_AMOUNT"); 79 | uint256 balance = IERC20(apeXToken).balanceOf(address(this)); 80 | require(amount <= balance, "PCVTreasury.grantApeX: NOT_ENOUGH_BALANCE"); 81 | TransferHelper.safeTransfer(apeXToken, to, amount); 82 | emit ApeXGranted(to, amount); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /contracts/mocks/MockAmmM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "../interfaces/IVault.sol"; 7 | 8 | contract MockAmmOfMargin is ERC20 { 9 | address public baseToken; 10 | address public quoteToken; 11 | address public margin; 12 | address public factory; 13 | uint112 private baseReserve; 14 | uint112 private quoteReserve; 15 | uint32 private blockTimestampLast; 16 | uint256 public price = 2e9; 17 | 18 | constructor(string memory name, string memory symbol) ERC20(name, symbol) { 19 | factory = msg.sender; 20 | } 21 | 22 | function initialize(address baseToken_, address quoteToken_) external { 23 | baseToken = baseToken_; 24 | quoteToken = quoteToken_; 25 | } 26 | 27 | function setMargin(address margin_) external { 28 | margin = margin_; 29 | } 30 | 31 | //2000usdc/eth -> 2000*(1e6/1e18)*1e18 32 | function setPrice(uint256 _price) external { 33 | price = _price; 34 | } 35 | 36 | function setReserves(uint112 reserveBase, uint112 reserveQuote) external { 37 | baseReserve = reserveBase; 38 | quoteReserve = reserveQuote; 39 | } 40 | 41 | function getReserves() 42 | public 43 | view 44 | returns ( 45 | uint112 reserveBase, 46 | uint112 reserveQuote, 47 | uint32 blockTimestamp 48 | ) 49 | { 50 | reserveBase = baseReserve; 51 | reserveQuote = quoteReserve; 52 | blockTimestamp = blockTimestampLast; 53 | } 54 | 55 | function mint(address to) 56 | external 57 | returns ( 58 | uint256 baseAmount, 59 | uint256 quoteAmount, 60 | uint256 liquidity 61 | ) 62 | { 63 | baseAmount = 1000; 64 | quoteAmount = 1000; 65 | liquidity = 1000; 66 | _mint(to, liquidity); 67 | } 68 | 69 | function deposit(address to, uint256 amount) external { 70 | IVault(margin).deposit(to, amount); 71 | } 72 | 73 | function withdraw( 74 | address user, 75 | address receiver, 76 | uint256 amount 77 | ) external { 78 | IVault(margin).withdraw(user, receiver, amount); 79 | } 80 | 81 | function estimateSwap( 82 | address inputToken, 83 | address outputToken, 84 | uint256 inputAmount, 85 | uint256 outputAmount 86 | ) external view returns (uint256[2] memory amounts) { 87 | inputToken = inputToken; 88 | outputToken = outputToken; 89 | if (inputToken == baseToken) { 90 | if (inputAmount != 0) { 91 | amounts = [0, (inputAmount * price) / 1e18]; 92 | } else { 93 | amounts = [(outputAmount * 1e18) / price, 0]; 94 | } 95 | } else { 96 | if (inputAmount != 0) { 97 | amounts = [0, (inputAmount * 1e18) / price]; 98 | } else { 99 | amounts = [(outputAmount * price) / 1e18, 0]; 100 | } 101 | } 102 | } 103 | 104 | function swap( 105 | address trader, 106 | address inputToken, 107 | address outputToken, 108 | uint256 inputAmount, 109 | uint256 outputAmount 110 | ) external view returns (uint256[2] memory amounts) { 111 | inputToken = inputToken; 112 | outputToken = outputToken; 113 | 114 | if (inputToken == baseToken) { 115 | if (inputAmount != 0) { 116 | amounts = [0, (inputAmount * price) / 1e18]; 117 | } else { 118 | amounts = [(outputAmount * 1e18) / price, 0]; 119 | } 120 | } else { 121 | if (inputAmount != 0) { 122 | amounts = [0, (inputAmount * 1e18) / price]; 123 | } else { 124 | amounts = [(outputAmount * price) / 1e18, 0]; 125 | } 126 | } 127 | } 128 | 129 | function forceSwap( 130 | address trader, 131 | address inputToken, 132 | address outputToken, 133 | uint256 inputAmount, 134 | uint256 outputAmount 135 | ) external { 136 | if (inputToken == baseToken) { 137 | baseReserve += uint112(inputAmount); 138 | quoteReserve -= uint112(outputAmount); 139 | } else { 140 | baseReserve -= uint112(outputAmount); 141 | quoteReserve += uint112(inputAmount); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contracts/mocks/PriceOracleForTest.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IERC20.sol"; 5 | import "../interfaces/IAmm.sol"; 6 | import "../interfaces/IConfig.sol"; 7 | import "../interfaces/IPriceOracle.sol"; 8 | import "../libraries/FullMath.sol"; 9 | 10 | contract PriceOracleForTest is IPriceOracle { 11 | struct Reserves { 12 | uint256 base; 13 | uint256 quote; 14 | } 15 | mapping(address => mapping(address => Reserves)) public getReserves; 16 | 17 | function setReserve( 18 | address baseToken, 19 | address quoteToken, 20 | uint256 reserveBase, 21 | uint256 reserveQuote 22 | ) external { 23 | getReserves[baseToken][quoteToken] = Reserves(reserveBase, reserveQuote); 24 | } 25 | 26 | function setupTwap(address amm) external override { 27 | return; 28 | } 29 | 30 | function updateAmmTwap(address pair) external override {} 31 | 32 | function quote( 33 | address baseToken, 34 | address quoteToken, 35 | uint256 baseAmount 36 | ) public view override returns (uint256 quoteAmount, uint8 source) { 37 | Reserves memory reserves = getReserves[baseToken][quoteToken]; 38 | require(baseAmount > 0, "INSUFFICIENT_AMOUNT"); 39 | require(reserves.base > 0 && reserves.quote > 0, "INSUFFICIENT_LIQUIDITY"); 40 | quoteAmount = (baseAmount * reserves.quote) / reserves.base; 41 | } 42 | 43 | function quoteFromAmmTwap(address amm, uint256 baseAmount) public view override returns (uint256 quoteAmount) { 44 | quoteAmount = 0; 45 | } 46 | 47 | function getIndexPrice(address amm) public view override returns (uint256) { 48 | address baseToken = IAmm(amm).baseToken(); 49 | address quoteToken = IAmm(amm).quoteToken(); 50 | uint256 baseDecimals = IERC20(baseToken).decimals(); 51 | uint256 quoteDecimals = IERC20(quoteToken).decimals(); 52 | (uint256 quoteAmount, ) = quote(baseToken, quoteToken, 10**baseDecimals); 53 | return quoteAmount * (10**(18 - quoteDecimals)); 54 | } 55 | 56 | function getMarketPrice(address amm) public view override returns (uint256) {} 57 | 58 | function getMarkPrice(address amm) public view override returns (uint256 price, bool isIndexPrice) { 59 | (uint256 baseReserve, uint256 quoteReserve, ) = IAmm(amm).getReserves(); 60 | uint8 baseDecimals = IERC20(IAmm(amm).baseToken()).decimals(); 61 | uint8 quoteDecimals = IERC20(IAmm(amm).quoteToken()).decimals(); 62 | uint256 exponent = uint256(10**(18 + baseDecimals - quoteDecimals)); 63 | price = FullMath.mulDiv(exponent, quoteReserve, baseReserve); 64 | } 65 | 66 | function getMarkPriceInRatio( 67 | address amm, 68 | uint256 quoteAmount, 69 | uint256 baseAmount 70 | ) 71 | public 72 | view 73 | override 74 | returns ( 75 | uint256, 76 | uint256, 77 | bool 78 | ) 79 | { 80 | return (0, 0, false); 81 | } 82 | 83 | function getMarkPriceAfterSwap( 84 | address amm, 85 | uint256 quoteAmount, 86 | uint256 baseAmount 87 | ) external view override returns (uint256 price, bool isIndexPrice) { 88 | return (0, false); 89 | } 90 | 91 | function getMarkPriceAcc( 92 | address amm, 93 | uint8 beta, 94 | uint256 quoteAmount, 95 | bool negative 96 | ) public view override returns (uint256 baseAmount) { 97 | (, uint256 quoteReserve, ) = IAmm(amm).getReserves(); 98 | (uint256 markPrice, ) = getMarkPrice(amm); 99 | uint256 rvalue = FullMath.mulDiv(markPrice, (2 * quoteAmount * beta) / 100, quoteReserve); 100 | uint256 price; 101 | if (negative) { 102 | price = markPrice - rvalue; 103 | } else { 104 | price = markPrice + rvalue; 105 | } 106 | baseAmount = (quoteAmount * 1e18) / price; 107 | } 108 | 109 | //premiumFraction is (markPrice - indexPrice) / 24h / indexPrice 110 | function getPremiumFraction(address amm) public view override returns (int256) { 111 | (uint256 markPriceUint, ) = getMarkPrice(amm); 112 | int256 markPrice = int256(markPriceUint); 113 | int256 indexPrice = int256(getIndexPrice(amm)); 114 | require(markPrice > 0 && indexPrice > 0, "PriceOracle.getPremiumFraction: INVALID_PRICE"); 115 | return ((markPrice - indexPrice) * 1e18) / (24 * 3600) / indexPrice; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contracts/interfaces/IRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IRouter { 5 | event CollectFee(address indexed trader, address indexed margin, uint256 fee); 6 | event PCVAdded(address indexed trader, address indexed amm, uint256 liquidity, uint256 baseAmount); 7 | 8 | function config() external view returns (address); 9 | 10 | function pairFactory() external view returns (address); 11 | 12 | function pcvTreasury() external view returns (address); 13 | 14 | function WETH() external view returns (address); 15 | 16 | function addLiquidity( 17 | address baseToken, 18 | address quoteToken, 19 | uint256 baseAmount, 20 | uint256 quoteAmountMin, 21 | uint256 deadline, 22 | bool pcv 23 | ) external returns (uint256 quoteAmount, uint256 liquidity); 24 | 25 | function addLiquidityETH( 26 | address quoteToken, 27 | uint256 quoteAmountMin, 28 | uint256 deadline, 29 | bool pcv 30 | ) 31 | external 32 | payable 33 | returns ( 34 | uint256 ethAmount, 35 | uint256 quoteAmount, 36 | uint256 liquidity 37 | ); 38 | 39 | function removeLiquidity( 40 | address baseToken, 41 | address quoteToken, 42 | uint256 liquidity, 43 | uint256 baseAmountMin, 44 | uint256 deadline 45 | ) external returns (uint256 baseAmount, uint256 quoteAmount); 46 | 47 | function removeLiquidityETH( 48 | address quoteToken, 49 | uint256 liquidity, 50 | uint256 ethAmountMin, 51 | uint256 deadline 52 | ) external returns (uint256 ethAmount, uint256 quoteAmount); 53 | 54 | function deposit( 55 | address baseToken, 56 | address quoteToken, 57 | address holder, 58 | uint256 amount 59 | ) external; 60 | 61 | function depositETH(address quoteToken, address holder) external payable; 62 | 63 | function withdraw( 64 | address baseToken, 65 | address quoteToken, 66 | uint256 amount 67 | ) external; 68 | 69 | function withdrawETH(address quoteToken, uint256 amount) external; 70 | 71 | function openPositionWithWallet( 72 | address baseToken, 73 | address quoteToken, 74 | uint8 side, 75 | uint256 marginAmount, 76 | uint256 quoteAmount, 77 | uint256 baseAmountLimit, 78 | uint256 deadline 79 | ) external returns (uint256 baseAmount); 80 | 81 | function openPositionETHWithWallet( 82 | address quoteToken, 83 | uint8 side, 84 | uint256 quoteAmount, 85 | uint256 baseAmountLimit, 86 | uint256 deadline 87 | ) external payable returns (uint256 baseAmount); 88 | 89 | function openPositionWithMargin( 90 | address baseToken, 91 | address quoteToken, 92 | uint8 side, 93 | uint256 quoteAmount, 94 | uint256 baseAmountLimit, 95 | uint256 deadline 96 | ) external returns (uint256 baseAmount); 97 | 98 | function closePosition( 99 | address baseToken, 100 | address quoteToken, 101 | uint256 quoteAmount, 102 | uint256 deadline, 103 | bool autoWithdraw 104 | ) external returns (uint256 baseAmount, uint256 withdrawAmount); 105 | 106 | function closePositionETH( 107 | address quoteToken, 108 | uint256 quoteAmount, 109 | uint256 deadline 110 | ) external returns (uint256 baseAmount, uint256 withdrawAmount); 111 | 112 | function liquidate( 113 | address baseToken, 114 | address quoteToken, 115 | address trader, 116 | address to 117 | ) external returns (uint256 quoteAmount, uint256 baseAmount, uint256 bonus); 118 | 119 | function getReserves(address baseToken, address quoteToken) 120 | external 121 | view 122 | returns (uint256 reserveBase, uint256 reserveQuote); 123 | 124 | function getQuoteAmount( 125 | address baseToken, 126 | address quoteToken, 127 | uint8 side, 128 | uint256 baseAmount 129 | ) external view returns (uint256 quoteAmount); 130 | 131 | function getWithdrawable( 132 | address baseToken, 133 | address quoteToken, 134 | address holder 135 | ) external view returns (uint256 amount); 136 | 137 | function getPosition( 138 | address baseToken, 139 | address quoteToken, 140 | address holder 141 | ) 142 | external 143 | view 144 | returns ( 145 | int256 baseSize, 146 | int256 quoteSize, 147 | uint256 tradeSize 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /contracts/interfaces/IConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IConfig { 5 | event PriceOracleChanged(address indexed oldOracle, address indexed newOracle); 6 | event RebasePriceGapChanged(uint256 oldGap, uint256 newGap); 7 | event RebaseIntervalChanged(uint256 oldInterval, uint256 newInterval); 8 | event TradingSlippageChanged(uint256 oldTradingSlippage, uint256 newTradingSlippage); 9 | event RouterRegistered(address indexed router); 10 | event RouterUnregistered(address indexed router); 11 | event SetLiquidateFeeRatio(uint256 oldLiquidateFeeRatio, uint256 liquidateFeeRatio); 12 | event SetLiquidateThreshold(uint256 oldLiquidateThreshold, uint256 liquidateThreshold); 13 | event SetLpWithdrawThresholdForNet(uint256 oldLpWithdrawThresholdForNet, uint256 lpWithdrawThresholdForNet); 14 | event SetLpWithdrawThresholdForTotal(uint256 oldLpWithdrawThresholdForTotal, uint256 lpWithdrawThresholdForTotal); 15 | event SetInitMarginRatio(uint256 oldInitMarginRatio, uint256 initMarginRatio); 16 | event SetBeta(uint256 oldBeta, uint256 beta); 17 | event SetFeeParameter(uint256 oldFeeParameter, uint256 feeParameter); 18 | event SetMaxCPFBoost(uint256 oldMaxCPFBoost, uint256 maxCPFBoost); 19 | event SetEmergency(address indexed router); 20 | 21 | /// @notice get price oracle address. 22 | function priceOracle() external view returns (address); 23 | 24 | /// @notice get beta of amm. 25 | function beta() external view returns (uint8); 26 | 27 | /// @notice get feeParameter of amm. 28 | function feeParameter() external view returns (uint256); 29 | 30 | /// @notice get init margin ratio of margin. 31 | function initMarginRatio() external view returns (uint256); 32 | 33 | /// @notice get liquidate threshold of margin. 34 | function liquidateThreshold() external view returns (uint256); 35 | 36 | /// @notice get liquidate fee ratio of margin. 37 | function liquidateFeeRatio() external view returns (uint256); 38 | 39 | /// @notice get trading slippage of amm. 40 | function tradingSlippage() external view returns (uint256); 41 | 42 | /// @notice get rebase gap of amm. 43 | function rebasePriceGap() external view returns (uint256); 44 | 45 | /// @notice get lp withdraw threshold of amm. 46 | function lpWithdrawThresholdForNet() external view returns (uint256); 47 | 48 | /// @notice get lp withdraw threshold of amm. 49 | function lpWithdrawThresholdForTotal() external view returns (uint256); 50 | 51 | function rebaseInterval() external view returns (uint256); 52 | 53 | function routerMap(address) external view returns (bool); 54 | 55 | function maxCPFBoost() external view returns (uint256); 56 | 57 | function inEmergency(address router) external view returns (bool); 58 | 59 | function registerRouter(address router) external; 60 | 61 | function unregisterRouter(address router) external; 62 | 63 | /// @notice Set a new oracle 64 | /// @param newOracle new oracle address. 65 | function setPriceOracle(address newOracle) external; 66 | 67 | /// @notice Set a new beta of amm 68 | /// @param newBeta new beta. 69 | function setBeta(uint8 newBeta) external; 70 | 71 | /// @notice Set a new rebase gap of amm 72 | /// @param newGap new gap. 73 | function setRebasePriceGap(uint256 newGap) external; 74 | 75 | function setRebaseInterval(uint256 interval) external; 76 | 77 | /// @notice Set a new trading slippage of amm 78 | /// @param newTradingSlippage . 79 | function setTradingSlippage(uint256 newTradingSlippage) external; 80 | 81 | /// @notice Set a new init margin ratio of margin 82 | /// @param marginRatio new init margin ratio. 83 | function setInitMarginRatio(uint256 marginRatio) external; 84 | 85 | /// @notice Set a new liquidate threshold of margin 86 | /// @param threshold new liquidate threshold of margin. 87 | function setLiquidateThreshold(uint256 threshold) external; 88 | 89 | /// @notice Set a new lp withdraw threshold of amm net position 90 | /// @param newLpWithdrawThresholdForNet new lp withdraw threshold of amm. 91 | function setLpWithdrawThresholdForNet(uint256 newLpWithdrawThresholdForNet) external; 92 | 93 | /// @notice Set a new lp withdraw threshold of amm total position 94 | /// @param newLpWithdrawThresholdForTotal new lp withdraw threshold of amm. 95 | function setLpWithdrawThresholdForTotal(uint256 newLpWithdrawThresholdForTotal) external; 96 | 97 | /// @notice Set a new liquidate fee of margin 98 | /// @param feeRatio new liquidate fee of margin. 99 | function setLiquidateFeeRatio(uint256 feeRatio) external; 100 | 101 | /// @notice Set a new feeParameter. 102 | /// @param newFeeParameter New feeParameter get from AMM swap fee. 103 | /// @dev feeParameter = (1/fee -1 ) *100 where fee set by owner. 104 | function setFeeParameter(uint256 newFeeParameter) external; 105 | 106 | function setMaxCPFBoost(uint256 newMaxCPFBoost) external; 107 | 108 | function setEmergency(address router) external; 109 | } 110 | -------------------------------------------------------------------------------- /contracts/interfaces/IMargin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IMargin { 5 | struct Position { 6 | int256 quoteSize; //quote amount of position 7 | int256 baseSize; //margin + fundingFee + unrealizedPnl + deltaBaseWhenClosePosition 8 | uint256 tradeSize; //if quoteSize>0 unrealizedPnl = baseValueOfQuoteSize - tradeSize; if quoteSize<0 unrealizedPnl = tradeSize - baseValueOfQuoteSize; 9 | } 10 | 11 | event AddMargin(address indexed trader, uint256 depositAmount, Position position); 12 | event RemoveMargin( 13 | address indexed trader, 14 | address indexed to, 15 | uint256 withdrawAmount, 16 | int256 fundingFee, 17 | uint256 withdrawAmountFromMargin, 18 | Position position 19 | ); 20 | event OpenPosition( 21 | address indexed trader, 22 | uint8 side, 23 | uint256 baseAmount, 24 | uint256 quoteAmount, 25 | int256 fundingFee, 26 | Position position 27 | ); 28 | event ClosePosition( 29 | address indexed trader, 30 | uint256 quoteAmount, 31 | uint256 baseAmount, 32 | int256 fundingFee, 33 | Position position 34 | ); 35 | event Liquidate( 36 | address indexed liquidator, 37 | address indexed trader, 38 | address indexed to, 39 | uint256 quoteAmount, 40 | uint256 baseAmount, 41 | uint256 bonus, 42 | int256 fundingFee, 43 | Position position 44 | ); 45 | event UpdateCPF(uint256 timeStamp, int256 cpf); 46 | 47 | /// @notice only factory can call this function 48 | /// @param baseToken_ margin's baseToken. 49 | /// @param quoteToken_ margin's quoteToken. 50 | /// @param amm_ amm address. 51 | function initialize( 52 | address baseToken_, 53 | address quoteToken_, 54 | address amm_ 55 | ) external; 56 | 57 | /// @notice add margin to trader 58 | /// @param trader . 59 | /// @param depositAmount base amount to add. 60 | function addMargin(address trader, uint256 depositAmount) external; 61 | 62 | /// @notice remove margin to msg.sender 63 | /// @param withdrawAmount base amount to withdraw. 64 | function removeMargin( 65 | address trader, 66 | address to, 67 | uint256 withdrawAmount 68 | ) external; 69 | 70 | /// @notice open position with side and quoteAmount by msg.sender 71 | /// @param side long or short. 72 | /// @param quoteAmount quote amount. 73 | function openPosition( 74 | address trader, 75 | uint8 side, 76 | uint256 quoteAmount 77 | ) external returns (uint256 baseAmount); 78 | 79 | /// @notice close msg.sender's position with quoteAmount 80 | /// @param quoteAmount quote amount to close. 81 | function closePosition(address trader, uint256 quoteAmount) external returns (uint256 baseAmount); 82 | 83 | /// @notice liquidate trader 84 | function liquidate(address trader, address to) 85 | external 86 | returns ( 87 | uint256 quoteAmount, 88 | uint256 baseAmount, 89 | uint256 bonus 90 | ); 91 | 92 | function updateCPF() external returns (int256); 93 | 94 | /// @notice get factory address 95 | function factory() external view returns (address); 96 | 97 | /// @notice get config address 98 | function config() external view returns (address); 99 | 100 | /// @notice get base token address 101 | function baseToken() external view returns (address); 102 | 103 | /// @notice get quote token address 104 | function quoteToken() external view returns (address); 105 | 106 | /// @notice get amm address of this margin 107 | function amm() external view returns (address); 108 | 109 | /// @notice get all users' net position of quote 110 | function netPosition() external view returns (int256 netQuotePosition); 111 | 112 | /// @notice get all users' net position of quote 113 | function totalPosition() external view returns (uint256 totalQuotePosition); 114 | 115 | /// @notice get trader's position 116 | function getPosition(address trader) 117 | external 118 | view 119 | returns ( 120 | int256 baseSize, 121 | int256 quoteSize, 122 | uint256 tradeSize 123 | ); 124 | 125 | /// @notice get withdrawable margin of trader 126 | function getWithdrawable(address trader) external view returns (uint256 amount); 127 | 128 | /// @notice check if can liquidate this trader's position 129 | function canLiquidate(address trader) external view returns (bool); 130 | 131 | /// @notice calculate the latest funding fee with current position 132 | function calFundingFee(address trader) external view returns (int256 fundingFee); 133 | 134 | /// @notice calculate the latest debt ratio with Pnl and funding fee 135 | function calDebtRatio(address trader) external view returns (uint256 debtRatio); 136 | 137 | function calUnrealizedPnl(address trader) external view returns (int256); 138 | 139 | function getNewLatestCPF() external view returns (int256); 140 | 141 | function querySwapBaseWithAmm(bool isLong, uint256 quoteAmount) external view returns (uint256); 142 | } 143 | -------------------------------------------------------------------------------- /test/erc20.json: -------------------------------------------------------------------------------- 1 | { 2 | "0xF3c6F5F265F503f53EAD8aae90FC257A5aa49AC1": 1, 3 | "0xB9CcDD7Bedb7157798e10Ff06C7F10e0F37C6BdD": 2, 4 | "0xf94DbB18cc2a7852C9CEd052393d517408E8C20C": 3, 5 | "0xf0591a60b8dBa2420408Acc5eDFA4f8A15d87308": 4, 6 | "0x6A2dE67981CbE91209c1046D67eF7a45631d0666": 5, 7 | "0x7C262baf13794f54e3514539c411f92716996C38": 6, 8 | "0x57E7c6B647C004CFB7A38E08fDDef09Af5Ea55eD": 7, 9 | "0x05fc93DeFFFe436822100E795F376228470FB514": 8, 10 | "0x6b6C7139B48156d7EC90eD4c55C56bDFCB1C19D2": 9, 11 | "0x7D13f07889F04a593a3E12f5d3f8Bf850d07465B": 10, 12 | "0xb86739476a4820FcC39Ff1C413d9af0b96c1589F": 11, 13 | "0xf66705E0Ae4e5DfC02b2633356f5305662F00d3b": 12, 14 | "0xC7AA922f0823DeE2eD721E61ebCCF2F9596017Fb": 13, 15 | "0x6E9Ead46916950088E236A77bb7b6309170827CA": 14, 16 | "0x656231095A6700620062B308C900E124461C48B6": 15, 17 | "0xCb73bE1851f2133895C05D408666475bA8Da351e": 16, 18 | "0x6FCBCB45deE6649450932f7FF142C7c434CED9a6": 17, 19 | "0xB34bf945E5a5698087820812e9CCBA0686D2a783": 18, 20 | "0x31f161a781a30AB4bF4Bf11175e3098204FB5235": 19, 21 | "0xde03a8041B40FF95F7F6b6ca0d1Da80fbBD07925": 20, 22 | "0xdc7B752019AC5eFA067Bd3dE17Fc2D2c7C8d881e": 21, 23 | "0xcB4A9ae3d5C4c9BF3c688d387230559018FB90C3": 22, 24 | "0x97Ac383e64d5a1A2A08c646C87B6e0546F7c164B": 23, 25 | "0xc0B7C64d370A9ffcFb9ef675809126c5cAfA9619": 24, 26 | "0xC910240362A5dda9e6cE8fAc86C329864d9Da15d": 25, 27 | "0x7e72833a9D8Da5458470f2B226f1c095ef335e86": 26, 28 | "0x40DeF14b2793e99f1f453FbD98A0f251a1D19f4f": 27, 29 | "0xBC61c73CFc191321DA837def848784c002279a01": 28, 30 | "0x32Cc3F29cde7ac9c000FEbf0D8F28B94F1A34441": 29, 31 | "0x538F43872aC14d3130721Df4F02a3Ff05053A2d9": 30, 32 | "0xDE78e3462c9F976257E5E4Ed821BE7B306B23450": 31, 33 | "0xF1079DD1048A65cA9f9153246164758203d1aEd5": 32, 34 | "0xAb7Fb5958785b20bdccd2A65d15F139B60080fAc": 33, 35 | "0xc6b467aCAa5B07b8182749524385B79DBb909B14": 34, 36 | "0xE7fA80757FeAb870E0bF3b3dc8d4647f403A65ac": 35, 37 | "0x72381936D8e22a52F9a6ea62e23628084085D05b": 36, 38 | "0x3fC98BAD7384a354f8b083Fc5A7D621DF5fB9F41": 37, 39 | "0x5e471D67A610f541B63a8789A9BE1F0fAcd9E244": 38, 40 | "0x5a693Fc88b80Bd7e57f676Bd5e0945995f68bC47": 39, 41 | "0x69eA0b9B0b489B87E061e3e85886D668b24157Ad": 40, 42 | "0x7D0Fe663D9488F6793D813e51EC1DC600F289ad3": 41, 43 | "0x162F49fE6F365d04Db07F77377699aeFE2E8A2cf": 42, 44 | "0x8A1F2B46A35D10F0EafbE6c7f0671d8DB847dcA2": 43, 45 | "0x4aA6E2Fe3f306CB777dFeA344daaAd33eB50f972": 44, 46 | "0x1fCBa490902B2BD44ba98359C7075e2C8a2b9F15": 45, 47 | "0xdd1f7Ea709BD594D834411AE22D81a5B6a91008F": 46, 48 | "0x406b7968735b79688C6694634f2Ef5CF01c386F5": 47, 49 | "0x7152dc7a0eC646A7bCD3b00EF4Ca984E337da2B3": 48, 50 | "0xdf9424b7563A00386217471cfAC8944185505c56": 49, 51 | "0xaDF30D969b396DFC5035Cb3921034Bfb86CC055d": 50, 52 | "0xf970e1f7e89a57485E139F9EB4652181Ef270515": 51, 53 | "0xBd8BcBdF78205590FD576acaf110d70069eE7125": 52, 54 | "0xd7663Ca75082939012A9b5DCaFaDABEA51352F70": 53, 55 | "0x75662678a74C6aD63501519F656CE4Db04e1EF49": 54, 56 | "0xC1f94BCA2146B462685FC04Bf10f8b8CB7a305a3": 55, 57 | "0x7DD3A4cCf156475AE927E9aedc91E9f33AABc79d": 56, 58 | "0x85c5EE48A6687c9D903052a22f5764Bed2B4A6A8": 57, 59 | "0x73f24B3cB7FDAf629d2DC44f67ADaA99005719B0": 58, 60 | "0x9Cce64165E28dEA01a8b9c977F4dbD9D791EbcFf": 59, 61 | "0xcB667d9F540E721858e77E6667e281Aa6fFD5C17": 60, 62 | "0x0f39bceBE74751D89c37a2671DE0c750b71cA152": 61, 63 | "0xC0CDEE637cd0Ef7Ef7ab2696ffADc9C78F4daa0B": 62, 64 | "0x0cF605Ad65B1A541Ca6390606F944D176D5B9950": 63, 65 | "0x614de94D2E18c174bc0155EFef55bAa9cB55bAf2": 64, 66 | "0xa47A8fa265bf540184fA3499566761A608Be84EE": 65, 67 | "0xD2ED9f212c6f5d127757fA700cc55235F5cBc167": 66, 68 | "0xfA4563612C9De62302364ee8042635e44c8327fF": 67, 69 | "0x0350D208F3D94Af84724e437fAa7ebe5A3C35aC7": 68, 70 | "0x31de7522f31322081516703F78ce8eA128d9D6f7": 69, 71 | "0x3e51a90d40F8dC43d2b8720B3671aa208b0316ac": 70, 72 | "0x9BDFE65726326c104a302B172e49c4946E481306": 71, 73 | "0xe7B6bdA3990D0F6892cB1c37F4f2867a8Df4Fe5e": 72, 74 | "0x42B6cC78074eF1C5eC4AEe844B4B9c27b199831F": 73, 75 | "0x1E5eF4320142B6721C27846c9Ab4D6F0a0aFD2CE": 74, 76 | "0xA998c01B7c9674490480ec68bCf27C836CF9B495": 75, 77 | "0xa087344Bc4A05D2885aa9531ae6694e0C5dEb728": 76, 78 | "0x8874a8B06bd074953a9b22CaaC0Ce3bCf1260fB4": 77, 79 | "0x8943b759EaAa0e51b93ddB12B19e9EB71A361c69": 78, 80 | "0x4540e3Ef6dC7a420cd44767F98EF15BEEA28606D": 79, 81 | "0x4d9366B189AA78B9764Bd50B22F9398ABB4AcFbD": 80, 82 | "0x250fC1677986e6c3CEb348a378919f5b0Eb487ea": 81, 83 | "0x1668395E2FDEC223E175111ff8bDce4180A3B680": 82, 84 | "0x904dac1641aC5DAB76B7b2B2AB2779E98ac6BC74": 83, 85 | "0x169D0Cc3e36D3C9253Dd8418421Af5F84a75EC76": 84, 86 | "0x012ed55a0876Ea9e58277197DC14CbA47571CE28": 85, 87 | "0xc07b1400fB950253fbfC5484601036f18c8A91CC": 86, 88 | "0x3741c4751bBff7ba5D58BDA8F1c25Cb71b6b95D2": 87, 89 | "0x01dC7F8C928CeA27D8fF928363111c291bEB20b1": 88, 90 | "0x41560a0CC92C4267614721140a031aE20051Bb65": 89, 91 | "0xF1d322d48E47eb4A806bAB843B5C79Af641bb8cD": 90, 92 | "0x332BC77780057942cAa2c7bad21a04E91B5Ed687": 91, 93 | "0x8519E69FfaF870479534b362ce34F666533aE758": 92, 94 | "0xBF134F1BD442c77F01d4784D991F3c191ce700cf": 93, 95 | "0xb7649E4000AD4748dc2907eCdcAC4ae3d59da0D5": 94, 96 | "0x84664986ad6D1237010be3BFC0F88555edc6987f": 95, 97 | "0x983a9ed0e4A274314231BFce58Ec973f0D298c9d": 96, 98 | "0x9F0A64c6956D7205E883ae8A3C19577f1cadD78F": 97, 99 | "0xB96cE59522314ACB1502Dc8d3e192995e36439c1": 98, 100 | "0x5A553d59435Df0688fd5dEa1aa66C7430541ffB3": 99 101 | } -------------------------------------------------------------------------------- /contracts/mocks/MockConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | //config is upgradable proxy, contains configurations of core contracts 5 | contract MockConfig { 6 | event PriceOracleChanged(address indexed oldOracle, address indexed newOracle); 7 | event RebasePriceGapChanged(uint256 oldGap, uint256 newGap); 8 | event TradingSlippageChanged(uint256 oldTradingSlippage, uint256 newTradingSlippage); 9 | event RouterRegistered(address indexed router); 10 | event RouterUnregistered(address indexed router); 11 | event SetLiquidateFeeRatio(uint256 oldLiquidateFeeRatio, uint256 liquidateFeeRatio); 12 | event SetLiquidateThreshold(uint256 oldLiquidateThreshold, uint256 liquidateThreshold); 13 | event SetInitMarginRatio(uint256 oldInitMarginRatio, uint256 initMarginRatio); 14 | event SetBeta(uint256 oldBeta, uint256 beta); 15 | event SetFeeParameter(uint256 oldFeeParameter, uint256 feeParameter); 16 | event SetMaxCPFBoost(uint256 oldMaxCPFBoost, uint256 maxCPFBoost); 17 | event SetLpWithdrawThreshold(uint256 oldLpWithdrawThreshold, uint256 lpWithdrawThreshold); 18 | 19 | address public priceOracle; 20 | 21 | uint8 public beta = 50; // 50-200,50 means 0.5 22 | uint256 public maxCPFBoost = 10; // default 10 23 | uint256 public rebasePriceGap = 5; //0-100 , if 5 means 5% 24 | uint256 public tradingSlippage = 5; //0-100, if 5 means 5% 25 | uint256 public initMarginRatio = 800; //if 1000, means margin ratio >= 10% 26 | uint256 public liquidateThreshold = 10000; //if 10000, means debt ratio < 100% 27 | uint256 public liquidateFeeRatio = 100; //if 100, means liquidator bot get 1% as fee 28 | uint256 public feeParameter = 11; // 100 * (1/fee-1) 29 | uint256 public lpWithdrawThresholdForNet = 10; // 1-100 30 | uint256 public lpWithdrawThresholdForTotal = 50; 31 | mapping(address => bool) public routerMap; 32 | 33 | // constructor() { 34 | // owner = msg.sender; 35 | // } 36 | 37 | function setMaxCPFBoost(uint256 newMaxCPFBoost) external { 38 | emit SetMaxCPFBoost(maxCPFBoost, newMaxCPFBoost); 39 | maxCPFBoost = newMaxCPFBoost; 40 | } 41 | 42 | function setPriceOracle(address newOracle) external { 43 | require(newOracle != address(0), "Config: ZERO_ADDRESS"); 44 | emit PriceOracleChanged(priceOracle, newOracle); 45 | priceOracle = newOracle; 46 | } 47 | 48 | function setRebasePriceGap(uint256 newGap) external { 49 | require(newGap > 0 && newGap < 100, "Config: ZERO_GAP"); 50 | emit RebasePriceGapChanged(rebasePriceGap, newGap); 51 | rebasePriceGap = newGap; 52 | } 53 | 54 | function setTradingSlippage(uint256 newTradingSlippage) external { 55 | require(newTradingSlippage > 0 && newTradingSlippage < 100, "Config: TRADING_SLIPPAGE_RANGE_ERROR"); 56 | emit TradingSlippageChanged(tradingSlippage, newTradingSlippage); 57 | tradingSlippage = newTradingSlippage; 58 | } 59 | 60 | function setInitMarginRatio(uint256 marginRatio) external { 61 | require(marginRatio >= 100, "Config: INVALID_MARGIN_RATIO"); 62 | emit SetInitMarginRatio(initMarginRatio, marginRatio); 63 | initMarginRatio = marginRatio; 64 | } 65 | 66 | function setLiquidateThreshold(uint256 threshold) external { 67 | require(threshold > 9000 && threshold <= 10000, "Config: INVALID_LIQUIDATE_THRESHOLD"); 68 | emit SetLiquidateThreshold(liquidateThreshold, threshold); 69 | liquidateThreshold = threshold; 70 | } 71 | 72 | function setLiquidateFeeRatio(uint256 feeRatio) external { 73 | require(feeRatio > 0 && feeRatio <= 2000, "Config: INVALID_LIQUIDATE_FEE_RATIO"); 74 | emit SetLiquidateFeeRatio(liquidateFeeRatio, feeRatio); 75 | liquidateFeeRatio = feeRatio; 76 | } 77 | 78 | function setFeeParameter(uint256 newFeeParameter) external { 79 | emit SetFeeParameter(feeParameter, newFeeParameter); 80 | feeParameter = newFeeParameter; 81 | } 82 | 83 | function setBeta(uint8 newBeta) external { 84 | require(newBeta >= 50 && newBeta <= 200, "Config: INVALID_BETA"); 85 | emit SetBeta(beta, newBeta); 86 | beta = newBeta; 87 | } 88 | 89 | //must be careful, expose all traders's position 90 | function registerRouter(address router) external { 91 | require(router != address(0), "Config: ZERO_ADDRESS"); 92 | require(!routerMap[router], "Config: REGISTERED"); 93 | routerMap[router] = true; 94 | 95 | emit RouterRegistered(router); 96 | } 97 | 98 | function unregisterRouter(address router) external { 99 | require(router != address(0), "Config: ZERO_ADDRESS"); 100 | require(routerMap[router], "Config: UNREGISTERED"); 101 | delete routerMap[router]; 102 | 103 | emit RouterUnregistered(router); 104 | } 105 | 106 | function setLpWithdrawThresholdForNet(uint256 newLpWithdrawThresholdForNet) external { 107 | require(newLpWithdrawThresholdForNet > 1 && newLpWithdrawThresholdForNet <= 100, "Config: INVALID_LIQUIDATE_THRESHOLD"); 108 | emit SetLpWithdrawThreshold(lpWithdrawThresholdForNet, newLpWithdrawThresholdForNet); 109 | lpWithdrawThresholdForNet = newLpWithdrawThresholdForNet; 110 | } 111 | 112 | function setLpWithdrawThresholdForTotal(uint256 newLpWithdrawThresholdForTotal) external { 113 | // require(newLpWithdrawThresholdForTotal > 1 && newLpWithdrawThresholdForTotal <= 100, "Config: INVALID_LIQUIDATE_THRESHOLD"); 114 | emit SetLpWithdrawThreshold(lpWithdrawThresholdForTotal, newLpWithdrawThresholdForTotal); 115 | lpWithdrawThresholdForTotal = newLpWithdrawThresholdForTotal; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contracts/core/Config.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IERC20.sol"; 5 | import "../interfaces/IConfig.sol"; 6 | import "../utils/Ownable.sol"; 7 | 8 | contract Config is IConfig, Ownable { 9 | address public override priceOracle; 10 | 11 | uint8 public override beta = 120; // 50-200,50 means 0.5 12 | uint256 public override maxCPFBoost = 10; // default 10 13 | uint256 public override rebasePriceGap = 3; //0-100 , if 5 means 5% 14 | uint256 public override rebaseInterval = 900; // in seconds 15 | uint256 public override tradingSlippage = 5; //0-100, if 5 means 5% 16 | uint256 public override initMarginRatio = 800; //if 1000, means margin ratio >= 10% 17 | uint256 public override liquidateThreshold = 10000; //if 10000, means debt ratio < 100% 18 | uint256 public override liquidateFeeRatio = 100; //if 100, means liquidator bot get 1% as fee 19 | uint256 public override feeParameter = 11; // 100 * (1/fee-1) 20 | uint256 public override lpWithdrawThresholdForNet = 10; // 1-100 21 | uint256 public override lpWithdrawThresholdForTotal = 50; // 22 | 23 | mapping(address => bool) public override routerMap; 24 | mapping(address => bool) public override inEmergency; 25 | 26 | constructor() { 27 | owner = msg.sender; 28 | } 29 | 30 | function setMaxCPFBoost(uint256 newMaxCPFBoost) external override onlyOwner { 31 | emit SetMaxCPFBoost(maxCPFBoost, newMaxCPFBoost); 32 | maxCPFBoost = newMaxCPFBoost; 33 | } 34 | 35 | function setPriceOracle(address newOracle) external override onlyOwner { 36 | require(newOracle != address(0), "Config: ZERO_ADDRESS"); 37 | emit PriceOracleChanged(priceOracle, newOracle); 38 | priceOracle = newOracle; 39 | } 40 | 41 | function setRebasePriceGap(uint256 newGap) external override onlyOwner { 42 | require(newGap > 0 && newGap < 100, "Config: ZERO_GAP"); 43 | emit RebasePriceGapChanged(rebasePriceGap, newGap); 44 | rebasePriceGap = newGap; 45 | } 46 | 47 | function setRebaseInterval(uint256 interval) external override onlyOwner { 48 | emit RebaseIntervalChanged(rebaseInterval, interval); 49 | rebaseInterval = interval; 50 | } 51 | 52 | function setTradingSlippage(uint256 newTradingSlippage) external override onlyOwner { 53 | require(newTradingSlippage > 0 && newTradingSlippage < 100, "Config: TRADING_SLIPPAGE_RANGE_ERROR"); 54 | emit TradingSlippageChanged(tradingSlippage, newTradingSlippage); 55 | tradingSlippage = newTradingSlippage; 56 | } 57 | 58 | function setInitMarginRatio(uint256 marginRatio) external override onlyOwner { 59 | require(marginRatio >= 100, "Config: INVALID_MARGIN_RATIO"); 60 | emit SetInitMarginRatio(initMarginRatio, marginRatio); 61 | initMarginRatio = marginRatio; 62 | } 63 | 64 | function setLiquidateThreshold(uint256 threshold) external override onlyOwner { 65 | require(threshold > 9000 && threshold <= 10000, "Config: INVALID_LIQUIDATE_THRESHOLD"); 66 | emit SetLiquidateThreshold(liquidateThreshold, threshold); 67 | liquidateThreshold = threshold; 68 | } 69 | 70 | function setLiquidateFeeRatio(uint256 feeRatio) external override onlyOwner { 71 | require(feeRatio > 0 && feeRatio <= 2000, "Config: INVALID_LIQUIDATE_FEE_RATIO"); 72 | emit SetLiquidateFeeRatio(liquidateFeeRatio, feeRatio); 73 | liquidateFeeRatio = feeRatio; 74 | } 75 | 76 | function setFeeParameter(uint256 newFeeParameter) external override onlyOwner { 77 | emit SetFeeParameter(feeParameter, newFeeParameter); 78 | feeParameter = newFeeParameter; 79 | } 80 | 81 | function setLpWithdrawThresholdForNet(uint256 newLpWithdrawThresholdForNet) external override onlyOwner { 82 | require(lpWithdrawThresholdForNet >= 1 && lpWithdrawThresholdForNet <= 100, "Config: INVALID_LIQUIDATE_THRESHOLD_FOR_NET"); 83 | emit SetLpWithdrawThresholdForNet(lpWithdrawThresholdForNet, newLpWithdrawThresholdForNet); 84 | lpWithdrawThresholdForNet = newLpWithdrawThresholdForNet; 85 | } 86 | 87 | function setLpWithdrawThresholdForTotal(uint256 newLpWithdrawThresholdForTotal) external override onlyOwner { 88 | // require(lpWithdrawThresholdForTotal >= 1 && lpWithdrawThresholdForTotal <= 100, "Config: INVALID_LIQUIDATE_THRESHOLD_FOR_TOTAL"); 89 | emit SetLpWithdrawThresholdForTotal(lpWithdrawThresholdForTotal, newLpWithdrawThresholdForTotal); 90 | lpWithdrawThresholdForTotal = newLpWithdrawThresholdForTotal; 91 | } 92 | 93 | function setBeta(uint8 newBeta) external override onlyOwner { 94 | require(newBeta >= 50 && newBeta <= 200, "Config: INVALID_BETA"); 95 | emit SetBeta(beta, newBeta); 96 | beta = newBeta; 97 | } 98 | 99 | function registerRouter(address router) external override onlyOwner { 100 | require(router != address(0), "Config: ZERO_ADDRESS"); 101 | require(!routerMap[router], "Config: REGISTERED"); 102 | routerMap[router] = true; 103 | 104 | emit RouterRegistered(router); 105 | } 106 | 107 | function unregisterRouter(address router) external override onlyOwner { 108 | require(router != address(0), "Config: ZERO_ADDRESS"); 109 | require(routerMap[router], "Config: UNREGISTERED"); 110 | delete routerMap[router]; 111 | 112 | emit RouterUnregistered(router); 113 | } 114 | 115 | function setEmergency(address router) external override onlyOwner { 116 | require(routerMap[router], "Config: UNREGISTERED"); 117 | inEmergency[router] = true; 118 | emit SetEmergency(router); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/shared/utilities.js: -------------------------------------------------------------------------------- 1 | const BN = require("bn.js"); 2 | const { ethers } = require("hardhat"); 3 | const { address } = require("hardhat/internal/core/config/config-validation"); 4 | 5 | const maxUint256 = ethers.constants.MaxUint256; 6 | 7 | function newWallet() { 8 | return ethers.Wallet.createRandom(); 9 | } 10 | 11 | function bigNumberify(n) { 12 | return ethers.BigNumber.from(n); 13 | } 14 | 15 | function expandDecimals(n, decimals) { 16 | return bigNumberify(n).mul(bigNumberify(10).pow(decimals)); 17 | } 18 | 19 | async function send(provider, method, params = []) { 20 | await provider.send(method, params); 21 | } 22 | 23 | async function mineBlock(provider) { 24 | await send(provider, "evm_mine"); 25 | } 26 | 27 | async function increaseTime(provider, seconds) { 28 | await send(provider, "evm_increaseTime", [seconds]); 29 | } 30 | 31 | async function gasUsed(provider, tx) { 32 | return (await provider.getTransactionReceipt(tx.hash)).gasUsed; 33 | } 34 | 35 | async function getNetworkFee(provider, tx) { 36 | const gas = await gasUsed(provider, tx); 37 | return gas.mul(tx.gasPrice); 38 | } 39 | 40 | async function reportGasUsed(provider, tx, label) { 41 | const { gasUsed } = await provider.getTransactionReceipt(tx.hash); 42 | console.info(label, gasUsed.toString()); 43 | } 44 | 45 | async function getBlockTime(provider) { 46 | const blockNumber = await provider.getBlockNumber(); 47 | const block = await provider.getBlock(blockNumber); 48 | return block.timestamp; 49 | } 50 | 51 | async function getTxnBalances(provider, user, txn, callback) { 52 | const balance0 = await provider.getBalance(user.address); 53 | const tx = await txn(); 54 | const fee = await getNetworkFee(provider, tx); 55 | const balance1 = await provider.getBalance(user.address); 56 | callback(balance0, balance1, fee); 57 | } 58 | 59 | function print(label, value, decimals) { 60 | if (decimals === 0) { 61 | console.log(label, value.toString()); 62 | return; 63 | } 64 | const valueStr = ethers.utils.formatUnits(value, decimals); 65 | console.log(label, valueStr); 66 | } 67 | 68 | function getPriceBitArray(prices) { 69 | let priceBitArray = []; 70 | let shouldExit = false; 71 | 72 | for (let i = 0; i < parseInt((prices.length - 1) / 8) + 1; i++) { 73 | let priceBits = new BN("0"); 74 | for (let j = 0; j < 8; j++) { 75 | let index = i * 8 + j; 76 | if (index >= prices.length) { 77 | shouldExit = true; 78 | break; 79 | } 80 | 81 | const price = new BN(prices[index]); 82 | if (price.gt(new BN("2147483648"))) { // 2^31 83 | throw new Error(`price exceeds bit limit ${price.toString()}`); 84 | } 85 | priceBits = priceBits.or(price.shln(j * 32)); 86 | } 87 | 88 | priceBitArray.push(priceBits.toString()); 89 | 90 | if (shouldExit) { 91 | break; 92 | } 93 | } 94 | 95 | return priceBitArray; 96 | } 97 | 98 | function getPriceBits(prices) { 99 | if (prices.length > 8) { 100 | throw new Error("max prices.length exceeded"); 101 | } 102 | 103 | let priceBits = new BN("0"); 104 | 105 | for (let j = 0; j < 8; j++) { 106 | let index = j; 107 | if (index >= prices.length) { 108 | break; 109 | } 110 | 111 | const price = new BN(prices[index]); 112 | if (price.gt(new BN("2147483648"))) { // 2^31 113 | throw new Error(`price exceeds bit limit ${price.toString()}`); 114 | } 115 | 116 | priceBits = priceBits.or(price.shln(j * 32)); 117 | } 118 | 119 | return priceBits.toString(); 120 | } 121 | 122 | async function deployContract(name, args) { 123 | const contractFactory = await ethers.getContractFactory(name); 124 | return await contractFactory.deploy(...args); 125 | } 126 | 127 | async function deploy(owner) { 128 | let weth = await deployContract("MockWETH", []); 129 | let usdc = await deployContract("MockToken", ["mock usdc", "musdc"]); 130 | let priceOracle = await deployContract("PriceOracleForTest", []); 131 | let config = await deployContract("Config", []); 132 | let pairFactory = await deployContract("PairFactory", []); 133 | let marginFactory = await deployContract("MarginFactory", [pairFactory.address, config.address]); 134 | let ammFactory = await deployContract("AmmFactory", [pairFactory.address, config.address, owner.address]); 135 | let router = await deployContract("Router", []); 136 | let routerForKeeper = await deployContract("RouterForKeeper", [pairFactory.address, weth.address]); 137 | let orderBook = await deployContract("OrderBook", [routerForKeeper.address, owner.address]); 138 | 139 | return { 140 | weth, 141 | usdc, 142 | priceOracle, 143 | config, 144 | pairFactory, 145 | marginFactory, 146 | ammFactory, 147 | router, 148 | routerForKeeper, 149 | orderBook 150 | }; 151 | } 152 | 153 | async function init(owner, treasury, weth, usdc, priceOracle, config, pairFactory, marginFactory, ammFactory, router, routerForKeeper) { 154 | await router.initialize(config.address, pairFactory.address, treasury.address, weth.address); 155 | await config.registerRouter(router.address); 156 | await config.registerRouter(routerForKeeper.address); 157 | await config.registerRouter(owner.address); 158 | await config.setPriceOracle(priceOracle.address); 159 | await pairFactory.init(ammFactory.address, marginFactory.address); 160 | 161 | await pairFactory.createPair(weth.address, usdc.address); 162 | await priceOracle.setReserve(weth.address, usdc.address, 10000, 20000); 163 | } 164 | 165 | module.exports = { 166 | newWallet, 167 | maxUint256, 168 | bigNumberify, 169 | expandDecimals, 170 | mineBlock, 171 | increaseTime, 172 | gasUsed, 173 | getNetworkFee, 174 | reportGasUsed, 175 | getBlockTime, 176 | getTxnBalances, 177 | print, 178 | getPriceBitArray, 179 | getPriceBits, 180 | deployContract, 181 | deploy, 182 | init 183 | }; 184 | -------------------------------------------------------------------------------- /contracts/libraries/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Contains 512-bit math functions 5 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 6 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 7 | library FullMath { 8 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 9 | /// @param a The multiplicand 10 | /// @param b The multiplier 11 | /// @param denominator The divisor 12 | /// @return result The 256-bit result 13 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 14 | function mulDiv( 15 | uint256 a, 16 | uint256 b, 17 | uint256 denominator 18 | ) internal pure returns (uint256 result) { 19 | // 512-bit multiply [prod1 prod0] = a * b 20 | // Compute the product mod 2**256 and mod 2**256 - 1 21 | // then use the Chinese Remainder Theorem to reconstruct 22 | // the 512 bit result. The result is stored in two 256 23 | // variables such that product = prod1 * 2**256 + prod0 24 | uint256 prod0; // Least significant 256 bits of the product 25 | uint256 prod1; // Most significant 256 bits of the product 26 | 27 | // todo unchecked 28 | unchecked { 29 | assembly { 30 | let mm := mulmod(a, b, not(0)) 31 | prod0 := mul(a, b) 32 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 33 | } 34 | 35 | // Handle non-overflow cases, 256 by 256 division 36 | if (prod1 == 0) { 37 | require(denominator > 0); 38 | assembly { 39 | result := div(prod0, denominator) 40 | } 41 | return result; 42 | } 43 | 44 | // Make sure the result is less than 2**256. 45 | // Also prevents denominator == 0 46 | require(denominator > prod1); 47 | 48 | /////////////////////////////////////////////// 49 | // 512 by 256 division. 50 | /////////////////////////////////////////////// 51 | 52 | // Make division exact by subtracting the remainder from [prod1 prod0] 53 | // Compute remainder using mulmod 54 | uint256 remainder; 55 | assembly { 56 | remainder := mulmod(a, b, denominator) 57 | } 58 | // Subtract 256 bit number from 512 bit number 59 | assembly { 60 | prod1 := sub(prod1, gt(remainder, prod0)) 61 | prod0 := sub(prod0, remainder) 62 | } 63 | 64 | // Factor powers of two out of denominator 65 | // Compute largest power of two divisor of denominator. 66 | // Always >= 1. 67 | uint256 twos = (~denominator + 1) & denominator; 68 | // Divide denominator by power of two 69 | assembly { 70 | denominator := div(denominator, twos) 71 | } 72 | 73 | // Divide [prod1 prod0] by the factors of two 74 | assembly { 75 | prod0 := div(prod0, twos) 76 | } 77 | // Shift in bits from prod1 into prod0. For this we need 78 | // to flip `twos` such that it is 2**256 / twos. 79 | // If twos is zero, then it becomes one 80 | assembly { 81 | twos := add(div(sub(0, twos), twos), 1) 82 | } 83 | 84 | prod0 |= prod1 * twos; 85 | 86 | // Invert denominator mod 2**256 87 | // Now that denominator is an odd number, it has an inverse 88 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 89 | // Compute the inverse by starting with a seed that is correct 90 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 91 | uint256 inv = (3 * denominator) ^ 2; 92 | // Now use Newton-Raphson iteration to improve the precision. 93 | // Thanks to Hensel's lifting lemma, this also works in modular 94 | // arithmetic, doubling the correct bits in each step. 95 | 96 | inv *= 2 - denominator * inv; // inverse mod 2**8 97 | inv *= 2 - denominator * inv; // inverse mod 2**16 98 | inv *= 2 - denominator * inv; // inverse mod 2**32 99 | inv *= 2 - denominator * inv; // inverse mod 2**64 100 | inv *= 2 - denominator * inv; // inverse mod 2**128 101 | inv *= 2 - denominator * inv; // inverse mod 2**256 102 | 103 | // Because the division is now exact we can divide by multiplying 104 | // with the modular inverse of denominator. This will give us the 105 | // correct result modulo 2**256. Since the precoditions guarantee 106 | // that the outcome is less than 2**256, this is the final result. 107 | // We don't need to compute the high bits of the result and prod1 108 | // is no longer required. 109 | result = prod0 * inv; 110 | return result; 111 | } 112 | } 113 | 114 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 115 | /// @param a The multiplicand 116 | /// @param b The multiplier 117 | /// @param denominator The divisor 118 | /// @return result The 256-bit result 119 | function mulDivRoundingUp( 120 | uint256 a, 121 | uint256 b, 122 | uint256 denominator 123 | ) internal pure returns (uint256 result) { 124 | result = mulDiv(a, b, denominator); 125 | if (mulmod(a, b, denominator) > 0) { 126 | require(result < type(uint256).max); 127 | result++; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/core/fee-treasury.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { BigNumber } = require("@ethersproject/bignumber"); 3 | const { ethers } = require("hardhat"); 4 | 5 | describe("FeeTreasury contract", function () { 6 | let owner; 7 | let operator; 8 | let rewardForStaking; 9 | let rewardForCashback; 10 | let weth; 11 | let usdc; 12 | let wbtc; 13 | let swapRouter; 14 | let v3factory; 15 | let v3pool1; 16 | let v3pool2; 17 | let marginAddress = "0xb9cBC6759a4b71C127047c6aAcdDB569168A5046"; 18 | let amm1; 19 | let amm2; 20 | let amm3; 21 | let feeTreasury; 22 | 23 | beforeEach(async function () { 24 | [owner, operator, rewardForStaking, rewardForCashback] = await ethers.getSigners(); 25 | 26 | const MockWETH = await ethers.getContractFactory("MockWETH"); 27 | weth = await MockWETH.deploy(); 28 | await weth.deposit({ 29 | value: ethers.utils.parseEther("1"), 30 | }); 31 | 32 | const MyToken = await ethers.getContractFactory("MyToken"); 33 | usdc = await MyToken.deploy("USDC", "USDC", 6, BigNumber.from("1000000000000000")); 34 | wbtc = await MyToken.deploy("WBTC", "WBTC", 8, BigNumber.from("100000000000000000")); 35 | 36 | const MockUniswapV3Factory = await ethers.getContractFactory("MockUniswapV3Factory"); 37 | v3factory = await MockUniswapV3Factory.deploy(); 38 | 39 | const MockUniswapV3Pool = await ethers.getContractFactory("MockUniswapV3Pool"); 40 | v3pool1 = await MockUniswapV3Pool.deploy(weth.address, usdc.address, 500); 41 | await v3pool1.setLiquidity(1000000000); 42 | await v3factory.setPool(weth.address, usdc.address, 500, v3pool1.address); 43 | 44 | v3pool2 = await MockUniswapV3Pool.deploy(wbtc.address, usdc.address, 500); 45 | await v3pool2.setLiquidity(1000000000); 46 | await v3factory.setPool(wbtc.address, weth.address, 500, v3pool2.address); 47 | 48 | const MockSwapRouter = await ethers.getContractFactory("MockSwapRouter"); 49 | swapRouter = await MockSwapRouter.deploy(weth.address, v3factory.address); 50 | 51 | const MyAmm = await ethers.getContractFactory("MyAmm"); 52 | amm1 = await MyAmm.deploy(); 53 | await amm1.initialize(weth.address, usdc.address, marginAddress); 54 | 55 | amm2 = await MyAmm.deploy(); 56 | await amm2.initialize(wbtc.address, usdc.address, marginAddress); 57 | 58 | amm3 = await MyAmm.deploy(); 59 | await amm3.initialize(weth.address, wbtc.address, marginAddress); 60 | 61 | const FeeTreasury = await ethers.getContractFactory("FeeTreasury"); 62 | feeTreasury = await FeeTreasury.deploy(swapRouter.address, usdc.address, operator.address, 1000000); 63 | await feeTreasury.setRewardForStaking(rewardForStaking.address); 64 | await feeTreasury.setRewardForCashback(rewardForCashback.address); 65 | }); 66 | 67 | describe("batchRemoveLiquidity", function () { 68 | beforeEach(async function () { 69 | await weth.transfer(amm1.address, 1000); 70 | await amm1.mint(feeTreasury.address); 71 | await weth.transfer(amm3.address, 1000); 72 | await amm3.mint(feeTreasury.address); 73 | }); 74 | 75 | it("not operator", async function () { 76 | await expect(feeTreasury.batchRemoveLiquidity([amm1.address])).to.be.revertedWith("FORBIDDEN"); 77 | }); 78 | 79 | it("operator remove one amm", async function () { 80 | let feeTreasuryWithOperator = feeTreasury.connect(operator); 81 | await feeTreasuryWithOperator.batchRemoveLiquidity([amm1.address]); 82 | let balance = await weth.balanceOf(feeTreasury.address); 83 | console.log("balance:", balance.toString()); 84 | expect(balance).to.be.equal(100); 85 | }); 86 | 87 | it("operator remove two amms", async function () { 88 | let feeTreasuryWithOperator = feeTreasury.connect(operator); 89 | await feeTreasuryWithOperator.batchRemoveLiquidity([amm1.address, amm3.address]); 90 | let balance = await weth.balanceOf(feeTreasury.address); 91 | console.log("balance:", balance.toString()); 92 | expect(balance).to.be.equal(200); 93 | }); 94 | }); 95 | 96 | describe("batchSwapToETH", function () { 97 | beforeEach(async function () { 98 | await weth.transfer(amm1.address, 1000); 99 | await amm1.mint(feeTreasury.address); 100 | await wbtc.transfer(amm2.address, 1000); 101 | await amm2.mint(feeTreasury.address); 102 | await weth.transfer(v3pool1.address, 100); 103 | await usdc.transfer(v3pool1.address, 100); 104 | await weth.transfer(v3pool2.address, 100); 105 | await wbtc.transfer(v3pool2.address, 100); 106 | }); 107 | 108 | it("not operator", async function () { 109 | await expect(feeTreasury.batchSwapToETH([wbtc.address])).to.be.revertedWith("FORBIDDEN"); 110 | }); 111 | 112 | it("operator execute", async function () { 113 | let feeTreasuryWithOperator = feeTreasury.connect(operator); 114 | await feeTreasuryWithOperator.batchRemoveLiquidity([amm2.address]); 115 | await feeTreasuryWithOperator.batchSwapToETH([wbtc.address]); 116 | }); 117 | }); 118 | 119 | describe("distribute", function () { 120 | beforeEach(async function () { 121 | await weth.transfer(amm1.address, 1000); 122 | await amm1.mint(feeTreasury.address); 123 | await wbtc.transfer(amm2.address, 1000); 124 | await amm2.mint(feeTreasury.address); 125 | await weth.transfer(v3pool1.address, 100); 126 | await usdc.transfer(v3pool1.address, 100); 127 | await weth.transfer(v3pool2.address, 100); 128 | await wbtc.transfer(v3pool2.address, 100); 129 | }); 130 | 131 | it("not operator", async function () { 132 | await expect(feeTreasury.distribute()).to.be.revertedWith("FORBIDDEN"); 133 | }); 134 | 135 | it("operator execute", async function () { 136 | let feeTreasuryWithOperator = feeTreasury.connect(operator); 137 | await feeTreasuryWithOperator.batchRemoveLiquidity([amm2.address]); 138 | await feeTreasuryWithOperator.batchSwapToETH([wbtc.address]); 139 | await feeTreasuryWithOperator.distribute(); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /contracts/libraries/FixedPoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./FullMath.sol"; 5 | 6 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 7 | library FixedPoint { 8 | // range: [0, 2**112 - 1] 9 | // resolution: 1 / 2**112 10 | struct uq112x112 { 11 | uint224 _x; 12 | } 13 | 14 | // range: [0, 2**144 - 1] 15 | // resolution: 1 / 2**112 16 | struct uq144x112 { 17 | uint256 _x; 18 | } 19 | 20 | uint8 public constant RESOLUTION = 112; 21 | uint256 public constant Q112 = 0x10000000000000000000000000000; // 2**112 22 | uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000; // 2**224 23 | uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits) 24 | 25 | // encode a uint112 as a UQ112x112 26 | function encode(uint112 x) internal pure returns (uq112x112 memory) { 27 | return uq112x112(uint224(x) << RESOLUTION); 28 | } 29 | 30 | // encodes a uint144 as a UQ144x112 31 | function encode144(uint144 x) internal pure returns (uq144x112 memory) { 32 | return uq144x112(uint256(x) << RESOLUTION); 33 | } 34 | 35 | // decode a UQ112x112 into a uint112 by truncating after the radix point 36 | function decode(uq112x112 memory self) internal pure returns (uint112) { 37 | return uint112(self._x >> RESOLUTION); 38 | } 39 | 40 | // decode a UQ144x112 into a uint144 by truncating after the radix point 41 | function decode144(uq144x112 memory self) internal pure returns (uint144) { 42 | return uint144(self._x >> RESOLUTION); 43 | } 44 | 45 | // multiply a UQ112x112 by a uint, returning a UQ144x112 46 | // reverts on overflow 47 | function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { 48 | uint256 z = 0; 49 | require(y == 0 || (z = self._x * y) / y == self._x, 'FixedPoint::mul: overflow'); 50 | return uq144x112(z); 51 | } 52 | 53 | // multiply a UQ112x112 by an int and decode, returning an int 54 | // reverts on overflow 55 | function muli(uq112x112 memory self, int256 y) internal pure returns (int256) { 56 | uint256 z = FullMath.mulDiv(self._x, uint256(y < 0 ? -y : y), Q112); 57 | require(z < 2**255, 'FixedPoint::muli: overflow'); 58 | return y < 0 ? -int256(z) : int256(z); 59 | } 60 | 61 | // multiply a UQ112x112 by a UQ112x112, returning a UQ112x112 62 | // lossy 63 | function muluq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { 64 | if (self._x == 0 || other._x == 0) { 65 | return uq112x112(0); 66 | } 67 | uint112 upper_self = uint112(self._x >> RESOLUTION); // * 2^0 68 | uint112 lower_self = uint112(self._x & LOWER_MASK); // * 2^-112 69 | uint112 upper_other = uint112(other._x >> RESOLUTION); // * 2^0 70 | uint112 lower_other = uint112(other._x & LOWER_MASK); // * 2^-112 71 | 72 | // partial products 73 | uint224 upper = uint224(upper_self) * upper_other; // * 2^0 74 | uint224 lower = uint224(lower_self) * lower_other; // * 2^-224 75 | uint224 uppers_lowero = uint224(upper_self) * lower_other; // * 2^-112 76 | uint224 uppero_lowers = uint224(upper_other) * lower_self; // * 2^-112 77 | 78 | // so the bit shift does not overflow 79 | require(upper <= type(uint112).max, 'FixedPoint::muluq: upper overflow'); 80 | 81 | // this cannot exceed 256 bits, all values are 224 bits 82 | uint256 sum = uint256(upper << RESOLUTION) + uppers_lowero + uppero_lowers + (lower >> RESOLUTION); 83 | 84 | // so the cast does not overflow 85 | require(sum <= type(uint224).max, 'FixedPoint::muluq: sum overflow'); 86 | 87 | return uq112x112(uint224(sum)); 88 | } 89 | 90 | // divide a UQ112x112 by a UQ112x112, returning a UQ112x112 91 | function divuq(uq112x112 memory self, uq112x112 memory other) internal pure returns (uq112x112 memory) { 92 | require(other._x > 0, 'FixedPoint::divuq: division by zero'); 93 | if (self._x == other._x) { 94 | return uq112x112(uint224(Q112)); 95 | } 96 | if (self._x <= type(uint144).max) { 97 | uint256 value = (uint256(self._x) << RESOLUTION) / other._x; 98 | require(value <= type(uint224).max, 'FixedPoint::divuq: overflow'); 99 | return uq112x112(uint224(value)); 100 | } 101 | 102 | uint256 result = FullMath.mulDiv(Q112, self._x, other._x); 103 | require(result <= type(uint224).max, 'FixedPoint::divuq: overflow'); 104 | return uq112x112(uint224(result)); 105 | } 106 | 107 | // returns a UQ112x112 which represents the ratio of the numerator to the denominator 108 | // can be lossy 109 | function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) { 110 | require(denominator > 0, 'FixedPoint::fraction: division by zero'); 111 | if (numerator == 0) return FixedPoint.uq112x112(0); 112 | 113 | if (numerator <= type(uint144).max) { 114 | uint256 result = (numerator << RESOLUTION) / denominator; 115 | require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); 116 | return uq112x112(uint224(result)); 117 | } else { 118 | uint256 result = FullMath.mulDiv(numerator, Q112, denominator); 119 | require(result <= type(uint224).max, 'FixedPoint::fraction: overflow'); 120 | return uq112x112(uint224(result)); 121 | } 122 | } 123 | 124 | // take the reciprocal of a UQ112x112 125 | // reverts on overflow 126 | // lossy 127 | function reciprocal(uq112x112 memory self) internal pure returns (uq112x112 memory) { 128 | require(self._x != 0, 'FixedPoint::reciprocal: reciprocal of zero'); 129 | require(self._x != 1, 'FixedPoint::reciprocal: overflow'); 130 | return uq112x112(uint224(Q224 / self._x)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/limitorder/routerForKeeper.js: -------------------------------------------------------------------------------- 1 | const { expect, use } = require("chai"); 2 | const { utils } = require("ethers"); 3 | const { waffle } = require("hardhat"); 4 | const { solidity } = require("ethereum-waffle"); 5 | const { deploy, init } = require("../shared/utilities"); 6 | 7 | use(solidity); 8 | 9 | describe("RouterForKeeper UT", function() { 10 | 11 | const provider = waffle.provider; 12 | let [owner, treasury, addr1] = provider.getWallets(); 13 | 14 | let weth, usdc, priceOracle, config, pairFactory, marginFactory, ammFactory, router, routerForKeeper, order, 15 | orderBook; 16 | 17 | beforeEach(async function() { 18 | ({ 19 | weth, 20 | usdc, 21 | priceOracle, 22 | config, 23 | pairFactory, 24 | marginFactory, 25 | ammFactory, 26 | router, 27 | routerForKeeper, 28 | orderBook 29 | } = await deploy(owner)); 30 | await init(owner, treasury, weth, usdc, priceOracle, config, pairFactory, marginFactory, ammFactory, router, routerForKeeper); 31 | 32 | await weth.approve(router.address, 100000000000000); 33 | await router.addLiquidity(weth.address, usdc.address, 100000000000000, 0, 9999999999, false); 34 | 35 | await usdc.mint(owner.address, 10000000); 36 | await weth.approve(routerForKeeper.address, 10000); 37 | await routerForKeeper.setOrderBook(owner.address); 38 | 39 | order = { 40 | routerToExecute: routerForKeeper.address, 41 | trader: owner.address, 42 | baseToken: weth.address, 43 | quoteToken: usdc.address, 44 | side: 0, 45 | baseAmount: 1000, 46 | quoteAmount: 300, 47 | slippage: 500, 48 | limitPrice: "2100000000000000000", //2.1 49 | deadline: 999999999999, 50 | withWallet: true, 51 | nonce: utils.formatBytes32String("this is open long nonce") 52 | }; 53 | 54 | wrongOrder = { 55 | routerToExecute: routerForKeeper.address, 56 | trader: addr1.address, 57 | baseToken: weth.address, 58 | quoteToken: usdc.address, 59 | side: 0, 60 | baseAmount: 0, //wrong amount 61 | quoteAmount: 30000, 62 | slippage: 500, 63 | limitPrice: "2100000000000000000", 64 | deadline: 999999999999, 65 | withWallet: true, 66 | nonce: utils.formatBytes32String("this is wrong open long nonce") 67 | }; 68 | 69 | orderShort = { 70 | routerToExecute: routerForKeeper.address, 71 | trader: owner.address, 72 | baseToken: weth.address, 73 | quoteToken: usdc.address, 74 | side: 1, 75 | baseAmount: 10000, 76 | quoteAmount: 30000, 77 | slippage: 500, 78 | limitPrice: "1900000000000000000", //1.9 79 | deadline: 999999999999, 80 | withWallet: true, 81 | nonce: utils.formatBytes32String("this is open short nonce") 82 | }; 83 | 84 | closeOrder = { 85 | routerToExecute: routerForKeeper.address, 86 | trader: owner.address, 87 | baseToken: weth.address, 88 | quoteToken: usdc.address, 89 | side: 0, 90 | quoteAmount: 30, 91 | limitPrice: "1900000000000000000", //1.9 92 | deadline: 999999999999, 93 | autoWithdraw: true, 94 | nonce: utils.formatBytes32String("this is close long nonce") 95 | }; 96 | 97 | closeOrderShort = { 98 | routerToExecute: routerForKeeper.address, 99 | trader: owner.address, 100 | baseToken: weth.address, 101 | quoteToken: usdc.address, 102 | side: 1, 103 | quoteAmount: 30000, 104 | limitPrice: "2100000000000000000", //2.1 105 | deadline: 999999999999, 106 | autoWithdraw: false, 107 | nonce: utils.formatBytes32String("this is close short nonce") 108 | }; 109 | }); 110 | 111 | describe("openPositionWithWallet", function() { 112 | 113 | it("can open position with wallet", async function() { 114 | await routerForKeeper.openPositionWithWallet(order); 115 | let result = await router.getPosition(weth.address, usdc.address, owner.address); 116 | expect(result.quoteSize.toNumber()).to.be.equal(-300); 117 | }); 118 | 119 | it("revert when open position with wrong pair", async function() { 120 | order.baseToken = owner.address; 121 | await expect(routerForKeeper.openPositionWithWallet(order)).to.be.revertedWith( 122 | "RFK.OPWW: NOT_FOUND_MARGIN" 123 | ); 124 | }); 125 | 126 | it("revert when open position with invalid side", async function() { 127 | order.side = 2; 128 | await expect(routerForKeeper.openPositionWithWallet(order)).to.be.revertedWith( 129 | "RFK.OPWW: INVALID_SIDE" 130 | ); 131 | }); 132 | }); 133 | 134 | describe("openPositionWithMargin", function() { 135 | beforeEach(async function() { 136 | await weth.approve(router.address, 100000000); 137 | await router.deposit(weth.address, usdc.address, owner.address, 10); 138 | }); 139 | 140 | it("can open position with margin", async function() { 141 | await router.deposit(weth.address, usdc.address, owner.address, 1000000); 142 | await routerForKeeper.openPositionWithMargin(order); 143 | }); 144 | 145 | it("revert when open position with wrong pair", async function() { 146 | order.baseToken = owner.address; 147 | await expect(routerForKeeper.openPositionWithMargin(order)).to.be.revertedWith( 148 | "RFK.OPWM: NOT_FOUND_MARGIN" 149 | ); 150 | }); 151 | 152 | it("revert when open position with invalid side", async function() { 153 | order.side = 2; 154 | await expect(routerForKeeper.openPositionWithMargin(order)).to.be.revertedWith( 155 | "RFK.OPWM: INVALID_SIDE" 156 | ); 157 | }); 158 | }); 159 | 160 | describe("closePosition", function() { 161 | beforeEach(async function() { 162 | await routerForKeeper.openPositionWithWallet(order); 163 | await router.getPosition(weth.address, usdc.address, owner.address); 164 | }); 165 | 166 | it("can close position", async function() { 167 | await routerForKeeper.closePosition(closeOrder); 168 | }); 169 | 170 | it("revert when open position with wrong pair", async function() { 171 | closeOrder.baseToken = owner.address; 172 | await expect(routerForKeeper.closePosition(closeOrder)).to.be.revertedWith( 173 | "RFK.CP: NOT_FOUND_MARGIN" 174 | ); 175 | }); 176 | 177 | it("revert when close a long position with side=1", async function() { 178 | closeOrder.side = 1; 179 | await expect(routerForKeeper.closePosition(closeOrder)).to.be.revertedWith( 180 | "RFK.CP: SIDE_NOT_MATCH" 181 | ); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /contracts/libraries/V2Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/uniswapV2/IUniswapV2Pair.sol"; 5 | import "./FixedPoint.sol"; 6 | 7 | library V2Oracle { 8 | struct Observation { 9 | uint256 timestamp; 10 | uint256 price0Cumulative; 11 | uint256 price1Cumulative; 12 | } 13 | 14 | // returns the index of the observation corresponding to the given timestamp 15 | function observationIndexOf(uint256 timestamp, uint256 periodSize, uint16 granularity) internal pure returns (uint16 index) { 16 | uint256 epochPeriod = timestamp / periodSize; 17 | return uint16(epochPeriod % granularity); 18 | } 19 | 20 | // returns the observation from the oldest epoch (at the beginning of the window) relative to the current time 21 | function getFirstObservationInWindow(Observation[] storage self, uint256 periodSize, uint16 granularity) internal view returns (Observation storage firstObservation) { 22 | uint16 observationIndex = observationIndexOf(block.timestamp, periodSize, granularity); 23 | // no overflow issue. if observationIndex + 1 overflows, result is still zero. 24 | uint16 firstObservationIndex = (observationIndex + 1) % granularity; 25 | firstObservation = self[firstObservationIndex]; 26 | } 27 | 28 | 29 | // update the cumulative price for the observation at the current timestamp. each observation is updated at most 30 | // once per epoch period. 31 | function update(Observation[] storage self, address pair, uint256 periodSize, uint16 granularity) internal { 32 | // populate the array with empty observations (first call only) 33 | for (uint256 i = self.length; i < granularity; i++) { 34 | self.push(); 35 | } 36 | 37 | // get the observation for the current period 38 | uint16 observationIndex = observationIndexOf(block.timestamp, periodSize, granularity); 39 | Observation memory observation = self[observationIndex]; 40 | 41 | // we only want to commit updates once per period (i.e. windowSize / granularity) 42 | uint timeElapsed = block.timestamp - observation.timestamp; 43 | if (timeElapsed > periodSize) { 44 | (uint256 price0Cumulative, uint256 price1Cumulative,) = currentCumulativePrices(pair); 45 | observation.timestamp = block.timestamp; 46 | observation.price0Cumulative = price0Cumulative; 47 | observation.price1Cumulative = price1Cumulative; 48 | self[observationIndex] = observation; 49 | } 50 | } 51 | 52 | // given the cumulative prices of the start and end of a period, and the length of the period, compute the average 53 | // price in terms of how much amount out is received for the amount in 54 | function computeAmountOut( 55 | uint256 priceCumulativeStart, uint256 priceCumulativeEnd, 56 | uint256 timeElapsed, uint256 amountIn 57 | ) private pure returns (uint256 amountOut) { 58 | // overflow is desired. 59 | FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112( 60 | uint224((priceCumulativeEnd - priceCumulativeStart) / timeElapsed) 61 | ); 62 | FixedPoint.uq144x112 memory amountOut144x112 = FixedPoint.mul(priceAverage, amountIn); 63 | amountOut = FixedPoint.decode144(amountOut144x112); 64 | } 65 | 66 | // returns the amount out corresponding to the amount in for a given token using the moving average over the time 67 | // range [now - [windowSize, windowSize - periodSize * 2], now] 68 | // update must have been called for the bucket corresponding to timestamp `now - windowSize` 69 | function consult( 70 | Observation[] storage self, 71 | address pair, 72 | address tokenIn, 73 | address tokenOut, 74 | uint256 amountIn, 75 | uint256 windowSize, 76 | uint256 periodSize, 77 | uint16 granularity 78 | ) internal view returns (uint256 amountOut) { 79 | Observation memory firstObservation = getFirstObservationInWindow(self, periodSize, granularity); 80 | 81 | uint timeElapsed = block.timestamp - firstObservation.timestamp; 82 | require(timeElapsed <= windowSize, 'V2Oracle: MISSING_HISTORICAL_OBSERVATION'); 83 | // should never happen. 84 | require(timeElapsed >= windowSize - periodSize * 2, 'V2Oracle: UNEXPECTED_TIME_ELAPSED'); 85 | 86 | (uint256 price0Cumulative, uint256 price1Cumulative,) = currentCumulativePrices(pair); 87 | (address token0,) = sortTokens(tokenIn, tokenOut); 88 | 89 | if (token0 == tokenIn) { 90 | return computeAmountOut(firstObservation.price0Cumulative, price0Cumulative, timeElapsed, amountIn); 91 | } else { 92 | return computeAmountOut(firstObservation.price1Cumulative, price1Cumulative, timeElapsed, amountIn); 93 | } 94 | } 95 | 96 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 97 | function currentCumulativePrices( 98 | address pair 99 | ) internal view returns (uint256 price0Cumulative, uint256 price1Cumulative, uint32 blockTimestamp) { 100 | blockTimestamp = uint32(block.timestamp % 2 ** 32); 101 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 102 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 103 | 104 | // if time has elapsed since the last update on the pair, mock the accumulated price values 105 | (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); 106 | if (blockTimestampLast != blockTimestamp) { 107 | // subtraction overflow is desired 108 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 109 | // addition overflow is desired 110 | // counterfactual 111 | price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; 112 | // counterfactual 113 | price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; 114 | } 115 | } 116 | 117 | // returns sorted token addresses, used to handle return values from pairs sorted in this order 118 | function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { 119 | require(tokenA != tokenB, 'V2Oracle: IDENTICAL_ADDRESSES'); 120 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 121 | require(token0 != address(0), 'V2Oracle: ZERO_ADDRESS'); 122 | } 123 | } -------------------------------------------------------------------------------- /contracts/core/RouterForKeeper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IAmmFactory.sol"; 5 | import "../interfaces/IERC20.sol"; 6 | import "../interfaces/IRouterForKeeper.sol"; 7 | import "../interfaces/IOrderBook.sol"; 8 | import "../interfaces/IPairFactory.sol"; 9 | import "../interfaces/IMargin.sol"; 10 | import "../interfaces/IAmm.sol"; 11 | import "../interfaces/IWETH.sol"; 12 | import "../libraries/TransferHelper.sol"; 13 | import "../libraries/SignedMath.sol"; 14 | import "../libraries/FullMath.sol"; 15 | import "../utils/Ownable.sol"; 16 | 17 | contract RouterForKeeper is IRouterForKeeper, Ownable { 18 | using SignedMath for int256; 19 | using FullMath for uint256; 20 | 21 | address public immutable override pairFactory; 22 | address public immutable override WETH; 23 | address public orderBook; 24 | mapping(address => mapping(address => uint256)) public balanceOf; 25 | 26 | modifier ensure(uint256 deadline) { 27 | require(deadline >= block.timestamp, "RFK: EXPIRED"); 28 | _; 29 | } 30 | 31 | modifier onlyOrderBook() { 32 | require(msg.sender == orderBook, "RFK:only orderboook"); 33 | _; 34 | } 35 | 36 | constructor(address pairFactory_, address _WETH) { 37 | owner = msg.sender; 38 | pairFactory = pairFactory_; 39 | WETH = _WETH; 40 | } 41 | 42 | receive() external payable { 43 | assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract 44 | } 45 | 46 | function setOrderBook(address newOrderBook) external override onlyOwner { 47 | require(newOrderBook != address(0), "RFK.SOB: ZERO_ADDRESS"); 48 | orderBook = newOrderBook; 49 | } 50 | 51 | function openPositionWithWallet(IOrderBook.OpenPositionOrder memory order) 52 | external 53 | override 54 | ensure(order.deadline) 55 | onlyOrderBook 56 | returns (uint256 baseAmount) 57 | { 58 | address margin = IPairFactory(pairFactory).getMargin(order.baseToken, order.quoteToken); 59 | require(margin != address(0), "RFK.OPWW: NOT_FOUND_MARGIN"); 60 | require(order.side == 0 || order.side == 1, "RFK.OPWW: INVALID_SIDE"); 61 | 62 | TransferHelper.safeTransferFrom(order.baseToken, order.trader, margin, order.baseAmount); 63 | 64 | IMargin(margin).addMargin(order.trader, order.baseAmount); 65 | baseAmount = IMargin(margin).openPosition(order.trader, order.side, order.quoteAmount); 66 | if (order.side == 0) { 67 | _collectFee(baseAmount, margin); 68 | } 69 | } 70 | 71 | function openPositionWithMargin(IOrderBook.OpenPositionOrder memory order) 72 | external 73 | override 74 | ensure(order.deadline) 75 | onlyOrderBook 76 | returns (uint256 baseAmount) 77 | { 78 | address margin = IPairFactory(pairFactory).getMargin(order.baseToken, order.quoteToken); 79 | require(margin != address(0), "RFK.OPWM: NOT_FOUND_MARGIN"); 80 | require(order.side == 0 || order.side == 1, "RFK.OPWM: INVALID_SIDE"); 81 | baseAmount = IMargin(margin).openPosition(order.trader, order.side, order.quoteAmount); 82 | if (order.side == 0) { 83 | _collectFee(baseAmount, margin); 84 | } 85 | } 86 | 87 | function closePosition(IOrderBook.ClosePositionOrder memory order) 88 | external 89 | override 90 | ensure(order.deadline) 91 | onlyOrderBook 92 | returns (uint256 baseAmount, uint256 withdrawAmount) 93 | { 94 | address margin = IPairFactory(pairFactory).getMargin(order.baseToken, order.quoteToken); 95 | require(margin != address(0), "RFK.CP: NOT_FOUND_MARGIN"); 96 | (, int256 quoteSizeBefore, ) = IMargin(margin).getPosition(order.trader); 97 | require( 98 | quoteSizeBefore > 0 ? order.side == 1 : order.side == 0, 99 | "RFK.CP: SIDE_NOT_MATCH" 100 | ); 101 | if (!order.autoWithdraw) { 102 | baseAmount = IMargin(margin).closePosition(order.trader, order.quoteAmount); 103 | if (quoteSizeBefore > 0) { 104 | _collectFee(baseAmount, margin); 105 | } 106 | } else { 107 | { 108 | baseAmount = IMargin(margin).closePosition(order.trader, order.quoteAmount); 109 | if (quoteSizeBefore > 0) { 110 | _collectFee(baseAmount, margin); 111 | } 112 | (int256 baseSize, int256 quoteSizeAfter, uint256 tradeSize) = IMargin(margin).getPosition(order.trader); 113 | int256 unrealizedPnl = IMargin(margin).calUnrealizedPnl(order.trader); 114 | int256 traderMargin; 115 | if (quoteSizeAfter < 0) { 116 | // long, traderMargin = baseSize - tradeSize + unrealizedPnl 117 | traderMargin = baseSize.subU(tradeSize) + unrealizedPnl; 118 | } else { 119 | // short, traderMargin = baseSize + tradeSize + unrealizedPnl 120 | traderMargin = baseSize.addU(tradeSize) + unrealizedPnl; 121 | } 122 | withdrawAmount = 123 | traderMargin.abs() - 124 | (traderMargin.abs() * quoteSizeAfter.abs()) / 125 | quoteSizeBefore.abs(); 126 | } 127 | 128 | uint256 withdrawable = IMargin(margin).getWithdrawable(order.trader); 129 | if (withdrawable < withdrawAmount) { 130 | withdrawAmount = withdrawable; 131 | } 132 | if (withdrawAmount > 0) { 133 | IMargin(margin).removeMargin(order.trader, order.trader, withdrawAmount); 134 | } 135 | } 136 | } 137 | 138 | //if eth is 2000usdc, then here return 2000*1e18, 18, 6 139 | function getSpotPriceWithMultiplier(address baseToken, address quoteToken) 140 | external 141 | view 142 | override 143 | returns ( 144 | uint256 spotPriceWithMultiplier, 145 | uint256 baseDecimal, 146 | uint256 quoteDecimal 147 | ) 148 | { 149 | address amm = IPairFactory(pairFactory).getAmm(baseToken, quoteToken); 150 | (uint256 reserveBase, uint256 reserveQuote, ) = IAmm(amm).getReserves(); 151 | 152 | uint256 baseDecimals = IERC20(baseToken).decimals(); 153 | uint256 quoteDecimals = IERC20(quoteToken).decimals(); 154 | uint256 exponent = uint256(10**(18 + baseDecimals - quoteDecimals)); 155 | 156 | return (exponent.mulDiv(reserveQuote, reserveBase), baseDecimals, quoteDecimals); 157 | } 158 | 159 | function _collectFee(uint256 baseAmount, address margin) internal { 160 | uint256 fee = baseAmount / 1000; 161 | address feeTreasury = IAmmFactory(IPairFactory(pairFactory).ammFactory()).feeTo(); 162 | IMargin(margin).removeMargin(msg.sender, feeTreasury, fee); 163 | emit CollectFee(msg.sender, margin, fee); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /contracts/core/OrderBook.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../utils/Ownable.sol"; 5 | import "../utils/Reentrant.sol"; 6 | import "../libraries/TransferHelper.sol"; 7 | import "../interfaces/IOrderBook.sol"; 8 | import "../interfaces/IRouterForKeeper.sol"; 9 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | 11 | contract OrderBook is IOrderBook, Ownable, Reentrant { 12 | using ECDSA for bytes32; 13 | 14 | address public routerForKeeper; 15 | mapping(bytes => bool) public usedNonce; 16 | mapping(address => bool) public bots; 17 | 18 | modifier onlyBot() { 19 | require(bots[msg.sender] == true, "OB: ONLY_BOT"); 20 | _; 21 | } 22 | 23 | constructor(address routerForKeeper_, address bot_) { 24 | require(routerForKeeper_ != address(0), "OB: ZERO_ADDRESS"); 25 | owner = msg.sender; 26 | routerForKeeper = routerForKeeper_; 27 | bots[bot_] = true; 28 | } 29 | 30 | function addBot(address newBot) external override onlyOwner { 31 | require(newBot != address(0), "OB.ST: ZERO_ADDRESS"); 32 | bots[newBot] = true; 33 | } 34 | 35 | function reverseBotState(address bot) external override onlyOwner { 36 | require(bot != address(0), "OB.PT: ZERO_ADDRESS"); 37 | bots[bot] = !bots[bot]; 38 | } 39 | 40 | function batchExecuteOpen( 41 | OpenPositionOrder[] memory orders, 42 | bytes[] memory signatures, 43 | bool requireSuccess 44 | ) external override nonReentrant onlyBot returns (RespData[] memory respData) { 45 | require(orders.length == signatures.length, "OB.BE: LENGTH_NOT_MATCH"); 46 | respData = new RespData[](orders.length); 47 | for (uint256 i = 0; i < orders.length; i++) { 48 | respData[i] = _executeOpen(orders[i], signatures[i], requireSuccess); 49 | } 50 | emit BatchExecuteOpen(orders, signatures, requireSuccess); 51 | } 52 | 53 | function batchExecuteClose( 54 | ClosePositionOrder[] memory orders, 55 | bytes[] memory signatures, 56 | bool requireSuccess 57 | ) external override nonReentrant onlyBot returns (RespData[] memory respData) { 58 | require(orders.length == signatures.length, "OB.BE: LENGTH_NOT_MATCH"); 59 | respData = new RespData[](orders.length); 60 | for (uint256 i = 0; i < orders.length; i++) { 61 | respData[i] = _executeClose(orders[i], signatures[i], requireSuccess); 62 | } 63 | 64 | emit BatchExecuteClose(orders, signatures, requireSuccess); 65 | } 66 | 67 | function executeOpen(OpenPositionOrder memory order, bytes memory signature) external override nonReentrant onlyBot { 68 | _executeOpen(order, signature, true); 69 | 70 | emit ExecuteOpen(order, signature); 71 | } 72 | 73 | function executeClose(ClosePositionOrder memory order, bytes memory signature) external override nonReentrant onlyBot { 74 | _executeClose(order, signature, true); 75 | 76 | emit ExecuteClose(order, signature); 77 | } 78 | 79 | function verifyOpen(OpenPositionOrder memory order, bytes memory signature) public view override returns (bool) { 80 | address recover = keccak256(abi.encode(order)).toEthSignedMessageHash().recover(signature); 81 | require(order.trader == recover, "OB.VO: NOT_SIGNER"); 82 | require(!usedNonce[order.nonce], "OB.VO: NONCE_USED"); 83 | require(block.timestamp < order.deadline, "OB.VO: EXPIRED"); 84 | return true; 85 | } 86 | 87 | function verifyClose(ClosePositionOrder memory order, bytes memory signature) public view override returns (bool) { 88 | address recover = keccak256(abi.encode(order)).toEthSignedMessageHash().recover(signature); 89 | require(order.trader == recover, "OB.VC: NOT_SIGNER"); 90 | require(!usedNonce[order.nonce], "OB.VC: NONCE_USED"); 91 | require(block.timestamp < order.deadline, "OB.VC: EXPIRED"); 92 | return true; 93 | } 94 | 95 | function setRouterForKeeper(address _routerForKeeper) external override onlyOwner { 96 | require(_routerForKeeper != address(0), "OB.SRK: ZERO_ADDRESS"); 97 | 98 | routerForKeeper = _routerForKeeper; 99 | emit SetRouterForKeeper(_routerForKeeper); 100 | } 101 | 102 | function _executeOpen( 103 | OpenPositionOrder memory order, 104 | bytes memory signature, 105 | bool requireSuccess 106 | ) internal returns (RespData memory) { 107 | require(verifyOpen(order, signature)); 108 | require(order.routerToExecute == routerForKeeper, "OB.EO: WRONG_ROUTER"); 109 | require(order.baseToken != address(0), "OB.EO: ORDER_NOT_FOUND"); 110 | require(order.side == 0 || order.side == 1, "OB.EO: INVALID_SIDE"); 111 | 112 | (uint256 currentPrice, , ) = IRouterForKeeper(routerForKeeper) 113 | .getSpotPriceWithMultiplier(order.baseToken, order.quoteToken); 114 | 115 | // long 0 : price <= limitPrice * (1 + slippage) 116 | // short 1 : price >= limitPrice * (1 - slippage) 117 | uint256 slippagePrice = (order.side == 0) ? (order.limitPrice * (10000 + order.slippage)) / 10000 : (order.limitPrice * (10000 - order.slippage)) / 10000; 118 | require(order.side == 0 ? currentPrice <= slippagePrice : currentPrice >= slippagePrice, "OB.EO: SLIPPAGE_NOT_FUIFILL"); 119 | 120 | bool success; 121 | bytes memory ret; 122 | 123 | if (order.withWallet) { 124 | (success, ret) = routerForKeeper.call( 125 | abi.encodeWithSelector( 126 | IRouterForKeeper.openPositionWithWallet.selector, 127 | order 128 | ) 129 | ); 130 | } else { 131 | (success, ret) = routerForKeeper.call( 132 | abi.encodeWithSelector( 133 | IRouterForKeeper.openPositionWithMargin.selector, 134 | order 135 | ) 136 | ); 137 | } 138 | emit ExecuteLog(order.nonce, success); 139 | if (requireSuccess) { 140 | require(success, "OB.EO: CALL_FAILED"); 141 | } 142 | 143 | usedNonce[order.nonce] = true; 144 | return RespData({success : success, result : ret}); 145 | } 146 | 147 | function _executeClose( 148 | ClosePositionOrder memory order, 149 | bytes memory signature, 150 | bool requireSuccess 151 | ) internal returns (RespData memory) { 152 | require(verifyClose(order, signature)); 153 | require(order.routerToExecute == routerForKeeper, "OB.EC: WRONG_ROUTER"); 154 | require(order.baseToken != address(0), "OB.EC: ORDER_NOT_FOUND"); 155 | require(order.side == 0 || order.side == 1, "OB.EC: INVALID_SIDE"); 156 | 157 | (uint256 currentPrice, ,) = IRouterForKeeper(routerForKeeper).getSpotPriceWithMultiplier( 158 | order.baseToken, 159 | order.quoteToken 160 | ); 161 | 162 | require( 163 | order.side == 0 ? currentPrice >= order.limitPrice : currentPrice <= order.limitPrice, 164 | "OB.EC: WRONG_PRICE" 165 | ); 166 | 167 | (bool success, bytes memory ret) = routerForKeeper.call( 168 | abi.encodeWithSelector(IRouterForKeeper.closePosition.selector, order) 169 | ); 170 | emit ExecuteLog(order.nonce, success); 171 | 172 | if (requireSuccess) { 173 | require(success, "OB.EC: CALL_FAILED"); 174 | } 175 | 176 | usedNonce[order.nonce] = true; 177 | return RespData({success : success, result : ret}); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /contracts/core/FeeTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/IERC20.sol"; 5 | import "../interfaces/IAmm.sol"; 6 | import "../utils/Ownable.sol"; 7 | import "../libraries/TransferHelper.sol"; 8 | import "../libraries/TickMath.sol"; 9 | import "../interfaces/uniswapV3/IUniswapV3Factory.sol"; 10 | import "../interfaces/uniswapV3/IUniswapV3Pool.sol"; 11 | import "../interfaces/uniswapV3/ISwapRouter.sol"; 12 | import "../interfaces/IWETH.sol"; 13 | 14 | contract FeeTreasury is Ownable { 15 | event RatioForStakingChanged(uint8 oldRatio, uint8 newRatio); 16 | event RewardForStakingChanged(address indexed oldReward, address indexed newReward); 17 | event RewardForCashbackChanged(address indexed oldReward, address indexed newReward); 18 | event OperatorChanged(address indexed oldOperator, address indexed newOperator); 19 | event SettlementIntervalChanged(uint256 oldInterval, uint256 newInterval); 20 | event DistributeToStaking( 21 | address indexed rewardForStaking, 22 | uint256 ethAmount, 23 | uint256 usdcAmount, 24 | uint256 timestamp 25 | ); 26 | event DistributeToCashback( 27 | address indexed rewardForCashback, 28 | uint256 ethAmount, 29 | uint256 usdcAmount, 30 | uint256 timestamp 31 | ); 32 | 33 | ISwapRouter public v3Router; 34 | address public v3Factory; 35 | address public WETH; 36 | address public USDC; 37 | address public operator; 38 | uint24[3] public v3Fees; 39 | 40 | uint8 public ratioForStaking = 33; 41 | // the Reward contract address for staking 42 | address public rewardForStaking; 43 | // the Reward contract address for cashback 44 | address public rewardForCashback; 45 | 46 | uint256 public settlementInterval = 7*24*3600; // one week 47 | uint256 public nextSettleTime; 48 | 49 | modifier check() { 50 | require(msg.sender == operator, "FORBIDDEN"); 51 | require(block.timestamp >= nextSettleTime, "NOT_REACH_TIME"); 52 | _; 53 | } 54 | 55 | constructor( 56 | ISwapRouter v3Router_, 57 | address USDC_, 58 | address operator_, 59 | uint256 nextSettleTime_ 60 | ) { 61 | owner = msg.sender; 62 | v3Router = v3Router_; 63 | v3Factory = v3Router.factory(); 64 | WETH = v3Router.WETH9(); 65 | USDC = USDC_; 66 | operator = operator_; 67 | nextSettleTime = nextSettleTime_; 68 | v3Fees[0] = 500; 69 | v3Fees[1] = 3000; 70 | v3Fees[2] = 10000; 71 | } 72 | 73 | receive() external payable { 74 | assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract 75 | } 76 | 77 | function setRatioForStaking(uint8 newrRatio) external onlyOwner { 78 | require(newrRatio <= 100, "OVER_100%"); 79 | emit RatioForStakingChanged(ratioForStaking, newrRatio); 80 | ratioForStaking = newrRatio; 81 | } 82 | 83 | function setRewardForStaking(address newReward) external onlyOwner { 84 | require(newReward != address(0), "ZERO_ADDRESS"); 85 | emit RewardForStakingChanged(rewardForStaking, newReward); 86 | rewardForStaking = newReward; 87 | } 88 | 89 | function setRewardForCashback(address newReward) external onlyOwner { 90 | require(newReward != address(0), "ZERO_ADDRESS"); 91 | emit RewardForCashbackChanged(rewardForCashback, newReward); 92 | rewardForCashback = newReward; 93 | } 94 | 95 | function setOperator(address newOperator) external onlyOwner { 96 | require(newOperator != address(0), "ZERO_ADDRESS"); 97 | emit OperatorChanged(operator, newOperator); 98 | operator = newOperator; 99 | } 100 | 101 | function setSettlementInterval(uint256 newInterval) external onlyOwner { 102 | require(newInterval > 0, "ZERO"); 103 | emit SettlementIntervalChanged(settlementInterval, newInterval); 104 | settlementInterval = newInterval; 105 | } 106 | 107 | function batchRemoveLiquidity(address[] memory amms) external check { 108 | for (uint256 i = 0; i < amms.length; i++) { 109 | address amm = amms[i]; 110 | IAmm(amm).collectFee(); 111 | 112 | uint256 liquidity = IERC20(amm).balanceOf(address(this)); 113 | if (liquidity == 0) continue; 114 | 115 | TransferHelper.safeTransfer(amm, amm, liquidity); 116 | IAmm(amm).burn(address(this)); 117 | } 118 | } 119 | 120 | function batchSwapToETH(address[] memory tokens) external check { 121 | for (uint256 i = 0; i < tokens.length; i++) { 122 | address token = tokens[i]; 123 | uint256 balance = IERC20(token).balanceOf(address(this)); 124 | if (balance > 0 && token != WETH && token != USDC) { 125 | // query target pool 126 | address pool; 127 | uint256 poolLiquidity; 128 | for (uint256 j = 0; j < v3Fees.length; j++) { 129 | address tempPool = IUniswapV3Factory(v3Factory).getPool(token, WETH, v3Fees[j]); 130 | if (tempPool == address(0)) continue; 131 | uint256 tempLiquidity = uint256(IUniswapV3Pool(tempPool).liquidity()); 132 | // use the max liquidity pool as target pool 133 | if (tempLiquidity > poolLiquidity) { 134 | poolLiquidity = tempLiquidity; 135 | pool = tempPool; 136 | } 137 | } 138 | 139 | // swap token to WETH 140 | uint256 allowance = IERC20(token).allowance(address(this), address(v3Router)); 141 | if (allowance < balance) { 142 | IERC20(token).approve(address(v3Router), type(uint256).max); 143 | } 144 | ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ 145 | tokenIn: token, 146 | tokenOut: WETH, 147 | fee: IUniswapV3Pool(pool).fee(), 148 | recipient: address(this), 149 | amountIn: balance, 150 | amountOutMinimum: 1, 151 | sqrtPriceLimitX96: token < WETH ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 152 | }); 153 | v3Router.exactInputSingle(params); 154 | } 155 | } 156 | uint256 wethBalance = IERC20(WETH).balanceOf(address(this)); 157 | if (wethBalance > 0) IWETH(WETH).withdraw(wethBalance); 158 | } 159 | 160 | function distribute() external check { 161 | require(rewardForCashback != address(0), "NOT_FOUND_REWARD_FOR_CASHBACK"); 162 | uint256 ethBalance = address(this).balance; 163 | uint256 usdcBalance = IERC20(USDC).balanceOf(address(this)); 164 | 165 | if (rewardForStaking == address(0)) { 166 | if (ethBalance > 0) TransferHelper.safeTransferETH(rewardForCashback, ethBalance); 167 | if (usdcBalance > 0) TransferHelper.safeTransfer(USDC, rewardForCashback, usdcBalance); 168 | emit DistributeToCashback(rewardForCashback, ethBalance, usdcBalance, block.timestamp); 169 | } else { 170 | uint256 ethForStaking = ethBalance * ratioForStaking / 100; 171 | uint256 ethForCashback = ethBalance - ethForStaking; 172 | 173 | uint256 usdcForStaking = usdcBalance * ratioForStaking / 100; 174 | uint256 usdcForCashback = usdcBalance - usdcForStaking; 175 | 176 | if (ethForStaking > 0) TransferHelper.safeTransferETH(rewardForStaking, ethForStaking); 177 | if (ethForCashback > 0) TransferHelper.safeTransferETH(rewardForCashback, ethForCashback); 178 | if (usdcForStaking > 0) TransferHelper.safeTransfer(USDC, rewardForStaking, usdcForStaking); 179 | if (usdcForCashback > 0) TransferHelper.safeTransfer(USDC, rewardForCashback, usdcForCashback); 180 | 181 | emit DistributeToStaking(rewardForStaking, ethForStaking, usdcForStaking, block.timestamp); 182 | emit DistributeToCashback(rewardForCashback, ethForCashback, usdcForCashback, block.timestamp); 183 | } 184 | 185 | nextSettleTime = nextSettleTime + settlementInterval; 186 | } 187 | 188 | function withdrawETH(address to) external onlyOwner { 189 | payable(to).transfer(address(this).balance); 190 | } 191 | 192 | function withdrawERC20Token(address token_, address to, uint256 amount) external onlyOwner returns (bool) { 193 | uint256 balance = IERC20(token_).balanceOf(address(this)); 194 | require(balance >= amount, "NOT_ENOUGH_BALANCE"); 195 | require(IERC20(token_).transfer(to, amount)); 196 | return true; 197 | } 198 | } --------------------------------------------------------------------------------