├── .gitignore ├── .solhint.json ├── LICENSE ├── README.md ├── contracts ├── SyncSwapRouter.sol ├── abstract │ ├── Multicall.sol │ └── SelfPermit.sol ├── interfaces │ ├── ICallback.sol │ ├── IRouter.sol │ ├── IStakingPool.sol │ ├── IWETH.sol │ ├── factory │ │ ├── IBasePoolFactory.sol │ │ └── IPoolFactory.sol │ ├── master │ │ ├── IFeeManager.sol │ │ ├── IFeeRecipient.sol │ │ ├── IFeeRegistry.sol │ │ ├── IForwarderRegistry.sol │ │ └── IPoolMaster.sol │ ├── pool │ │ ├── IBasePool.sol │ │ ├── IClassicPool.sol │ │ ├── IPool.sol │ │ └── IStablePool.sol │ ├── token │ │ ├── IERC165.sol │ │ ├── IERC20.sol │ │ ├── IERC20Base.sol │ │ ├── IERC20Permit.sol │ │ ├── IERC20Permit2.sol │ │ └── IERC20PermitAllowed.sol │ └── vault │ │ ├── IERC3156FlashBorrower.sol │ │ ├── IERC3156FlashLender.sol │ │ ├── IFlashLoan.sol │ │ ├── IFlashLoanRecipient.sol │ │ └── IVault.sol ├── libraries │ ├── ECDSA.sol │ ├── ERC20Permit2.sol │ ├── Math.sol │ ├── MetadataHelper.sol │ ├── Ownable.sol │ ├── Ownable2Step.sol │ ├── Pausable.sol │ ├── ReentrancyGuard.sol │ ├── SignatureChecker.sol │ ├── StableMath.sol │ └── TransferHelper.sol ├── master │ ├── FeeRegistry.sol │ ├── ForwarderRegistry.sol │ ├── SyncSwapFeeManager.sol │ ├── SyncSwapFeeRecipient.sol │ └── SyncSwapPoolMaster.sol ├── pool │ ├── BasePoolFactory.sol │ ├── classic │ │ ├── SyncSwapClassicPool.sol │ │ └── SyncSwapClassicPoolFactory.sol │ └── stable │ │ ├── SyncSwapStablePool.sol │ │ └── SyncSwapStablePoolFactory.sol ├── test │ ├── DeflatingERC20.sol │ ├── RouterEventEmitter.sol │ ├── TestERC20.sol │ ├── TestSyncSwapLPToken.sol │ └── TestWETH9.sol └── vault │ ├── SyncSwapVault.sol │ └── VaultFlashLoans.sol ├── deploy-utils └── helper.ts ├── deploy ├── deployFactoryMainnet.ts ├── deployFeeManagerMainnet.ts ├── deployFeeManagerTestnet.ts ├── deployFeeRecipientMainnet.ts ├── deployFeeRecipientTestnet.ts ├── deployFeeRegistryMainnet.ts ├── deployForwarderRegistry.ts ├── deployMainnet.ts ├── deployPolygonTestnet.ts ├── deployPoolMasterMainnet.ts ├── deployScrollTestnet.ts ├── deployTemp.ts ├── deployTestnet.ts └── deployVaultMainnet.ts ├── hardhat.config.ts ├── nethereum-gen.settings ├── package.json ├── router.sol ├── test ├── ClassicPool.spec.ts ├── ClassicPoolFactory.spec.ts ├── ERC20Permit2.ts ├── StablePool.spec.ts ├── StablePoolFactory.spec.ts ├── SyncSwapVault.spec.ts └── shared │ ├── constants.ts │ ├── fixtures.ts │ ├── helper.ts │ └── utilities.ts ├── tsconfig.json ├── yarn.lock ├── zksolc-linux-amd64-musl-v1.3.5 └── zksolc-linux-amd64-musl-v1.3.8 /.gitignore: -------------------------------------------------------------------------------- 1 | # node packages 2 | node_modules/ 3 | 4 | # builds 5 | artifacts/ 6 | artifacts-zk/ 7 | 8 | cache/ 9 | cache-zk/ 10 | 11 | bin/ 12 | 13 | # ide files 14 | .vscode/ 15 | 16 | # secrets 17 | .env 18 | secrets.json 19 | 20 | # logs 21 | yarn-error.log 22 | 23 | # workspace 24 | misc/ 25 | flatten/ -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "func-visibility": ["warn", { "ignoreConstructors": true }], 5 | "compiler-version": "off", 6 | "constructor-syntax": "warn" 7 | } 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SyncSwap Core Contracts 2 | 3 | [![MythXBadge](https://badgen.net/https/api.mythx.io/v1/projects/c7d96902-7ec0-4d35-b008-71da992bca06/badge/data?cache=300&icon=https://raw.githubusercontent.com/ConsenSys/mythx-github-badge/main/logo_white.svg)](https://docs.mythx.io/dashboard/github-badges) 4 | 5 | SyncSwap is a seamless and efficient decentralized exchange on the zkSync Era, featuring the future-proofing multi-pool design to allow the integration of various pool models. 6 | 7 | This repository contains the core contracts of the SyncSwap protocol. For an overview of the architecture, see the [API documentation](https://syncswap.gitbook.io/api-documentation/). 8 | 9 | ## Build 10 | To build artifacts, please make sure you have Yarn installed. 11 | ``` 12 | $ yarn 13 | $ yarn build 14 | $ yarn test 15 | ``` 16 | 17 | This repository is designed for the zkSync Era. To build artifacts for the zkSync Era run: 18 | ``` 19 | $ yarn build-zk 20 | ``` 21 | 22 | ## License 23 | 24 | Most repository files are licensed under the GNU Affero General Public License v3.0 (GPL v3). 25 | 26 | ## Resources 27 | - [zkSync Era Documentation](https://v2-docs.zksync.io/dev/) 28 | -------------------------------------------------------------------------------- /contracts/SyncSwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./interfaces/IWETH.sol"; 6 | import "./interfaces/IRouter.sol"; 7 | import "./interfaces/IStakingPool.sol"; 8 | import "./interfaces/vault/IVault.sol"; 9 | import "./interfaces/pool/IPool.sol"; 10 | import "./interfaces/pool/IBasePool.sol"; 11 | import "./interfaces/token/IERC20Permit.sol"; 12 | import "./interfaces/factory/IPoolFactory.sol"; 13 | 14 | import "./libraries/TransferHelper.sol"; 15 | 16 | import "./abstract/SelfPermit.sol"; 17 | import "./abstract/Multicall.sol"; 18 | 19 | error NotEnoughLiquidityMinted(); 20 | error TooLittleReceived(); 21 | error Expired(); 22 | 23 | /// @notice The router is a universal interface for users to access 24 | /// functions across different protocol parts in one place. 25 | /// 26 | /// It handles the allowances and transfers of tokens, and 27 | /// allows chained swaps/operations across multiple pools, with 28 | /// additional features like slippage protection and permit support. 29 | /// 30 | contract SyncSwapRouter is IRouter, SelfPermit, Multicall { 31 | 32 | struct TokenInput { 33 | address token; 34 | uint amount; 35 | } 36 | 37 | address public immutable vault; 38 | address public immutable wETH; 39 | address private constant NATIVE_ETH = address(0); 40 | 41 | mapping(address => mapping(address => bool)) public isPoolEntered; 42 | mapping(address => address[]) public enteredPools; 43 | 44 | modifier ensure(uint deadline) { 45 | // solhint-disable-next-line not-rely-on-time 46 | if (block.timestamp > deadline) { 47 | revert Expired(); 48 | } 49 | _; 50 | } 51 | 52 | constructor(address _vault, address _wETH) { 53 | vault = _vault; 54 | wETH = _wETH; 55 | } 56 | 57 | function enteredPoolsLength(address account) external view returns (uint) { 58 | return enteredPools[account].length; 59 | } 60 | 61 | // Add Liquidity 62 | function _transferFromSender(address token, address to, uint amount) private { 63 | if (token == NATIVE_ETH) { 64 | // Deposit ETH to the vault. 65 | IVault(vault).deposit{value: amount}(token, to); 66 | } else { 67 | // Transfer tokens to the vault. 68 | TransferHelper.safeTransferFrom(token, msg.sender, vault, amount); 69 | 70 | // Notify the vault to deposit. 71 | IVault(vault).deposit(token, to); 72 | } 73 | } 74 | 75 | function _transferAndAddLiquidity( 76 | address pool, 77 | TokenInput[] calldata inputs, 78 | bytes calldata data, 79 | uint minLiquidity, 80 | address callback, 81 | bytes calldata callbackData 82 | ) private returns (uint liquidity) { 83 | // Send all input tokens to the pool. 84 | uint n = inputs.length; 85 | 86 | TokenInput memory input; 87 | 88 | for (uint i; i < n; ) { 89 | input = inputs[i]; 90 | 91 | _transferFromSender(input.token, pool, input.amount); 92 | 93 | unchecked { 94 | ++i; 95 | } 96 | } 97 | 98 | liquidity = IPool(pool).mint(data, msg.sender, callback, callbackData); 99 | 100 | if (liquidity < minLiquidity) { 101 | revert NotEnoughLiquidityMinted(); 102 | } 103 | } 104 | 105 | function _markPoolEntered(address pool) private { 106 | if (!isPoolEntered[pool][msg.sender]) { 107 | isPoolEntered[pool][msg.sender] = true; 108 | enteredPools[msg.sender].push(pool); 109 | } 110 | } 111 | 112 | function addLiquidity( 113 | address pool, 114 | TokenInput[] calldata inputs, 115 | bytes calldata data, 116 | uint minLiquidity, 117 | address callback, 118 | bytes calldata callbackData 119 | ) external payable returns (uint liquidity) { 120 | liquidity = _transferAndAddLiquidity( 121 | pool, 122 | inputs, 123 | data, 124 | minLiquidity, 125 | callback, 126 | callbackData 127 | ); 128 | } 129 | 130 | function addLiquidity2( 131 | address pool, 132 | TokenInput[] calldata inputs, 133 | bytes calldata data, 134 | uint minLiquidity, 135 | address callback, 136 | bytes calldata callbackData 137 | ) external payable returns (uint liquidity) { 138 | liquidity = _transferAndAddLiquidity( 139 | pool, 140 | inputs, 141 | data, 142 | minLiquidity, 143 | callback, 144 | callbackData 145 | ); 146 | 147 | _markPoolEntered(pool); 148 | } 149 | 150 | function addLiquidityWithPermit( 151 | address pool, 152 | TokenInput[] calldata inputs, 153 | bytes calldata data, 154 | uint minLiquidity, 155 | address callback, 156 | bytes calldata callbackData, 157 | SplitPermitParams[] memory permits 158 | ) public payable returns (uint liquidity) { 159 | // Approve all tokens via permit. 160 | uint n = permits.length; 161 | 162 | SplitPermitParams memory params; 163 | 164 | for (uint i; i < n; ) { 165 | params = permits[i]; 166 | 167 | IERC20Permit(params.token).permit( 168 | msg.sender, 169 | address(this), 170 | params.approveAmount, 171 | params.deadline, 172 | params.v, 173 | params.r, 174 | params.s 175 | ); 176 | 177 | unchecked { 178 | ++i; 179 | } 180 | } 181 | 182 | liquidity = _transferAndAddLiquidity( 183 | pool, 184 | inputs, 185 | data, 186 | minLiquidity, 187 | callback, 188 | callbackData 189 | ); 190 | } 191 | 192 | function addLiquidityWithPermit2( 193 | address pool, 194 | TokenInput[] calldata inputs, 195 | bytes calldata data, 196 | uint minLiquidity, 197 | address callback, 198 | bytes calldata callbackData, 199 | SplitPermitParams[] memory permits 200 | ) public payable returns (uint liquidity) { 201 | liquidity = addLiquidityWithPermit( 202 | pool, 203 | inputs, 204 | data, 205 | minLiquidity, 206 | callback, 207 | callbackData, 208 | permits 209 | ); 210 | 211 | _markPoolEntered(pool); 212 | } 213 | 214 | // Burn Liquidity 215 | function _transferAndBurnLiquidity( 216 | address pool, 217 | uint liquidity, 218 | bytes memory data, 219 | uint[] memory minAmounts, 220 | address callback, 221 | bytes calldata callbackData 222 | ) private returns (IPool.TokenAmount[] memory amounts) { 223 | IBasePool(pool).transferFrom(msg.sender, pool, liquidity); 224 | 225 | amounts = IPool(pool).burn(data, msg.sender, callback, callbackData); 226 | 227 | uint n = amounts.length; 228 | 229 | for (uint i; i < n; ) { 230 | if (amounts[i].amount < minAmounts[i]) { 231 | revert TooLittleReceived(); 232 | } 233 | 234 | unchecked { 235 | ++i; 236 | } 237 | } 238 | } 239 | 240 | function burnLiquidity( 241 | address pool, 242 | uint liquidity, 243 | bytes calldata data, 244 | uint[] calldata minAmounts, 245 | address callback, 246 | bytes calldata callbackData 247 | ) external returns (IPool.TokenAmount[] memory amounts) { 248 | amounts = _transferAndBurnLiquidity( 249 | pool, 250 | liquidity, 251 | data, 252 | minAmounts, 253 | callback, 254 | callbackData 255 | ); 256 | } 257 | 258 | function burnLiquidityWithPermit( 259 | address pool, 260 | uint liquidity, 261 | bytes calldata data, 262 | uint[] calldata minAmounts, 263 | address callback, 264 | bytes calldata callbackData, 265 | ArrayPermitParams memory permit 266 | ) external returns (IPool.TokenAmount[] memory amounts) { 267 | // Approve liquidity via permit. 268 | IBasePool(pool).permit2( 269 | msg.sender, 270 | address(this), 271 | permit.approveAmount, 272 | permit.deadline, 273 | permit.signature 274 | ); 275 | 276 | amounts = _transferAndBurnLiquidity( 277 | pool, 278 | liquidity, 279 | data, 280 | minAmounts, 281 | callback, 282 | callbackData 283 | ); 284 | } 285 | 286 | // Burn Liquidity Single 287 | function _transferAndBurnLiquiditySingle( 288 | address pool, 289 | uint liquidity, 290 | bytes memory data, 291 | uint minAmount, 292 | address callback, 293 | bytes memory callbackData 294 | ) private returns (IPool.TokenAmount memory amountOut) { 295 | IBasePool(pool).transferFrom(msg.sender, pool, liquidity); 296 | 297 | amountOut = IPool(pool).burnSingle(data, msg.sender, callback, callbackData); 298 | 299 | if (amountOut.amount < minAmount) { 300 | revert TooLittleReceived(); 301 | } 302 | } 303 | 304 | function burnLiquiditySingle( 305 | address pool, 306 | uint liquidity, 307 | bytes memory data, 308 | uint minAmount, 309 | address callback, 310 | bytes memory callbackData 311 | ) external returns (IPool.TokenAmount memory amountOut) { 312 | amountOut = _transferAndBurnLiquiditySingle( 313 | pool, 314 | liquidity, 315 | data, 316 | minAmount, 317 | callback, 318 | callbackData 319 | ); 320 | } 321 | 322 | function burnLiquiditySingleWithPermit( 323 | address pool, 324 | uint liquidity, 325 | bytes memory data, 326 | uint minAmount, 327 | address callback, 328 | bytes memory callbackData, 329 | ArrayPermitParams calldata permit 330 | ) external returns (IPool.TokenAmount memory amountOut) { 331 | // Approve liquidity via permit. 332 | IBasePool(pool).permit2( 333 | msg.sender, 334 | address(this), 335 | permit.approveAmount, 336 | permit.deadline, 337 | permit.signature 338 | ); 339 | 340 | amountOut = _transferAndBurnLiquiditySingle( 341 | pool, 342 | liquidity, 343 | data, 344 | minAmount, 345 | callback, 346 | callbackData 347 | ); 348 | } 349 | 350 | // Swap 351 | function _swap( 352 | SwapPath[] memory paths, 353 | uint amountOutMin 354 | ) private returns (IPool.TokenAmount memory amountOut) { 355 | uint pathsLength = paths.length; 356 | 357 | SwapPath memory path; 358 | SwapStep memory step; 359 | IPool.TokenAmount memory tokenAmount; 360 | uint stepsLength; 361 | uint j; 362 | 363 | for (uint i; i < pathsLength; ) { 364 | path = paths[i]; 365 | 366 | // Prefund the first step. 367 | step = path.steps[0]; 368 | _transferFromSender(path.tokenIn, step.pool, path.amountIn); 369 | 370 | // Cache steps length. 371 | stepsLength = path.steps.length; 372 | 373 | for (j = 0; j < stepsLength; ) { 374 | if (j == stepsLength - 1) { 375 | // Accumulate output amount at the last step. 376 | tokenAmount = IBasePool(step.pool).swap( 377 | step.data, msg.sender, step.callback, step.callbackData 378 | ); 379 | 380 | amountOut.token = tokenAmount.token; 381 | amountOut.amount += tokenAmount.amount; 382 | 383 | break; 384 | } else { 385 | // Swap and send tokens to the next step. 386 | IBasePool(step.pool).swap(step.data, msg.sender, step.callback, step.callbackData); 387 | 388 | // Cache the next step. 389 | step = path.steps[j + 1]; 390 | } 391 | 392 | unchecked { 393 | ++j; 394 | } 395 | } 396 | 397 | unchecked { 398 | ++i; 399 | } 400 | } 401 | 402 | if (amountOut.amount < amountOutMin) { 403 | revert TooLittleReceived(); 404 | } 405 | } 406 | 407 | function swap( 408 | SwapPath[] memory paths, 409 | uint amountOutMin, 410 | uint deadline 411 | ) external payable ensure(deadline) returns (IPool.TokenAmount memory amountOut) { 412 | amountOut = _swap( 413 | paths, 414 | amountOutMin 415 | ); 416 | } 417 | 418 | function swapWithPermit( 419 | SwapPath[] memory paths, 420 | uint amountOutMin, 421 | uint deadline, 422 | SplitPermitParams calldata permit 423 | ) external payable ensure(deadline) returns (IPool.TokenAmount memory amountOut) { 424 | // Approve input tokens via permit. 425 | IERC20Permit(permit.token).permit( 426 | msg.sender, 427 | address(this), 428 | permit.approveAmount, 429 | permit.deadline, 430 | permit.v, 431 | permit.r, 432 | permit.s 433 | ); 434 | 435 | amountOut = _swap( 436 | paths, 437 | amountOutMin 438 | ); 439 | } 440 | 441 | /// @notice Wrapper function to allow pool deployment to be batched. 442 | function createPool(address _factory, bytes calldata data) external payable returns (address) { 443 | return IPoolFactory(_factory).createPool(data); 444 | } 445 | 446 | function stake(address stakingPool, address token, uint amount, address onBehalf) external { 447 | TransferHelper.safeTransferFrom(token, msg.sender, address(this), amount); 448 | 449 | if (IERC20(token).allowance(address(this), stakingPool) < amount) { 450 | TransferHelper.safeApprove(token, stakingPool, type(uint).max); 451 | } 452 | 453 | IStakingPool(stakingPool).stake(amount, onBehalf); 454 | } 455 | } -------------------------------------------------------------------------------- /contracts/abstract/Multicall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | /// @notice Helper utility that enables calling multiple local methods in a single call. 6 | /// @author Modified from Uniswap (https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/Multicall.sol) 7 | /// License-Identifier: GPL-2.0-or-later 8 | abstract contract Multicall { 9 | function multicall(bytes[] calldata data) public payable returns (bytes[] memory results) { 10 | results = new bytes[](data.length); 11 | 12 | for (uint i; i < data.length;) { 13 | (bool success, bytes memory result) = address(this).delegatecall(data[i]); 14 | 15 | if (!success) { 16 | // Next 5 lines from https://ethereum.stackexchange.com/a/83577 17 | if (result.length < 68) revert(); 18 | assembly { 19 | result := add(result, 0x04) 20 | } 21 | revert(abi.decode(result, (string))); 22 | } 23 | 24 | results[i] = result; 25 | 26 | // cannot realistically overflow on human timescales 27 | unchecked { 28 | ++i; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /contracts/abstract/SelfPermit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | import "../interfaces/token/IERC20Permit2.sol"; 6 | import "../interfaces/token/IERC20PermitAllowed.sol"; 7 | 8 | abstract contract SelfPermit { 9 | function selfPermit( 10 | address token, 11 | uint value, 12 | uint deadline, 13 | uint8 v, 14 | bytes32 r, 15 | bytes32 s 16 | ) public payable { 17 | IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s); 18 | } 19 | 20 | function selfPermitIfNecessary( 21 | address token, 22 | uint value, 23 | uint deadline, 24 | uint8 v, 25 | bytes32 r, 26 | bytes32 s 27 | ) external payable { 28 | if (IERC20(token).allowance(msg.sender, address(this)) < value) { 29 | selfPermit(token, value, deadline, v, r, s); 30 | } 31 | } 32 | 33 | function selfPermitAllowed( 34 | address token, 35 | uint256 nonce, 36 | uint256 expiry, 37 | uint8 v, 38 | bytes32 r, 39 | bytes32 s 40 | ) public payable { 41 | IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); 42 | } 43 | 44 | function selfPermitAllowedIfNecessary( 45 | address token, 46 | uint256 nonce, 47 | uint256 expiry, 48 | uint8 v, 49 | bytes32 r, 50 | bytes32 s 51 | ) external payable { 52 | if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max) { 53 | selfPermitAllowed(token, nonce, expiry, v, r, s); 54 | } 55 | } 56 | 57 | function selfPermit2( 58 | address token, 59 | uint value, 60 | uint deadline, 61 | bytes calldata signature 62 | ) public payable { 63 | IERC20Permit2(token).permit2(msg.sender, address(this), value, deadline, signature); 64 | } 65 | 66 | function selfPermit2IfNecessary( 67 | address token, 68 | uint value, 69 | uint deadline, 70 | bytes calldata signature 71 | ) external payable { 72 | if (IERC20(token).allowance(msg.sender, address(this)) < value) { 73 | selfPermit2(token, value, deadline, signature); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /contracts/interfaces/ICallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | /// @dev The callback interface for SyncSwap base pool operations. 6 | /// Note additional checks will be required for some callbacks, see below for more information. 7 | /// Visit the documentation https://syncswap.gitbook.io/api-documentation/ for more details. 8 | interface ICallback { 9 | 10 | struct BaseMintCallbackParams { 11 | address sender; 12 | address to; 13 | uint reserve0; 14 | uint reserve1; 15 | uint balance0; 16 | uint balance1; 17 | uint amount0; 18 | uint amount1; 19 | uint fee0; 20 | uint fee1; 21 | uint newInvariant; 22 | uint oldInvariant; 23 | uint totalSupply; 24 | uint liquidity; 25 | uint24 swapFee; 26 | bytes callbackData; 27 | } 28 | function syncSwapBaseMintCallback(BaseMintCallbackParams calldata params) external; 29 | 30 | struct BaseBurnCallbackParams { 31 | address sender; 32 | address to; 33 | uint balance0; 34 | uint balance1; 35 | uint liquidity; 36 | uint totalSupply; 37 | uint amount0; 38 | uint amount1; 39 | uint8 withdrawMode; 40 | bytes callbackData; 41 | } 42 | function syncSwapBaseBurnCallback(BaseBurnCallbackParams calldata params) external; 43 | 44 | struct BaseBurnSingleCallbackParams { 45 | address sender; 46 | address to; 47 | address tokenIn; 48 | address tokenOut; 49 | uint balance0; 50 | uint balance1; 51 | uint liquidity; 52 | uint totalSupply; 53 | uint amount0; 54 | uint amount1; 55 | uint amountOut; 56 | uint amountSwapped; 57 | uint feeIn; 58 | uint24 swapFee; 59 | uint8 withdrawMode; 60 | bytes callbackData; 61 | } 62 | /// @dev Note the `tokenOut` parameter can be decided by the caller, and the correctness is not guaranteed. 63 | /// Additional checks MUST be performed in callback to ensure the `tokenOut` is one of the pools tokens if the sender 64 | /// is not a trusted source to avoid potential issues. 65 | function syncSwapBaseBurnSingleCallback(BaseBurnSingleCallbackParams calldata params) external; 66 | 67 | struct BaseSwapCallbackParams { 68 | address sender; 69 | address to; 70 | address tokenIn; 71 | address tokenOut; 72 | uint reserve0; 73 | uint reserve1; 74 | uint balance0; 75 | uint balance1; 76 | uint amountIn; 77 | uint amountOut; 78 | uint feeIn; 79 | uint24 swapFee; 80 | uint8 withdrawMode; 81 | bytes callbackData; 82 | } 83 | /// @dev Note the `tokenIn` parameter can be decided by the caller, and the correctness is not guaranteed. 84 | /// Additional checks MUST be performed in callback to ensure the `tokenIn` is one of the pools tokens if the sender 85 | /// is not a trusted source to avoid potential issues. 86 | function syncSwapBaseSwapCallback(BaseSwapCallbackParams calldata params) external; 87 | } -------------------------------------------------------------------------------- /contracts/interfaces/IRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IRouter { 6 | struct SwapStep { 7 | address pool; 8 | bytes data; 9 | address callback; 10 | bytes callbackData; 11 | } 12 | 13 | struct SwapPath { 14 | SwapStep[] steps; 15 | address tokenIn; 16 | uint amountIn; 17 | } 18 | 19 | struct SplitPermitParams { 20 | address token; 21 | uint approveAmount; 22 | uint deadline; 23 | uint8 v; 24 | bytes32 r; 25 | bytes32 s; 26 | } 27 | 28 | struct ArrayPermitParams { 29 | uint approveAmount; 30 | uint deadline; 31 | bytes signature; 32 | } 33 | } -------------------------------------------------------------------------------- /contracts/interfaces/IStakingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IStakingPool { 6 | function stake(uint amount, address onBehalf) external; 7 | } -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IWETH { 6 | function deposit() external payable; 7 | function transfer(address to, uint value) external returns (bool); 8 | function transferFrom(address from, address to, uint value) external returns (bool); 9 | function withdraw(uint) external; 10 | } -------------------------------------------------------------------------------- /contracts/interfaces/factory/IBasePoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IPoolFactory.sol"; 6 | 7 | interface IBasePoolFactory is IPoolFactory { 8 | event PoolCreated( 9 | address indexed token0, 10 | address indexed token1, 11 | address pool 12 | ); 13 | 14 | function getPool(address tokenA, address tokenB) external view returns (address pool); 15 | 16 | function getSwapFee( 17 | address pool, 18 | address sender, 19 | address tokenIn, 20 | address tokenOut, 21 | bytes calldata data 22 | ) external view returns (uint24 swapFee); 23 | } -------------------------------------------------------------------------------- /contracts/interfaces/factory/IPoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IPoolFactory { 6 | function master() external view returns (address); 7 | 8 | function getDeployData() external view returns (bytes memory); 9 | 10 | function createPool(bytes calldata data) external returns (address pool); 11 | } -------------------------------------------------------------------------------- /contracts/interfaces/master/IFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | /// @notice The manager contract to control fees. 6 | /// Management functions are omitted. 7 | interface IFeeManager { 8 | function getSwapFee( 9 | address pool, 10 | address sender, 11 | address tokenIn, 12 | address tokenOut, 13 | bytes calldata data) external view returns (uint24); 14 | function getProtocolFee(address pool) external view returns (uint24); 15 | function getFeeRecipient() external view returns (address); 16 | } -------------------------------------------------------------------------------- /contracts/interfaces/master/IFeeRecipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IFeeRecipient { 6 | /// @dev Notifies the fee recipient after sent fees. 7 | function notifyFees( 8 | uint16 feeType, 9 | address token, 10 | uint amount, 11 | uint feeRate, 12 | bytes calldata data 13 | ) external; 14 | } -------------------------------------------------------------------------------- /contracts/interfaces/master/IFeeRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IFeeRegistry { 6 | function isFeeSender(address sender) external view returns (bool); 7 | } -------------------------------------------------------------------------------- /contracts/interfaces/master/IForwarderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IForwarderRegistry { 6 | function isForwarder(address forwarder) external view returns (bool); 7 | } -------------------------------------------------------------------------------- /contracts/interfaces/master/IPoolMaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IFeeManager.sol"; 6 | import "./IForwarderRegistry.sol"; 7 | 8 | /// @dev The master contract to create pools and manage whitelisted factories. 9 | /// Inheriting the fee manager interface to support fee queries. 10 | interface IPoolMaster is IFeeManager, IForwarderRegistry { 11 | event SetFactoryWhitelisted(address indexed factory, bool whitelisted); 12 | 13 | event RegisterPool( 14 | address indexed factory, 15 | address indexed pool, 16 | uint16 indexed poolType, 17 | bytes data 18 | ); 19 | 20 | event UpdateForwarderRegistry(address indexed newForwarderRegistry); 21 | 22 | event UpdateFeeManager(address indexed newFeeManager); 23 | 24 | function vault() external view returns (address); 25 | 26 | function feeManager() external view returns (address); 27 | 28 | function pools(uint) external view returns (address); 29 | 30 | function poolsLength() external view returns (uint); 31 | 32 | // Forwarder Registry 33 | function setForwarderRegistry(address) external; 34 | 35 | // Fees 36 | function setFeeManager(address) external; 37 | 38 | // Factories 39 | function isFactoryWhitelisted(address) external view returns (bool); 40 | 41 | function setFactoryWhitelisted(address factory, bool whitelisted) external; 42 | 43 | // Pools 44 | function isPool(address) external view returns (bool); 45 | 46 | function getPool(bytes32) external view returns (address); 47 | 48 | function createPool(address factory, bytes calldata data) external returns (address pool); 49 | 50 | function registerPool(address pool, uint16 poolType, bytes calldata data) external; 51 | } -------------------------------------------------------------------------------- /contracts/interfaces/pool/IBasePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IPool.sol"; 6 | import "../token/IERC20Permit2.sol"; 7 | 8 | interface IBasePool is IPool, IERC20Permit2 { 9 | function token0() external view returns (address); 10 | function token1() external view returns (address); 11 | 12 | function reserve0() external view returns (uint); 13 | function reserve1() external view returns (uint); 14 | function invariantLast() external view returns (uint); 15 | 16 | function getReserves() external view returns (uint, uint); 17 | function getAmountOut(address tokenIn, uint amountIn, address sender) external view returns (uint amountOut); 18 | function getAmountIn(address tokenOut, uint amountOut, address sender) external view returns (uint amountIn); 19 | 20 | event Mint( 21 | address indexed sender, 22 | uint amount0, 23 | uint amount1, 24 | uint liquidity, 25 | address indexed to 26 | ); 27 | 28 | event Burn( 29 | address indexed sender, 30 | uint amount0, 31 | uint amount1, 32 | uint liquidity, 33 | address indexed to 34 | ); 35 | 36 | event Swap( 37 | address indexed sender, 38 | uint amount0In, 39 | uint amount1In, 40 | uint amount0Out, 41 | uint amount1Out, 42 | address indexed to 43 | ); 44 | 45 | event Sync( 46 | uint reserve0, 47 | uint reserve1 48 | ); 49 | } -------------------------------------------------------------------------------- /contracts/interfaces/pool/IClassicPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IBasePool.sol"; 6 | 7 | interface IClassicPool is IBasePool { 8 | } -------------------------------------------------------------------------------- /contracts/interfaces/pool/IPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IPool { 6 | struct TokenAmount { 7 | address token; 8 | uint amount; 9 | } 10 | 11 | /// @dev Returns the address of pool master. 12 | function master() external view returns (address); 13 | 14 | /// @dev Returns the vault. 15 | function vault() external view returns (address); 16 | 17 | /// @dev Returns the pool type. 18 | function poolType() external view returns (uint16); 19 | 20 | /// @dev Returns the assets of the pool. 21 | function getAssets() external view returns (address[] memory assets); 22 | 23 | /// @dev Returns the swap fee of the pool. 24 | function getSwapFee(address sender, address tokenIn, address tokenOut, bytes calldata data) external view returns (uint24 swapFee); 25 | 26 | /// @dev Returns the protocol fee of the pool. 27 | function getProtocolFee() external view returns (uint24 protocolFee); 28 | 29 | /// @dev Mints liquidity. 30 | function mint( 31 | bytes calldata data, 32 | address sender, 33 | address callback, 34 | bytes calldata callbackData 35 | ) external returns (uint liquidity); 36 | 37 | /// @dev Burns liquidity. 38 | function burn( 39 | bytes calldata data, 40 | address sender, 41 | address callback, 42 | bytes calldata callbackData 43 | ) external returns (TokenAmount[] memory tokenAmounts); 44 | 45 | /// @dev Burns liquidity with single output token. 46 | function burnSingle( 47 | bytes calldata data, 48 | address sender, 49 | address callback, 50 | bytes calldata callbackData 51 | ) external returns (TokenAmount memory tokenAmount); 52 | 53 | /// @dev Swaps between tokens. 54 | function swap( 55 | bytes calldata data, 56 | address sender, 57 | address callback, 58 | bytes calldata callbackData 59 | ) external returns (TokenAmount memory tokenAmount); 60 | } -------------------------------------------------------------------------------- /contracts/interfaces/pool/IStablePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IBasePool.sol"; 6 | 7 | interface IStablePool is IBasePool { 8 | function token0PrecisionMultiplier() external view returns (uint); 9 | function token1PrecisionMultiplier() external view returns (uint); 10 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC165.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IERC165 { 6 | /// @notice Query if a contract implements an interface 7 | /// @param interfaceID The interface identifier, as specified in ERC-165 8 | /// @dev Interface identification is specified in ERC-165. This function 9 | /// uses less than 30,000 gas. 10 | /// @return `true` if the contract implements `interfaceID` and 11 | /// `interfaceID` is not 0xffffffff, `false` otherwise 12 | function supportsInterface(bytes4 interfaceID) external view returns (bool); 13 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IERC20Base.sol"; 6 | 7 | interface IERC20 is IERC20Base { 8 | function name() external view returns (string memory); 9 | function symbol() external view returns (string memory); 10 | function decimals() external view returns (uint8); 11 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC20Base.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IERC20Base { 6 | function totalSupply() external view returns (uint); 7 | function balanceOf(address owner) external view returns (uint); 8 | function allowance(address owner, address spender) external view returns (uint); 9 | 10 | function approve(address spender, uint amount) external returns (bool); 11 | function transfer(address to, uint amount) external returns (bool); 12 | function transferFrom(address from, address to, uint amount) external returns (bool); 13 | 14 | event Approval(address indexed owner, address indexed spender, uint amount); 15 | event Transfer(address indexed from, address indexed to, uint amount); 16 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IERC20.sol"; 6 | 7 | interface IERC20Permit is IERC20 { 8 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; 9 | function nonces(address owner) external view returns (uint); 10 | function DOMAIN_SEPARATOR() external view returns (bytes32); 11 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC20Permit2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IERC20Permit.sol"; 6 | 7 | interface IERC20Permit2 is IERC20Permit { 8 | function permit2(address owner, address spender, uint amount, uint deadline, bytes calldata signature) external; 9 | } -------------------------------------------------------------------------------- /contracts/interfaces/token/IERC20PermitAllowed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | /// @title Interface for permit 6 | /// @notice Interface used by DAI/CHAI for permit 7 | interface IERC20PermitAllowed { 8 | /// @notice Approve the spender to spend some tokens via the holder signature 9 | /// @dev This is the permit interface used by DAI and CHAI 10 | /// @param holder The address of the token holder, the token owner 11 | /// @param spender The address of the token spender 12 | /// @param nonce The holder's nonce, increases at each call to permit 13 | /// @param expiry The timestamp at which the permit is no longer valid 14 | /// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0 15 | /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` 16 | /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` 17 | /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` 18 | function permit( 19 | address holder, 20 | address spender, 21 | uint256 nonce, 22 | uint256 expiry, 23 | bool allowed, 24 | uint8 v, 25 | bytes32 r, 26 | bytes32 s 27 | ) external; 28 | } -------------------------------------------------------------------------------- /contracts/interfaces/vault/IERC3156FlashBorrower.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | interface IERC3156FlashBorrower { 6 | /** 7 | * @dev Receive a flash loan. 8 | * @param initiator The initiator of the loan. 9 | * @param token The loan currency. 10 | * @param amount The amount of tokens lent. 11 | * @param fee The additional amount of tokens to repay. 12 | * @param data Arbitrary data structure, intended to contain user-defined parameters. 13 | * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" 14 | */ 15 | function onFlashLoan( 16 | address initiator, 17 | address token, 18 | uint256 amount, 19 | uint256 fee, 20 | bytes calldata data 21 | ) external returns (bytes32); 22 | } -------------------------------------------------------------------------------- /contracts/interfaces/vault/IERC3156FlashLender.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IERC3156FlashBorrower.sol"; 6 | 7 | interface IERC3156FlashLender { 8 | /** 9 | * @dev The amount of currency available to be lent. 10 | * @param token The loan currency. 11 | * @return The amount of `token` that can be borrowed. 12 | */ 13 | function maxFlashLoan( 14 | address token 15 | ) external view returns (uint256); 16 | 17 | /** 18 | * @dev The fee to be charged for a given loan. 19 | * @param token The loan currency. 20 | * @param amount The amount of tokens lent. 21 | * @return The amount of `token` to be charged for the loan, on top of the returned principal. 22 | */ 23 | function flashFee( 24 | address token, 25 | uint256 amount 26 | ) external view returns (uint256); 27 | 28 | /** 29 | * @dev Initiate a flash loan. 30 | * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. 31 | * @param token The loan currency. 32 | * @param amount The amount of tokens lent. 33 | * @param data Arbitrary data structure, intended to contain user-defined parameters. 34 | */ 35 | function flashLoan( 36 | IERC3156FlashBorrower receiver, 37 | address token, 38 | uint256 amount, 39 | bytes calldata data 40 | ) external returns (bool); 41 | } -------------------------------------------------------------------------------- /contracts/interfaces/vault/IFlashLoan.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IFlashLoanRecipient.sol"; 6 | import "./IERC3156FlashLender.sol"; 7 | 8 | interface IFlashLoan is IERC3156FlashLender { 9 | function flashLoanFeePercentage() external view returns (uint); 10 | 11 | /** 12 | * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, 13 | * and then reverting unless the tokens plus a proportional protocol fee have been returned. 14 | * 15 | * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount 16 | * for each token contract. `tokens` must be sorted in ascending order. 17 | * 18 | * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the 19 | * `receiveFlashLoan` call. 20 | * 21 | * Emits `FlashLoan` events. 22 | */ 23 | function flashLoanMultiple( 24 | IFlashLoanRecipient recipient, 25 | address[] memory tokens, 26 | uint[] memory amounts, 27 | bytes memory userData 28 | ) external; 29 | 30 | /** 31 | * @dev Emitted for each individual flash loan performed by `flashLoan`. 32 | */ 33 | event FlashLoan(address indexed recipient, address indexed token, uint amount, uint feeAmount); 34 | } -------------------------------------------------------------------------------- /contracts/interfaces/vault/IFlashLoanRecipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity >=0.7.0 <0.9.0; 4 | 5 | // Inspired by Aave Protocol's IFlashLoanReceiver. 6 | 7 | interface IFlashLoanRecipient { 8 | /** 9 | * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. 10 | * 11 | * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this 12 | * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the 13 | * Vault, or else the entire flash loan will revert. 14 | * 15 | * `userData` is the same value passed in the `IVault.flashLoan` call. 16 | */ 17 | function receiveFlashLoan( 18 | address[] memory tokens, 19 | uint[] memory amounts, 20 | uint[] memory feeAmounts, 21 | bytes memory userData 22 | ) external; 23 | } -------------------------------------------------------------------------------- /contracts/interfaces/vault/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity >=0.5.0; 4 | 5 | import "./IFlashLoan.sol"; 6 | 7 | interface IVault is IFlashLoan { 8 | function wETH() external view returns (address); 9 | 10 | function reserves(address token) external view returns (uint reserve); 11 | 12 | function balanceOf(address token, address owner) external view returns (uint balance); 13 | 14 | function deposit(address token, address to) external payable returns (uint amount); 15 | 16 | function depositETH(address to) external payable returns (uint amount); 17 | 18 | function transferAndDeposit(address token, address to, uint amount) external payable returns (uint); 19 | 20 | function transfer(address token, address to, uint amount) external; 21 | 22 | function withdraw(address token, address to, uint amount) external; 23 | 24 | function withdrawAlternative(address token, address to, uint amount, uint8 mode) external; 25 | 26 | function withdrawETH(address to, uint amount) external; 27 | } -------------------------------------------------------------------------------- /contracts/libraries/ECDSA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. 7 | * 8 | * These functions can be used to verify that a message was signed by the holder 9 | * of the private keys of a given address. 10 | * 11 | * Based on OpenZeppelin's ECDSA library. 12 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/561d1061fc568f04c7a65853538e834a889751e8/contracts/utils/cryptography/ECDSA.sol 13 | */ 14 | library ECDSA { 15 | 16 | /** 17 | * @dev Returns the address that signed a hashed message (`hash`) with 18 | * `signature` or error string. This address can then be used for verification purposes. 19 | * 20 | * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: 21 | * this function rejects them by requiring the `s` value to be in the lower 22 | * half order. 23 | * 24 | * IMPORTANT: `hash` _must_ be the result of a hash operation for the 25 | * verification to be secure: it is possible to craft signatures that 26 | * recover to arbitrary addresses for non-hashed data. A safe way to ensure 27 | * this is by receiving a hash of the original message (which may otherwise 28 | * be too long), and then calling {toEthSignedMessageHash} on it. 29 | * 30 | * Documentation for signature generation: 31 | * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] 32 | * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] 33 | */ 34 | function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { 35 | // Check the signature length 36 | if (signature.length != 65) { 37 | return address(0); 38 | } 39 | 40 | // Divide the signature in r, s and v variables 41 | bytes32 r; 42 | bytes32 s; 43 | uint8 v; 44 | 45 | // ecrecover takes the signature parameters, and the only way to get them 46 | // currently is to use assembly. 47 | /// @solidity memory-safe-assembly 48 | // solhint-disable-next-line no-inline-assembly 49 | assembly { 50 | r := mload(add(signature, 0x20)) 51 | s := mload(add(signature, 0x40)) 52 | v := byte(0, mload(add(signature, 0x60))) 53 | } 54 | 55 | // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature 56 | // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines 57 | // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most 58 | // signatures from current libraries generate a unique signature with an s-value in the lower half order. 59 | // 60 | // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value 61 | // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or 62 | // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept 63 | // these malleable signatures as well. 64 | if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { 65 | return address(0); 66 | } 67 | 68 | return ecrecover(hash, v, r, s); 69 | } 70 | } -------------------------------------------------------------------------------- /contracts/libraries/ERC20Permit2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/token/IERC165.sol"; 6 | import "../interfaces/token/IERC20Permit2.sol"; 7 | 8 | import "./SignatureChecker.sol"; 9 | 10 | error Expired(); 11 | error InvalidSignature(); 12 | 13 | /** 14 | * @dev A simple ERC20 implementation for pool's liquidity token, supports permit by both ECDSA signatures from 15 | * externally owned accounts (EOAs) as well as ERC1271 signatures from smart contract wallets like Argent. 16 | * 17 | * Based on Solmate's ERC20. 18 | * https://github.com/transmissions11/solmate/blob/bff24e835192470ed38bf15dbed6084c2d723ace/src/tokens/ERC20.sol 19 | */ 20 | contract ERC20Permit2 is IERC165, IERC20Permit2 { 21 | uint8 public immutable override decimals = 18; 22 | 23 | uint public override totalSupply; 24 | mapping(address => uint) public override balanceOf; 25 | mapping(address => mapping(address => uint)) public override allowance; 26 | 27 | bytes32 private constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") 28 | mapping(address => uint) public override nonces; 29 | 30 | // These members are actually immutable as 31 | // `_initialize` will only indent to be called once. 32 | string public override name; 33 | string public override symbol; 34 | uint private INITIAL_CHAIN_ID; 35 | bytes32 private INITIAL_DOMAIN_SEPARATOR; 36 | 37 | function _initialize(string memory _name, string memory _symbol) internal { 38 | name = _name; 39 | symbol = _symbol; 40 | 41 | INITIAL_CHAIN_ID = block.chainid; 42 | INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); 43 | } 44 | 45 | function supportsInterface(bytes4 interfaceID) external pure override returns (bool) { 46 | return 47 | interfaceID == this.supportsInterface.selector || // ERC-165 48 | interfaceID == this.permit.selector || // ERC-2612 49 | interfaceID == this.permit2.selector; // Permit2 50 | } 51 | 52 | function DOMAIN_SEPARATOR() public view override returns (bytes32) { 53 | return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator(); 54 | } 55 | 56 | function _computeDomainSeparator() private view returns (bytes32) { 57 | return keccak256( 58 | abi.encode( 59 | // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") 60 | 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, 61 | keccak256(bytes(name)), 62 | // keccak256(bytes("1")) 63 | 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6, 64 | block.chainid, 65 | address(this) 66 | ) 67 | ); 68 | } 69 | 70 | function _approve(address _owner, address _spender, uint _amount) private { 71 | allowance[_owner][_spender] = _amount; 72 | emit Approval(_owner, _spender, _amount); 73 | } 74 | 75 | function approve(address _spender, uint _amount) public override returns (bool) { 76 | _approve(msg.sender, _spender, _amount); 77 | return true; 78 | } 79 | 80 | function transfer(address _to, uint _amount) public override returns (bool) { 81 | balanceOf[msg.sender] -= _amount; 82 | 83 | // Cannot overflow because the sum of all user balances can't exceed the max uint256 value. 84 | unchecked { 85 | balanceOf[_to] += _amount; 86 | } 87 | 88 | emit Transfer(msg.sender, _to, _amount); 89 | return true; 90 | } 91 | 92 | function transferFrom(address _from, address _to, uint _amount) public override returns (bool) { 93 | uint256 _allowed = allowance[_from][msg.sender]; // Saves gas for limited approvals. 94 | if (_allowed != type(uint).max) { 95 | allowance[_from][msg.sender] = _allowed - _amount; 96 | } 97 | 98 | balanceOf[_from] -= _amount; 99 | // Cannot overflow because the sum of all user balances can't exceed the max uint256 value. 100 | unchecked { 101 | balanceOf[_to] += _amount; 102 | } 103 | 104 | emit Transfer(_from, _to, _amount); 105 | return true; 106 | } 107 | 108 | function _mint(address _to, uint _amount) internal { 109 | totalSupply += _amount; 110 | 111 | // Cannot overflow because the sum of all user balances can't exceed the max uint256 value. 112 | unchecked { 113 | balanceOf[_to] += _amount; 114 | } 115 | 116 | emit Transfer(address(0), _to, _amount); 117 | } 118 | 119 | function _burn(address _from, uint _amount) internal { 120 | balanceOf[_from] -= _amount; 121 | 122 | // Cannot underflow because a user's balance will never be larger than the total supply. 123 | unchecked { 124 | totalSupply -= _amount; 125 | } 126 | 127 | emit Transfer(_from, address(0), _amount); 128 | } 129 | 130 | modifier ensures(uint _deadline) { 131 | // solhint-disable-next-line not-rely-on-time 132 | if (block.timestamp > _deadline) { 133 | revert Expired(); 134 | } 135 | _; 136 | } 137 | 138 | function _permitHash( 139 | address _owner, 140 | address _spender, 141 | uint _amount, 142 | uint _deadline 143 | ) private returns (bytes32) { 144 | return keccak256( 145 | abi.encodePacked( 146 | "\x19\x01", 147 | DOMAIN_SEPARATOR(), 148 | keccak256(abi.encode(PERMIT_TYPEHASH, _owner, _spender, _amount, nonces[_owner]++, _deadline)) 149 | ) 150 | ); 151 | } 152 | 153 | function permit( 154 | address _owner, 155 | address _spender, 156 | uint _amount, 157 | uint _deadline, 158 | uint8 _v, 159 | bytes32 _r, 160 | bytes32 _s 161 | ) public override ensures(_deadline) { 162 | bytes32 _hash = _permitHash(_owner, _spender, _amount, _deadline); 163 | address _recoveredAddress = ecrecover(_hash, _v, _r, _s); 164 | 165 | if (_recoveredAddress != _owner) { 166 | revert InvalidSignature(); 167 | } 168 | if (_recoveredAddress == address(0)) { 169 | revert InvalidSignature(); 170 | } 171 | 172 | _approve(_owner, _spender, _amount); 173 | } 174 | 175 | function permit2( 176 | address _owner, 177 | address _spender, 178 | uint _amount, 179 | uint _deadline, 180 | bytes calldata _signature 181 | ) public override ensures(_deadline) { 182 | bytes32 _hash = _permitHash(_owner, _spender, _amount, _deadline); 183 | 184 | if (!SignatureChecker.isValidSignatureNow(_owner, _hash, _signature)) { 185 | revert InvalidSignature(); 186 | } 187 | 188 | _approve(_owner, _spender, _amount); 189 | } 190 | } -------------------------------------------------------------------------------- /contracts/libraries/Math.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /// @dev Math functions. 6 | /// @dev Modified from Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) 7 | library Math { 8 | 9 | /// @notice Compares a and b and returns 'true' if the difference between a and b 10 | /// is less than 1 or equal to each other. 11 | /// @param a uint256 to compare with. 12 | /// @param b uint256 to compare with. 13 | function within1(uint256 a, uint256 b) internal pure returns (bool) { 14 | unchecked { 15 | if (a > b) { 16 | return a - b <= 1; 17 | } 18 | return b - a <= 1; 19 | } 20 | } 21 | 22 | /// @dev Returns the square root of `x`. 23 | function sqrt(uint256 x) internal pure returns (uint256 z) { 24 | /// @solidity memory-safe-assembly 25 | assembly { 26 | // `floor(sqrt(2**15)) = 181`. `sqrt(2**15) - 181 = 2.84`. 27 | z := 181 // The "correct" value is 1, but this saves a multiplication later. 28 | 29 | // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad 30 | // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. 31 | 32 | // Let `y = x / 2**r`. 33 | // We check `y >= 2**(k + 8)` but shift right by `k` bits 34 | // each branch to ensure that if `x >= 256`, then `y >= 256`. 35 | let r := shl(7, lt(0xffffffffffffffffffffffffffffffffff, x)) 36 | r := or(r, shl(6, lt(0xffffffffffffffffff, shr(r, x)))) 37 | r := or(r, shl(5, lt(0xffffffffff, shr(r, x)))) 38 | r := or(r, shl(4, lt(0xffffff, shr(r, x)))) 39 | z := shl(shr(1, r), z) 40 | 41 | // Goal was to get `z*z*y` within a small factor of `x`. More iterations could 42 | // get y in a tighter range. Currently, we will have y in `[256, 256*(2**16))`. 43 | // We ensured `y >= 256` so that the relative difference between `y` and `y+1` is small. 44 | // That's not possible if `x < 256` but we can just verify those cases exhaustively. 45 | 46 | // Now, `z*z*y <= x < z*z*(y+1)`, and `y <= 2**(16+8)`, and either `y >= 256`, or `x < 256`. 47 | // Correctness can be checked exhaustively for `x < 256`, so we assume `y >= 256`. 48 | // Then `z*sqrt(y)` is within `sqrt(257)/sqrt(256)` of `sqrt(x)`, or about 20bps. 49 | 50 | // For `s` in the range `[1/256, 256]`, the estimate `f(s) = (181/1024) * (s+1)` 51 | // is in the range `(1/2.84 * sqrt(s), 2.84 * sqrt(s))`, 52 | // with largest error when `s = 1` and when `s = 256` or `1/256`. 53 | 54 | // Since `y` is in `[256, 256*(2**16))`, let `a = y/65536`, so that `a` is in `[1/256, 256)`. 55 | // Then we can estimate `sqrt(y)` using 56 | // `sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2**18`. 57 | 58 | // There is no overflow risk here since `y < 2**136` after the first branch above. 59 | z := shr(18, mul(z, add(shr(r, x), 65536))) // A `mul()` is saved from starting `z` at 181. 60 | 61 | // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. 62 | z := shr(1, add(z, div(x, z))) 63 | z := shr(1, add(z, div(x, z))) 64 | z := shr(1, add(z, div(x, z))) 65 | z := shr(1, add(z, div(x, z))) 66 | z := shr(1, add(z, div(x, z))) 67 | z := shr(1, add(z, div(x, z))) 68 | z := shr(1, add(z, div(x, z))) 69 | 70 | // If `x+1` is a perfect square, the Babylonian method cycles between 71 | // `floor(sqrt(x))` and `ceil(sqrt(x))`. This statement ensures we return floor. 72 | // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division 73 | // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. 74 | // If you don't care whether the floor or ceil square root is returned, you can remove this statement. 75 | z := sub(z, lt(div(x, z), z)) 76 | } 77 | } 78 | 79 | // Mul Div 80 | 81 | /// @dev Rounded down. 82 | function mulDiv( 83 | uint256 x, 84 | uint256 y, 85 | uint256 denominator 86 | ) internal pure returns (uint256 z) { 87 | assembly { 88 | // Store x * y in z for now. 89 | z := mul(x, y) 90 | 91 | // Equivalent to require(denominator != 0 && (x == 0 || (x * y) / x == y)) 92 | if iszero(and(iszero(iszero(denominator)), or(iszero(x), eq(div(z, x), y)))) { 93 | revert(0, 0) 94 | } 95 | 96 | // Divide z by the denominator. 97 | z := div(z, denominator) 98 | } 99 | } 100 | 101 | /// @dev Rounded down. 102 | /// This function assumes that `x` is not zero, and must be checked externally. 103 | function mulDivUnsafeFirst( 104 | uint256 x, 105 | uint256 y, 106 | uint256 denominator 107 | ) internal pure returns (uint256 z) { 108 | assembly { 109 | // Store x * y in z for now. 110 | z := mul(x, y) 111 | 112 | // Equivalent to require(denominator != 0 && (x * y) / x == y) 113 | if iszero(and(iszero(iszero(denominator)), eq(div(z, x), y))) { 114 | revert(0, 0) 115 | } 116 | 117 | // Divide z by the denominator. 118 | z := div(z, denominator) 119 | } 120 | } 121 | 122 | /// @dev Rounded down. 123 | /// This function assumes that `denominator` is not zero, and must be checked externally. 124 | function mulDivUnsafeLast( 125 | uint256 x, 126 | uint256 y, 127 | uint256 denominator 128 | ) internal pure returns (uint256 z) { 129 | assembly { 130 | // Store x * y in z for now. 131 | z := mul(x, y) 132 | 133 | // Equivalent to require(x == 0 || (x * y) / x == y) 134 | if iszero(or(iszero(x), eq(div(z, x), y))) { 135 | revert(0, 0) 136 | } 137 | 138 | // Divide z by the denominator. 139 | z := div(z, denominator) 140 | } 141 | } 142 | 143 | /// @dev Rounded down. 144 | /// This function assumes that both `x` and `denominator` are not zero, and must be checked externally. 145 | function mulDivUnsafeFirstLast( 146 | uint256 x, 147 | uint256 y, 148 | uint256 denominator 149 | ) internal pure returns (uint256 z) { 150 | assembly { 151 | // Store x * y in z for now. 152 | z := mul(x, y) 153 | 154 | // Equivalent to require((x * y) / x == y) 155 | if iszero(eq(div(z, x), y)) { 156 | revert(0, 0) 157 | } 158 | 159 | // Divide z by the denominator. 160 | z := div(z, denominator) 161 | } 162 | } 163 | 164 | // Mul 165 | 166 | /// @dev Optimized safe multiplication operation for minimal gas cost. 167 | /// Equivalent to * 168 | function mul( 169 | uint256 x, 170 | uint256 y 171 | ) internal pure returns (uint256 z) { 172 | assembly { 173 | // Store x * y in z for now. 174 | z := mul(x, y) 175 | 176 | // Equivalent to require(x == 0 || (x * y) / x == y) 177 | if iszero(or(iszero(x), eq(div(z, x), y))) { 178 | revert(0, 0) 179 | } 180 | } 181 | } 182 | 183 | /// @dev Optimized unsafe multiplication operation for minimal gas cost. 184 | /// This function assumes that `x` is not zero, and must be checked externally. 185 | function mulUnsafeFirst( 186 | uint256 x, 187 | uint256 y 188 | ) internal pure returns (uint256 z) { 189 | assembly { 190 | // Store x * y in z for now. 191 | z := mul(x, y) 192 | 193 | // Equivalent to require((x * y) / x == y) 194 | if iszero(eq(div(z, x), y)) { 195 | revert(0, 0) 196 | } 197 | } 198 | } 199 | 200 | // Div 201 | 202 | /// @dev Optimized safe division operation for minimal gas cost. 203 | /// Equivalent to / 204 | function div( 205 | uint256 x, 206 | uint256 y 207 | ) internal pure returns (uint256 z) { 208 | assembly { 209 | // Store x * y in z for now. 210 | z := div(x, y) 211 | 212 | // Equivalent to require(y != 0) 213 | if iszero(y) { 214 | revert(0, 0) 215 | } 216 | } 217 | } 218 | 219 | /// @dev Optimized unsafe division operation for minimal gas cost. 220 | /// Division by 0 will not reverts and returns 0, and must be checked externally. 221 | function divUnsafeLast( 222 | uint256 x, 223 | uint256 y 224 | ) internal pure returns (uint256 z) { 225 | assembly { 226 | z := div(x, y) 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /contracts/libraries/MetadataHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | library MetadataHelper { 6 | /** 7 | * @dev Returns symbol of the token. 8 | * 9 | * @param token The address of a ERC20 token. 10 | * 11 | * Return boolean indicating the status and the symbol as string; 12 | * 13 | * NOTE: Symbol is not the standard interface and some tokens may not support it. 14 | * Calling against these tokens will not success, with an empty result. 15 | */ 16 | function getSymbol(address token) internal view returns (bool, string memory) { 17 | // bytes4(keccak256(bytes("symbol()"))) 18 | (bool success, bytes memory returndata) = token.staticcall(abi.encodeWithSelector(0x95d89b41)); 19 | if (success) { 20 | return (true, abi.decode(returndata, (string))); 21 | } else { 22 | return (false, ""); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /contracts/libraries/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Contract module which provides a basic access control mechanism, where 8 | * there is an account (an owner) that can be granted exclusive access to 9 | * specific functions. 10 | * 11 | * By default, the owner account will be the one that deploys the contract. This 12 | * can later be changed with {transferOwnership}. 13 | * 14 | * This module is used through inheritance. It will make available the modifier 15 | * `onlyOwner`, which can be applied to your functions to restrict their use to 16 | * the owner. 17 | */ 18 | abstract contract Ownable { 19 | address private _owner; 20 | 21 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 22 | 23 | /** 24 | * @dev Initializes the contract setting the deployer as the initial owner. 25 | */ 26 | constructor() { 27 | _transferOwnership(msg.sender); 28 | } 29 | 30 | /** 31 | * @dev Throws if called by any account other than the owner. 32 | */ 33 | modifier onlyOwner() { 34 | _checkOwner(); 35 | _; 36 | } 37 | 38 | /** 39 | * @dev Returns the address of the current owner. 40 | */ 41 | function owner() public view virtual returns (address) { 42 | return _owner; 43 | } 44 | 45 | /** 46 | * @dev Throws if the sender is not the owner. 47 | */ 48 | function _checkOwner() internal view virtual { 49 | require(owner() == msg.sender, "Ownable: caller is not the owner"); 50 | } 51 | 52 | /** 53 | * @dev Leaves the contract without owner. It will not be possible to call 54 | * `onlyOwner` functions. Can only be called by the current owner. 55 | * 56 | * NOTE: Renouncing ownership will leave the contract without an owner, 57 | * thereby disabling any functionality that is only available to the owner. 58 | */ 59 | function renounceOwnership() public virtual onlyOwner { 60 | _transferOwnership(address(0)); 61 | } 62 | 63 | /** 64 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 65 | * Can only be called by the current owner. 66 | */ 67 | function transferOwnership(address newOwner) public virtual onlyOwner { 68 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 69 | _transferOwnership(newOwner); 70 | } 71 | 72 | /** 73 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 74 | * Internal function without access restriction. 75 | */ 76 | function _transferOwnership(address newOwner) internal virtual { 77 | address oldOwner = _owner; 78 | _owner = newOwner; 79 | emit OwnershipTransferred(oldOwner, newOwner); 80 | } 81 | } -------------------------------------------------------------------------------- /contracts/libraries/Ownable2Step.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.8.0) (access/Ownable2Step.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./Ownable.sol"; 7 | 8 | /** 9 | * @dev Contract module which provides access control mechanism, where 10 | * there is an account (an owner) that can be granted exclusive access to 11 | * specific functions. 12 | * 13 | * By default, the owner account will be the one that deploys the contract. This 14 | * can later be changed with {transferOwnership} and {acceptOwnership}. 15 | * 16 | * This module is used through inheritance. It will make available all functions 17 | * from parent (Ownable). 18 | */ 19 | abstract contract Ownable2Step is Ownable { 20 | address private _pendingOwner; 21 | 22 | event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); 23 | 24 | /** 25 | * @dev Returns the address of the pending owner. 26 | */ 27 | function pendingOwner() public view virtual returns (address) { 28 | return _pendingOwner; 29 | } 30 | 31 | /** 32 | * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. 33 | * Can only be called by the current owner. 34 | */ 35 | function transferOwnership(address newOwner) public virtual override onlyOwner { 36 | _pendingOwner = newOwner; 37 | emit OwnershipTransferStarted(owner(), newOwner); 38 | } 39 | 40 | /** 41 | * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. 42 | * Internal function without access restriction. 43 | */ 44 | function _transferOwnership(address newOwner) internal virtual override { 45 | delete _pendingOwner; 46 | super._transferOwnership(newOwner); 47 | } 48 | 49 | /** 50 | * @dev The new owner accepts the ownership transfer. 51 | */ 52 | function acceptOwnership() public virtual { 53 | require(pendingOwner() == msg.sender, "Ownable2Step: caller is not the new owner"); 54 | _transferOwnership(msg.sender); 55 | } 56 | } -------------------------------------------------------------------------------- /contracts/libraries/Pausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./Ownable.sol"; 7 | 8 | abstract contract Pausable is Ownable { 9 | /** 10 | * @dev Emitted when the pause is triggered by `account`. 11 | */ 12 | event Paused(address account); 13 | 14 | /** 15 | * @dev Emitted when the pause is lifted by `account`. 16 | */ 17 | event Unpaused(address account); 18 | 19 | bool private _paused; 20 | 21 | /** 22 | * @dev Modifier to make a function callable only when the contract is not paused. 23 | * 24 | * Requirements: 25 | * 26 | * - The contract must not be paused. 27 | */ 28 | modifier whenNotPaused() { 29 | _requireNotPaused(); 30 | _; 31 | } 32 | 33 | /** 34 | * @dev Modifier to make a function callable only when the contract is paused. 35 | * 36 | * Requirements: 37 | * 38 | * - The contract must be paused. 39 | */ 40 | modifier whenPaused() { 41 | _requirePaused(); 42 | _; 43 | } 44 | 45 | /** 46 | * @dev Returns true if the contract is paused, and false otherwise. 47 | */ 48 | function paused() public view virtual returns (bool) { 49 | return _paused; 50 | } 51 | 52 | /** 53 | * @dev Throws if the contract is paused. 54 | */ 55 | function _requireNotPaused() internal view virtual { 56 | require(!paused(), "Pausable: paused"); 57 | } 58 | 59 | /** 60 | * @dev Throws if the contract is not paused. 61 | */ 62 | function _requirePaused() internal view virtual { 63 | require(paused(), "Pausable: not paused"); 64 | } 65 | 66 | function setPaused(bool _status) external onlyOwner { 67 | if (_status) { 68 | _requireNotPaused(); 69 | emit Paused(msg.sender); 70 | } else { 71 | _requirePaused(); 72 | emit Unpaused(msg.sender); 73 | } 74 | _paused = _status; 75 | } 76 | 77 | /** 78 | * @dev Triggers stopped state. 79 | * 80 | * Requirements: 81 | * 82 | * - The contract must not be paused. 83 | */ 84 | function _pause() internal virtual whenNotPaused { 85 | _paused = true; 86 | emit Paused(msg.sender); 87 | } 88 | 89 | /** 90 | * @dev Returns to normal state. 91 | * 92 | * Requirements: 93 | * 94 | * - The contract must be paused. 95 | */ 96 | function _unpause() internal virtual whenPaused { 97 | _paused = false; 98 | emit Unpaused(msg.sender); 99 | } 100 | } -------------------------------------------------------------------------------- /contracts/libraries/ReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Contract module that helps prevent reentrant calls to a function. 8 | * 9 | * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier 10 | * available, which can be applied to functions to make sure there are no nested 11 | * (reentrant) calls to them. 12 | * 13 | * Note that because there is a single `nonReentrant` guard, functions marked as 14 | * `nonReentrant` may not call one another. This can be worked around by making 15 | * those functions `private`, and then adding `external` `nonReentrant` entry 16 | * points to them. 17 | * 18 | * TIP: If you would like to learn more about reentrancy and alternative ways 19 | * to protect against it, check out our blog post 20 | * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. 21 | */ 22 | abstract contract ReentrancyGuard { 23 | // Booleans are more expensive than uint256 or any type that takes up a full 24 | // word because each write operation emits an extra SLOAD to first read the 25 | // slot's contents, replace the bits taken up by the boolean, and then write 26 | // back. This is the compiler's defense against contract upgrades and 27 | // pointer aliasing, and it cannot be disabled. 28 | 29 | // The values being non-zero value makes deployment a bit more expensive, 30 | // but in exchange the refund on every call to nonReentrant will be lower in 31 | // amount. Since refunds are capped to a percentage of the total 32 | // transaction's gas, it is best to keep them low in cases like this one, to 33 | // increase the likelihood of the full refund coming into effect. 34 | uint256 private constant _NOT_ENTERED = 1; 35 | uint256 private constant _ENTERED = 2; 36 | 37 | uint256 private _status; 38 | 39 | constructor() { 40 | _status = _NOT_ENTERED; 41 | } 42 | 43 | /** 44 | * @dev Prevents a contract from calling itself, directly or indirectly. 45 | * Calling a `nonReentrant` function from another `nonReentrant` 46 | * function is not supported. It is possible to prevent this from happening 47 | * by making the `nonReentrant` function external, and making it call a 48 | * `private` function that does the actual work. 49 | */ 50 | modifier nonReentrant() { 51 | _nonReentrantBefore(); 52 | _; 53 | _nonReentrantAfter(); 54 | } 55 | 56 | function _nonReentrantBefore() private { 57 | // On the first call to nonReentrant, _status will be _NOT_ENTERED 58 | require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); 59 | 60 | // Any calls to nonReentrant after this point will fail 61 | _status = _ENTERED; 62 | } 63 | 64 | function _nonReentrantAfter() private { 65 | // By storing the original value once again, a refund is triggered (see 66 | // https://eips.ethereum.org/EIPS/eip-2200) 67 | _status = _NOT_ENTERED; 68 | } 69 | 70 | /** 71 | * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a 72 | * `nonReentrant` function in the call stack. 73 | */ 74 | function _reentrancyGuardEntered() internal view returns (bool) { 75 | return _status == _ENTERED; 76 | } 77 | } -------------------------------------------------------------------------------- /contracts/libraries/SignatureChecker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./ECDSA.sol"; 6 | 7 | /** 8 | * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA 9 | * signatures from externally owned accounts (EOAs) as well as ERC1271 signatures from smart contract wallets like 10 | * Argent and Gnosis Safe. 11 | * 12 | * Based on OpenZeppelin's SignatureChecker library. 13 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/561d1061fc568f04c7a65853538e834a889751e8/contracts/utils/cryptography/SignatureChecker.sol 14 | */ 15 | library SignatureChecker { 16 | 17 | bytes4 constant internal MAGICVALUE = 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)") 18 | 19 | /** 20 | * @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the 21 | * signature is validated against that smart contract using ERC1271, otherwise it's validated using `ECDSA.recover`. 22 | * 23 | * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus 24 | * change through time. It could return true at block N and false at block N+1 (or the opposite). 25 | */ 26 | function isValidSignatureNow( 27 | address signer, 28 | bytes32 hash, 29 | bytes memory signature 30 | ) internal view returns (bool) { 31 | (address recovered) = ECDSA.recover(hash, signature); 32 | if (recovered == signer) { 33 | if (recovered != address(0)) { 34 | return true; 35 | } 36 | } 37 | 38 | (bool success, bytes memory result) = signer.staticcall( 39 | abi.encodeWithSelector(MAGICVALUE, hash, signature) 40 | ); 41 | return ( 42 | success && 43 | result.length == 32 && 44 | abi.decode(result, (bytes32)) == bytes32(MAGICVALUE) 45 | ); 46 | } 47 | } -------------------------------------------------------------------------------- /contracts/libraries/StableMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./Math.sol"; 6 | 7 | library StableMath { 8 | 9 | /// @notice Calculate the new balances of the tokens given the indexes of the token 10 | /// that is swapped from (FROM) and the token that is swapped to (TO). 11 | /// This function is used as a helper function to calculate how much TO token 12 | /// the user should receive on swap. 13 | /// @dev Originally https://github.com/saddle-finance/saddle-contract/blob/0b76f7fb519e34b878aa1d58cffc8d8dc0572c12/contracts/SwapUtils.sol#L432. 14 | /// @param x The new total amount of FROM token. 15 | /// @return y The amount of TO token that should remain in the pool. 16 | function getY(uint x, uint d) internal pure returns (uint y) { 17 | //uint c = (d * d) / (x * 2); 18 | uint c = Math.mulDiv(d, d, Math.mulUnsafeFirst(2, x)); 19 | //c = (c * d) / 4000; 20 | c = Math.mulDivUnsafeLast(c, d, 4000); 21 | 22 | //uint b = x + (d / 2000); 23 | uint b = x + Math.divUnsafeLast(d, 2000); 24 | uint yPrev; 25 | y = d; 26 | 27 | /// @dev Iterative approximation. 28 | for (uint i; i < 256; ) { 29 | yPrev = y; 30 | //y = (y * y + c) / (y * 2 + b - d); 31 | y = Math.div(Math.mul(y, y) + c, Math.mulUnsafeFirst(2, y) + b - d); 32 | 33 | if (Math.within1(y, yPrev)) { 34 | break; 35 | } 36 | 37 | unchecked { 38 | ++i; 39 | } 40 | } 41 | } 42 | 43 | // Overflow checks should be applied before calling this function. 44 | // The maximum XPs are `3802571709128108338056982581425910818` of uint128. 45 | function computeDFromAdjustedBalances(uint xp0, uint xp1) internal pure returns (uint computed) { 46 | uint s = xp0 + xp1; 47 | 48 | if (s == 0) { 49 | computed = 0; 50 | } else { 51 | uint prevD; 52 | uint d = s; 53 | 54 | for (uint i; i < 256; ) { 55 | //uint dP = (((d * d) / xp0) * d) / xp1 / 4; 56 | uint dP = Math.divUnsafeLast(Math.mulDiv(Math.mulDiv(d, d, xp0), d, xp1), 4); 57 | prevD = d; 58 | //d = (((2000 * s) + 2 * dP) * d) / ((2000 - 1) * d + 3 * dP); 59 | d = Math.mulDivUnsafeFirst( 60 | // `s` cannot be zero and this value will never be zero. 61 | Math.mulUnsafeFirst(2000, s) + Math.mulUnsafeFirst(2, dP), 62 | d, 63 | Math.mulUnsafeFirst(1999, d) + Math.mulUnsafeFirst(3, dP) 64 | ); 65 | 66 | if (Math.within1(d, prevD)) { 67 | break; 68 | } 69 | 70 | unchecked { 71 | ++i; 72 | } 73 | } 74 | 75 | computed = d; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /contracts/libraries/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /// @dev The ETH transfer has failed. 6 | error ETHTransferFailed(); 7 | 8 | /// @dev The ERC20 `transferFrom` has failed. 9 | error TransferFromFailed(); 10 | 11 | /// @dev The ERC20 `transfer` has failed. 12 | error TransferFailed(); 13 | 14 | /// @dev The ERC20 `approve` has failed. 15 | error ApproveFailed(); 16 | 17 | /// @dev Helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true / false. 18 | library TransferHelper { 19 | function safeApprove( 20 | address token, 21 | address to, 22 | uint value 23 | ) internal { 24 | // bytes4(keccak256(bytes("approve(address,uint256)"))); 25 | // solhint-disable-next-line avoid-low-level-calls 26 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); 27 | 28 | if (!success || (data.length != 0 && !abi.decode(data, (bool)))) { 29 | revert ApproveFailed(); 30 | } 31 | } 32 | 33 | function safeTransfer( 34 | address token, 35 | address to, 36 | uint value 37 | ) internal { 38 | // bytes4(keccak256(bytes("transfer(address,uint256)"))); 39 | // solhint-disable-next-line avoid-low-level-calls 40 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); 41 | 42 | if (!success || (data.length != 0 && !abi.decode(data, (bool)))) { 43 | revert TransferFailed(); 44 | } 45 | } 46 | 47 | function safeTransferFrom( 48 | address token, 49 | address from, 50 | address to, 51 | uint value 52 | ) internal { 53 | // bytes4(keccak256(bytes("transferFrom(address,address,uint256)"))); 54 | // solhint-disable-next-line avoid-low-level-calls 55 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); 56 | 57 | if (!success || (data.length != 0 && !abi.decode(data, (bool)))) { 58 | revert TransferFromFailed(); 59 | } 60 | } 61 | 62 | function safeTransferETH(address to, uint value) internal { 63 | // solhint-disable-next-line avoid-low-level-calls 64 | (bool success, ) = to.call{value: value}(""); 65 | 66 | if (!success) { 67 | revert ETHTransferFailed(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /contracts/master/FeeRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/master/IPoolMaster.sol"; 6 | import "../interfaces/master/IFeeRegistry.sol"; 7 | 8 | import "../libraries/Ownable.sol"; 9 | 10 | contract FeeRegistry is IFeeRegistry, Ownable { 11 | /// @dev The pool master. 12 | address public immutable master; 13 | 14 | /// @dev Whether a fee sender is whitelisted. 15 | mapping(address => bool) public isSenderWhitelisted; 16 | 17 | event SetSenderWhitelisted(address indexed sender, bool indexed isWhitelisted); 18 | 19 | constructor(address _master) { 20 | master = _master; 21 | } 22 | 23 | /// @dev Returns whether the address is a valid fee sender. 24 | function isFeeSender(address sender) external view override returns (bool) { 25 | return isSenderWhitelisted[sender] || IPoolMaster(master).isPool(sender); 26 | } 27 | 28 | /// @dev Whitelists a fee sender explicitly. 29 | function setSenderWhitelisted(address sender, bool isWhitelisted) external onlyOwner { 30 | require(sender != address(0), "Invalid address"); 31 | require(isSenderWhitelisted[sender] != isWhitelisted, "Already set"); 32 | isSenderWhitelisted[sender] = isWhitelisted; 33 | emit SetSenderWhitelisted(sender, isWhitelisted); 34 | } 35 | } -------------------------------------------------------------------------------- /contracts/master/ForwarderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/master/IForwarderRegistry.sol"; 6 | 7 | import "../libraries/Ownable.sol"; 8 | 9 | /// @notice A simple registry for sender forwarder contracts (usually the routers). 10 | contract ForwarderRegistry is IForwarderRegistry, Ownable { 11 | mapping(address => bool) private _isForwarder; 12 | 13 | event AddForwarder(address forwarder); 14 | event RemoveForwarder(address forwarder); 15 | 16 | function isForwarder(address forwarder) external view override returns (bool) { 17 | return _isForwarder[forwarder]; 18 | } 19 | 20 | function addForwarder(address forwarder) external onlyOwner { 21 | require(forwarder != address(0), "Invalid address"); 22 | require(!_isForwarder[forwarder], "Already added"); 23 | _isForwarder[forwarder] = true; 24 | emit AddForwarder(forwarder); 25 | } 26 | 27 | function removeForwarder(address forwarder) external onlyOwner { 28 | require(_isForwarder[forwarder], "Not added"); 29 | delete _isForwarder[forwarder]; 30 | emit RemoveForwarder(forwarder); 31 | } 32 | } -------------------------------------------------------------------------------- /contracts/master/SyncSwapFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/pool/IPool.sol"; 6 | import "../interfaces/master/IFeeManager.sol"; 7 | 8 | import "../libraries/Ownable.sol"; 9 | 10 | /// @notice The fee manager manages swap fees for pools and protocol fee. 11 | /// The contract is an independent module and can be replaced in the future. 12 | /// 13 | contract SyncSwapFeeManager is IFeeManager, Ownable { 14 | uint24 private constant MAX_PROTOCOL_FEE = 1e5; /// @dev 100%. 15 | uint24 private constant MAX_SWAP_FEE = 10000; /// @dev 10%. 16 | uint24 private constant ZERO_CUSTOM_FEE = type(uint24).max; 17 | 18 | /// @dev The default swap fee by pool type. 19 | mapping(uint16 => uint24) public defaultSwapFee; /// @dev `300` for 0.3%. 20 | 21 | /// @dev The custom swap fee by pool address, use `ZERO_CUSTOM_FEE` for zero fee. 22 | //mapping(address => uint24) public poolSwapFee; 23 | 24 | /// @dev The custom swap fee by tokens, use `ZERO_CUSTOM_FEE` for zero fee. 25 | mapping(address => mapping(address => uint24)) public tokenSwapFee; 26 | 27 | /// @dev The protocol fee of swap fee by pool type. 28 | mapping(uint16 => uint24) public defaultProtocolFee; /// @dev `30000` for 30%. 29 | 30 | /// @dev The custom protocol fee by pool address, use `ZERO_CUSTOM_FEE` for zero fee. 31 | mapping(address => uint24) public poolProtocolFee; 32 | 33 | /// @dev The recipient of protocol fees. 34 | address public feeRecipient; 35 | 36 | // Events 37 | event SetDefaultSwapFee(uint16 indexed poolType, uint24 fee); 38 | event SetTokenSwapFee(address indexed tokenIn, address indexed tokenOut, uint24 fee); 39 | event SetDefaultProtocolFee(uint16 indexed poolType, uint24 fee); 40 | event SetPoolProtocolFee(address indexed pool, uint24 fee); 41 | event SetFeeRecipient(address indexed previousFeeRecipient, address indexed newFeeRecipient); 42 | 43 | constructor(address _feeRecipient) { 44 | feeRecipient = _feeRecipient; 45 | 46 | // Prefill fees for known pool types. 47 | // 1 Classic Pools 48 | defaultSwapFee[1] = 200; // 0.2%. 49 | defaultProtocolFee[1] = 50000; // 50%. 50 | 51 | // 2 Stable Pools 52 | defaultSwapFee[2] = 40; // 0.04%. 53 | defaultProtocolFee[2] = 50000; // 50%. 54 | } 55 | 56 | // Getters 57 | 58 | function getSwapFee( 59 | address pool, 60 | address /*sender*/, 61 | address tokenIn, 62 | address tokenOut, 63 | bytes calldata /*data*/ 64 | ) external view override returns (uint24 fee) { 65 | fee = tokenSwapFee[tokenIn][tokenOut]; 66 | 67 | if (fee == 0) { 68 | // not set, use default fee of the pool type. 69 | fee = defaultSwapFee[IPool(pool).poolType()]; 70 | } else { 71 | // has a pool swap fee. 72 | fee = (fee == ZERO_CUSTOM_FEE ? 0 : fee); 73 | } 74 | } 75 | 76 | function getProtocolFee(address pool) external view override returns (uint24 fee) { 77 | fee = poolProtocolFee[pool]; 78 | 79 | if (fee == 0) { 80 | // not set, use default fee of the pool type. 81 | fee = defaultProtocolFee[IPool(pool).poolType()]; 82 | } else { 83 | // has a pool protocol fee. 84 | fee = (fee == ZERO_CUSTOM_FEE ? 0 : fee); 85 | } 86 | } 87 | 88 | function getFeeRecipient() external view override returns (address) { 89 | return feeRecipient; 90 | } 91 | 92 | // Setters 93 | 94 | function setDefaultSwapFee(uint16 poolType, uint24 fee) external onlyOwner { 95 | require( 96 | fee <= MAX_SWAP_FEE, 97 | "Invalid fee" 98 | ); 99 | defaultSwapFee[poolType] = fee; 100 | emit SetDefaultSwapFee(poolType, fee); 101 | } 102 | 103 | function setTokenSwapFee(address tokenIn, address tokenOut, uint24 fee) external onlyOwner { 104 | require( 105 | fee == ZERO_CUSTOM_FEE || 106 | fee <= MAX_SWAP_FEE, 107 | "Invalid fee" 108 | ); 109 | tokenSwapFee[tokenIn][tokenOut] = fee; 110 | emit SetTokenSwapFee(tokenIn, tokenOut, fee); 111 | } 112 | 113 | function setDefaultProtocolFee(uint16 poolType, uint24 fee) external onlyOwner { 114 | require( 115 | fee <= MAX_PROTOCOL_FEE, 116 | "Invalid fee" 117 | ); 118 | defaultProtocolFee[poolType] = fee; 119 | emit SetDefaultProtocolFee(poolType, fee); 120 | } 121 | 122 | function setPoolProtocolFee(address pool, uint24 fee) external onlyOwner { 123 | require( 124 | fee == ZERO_CUSTOM_FEE || 125 | fee <= MAX_PROTOCOL_FEE, 126 | "Invalid fee" 127 | ); 128 | poolProtocolFee[pool] = fee; 129 | emit SetPoolProtocolFee(pool, fee); 130 | } 131 | 132 | function setFeeRecipient(address _feeRecipient) external onlyOwner { 133 | // Emit here to avoid caching the previous recipient. 134 | emit SetFeeRecipient(feeRecipient, _feeRecipient); 135 | feeRecipient = _feeRecipient; 136 | } 137 | } -------------------------------------------------------------------------------- /contracts/master/SyncSwapFeeRecipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/token/IERC20.sol"; 6 | import "../interfaces/master/IFeeRegistry.sol"; 7 | import "../interfaces/master/IFeeRecipient.sol"; 8 | 9 | import "../libraries/Ownable2Step.sol"; 10 | import "../libraries/TransferHelper.sol"; 11 | 12 | error InvalidFeeSender(); 13 | 14 | contract SyncSwapFeeRecipient is IFeeRecipient, Ownable2Step { 15 | 16 | uint public epochDuration = 3 days; 17 | 18 | /// @dev The registry for fee senders. 19 | address public feeRegistry; 20 | 21 | /// @dev The epoch fees of each token. 22 | mapping(uint => mapping(address => uint)) public fees; // epoch => token => amount 23 | 24 | /// @dev The fees tokens in each epoch. 25 | mapping(uint => address[]) public feeTokens; 26 | 27 | /// @dev Whether the address is a fee distributor. 28 | mapping(address => bool) public isFeeDistributor; 29 | 30 | /// @dev The fee distributors. 31 | address[] public feeDistributors; // for inspection only 32 | 33 | struct FeeTokenData { 34 | // The start time of a fee token from a sender. 35 | uint startTime; 36 | 37 | // The accumulated fee amount since start time. 38 | uint amount; 39 | } 40 | 41 | /// @dev The fee token data of a sender. 42 | mapping(address => mapping(address => FeeTokenData)) public feeTokenData; // sender => token => time 43 | 44 | event NotifyFees(address indexed sender, uint16 indexed feeType, address indexed token, uint amount, uint feeRate); 45 | event AddFeeDistributor(address indexed distributor); 46 | event RemoveFeeDistributor(address indexed distributor); 47 | event SetFeeRegistry(address indexed feeRegistry); 48 | event SetEpochDuration(uint epochDuration); 49 | 50 | constructor(address _feeRegistry) { 51 | feeRegistry = _feeRegistry; 52 | } 53 | 54 | function feeTokensLength(uint epoch) external view returns (uint) { 55 | return feeTokens[epoch].length; 56 | } 57 | 58 | function feeDistributorsLength() external view returns (uint) { 59 | return feeDistributors.length; 60 | } 61 | 62 | function getEpochStart(uint ts) public view returns (uint) { 63 | return ts - (ts % epochDuration); 64 | } 65 | 66 | /// @dev Notifies the fee recipient after sent fees. 67 | function notifyFees( 68 | uint16 feeType, 69 | address token, 70 | uint amount, 71 | uint feeRate, 72 | bytes calldata /*data*/ 73 | ) external override { 74 | if (!IFeeRegistry(feeRegistry).isFeeSender(msg.sender)) { 75 | revert InvalidFeeSender(); 76 | } 77 | 78 | uint epoch = getEpochStart(block.timestamp); 79 | uint epochTokenFees = fees[epoch][token]; 80 | 81 | // Unchecked to avoid potential overflow, since fees are only for inspection. 82 | unchecked { 83 | if (epochTokenFees == 0) { 84 | // Pushes new tokens to array. 85 | feeTokens[epoch].push(token); 86 | 87 | // Updates epoch fees for the token. 88 | fees[epoch][token] = amount; 89 | 90 | // Updates fee token data for sender. 91 | feeTokenData[msg.sender][token] = FeeTokenData({ 92 | startTime: block.timestamp, 93 | amount: amount 94 | }); 95 | } else { 96 | // Updates epoch fees for the token. 97 | fees[epoch][token] = (epochTokenFees + amount); 98 | 99 | // Updates fee token data for sender. 100 | feeTokenData[msg.sender][token].amount += amount; 101 | } 102 | } 103 | 104 | emit NotifyFees(msg.sender, feeType, token, amount, feeRate); 105 | } 106 | 107 | /// @dev Distributes fees to the recipient. 108 | function distributeFees(address to, address[] calldata tokens, uint[] calldata amounts) external { 109 | require(isFeeDistributor[msg.sender] || msg.sender == owner(), "No perms"); 110 | require(tokens.length == amounts.length, "Wrong length"); 111 | 112 | uint n = tokens.length; 113 | address token; uint amount; 114 | 115 | for (uint i; i < n; ) { 116 | token = tokens[i]; 117 | amount = amounts[i]; 118 | 119 | if (token == address(0)) { // ETH 120 | if (amount == 0) { 121 | amount = address(this).balance; 122 | } 123 | TransferHelper.safeTransferETH(to, amount); 124 | } else { 125 | if (amount == 0) { 126 | amount = IERC20(token).balanceOf(address(this)); 127 | } 128 | TransferHelper.safeTransfer(token, to, amount); 129 | } 130 | 131 | unchecked { 132 | ++i; 133 | } 134 | } 135 | } 136 | 137 | /// @dev Adds a new fee distributor. 138 | function addFeeDistributor(address distributor) external onlyOwner { 139 | require(distributor != address(0), "Invalid address"); 140 | require(!isFeeDistributor[distributor], "Already set"); 141 | isFeeDistributor[distributor] = true; 142 | feeDistributors.push(distributor); 143 | emit AddFeeDistributor(distributor); 144 | } 145 | 146 | /// @dev Removes a new fee distributor. 147 | function removeFeeDistributor(address distributor, bool updateArray) external onlyOwner { 148 | require(isFeeDistributor[distributor], "Not set"); 149 | delete isFeeDistributor[distributor]; 150 | if (updateArray) { 151 | uint n = feeDistributors.length; 152 | for (uint i; i < n; ) { 153 | if (feeDistributors[i] == distributor) { 154 | feeDistributors[i] = feeDistributors[n - 1]; 155 | feeDistributors[n - 1] = distributor; 156 | feeDistributors.pop(); 157 | break; 158 | } 159 | 160 | unchecked { 161 | ++i; 162 | } 163 | } 164 | } 165 | emit RemoveFeeDistributor(distributor); 166 | } 167 | 168 | /// @dev Sets a new fee registry. 169 | function setFeeRegistry(address _feeRegistry) external onlyOwner { 170 | require(_feeRegistry != address(0), "Invalid address"); 171 | feeRegistry = _feeRegistry; 172 | emit SetFeeRegistry(_feeRegistry); 173 | } 174 | 175 | function setEpochDuration(uint _epochDuration) external onlyOwner { 176 | require(_epochDuration != 0, "Invalid duration"); 177 | epochDuration = _epochDuration; 178 | emit SetEpochDuration(_epochDuration); 179 | } 180 | 181 | function withdrawERC20(address token, address to, uint amount) external onlyOwner { 182 | if (amount == 0) { 183 | amount = IERC20(token).balanceOf(address(this)); 184 | } 185 | TransferHelper.safeTransfer(token, to, amount); 186 | } 187 | 188 | function withdrawETH(address to, uint amount) external onlyOwner { 189 | if (amount == 0) { 190 | amount = address(this).balance; 191 | } 192 | TransferHelper.safeTransferETH(to, amount); 193 | } 194 | } -------------------------------------------------------------------------------- /contracts/master/SyncSwapPoolMaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/master/IPoolMaster.sol"; 6 | import "../interfaces/factory/IPoolFactory.sol"; 7 | 8 | import "../libraries/Ownable2Step.sol"; 9 | 10 | error NotWhitelistedFactory(); 11 | error PoolAlreadyExists(); 12 | 13 | /// @notice The pool master manages swap fees for pools, whitelist for factories, 14 | /// protocol fee and pool registry. 15 | /// 16 | /// It accepts pool registers from whitelisted factories, with the pool data on pool 17 | /// creation, to enable querying of the existence or fees of a pool by address or config. 18 | /// 19 | /// This contract provides a unified interface to query and manage fees across 20 | /// different pool types, and a unique registry for all pools. 21 | /// 22 | contract SyncSwapPoolMaster is IPoolMaster, Ownable2Step { 23 | 24 | /// @dev The vault that holds funds. 25 | address public immutable override vault; 26 | 27 | // Forwarder Registry 28 | 29 | /// @dev The registry of forwarder. 30 | address public forwarderRegistry; 31 | 32 | // Fees 33 | 34 | /// @dev The fee manager. 35 | address public override feeManager; 36 | 37 | // Factories 38 | 39 | /// @dev Whether an address is a factory. 40 | mapping(address => bool) public override isFactoryWhitelisted; 41 | 42 | // Pools 43 | 44 | /// @dev Whether an address is a pool. 45 | mapping(address => bool) public override isPool; 46 | 47 | /// @dev Pools by hash of its config. 48 | mapping(bytes32 => address) public override getPool; 49 | 50 | address[] public override pools; 51 | 52 | constructor(address _vault, address _forwarderRegistry, address _feeManager) { 53 | vault = _vault; 54 | forwarderRegistry = _forwarderRegistry; 55 | feeManager = _feeManager; 56 | } 57 | 58 | function poolsLength() external view override returns (uint) { 59 | return pools.length; 60 | } 61 | 62 | // Forwarder Registry 63 | 64 | function isForwarder(address forwarder) external view override returns (bool) { 65 | return IForwarderRegistry(forwarderRegistry).isForwarder(forwarder); 66 | } 67 | 68 | function setForwarderRegistry(address newForwarderRegistry) external override onlyOwner { 69 | forwarderRegistry = newForwarderRegistry; 70 | emit UpdateForwarderRegistry(newForwarderRegistry); 71 | } 72 | 73 | // Fees 74 | 75 | function getSwapFee( 76 | address pool, 77 | address sender, 78 | address tokenIn, 79 | address tokenOut, 80 | bytes calldata data 81 | ) external view override returns (uint24 fee) { 82 | fee = IFeeManager(feeManager).getSwapFee(pool, sender, tokenIn, tokenOut, data); 83 | } 84 | 85 | function getProtocolFee(address pool) external view override returns (uint24 fee) { 86 | fee = IFeeManager(feeManager).getProtocolFee(pool); 87 | } 88 | 89 | function getFeeRecipient() external view override returns (address recipient) { 90 | recipient = IFeeManager(feeManager).getFeeRecipient(); 91 | } 92 | 93 | function setFeeManager(address newFeeManager) external override onlyOwner { 94 | feeManager = newFeeManager; 95 | emit UpdateFeeManager(newFeeManager); 96 | } 97 | 98 | // Factories 99 | 100 | function setFactoryWhitelisted(address factory, bool whitelisted) external override onlyOwner { 101 | require(factory != address(0), "Invalid factory"); 102 | isFactoryWhitelisted[factory] = whitelisted; 103 | emit SetFactoryWhitelisted(factory, whitelisted); 104 | } 105 | 106 | // Pools 107 | 108 | /// @dev Create a pool with deployment data and, register it via the factory. 109 | function createPool(address factory, bytes calldata data) external override returns (address pool) { 110 | // The factory have to call `registerPool` to register the pool. 111 | // The pool whitelist is checked in `registerPool`. 112 | pool = IPoolFactory(factory).createPool(data); 113 | } 114 | 115 | /// @dev Register a pool to the mapping by its config. Can only be called by factories. 116 | function registerPool(address pool, uint16 poolType, bytes calldata data) external override { 117 | if (!isFactoryWhitelisted[msg.sender]) { 118 | revert NotWhitelistedFactory(); 119 | } 120 | 121 | require(pool != address(0)); 122 | 123 | // Double check to prevent duplicated pools. 124 | if (isPool[pool]) { 125 | revert PoolAlreadyExists(); 126 | } 127 | 128 | // Encode and hash pool config to get the mapping key. 129 | bytes32 hash = keccak256(abi.encode(poolType, data)); 130 | 131 | // Double check to prevent duplicated pools. 132 | if (getPool[hash] != address(0)) { 133 | revert PoolAlreadyExists(); 134 | } 135 | 136 | // Set to mappings. 137 | getPool[hash] = pool; 138 | isPool[pool] = true; 139 | pools.push(pool); 140 | 141 | emit RegisterPool(msg.sender, pool, poolType, data); 142 | } 143 | } -------------------------------------------------------------------------------- /contracts/pool/BasePoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/factory/IBasePoolFactory.sol"; 6 | import "../interfaces/master/IPoolMaster.sol"; 7 | 8 | error InvalidTokens(); 9 | 10 | abstract contract BasePoolFactory is IBasePoolFactory { 11 | /// @dev The pool master that control fees and registry. 12 | address public immutable master; 13 | 14 | /// @dev Pools by its two pool tokens. 15 | mapping(address => mapping(address => address)) public override getPool; 16 | 17 | bytes internal cachedDeployData; 18 | 19 | constructor(address _master) { 20 | master = _master; 21 | } 22 | 23 | function getDeployData() external view override returns (bytes memory deployData) { 24 | deployData = cachedDeployData; 25 | } 26 | 27 | function getSwapFee( 28 | address pool, 29 | address sender, 30 | address tokenIn, 31 | address tokenOut, 32 | bytes calldata data 33 | ) external view override returns (uint24 swapFee) { 34 | swapFee = IPoolMaster(master).getSwapFee(pool, sender, tokenIn, tokenOut, data); 35 | } 36 | 37 | function createPool(bytes calldata data) external override returns (address pool) { 38 | (address tokenA, address tokenB) = abi.decode(data, (address, address)); 39 | 40 | // Perform safety checks. 41 | if (tokenA == tokenB) { 42 | revert InvalidTokens(); 43 | } 44 | 45 | // Sort tokens. 46 | if (tokenB < tokenA) { 47 | (tokenA, tokenB) = (tokenB, tokenA); 48 | } 49 | if (tokenA == address(0)) { 50 | revert InvalidTokens(); 51 | } 52 | 53 | // Underlying implementation to deploy the pools and register them. 54 | pool = _createPool(tokenA, tokenB); 55 | 56 | // Populate mapping in both directions. 57 | // Not necessary as existence of the master, but keep them for better compatibility. 58 | getPool[tokenA][tokenB] = pool; 59 | getPool[tokenB][tokenA] = pool; 60 | 61 | emit PoolCreated(tokenA, tokenB, pool); 62 | } 63 | 64 | function _createPool(address tokenA, address tokenB) internal virtual returns (address) { 65 | } 66 | } -------------------------------------------------------------------------------- /contracts/pool/classic/SyncSwapClassicPoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../../interfaces/master/IPoolMaster.sol"; 6 | import "../../interfaces/token/IERC20.sol"; 7 | 8 | import "../BasePoolFactory.sol"; 9 | 10 | import "./SyncSwapClassicPool.sol"; 11 | 12 | contract SyncSwapClassicPoolFactory is BasePoolFactory { 13 | constructor(address _master) BasePoolFactory(_master) { 14 | } 15 | 16 | function _createPool(address token0, address token1) internal override returns (address pool) { 17 | // Perform sanity checks. 18 | IERC20(token0).balanceOf(address(this)); 19 | IERC20(token1).balanceOf(address(this)); 20 | 21 | bytes memory deployData = abi.encode(token0, token1); 22 | cachedDeployData = deployData; 23 | 24 | // The salt is same with deployment data. 25 | bytes32 salt = keccak256(deployData); 26 | pool = address(new SyncSwapClassicPool{salt: salt}()); // this will prevent duplicated pools. 27 | 28 | // Register the pool. The config is same with deployment data. 29 | IPoolMaster(master).registerPool(pool, 1, deployData); 30 | } 31 | } -------------------------------------------------------------------------------- /contracts/pool/stable/SyncSwapStablePoolFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../../interfaces/token/IERC20.sol"; 6 | import "../../interfaces/master/IPoolMaster.sol"; 7 | 8 | import "../BasePoolFactory.sol"; 9 | 10 | import "./SyncSwapStablePool.sol"; 11 | 12 | contract SyncSwapStablePoolFactory is BasePoolFactory { 13 | constructor(address _master) BasePoolFactory(_master) { 14 | } 15 | 16 | function _createPool(address token0, address token1) internal override returns (address pool) { 17 | // Tokens with decimals more than 18 are not supported and will lead to reverts. 18 | uint token0PrecisionMultiplier = 10 ** (18 - IERC20(token0).decimals()); 19 | uint token1PrecisionMultiplier = 10 ** (18 - IERC20(token1).decimals()); 20 | 21 | bytes memory deployData = abi.encode(token0, token1, token0PrecisionMultiplier, token1PrecisionMultiplier); 22 | cachedDeployData = deployData; 23 | 24 | // Remove precision multipliers from salt and config. 25 | deployData = abi.encode(token0, token1); 26 | 27 | bytes32 salt = keccak256(deployData); 28 | pool = address(new SyncSwapStablePool{salt: salt}()); // this will prevent duplicated pools. 29 | 30 | // Register the pool with config. 31 | IPoolMaster(master).registerPool(pool, 2, deployData); 32 | } 33 | } -------------------------------------------------------------------------------- /contracts/test/DeflatingERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract DeflatingERC20 { 6 | string public constant name = "Deflating Test Token"; 7 | string public constant symbol = "DTT"; 8 | uint8 public constant decimals = 18; 9 | uint public totalSupply; 10 | mapping(address => uint) public balanceOf; 11 | mapping(address => mapping(address => uint)) public allowance; 12 | 13 | bytes32 public DOMAIN_SEPARATOR; 14 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 15 | bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 16 | mapping(address => uint) public nonces; 17 | 18 | event Approval(address indexed owner, address indexed spender, uint value); 19 | event Transfer(address indexed from, address indexed to, uint value); 20 | 21 | constructor(uint _totalSupply) { 22 | uint 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 | _mint(msg.sender, _totalSupply); 36 | } 37 | 38 | function _mint(address to, uint value) internal { 39 | totalSupply = totalSupply + value; 40 | balanceOf[to] = balanceOf[to] + value; 41 | emit Transfer(address(0), to, value); 42 | } 43 | 44 | function _burn(address from, uint value) internal { 45 | balanceOf[from] = balanceOf[from] - value; 46 | totalSupply = totalSupply - value; 47 | emit Transfer(from, address(0), value); 48 | } 49 | 50 | function _approve(address owner, address spender, uint value) private { 51 | allowance[owner][spender] = value; 52 | emit Approval(owner, spender, value); 53 | } 54 | 55 | function _transfer(address from, address to, uint value) private { 56 | uint burnAmount = value / 100; 57 | _burn(from, burnAmount); 58 | uint transferAmount = value - burnAmount; 59 | balanceOf[from] = balanceOf[from] - transferAmount; 60 | balanceOf[to] = balanceOf[to] + transferAmount; 61 | emit Transfer(from, to, transferAmount); 62 | } 63 | 64 | function approve(address spender, uint value) external returns (bool) { 65 | _approve(msg.sender, spender, value); 66 | return true; 67 | } 68 | 69 | function transfer(address to, uint value) external returns (bool) { 70 | _transfer(msg.sender, to, value); 71 | return true; 72 | } 73 | 74 | function transferFrom(address from, address to, uint value) external returns (bool) { 75 | if (allowance[from][msg.sender] != type(uint).max) { 76 | allowance[from][msg.sender] = allowance[from][msg.sender] - value; 77 | } 78 | _transfer(from, to, value); 79 | return true; 80 | } 81 | 82 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 83 | require(deadline >= block.timestamp, "EXPIRED"); 84 | bytes32 digest = keccak256( 85 | abi.encodePacked( 86 | "\x19\x01", 87 | DOMAIN_SEPARATOR, 88 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 89 | ) 90 | ); 91 | address recoveredAddress = ecrecover(digest, v, r, s); 92 | require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNATURE"); 93 | _approve(owner, spender, value); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/test/RouterEventEmitter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../SyncSwapRouter.sol"; 6 | import "../interfaces/IRouter.sol"; 7 | 8 | contract RouterEventEmitter is IRouter { 9 | 10 | address public immutable vault; 11 | address public immutable wETH; 12 | address private constant NATIVE_ETH = address(0); 13 | 14 | constructor(address _vault, address _wETH) { 15 | vault = _vault; 16 | wETH = _wETH; 17 | } 18 | 19 | event Amounts(uint amount); 20 | 21 | receive() external payable {} 22 | 23 | function swap( 24 | address payable router, 25 | SwapPath[] memory paths, 26 | uint amountOutMin, 27 | uint deadline 28 | ) external { 29 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 30 | SyncSwapRouter(router).swap.selector, 31 | paths, 32 | amountOutMin, 33 | deadline 34 | )); 35 | assert(success); 36 | emit Amounts(abi.decode(returnData, (uint))); 37 | } 38 | } -------------------------------------------------------------------------------- /contracts/test/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract TestERC20 { 6 | string public constant name = "Test Token"; 7 | string public constant symbol = "TT"; 8 | uint8 public decimals; 9 | uint public totalSupply; 10 | mapping(address => uint) public balanceOf; 11 | mapping(address => mapping(address => uint)) public allowance; 12 | 13 | bytes32 public DOMAIN_SEPARATOR; 14 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 15 | bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 16 | mapping(address => uint) public nonces; 17 | 18 | event Approval(address indexed owner, address indexed spender, uint value); 19 | event Transfer(address indexed from, address indexed to, uint value); 20 | 21 | constructor(uint _totalSupply, uint8 _decimals) { 22 | uint chainId; 23 | assembly { 24 | chainId := chainid() 25 | } 26 | 27 | decimals = _decimals; 28 | 29 | DOMAIN_SEPARATOR = keccak256( 30 | abi.encode( 31 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 32 | keccak256(bytes(name)), 33 | keccak256(bytes("1")), 34 | chainId, 35 | address(this) 36 | ) 37 | ); 38 | 39 | _mint(msg.sender, _totalSupply); 40 | } 41 | 42 | function _mint(address to, uint value) internal { 43 | totalSupply = totalSupply + value; 44 | balanceOf[to] = balanceOf[to] + value; 45 | emit Transfer(address(0), to, value); 46 | } 47 | 48 | function _burn(address from, uint value) internal { 49 | balanceOf[from] = balanceOf[from] - value; 50 | totalSupply = totalSupply - value; 51 | emit Transfer(from, address(0), value); 52 | } 53 | 54 | function _approve(address owner, address spender, uint value) private { 55 | allowance[owner][spender] = value; 56 | emit Approval(owner, spender, value); 57 | } 58 | 59 | function _transfer(address from, address to, uint value) private { 60 | balanceOf[from] = balanceOf[from] - value; 61 | balanceOf[to] = balanceOf[to] + value; 62 | emit Transfer(from, to, value); 63 | } 64 | 65 | function approve(address spender, uint value) external returns (bool) { 66 | _approve(msg.sender, spender, value); 67 | return true; 68 | } 69 | 70 | function transfer(address to, uint value) external returns (bool) { 71 | _transfer(msg.sender, to, value); 72 | return true; 73 | } 74 | 75 | function transferFrom(address from, address to, uint value) external returns (bool) { 76 | if (allowance[from][msg.sender] != type(uint).max) { 77 | allowance[from][msg.sender] = allowance[from][msg.sender] - value; 78 | } 79 | _transfer(from, to, value); 80 | return true; 81 | } 82 | 83 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 84 | require(deadline >= block.timestamp, "EXPIRED"); 85 | bytes32 digest = keccak256( 86 | abi.encodePacked( 87 | "\x19\x01", 88 | DOMAIN_SEPARATOR, 89 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 90 | ) 91 | ); 92 | address recoveredAddress = ecrecover(digest, v, r, s); 93 | require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNATURE"); 94 | _approve(owner, spender, value); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /contracts/test/TestSyncSwapLPToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../libraries/ERC20Permit2.sol"; 6 | 7 | contract TestERC20Permit2 is ERC20Permit2 { 8 | constructor(uint _totalSupply) { 9 | _mint(msg.sender, _totalSupply); 10 | } 11 | } -------------------------------------------------------------------------------- /contracts/test/TestWETH9.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract TestWETH9 { 6 | string public name = "Wrapped Ether"; 7 | string public symbol = "WETH"; 8 | uint8 public decimals = 18; 9 | 10 | event Approval(address indexed src, address indexed guy, uint wad); 11 | event Transfer(address indexed src, address indexed dst, uint wad); 12 | event Deposit(address indexed dst, uint wad); 13 | event Withdrawal(address indexed src, uint wad); 14 | 15 | mapping (address => uint) public balanceOf; 16 | mapping (address => mapping (address => uint)) public allowance; 17 | 18 | receive() external payable { 19 | deposit(); 20 | } 21 | 22 | function deposit() public payable { 23 | balanceOf[msg.sender] += msg.value; 24 | emit Deposit(msg.sender, msg.value); 25 | } 26 | 27 | function withdraw(uint wad) public { 28 | balanceOf[msg.sender] -= wad; 29 | payable(msg.sender).transfer(wad); 30 | emit Withdrawal(msg.sender, wad); 31 | } 32 | 33 | function totalSupply() public view returns (uint) { 34 | return address(this).balance; 35 | } 36 | 37 | function approve(address guy, uint wad) public returns (bool) { 38 | allowance[msg.sender][guy] = wad; 39 | emit Approval(msg.sender, guy, wad); 40 | return true; 41 | } 42 | 43 | function transfer(address dst, uint wad) public returns (bool) { 44 | return transferFrom(msg.sender, dst, wad); 45 | } 46 | 47 | function transferFrom(address src, address dst, uint wad) public returns (bool) { 48 | if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) { 49 | allowance[src][msg.sender] -= wad; 50 | } 51 | 52 | balanceOf[src] -= wad; 53 | balanceOf[dst] += wad; 54 | 55 | emit Transfer(src, dst, wad); 56 | 57 | return true; 58 | } 59 | } -------------------------------------------------------------------------------- /contracts/vault/SyncSwapVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/IWETH.sol"; 6 | import "../interfaces/token/IERC20.sol"; 7 | 8 | import "../libraries/ReentrancyGuard.sol"; 9 | import "../libraries/TransferHelper.sol"; 10 | 11 | import "./VaultFlashLoans.sol"; 12 | 13 | /// @notice The vault stores all tokens supporting internal transfers to save gas. 14 | contract SyncSwapVault is VaultFlashLoans { 15 | 16 | address private constant NATIVE_ETH = address(0); 17 | address public immutable override wETH; 18 | 19 | mapping(address => mapping(address => uint)) private balances; // token -> account -> balance 20 | mapping(address => uint) public override reserves; // token -> reserve 21 | 22 | constructor(address _wETH) VaultFlashLoans(msg.sender) { 23 | wETH = _wETH; 24 | } 25 | 26 | receive() external payable { 27 | // Deposit ETH via fallback if not from the wETH withdraw. 28 | if (msg.sender != wETH) { 29 | deposit(NATIVE_ETH, msg.sender); 30 | } 31 | } 32 | 33 | function balanceOf(address token, address account) external view override returns (uint balance) { 34 | // Ensure the same `balances` as native ETH. 35 | if (token == wETH) { 36 | token = NATIVE_ETH; 37 | } 38 | 39 | return balances[token][account]; 40 | } 41 | 42 | // Deposit 43 | 44 | function deposit(address token, address to) public payable override nonReentrant returns (uint amount) { 45 | if (token == NATIVE_ETH) { 46 | // Use `msg.value` as amount for native ETH. 47 | amount = msg.value; 48 | } else { 49 | require(msg.value == 0); 50 | 51 | if (token == wETH) { 52 | // Ensure the same `reserves` and `balances` as native ETH. 53 | token = NATIVE_ETH; 54 | 55 | // Use balance as amount for wETH. 56 | amount = IERC20(wETH).balanceOf(address(this)); 57 | 58 | // Unwrap wETH to native ETH. 59 | IWETH(wETH).withdraw(amount); 60 | } else { 61 | // Derive real amount with balance and reserve for ERC20 tokens. 62 | amount = IERC20(token).balanceOf(address(this)) - reserves[token]; 63 | } 64 | } 65 | 66 | // Increase token reserve. 67 | reserves[token] += amount; 68 | 69 | // Increase token balance for recipient. 70 | unchecked { 71 | /// `balances` cannot overflow if `reserves` doesn't overflow. 72 | balances[token][to] += amount; 73 | } 74 | } 75 | 76 | function depositETH(address to) external payable override nonReentrant returns (uint amount) { 77 | // Use `msg.value` as amount for native ETH. 78 | amount = msg.value; 79 | 80 | // Increase token reserve. 81 | reserves[NATIVE_ETH] += amount; 82 | 83 | // Increase token balance for recipient. 84 | unchecked { 85 | /// `balances` cannot overflow if `reserves` doesn't overflow. 86 | balances[NATIVE_ETH][to] += amount; 87 | } 88 | } 89 | 90 | // Transfer tokens from sender and deposit, requires approval. 91 | function transferAndDeposit(address token, address to, uint amount) external payable override nonReentrant returns (uint) { 92 | if (token == NATIVE_ETH) { 93 | require(amount == msg.value); 94 | } else { 95 | require(msg.value == 0); 96 | 97 | if (token == wETH) { 98 | // Ensure the same `reserves` and `balances` as native ETH. 99 | token = NATIVE_ETH; 100 | 101 | // Receive wETH from sender. 102 | IWETH(wETH).transferFrom(msg.sender, address(this), amount); 103 | 104 | // Unwrap wETH to native ETH. 105 | IWETH(wETH).withdraw(amount); 106 | } else { 107 | // Receive ERC20 tokens from sender. 108 | TransferHelper.safeTransferFrom(token, msg.sender, address(this), amount); 109 | 110 | // Derive real amount with balance and reserve for ERC20 tokens. 111 | amount = IERC20(token).balanceOf(address(this)) - reserves[token]; 112 | } 113 | } 114 | 115 | // Increase token reserve. 116 | reserves[token] += amount; 117 | 118 | // Increase token balance for recipient. 119 | unchecked { 120 | /// `balances` cannot overflow if `reserves` doesn't overflow. 121 | balances[token][to] += amount; 122 | } 123 | 124 | return amount; 125 | } 126 | 127 | // Transfer 128 | 129 | function transfer(address token, address to, uint amount) external override nonReentrant { 130 | // Ensure the same `reserves` and `balances` as native ETH. 131 | if (token == wETH) { 132 | token = NATIVE_ETH; 133 | } 134 | 135 | // Decrease token balance for sender. 136 | balances[token][msg.sender] -= amount; 137 | 138 | // Increase token balance for recipient. 139 | unchecked { 140 | /// `balances` cannot overflow if `balances` doesn't underflow. 141 | balances[token][to] += amount; 142 | } 143 | } 144 | 145 | // Withdraw 146 | 147 | function _wrapAndTransferWETH(address to, uint amount) private { 148 | // Wrap native ETH to wETH. 149 | IWETH(wETH).deposit{value: amount}(); 150 | 151 | // Send wETH to recipient. 152 | IWETH(wETH).transfer(to, amount); 153 | } 154 | 155 | function withdraw(address token, address to, uint amount) external override nonReentrant { 156 | if (token == NATIVE_ETH) { 157 | // Send native ETH to recipient. 158 | TransferHelper.safeTransferETH(to, amount); 159 | } else { 160 | if (token == wETH) { 161 | // Ensure the same `reserves` and `balances` as native ETH. 162 | token = NATIVE_ETH; 163 | 164 | _wrapAndTransferWETH(to, amount); 165 | } else { 166 | // Send ERC20 tokens to recipient. 167 | TransferHelper.safeTransfer(token, to, amount); 168 | } 169 | } 170 | 171 | // Decrease token balance for sender. 172 | balances[token][msg.sender] -= amount; 173 | 174 | // Decrease token reserve. 175 | unchecked { 176 | /// `reserves` cannot underflow if `balances` doesn't underflow. 177 | reserves[token] -= amount; 178 | } 179 | } 180 | 181 | // Withdraw with mode. 182 | // 0 = DEFAULT 183 | // 1 = UNWRAPPED 184 | // 2 = WRAPPED 185 | function withdrawAlternative(address token, address to, uint amount, uint8 mode) external override nonReentrant { 186 | if (token == NATIVE_ETH) { 187 | if (mode == 2) { 188 | _wrapAndTransferWETH(to, amount); 189 | } else { 190 | // Send native ETH to recipient. 191 | TransferHelper.safeTransferETH(to, amount); 192 | } 193 | } else { 194 | if (token == wETH) { 195 | // Ensure the same `reserves` and `balances` as native ETH. 196 | token = NATIVE_ETH; 197 | 198 | if (mode == 1) { 199 | // Send native ETH to recipient. 200 | TransferHelper.safeTransferETH(to, amount); 201 | } else { 202 | _wrapAndTransferWETH(to, amount); 203 | } 204 | } else { 205 | // Send ERC20 tokens to recipient. 206 | TransferHelper.safeTransfer(token, to, amount); 207 | } 208 | } 209 | 210 | // Decrease token balance for sender. 211 | balances[token][msg.sender] -= amount; 212 | 213 | // Decrease token reserve. 214 | unchecked { 215 | /// `reserves` cannot underflow if `balances` doesn't underflow. 216 | reserves[token] -= amount; 217 | } 218 | } 219 | 220 | function withdrawETH(address to, uint amount) external override nonReentrant { 221 | // Send native ETH to recipient. 222 | TransferHelper.safeTransferETH(to, amount); 223 | 224 | // Decrease token balance for sender. 225 | balances[NATIVE_ETH][msg.sender] -= amount; 226 | 227 | // Decrease token reserve. 228 | unchecked { 229 | /// `reserves` cannot underflow if `balances` doesn't underflow. 230 | reserves[NATIVE_ETH] -= amount; 231 | } 232 | } 233 | } -------------------------------------------------------------------------------- /contracts/vault/VaultFlashLoans.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../interfaces/token/IERC20.sol"; 6 | import "../interfaces/master/IFeeRecipient.sol"; 7 | import "../interfaces/vault/IVault.sol"; 8 | import "../interfaces/vault/IFlashLoanRecipient.sol"; 9 | 10 | import "../libraries/Pausable.sol"; 11 | import "../libraries/ReentrancyGuard.sol"; 12 | import "../libraries/TransferHelper.sol"; 13 | 14 | /** 15 | * @dev Handles Flash Loans through the Vault. Calls the `receiveFlashLoan` hook on the flash loan recipient 16 | * contract, which implements the `IFlashLoanRecipient` interface. 17 | */ 18 | abstract contract VaultFlashLoans is IVault, ReentrancyGuard, Pausable { 19 | 20 | bytes32 public constant ERC3156_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); 21 | 22 | /// @dev Absolute maximum fee percentages (1e18 = 100%, 1e16 = 1%). 23 | uint private constant _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE = 10e16; // 10% 24 | 25 | // All fee percentages are 18-decimal fixed point numbers. 26 | // The flash loan fee is charged whenever a flash loan occurs, as a percentage of the tokens lent. 27 | uint public override flashLoanFeePercentage = 5e14; // 0.05% 28 | 29 | address public flashLoanFeeRecipient; 30 | 31 | // Events 32 | event FlashLoanFeePercentageChanged(uint oldFlashLoanFeePercentage, uint newFlashLoanFeePercentage); 33 | 34 | constructor(address _flashLoanFeeRecipient) { 35 | require( 36 | _flashLoanFeeRecipient != address(0), 37 | "INVALID_FLASH_LOAN_FEE_RECIPIENT" 38 | ); 39 | flashLoanFeeRecipient = _flashLoanFeeRecipient; 40 | } 41 | 42 | function setFlashLoanFeeRecipient(address _flashLoanFeeRecipient) external onlyOwner { 43 | require( 44 | _flashLoanFeeRecipient != address(0), 45 | "INVALID_FLASH_LOAN_FEE_RECIPIENT" 46 | ); 47 | flashLoanFeeRecipient = _flashLoanFeeRecipient; 48 | } 49 | 50 | function setFlashLoanFeePercentage(uint newFlashLoanFeePercentage) external onlyOwner { 51 | require( 52 | newFlashLoanFeePercentage <= _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE, 53 | "FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH" 54 | ); 55 | emit FlashLoanFeePercentageChanged(flashLoanFeePercentage, newFlashLoanFeePercentage); 56 | flashLoanFeePercentage = newFlashLoanFeePercentage; 57 | } 58 | 59 | /** 60 | * @dev Returns the protocol fee amount to charge for a flash loan of `amount`. 61 | */ 62 | function _calculateFlashLoanFeeAmount(uint amount) internal view returns (uint) { 63 | return amount * flashLoanFeePercentage / 1e18; 64 | } 65 | 66 | function _payFeeAmount(address token, uint amount) internal { 67 | if (amount != 0) { 68 | address _flashLoanFeeRecipient = flashLoanFeeRecipient; 69 | TransferHelper.safeTransfer(token, _flashLoanFeeRecipient, amount); 70 | IFeeRecipient(_flashLoanFeeRecipient).notifyFees(10, token, amount, flashLoanFeePercentage, ""); 71 | } 72 | } 73 | 74 | /** 75 | * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, 76 | * and then reverting unless the tokens plus a proportional protocol fee have been returned. 77 | * 78 | * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount 79 | * for each token contract. `tokens` must be sorted in ascending order. 80 | * 81 | * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the 82 | * `receiveFlashLoan` call. 83 | * 84 | * Emits `FlashLoan` events. 85 | */ 86 | function flashLoanMultiple( 87 | IFlashLoanRecipient recipient, 88 | address[] memory tokens, 89 | uint[] memory amounts, 90 | bytes memory userData 91 | ) external override nonReentrant whenNotPaused { 92 | uint tokensLength = tokens.length; 93 | require(tokensLength == amounts.length, "INPUT_LENGTH_MISMATCH"); 94 | 95 | uint[] memory feeAmounts = new uint[](tokensLength); 96 | uint[] memory preLoanBalances = new uint[](tokensLength); 97 | 98 | // Used to ensure `tokens` is sorted in ascending order, which ensures token uniqueness. 99 | address previousToken; 100 | uint i; 101 | 102 | address token; 103 | uint amount; 104 | 105 | for (; i < tokensLength; ) { 106 | token = tokens[i]; 107 | amount = amounts[i]; 108 | 109 | require(token > previousToken, token == address(0) ? "ZERO_TOKEN" : "UNSORTED_TOKENS"); 110 | previousToken = token; 111 | 112 | preLoanBalances[i] = IERC20(token).balanceOf(address(this)); 113 | feeAmounts[i] = _calculateFlashLoanFeeAmount(amount); 114 | 115 | require(preLoanBalances[i] >= amount, "INSUFFICIENT_FLASH_LOAN_BALANCE"); 116 | TransferHelper.safeTransfer(token, address(recipient), amount); 117 | 118 | unchecked { 119 | ++i; 120 | } 121 | } 122 | 123 | recipient.receiveFlashLoan(tokens, amounts, feeAmounts, userData); 124 | 125 | uint preLoanBalance; 126 | uint postLoanBalance; 127 | uint receivedFeeAmount; 128 | 129 | for (i = 0; i < tokensLength; ) { 130 | token = tokens[i]; 131 | preLoanBalance = preLoanBalances[i]; 132 | 133 | // Checking for loan repayment first (without accounting for fees) makes for simpler debugging, and results 134 | // in more accurate revert reasons if the flash loan protocol fee percentage is zero. 135 | postLoanBalance = IERC20(token).balanceOf(address(this)); 136 | require(postLoanBalance >= preLoanBalance, "INVALID_POST_LOAN_BALANCE"); 137 | 138 | // No need for checked arithmetic since we know the loan was fully repaid. 139 | receivedFeeAmount = postLoanBalance - preLoanBalance; 140 | require(receivedFeeAmount >= feeAmounts[i], "INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT"); 141 | 142 | _payFeeAmount(token, receivedFeeAmount); 143 | emit FlashLoan(address(recipient), token, amounts[i], receivedFeeAmount); 144 | 145 | unchecked { 146 | ++i; 147 | } 148 | } 149 | } 150 | 151 | // EIP-3156 Implementations 152 | 153 | /** 154 | * @dev The amount of currency available to be lent. 155 | * @param token The loan currency. 156 | * @return The amount of `token` that can be borrowed. 157 | */ 158 | function maxFlashLoan(address token) external view override returns (uint256) { 159 | return IERC20(token).balanceOf(address(this)); 160 | } 161 | 162 | /** 163 | * @dev The fee to be charged for a given loan. 164 | * @param amount The amount of tokens lent. 165 | * @return The amount of `token` to be charged for the loan, on top of the returned principal. 166 | */ 167 | function flashFee(address /*token*/, uint256 amount) external view override returns (uint256) { 168 | return _calculateFlashLoanFeeAmount(amount); 169 | } 170 | 171 | /** 172 | * @dev Initiate a flash loan. 173 | * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. 174 | * @param token The loan currency. 175 | * @param amount The amount of tokens lent. 176 | * @param userData Arbitrary data structure, intended to contain user-defined parameters. 177 | */ 178 | function flashLoan( 179 | IERC3156FlashBorrower receiver, 180 | address token, 181 | uint amount, 182 | bytes memory userData 183 | ) external override nonReentrant whenNotPaused returns (bool) { 184 | uint preLoanBalance = IERC20(token).balanceOf(address(this)); 185 | uint feeAmount = _calculateFlashLoanFeeAmount(amount); 186 | 187 | require(preLoanBalance >= amount, "INSUFFICIENT_FLASH_LOAN_BALANCE"); 188 | TransferHelper.safeTransfer(token, address(receiver), amount); 189 | 190 | require( 191 | receiver.onFlashLoan(msg.sender, token, amount, feeAmount, userData) == ERC3156_CALLBACK_SUCCESS, 192 | "IERC3156_CALLBACK_FAILED" 193 | ); 194 | 195 | // Checking for loan repayment first (without accounting for fees) makes for simpler debugging, and results 196 | // in more accurate revert reasons if the flash loan protocol fee percentage is zero. 197 | uint postLoanBalance = IERC20(token).balanceOf(address(this)); 198 | require(postLoanBalance >= preLoanBalance, "INVALID_POST_LOAN_BALANCE"); 199 | 200 | // No need for checked arithmetic since we know the loan was fully repaid. 201 | uint receivedFeeAmount = postLoanBalance - preLoanBalance; 202 | require(receivedFeeAmount >= feeAmount, "INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT"); 203 | 204 | _payFeeAmount(token, receivedFeeAmount); 205 | 206 | emit FlashLoan(address(receiver), token, amount, receivedFeeAmount); 207 | return true; 208 | } 209 | } -------------------------------------------------------------------------------- /deploy-utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; 2 | import { BytesLike, Contract, Overrides } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 5 | import { Wallet } from "zksync-web3"; 6 | import * as secrets from "../secrets.json"; 7 | 8 | let wallet: Wallet; 9 | let deployer: Deployer; 10 | 11 | const deployedContracts: Map = new Map(); 12 | 13 | function _createWalletAndDeployer( 14 | hre: HardhatRuntimeEnvironment 15 | ): [Wallet, Deployer] { 16 | const wallet = new Wallet(secrets.privateKey); 17 | console.log(`Wallet created ${wallet.address}`); 18 | return [wallet, new Deployer(hre, wallet)]; 19 | } 20 | 21 | export function initializeZkSyncWalletAndDeployer( 22 | hre: HardhatRuntimeEnvironment 23 | ): void { 24 | if (wallet === undefined || deployer === undefined) { 25 | console.log(`Creating wallet and deployer..`); 26 | [wallet, deployer] = _createWalletAndDeployer(hre); 27 | } 28 | } 29 | 30 | export enum ArgumentTypes { 31 | ACCOUNT, 32 | CONTRACT 33 | } 34 | 35 | export function createArgument( 36 | type: ArgumentTypes, 37 | contractName?: string | undefined 38 | ): string { 39 | if (type === ArgumentTypes.ACCOUNT) { 40 | return wallet.address; 41 | } 42 | 43 | if (type === ArgumentTypes.CONTRACT) { 44 | if (contractName === undefined) { 45 | throw Error(`Must specify a contract name for CONTRACT argument type.`); 46 | } 47 | const contract: Contract | undefined = deployedContracts.get(contractName); 48 | if (contract === undefined) { 49 | throw Error(`Contract ${contractName} not found on lookup argument.`); 50 | } 51 | return contract.address; 52 | } 53 | 54 | throw Error(`Unknown argument type: ${type}`); 55 | } 56 | 57 | export async function deployContract( 58 | contractName: string, 59 | artifactName: string, 60 | constructorArguments: any[], 61 | overrides?: Overrides | undefined, 62 | additionalFactoryDeps?: BytesLike[] | undefined 63 | ): Promise { 64 | console.log(`\nDeploying contract '${contractName}' with arguments ${constructorArguments}`); 65 | 66 | const artifact = await deployer.loadArtifact(artifactName); 67 | const contract = await deployer.deploy( 68 | artifact, 69 | constructorArguments, { 70 | ...overrides, 71 | //feeToken: FEE_TOKEN, 72 | gasLimit: '200000000', 73 | }, 74 | additionalFactoryDeps 75 | ); 76 | 77 | deployedContracts.set(contractName, contract); 78 | await contract.deployed(); 79 | console.log(`Contract '${contractName}' deployed to ${contract.address}`); 80 | 81 | return contract; 82 | } 83 | 84 | export async function deployContractEth( 85 | contractName: string, 86 | artifactName: string, 87 | constructorArguments: any[], 88 | overrides?: Overrides | undefined, 89 | ): Promise { 90 | console.log(`\nDeploying contract '${contractName}' with arguments ${constructorArguments}`); 91 | 92 | const factory = await ethers.getContractFactory(artifactName); 93 | const contract = await factory.deploy( 94 | ...constructorArguments, { 95 | ...overrides, 96 | }, 97 | ); 98 | 99 | deployedContracts.set(contractName, contract); 100 | await contract.deployed(); 101 | console.log(`Contract '${contractName}' deployed to ${contract.address}`); 102 | 103 | return contract; 104 | } -------------------------------------------------------------------------------- /deploy/deployFactoryMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const wETHAddress: string = '0x5aea5775959fbc2557cc8789bc1bf90a239d9a91'; // zkSync Mainnet WETH 8 | 9 | const vaultAddress = '0x621425a1Ef6abE91058E9712575dcc4258F8d091'; // zkSync Mainnet Vault 10 | const forwarderRegistryAddress = '0xF09B5EB4aa68Af47a8522155f8F73E93FB91F9d2'; // zkSync Mainnet Forwarder Registry 11 | const poolMasterAddress = '0xbB05918E9B4bA9Fe2c8384d223f0844867909Ffb'; 12 | 13 | const classicFactory = await deployContract('classicPoolFactory', 'SyncSwapClassicPoolFactory', 14 | [poolMasterAddress], 15 | ); 16 | 17 | const stableFactory = await deployContract('stablePoolFactory', 'SyncSwapStablePoolFactory', 18 | [poolMasterAddress], 19 | ); 20 | 21 | const router = await deployContract('router', 'SyncSwapRouter', 22 | [vaultAddress, wETHAddress], 23 | ); 24 | } -------------------------------------------------------------------------------- /deploy/deployFeeManagerMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const feeRecipientAddress = '0x432bcc3BC62DE9186f9E8763C82d43e418681e6C'; // Mainnet 8 | 9 | await deployContract('feeManager', 'SyncSwapFeeManager', 10 | [feeRecipientAddress] 11 | ); 12 | } -------------------------------------------------------------------------------- /deploy/deployFeeManagerTestnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const feeRecipientAddress = '0x98f25D9E5473f258106FAA90C5a3993Ca81d61Bd'; // Testnet 8 | 9 | await deployContract('feeManager', 'SyncSwapFeeManager', 10 | [feeRecipientAddress] 11 | ); 12 | } -------------------------------------------------------------------------------- /deploy/deployFeeRecipientMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const feeRegistryAddress = '0x512fB27961D8204A94151bC03d5722FeBdc527c2'; // Mainnet 8 | 9 | await deployContract('feeRecipient', 'SyncSwapFeeRecipient', 10 | [feeRegistryAddress] 11 | ); 12 | } -------------------------------------------------------------------------------- /deploy/deployFeeRecipientTestnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const feeRegistryAddress = '0x82Ec84c7368bb9089E1077c6e1703675c35A4237'; // Testnet 8 | 9 | await deployContract('feeRecipient', 'SyncSwapFeeRecipient', 10 | [feeRegistryAddress] 11 | ); 12 | } -------------------------------------------------------------------------------- /deploy/deployFeeRegistryMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const vaultAddress = '0x621425a1Ef6abE91058E9712575dcc4258F8d091'; // zkSync Mainnet Vault 8 | const forwarderRegistryAddress = '0xF09B5EB4aa68Af47a8522155f8F73E93FB91F9d2'; // zkSync Mainnet Forwarder Registry 9 | const poolMasterAddress = '0xbB05918E9B4bA9Fe2c8384d223f0844867909Ffb'; 10 | 11 | const feeRegistry = await deployContract('feeRegistry', 'FeeRegistry', 12 | [poolMasterAddress] 13 | ); 14 | 15 | console.log('Adding vault as fee sender...'); 16 | await feeRegistry.setSenderWhitelisted(vaultAddress, true); 17 | console.log('Added vault as fee sender.'); 18 | 19 | // 5. Fee Recipient 20 | const feeRecipient = await deployContract('feeRecipient', 'SyncSwapFeeRecipient', 21 | [feeRegistry.address] 22 | ); 23 | 24 | // 6. Fee Manager 25 | const feeManager = await deployContract('feeManager', 'SyncSwapFeeManager', 26 | [feeRecipient.address] 27 | ); 28 | } -------------------------------------------------------------------------------- /deploy/deployForwarderRegistry.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const forwarderRegistry = await deployContract('forwarderRegistry', 'ForwarderRegistry', 8 | [] 9 | ); 10 | } -------------------------------------------------------------------------------- /deploy/deployMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | 5 | export default async function (hre: HardhatRuntimeEnvironment) { 6 | initializeZkSyncWalletAndDeployer(hre); 7 | 8 | const wETHAddress: string = '0x5aea5775959fbc2557cc8789bc1bf90a239d9a91'; // zkSync Mainnet WETH 9 | 10 | // 1. Vault 11 | const vault = await deployContract('vault', 'SyncSwapVault', 12 | [wETHAddress], 13 | ); 14 | 15 | // 2. Forwarder Registry 16 | const forwarderRegistry = await deployContract('forwarderRegistry', 'ForwarderRegistry', 17 | [] 18 | ); 19 | 20 | // 3. Pool Master 21 | const master = await deployContract('master', 'SyncSwapPoolMaster', 22 | [vault.address, forwarderRegistry.address, ZERO_ADDRESS], 23 | ); 24 | 25 | // 4. Fee Registry 26 | const feeRegistry = await deployContract('feeRegistry', 'FeeRegistry', 27 | [master.address] 28 | ); 29 | 30 | console.log('Adding vault as fee sender...'); 31 | await feeRegistry.setSenderWhitelisted(vault.address, true); 32 | console.log('Added vault as fee sender.'); 33 | 34 | // 5. Fee Recipient 35 | const feeRecipient = await deployContract('feeRecipient', 'SyncSwapFeeRecipient', 36 | [feeRegistry.address] 37 | ); 38 | 39 | // 6. Fee Manager 40 | const feeManager = await deployContract('feeManager', 'SyncSwapFeeManager', 41 | [feeRecipient.address] 42 | ); 43 | 44 | console.log('Initializing fee manager to master...'); 45 | await master.setFeeManager(feeManager.address); 46 | console.log('Initialized fee manager to master.'); 47 | 48 | // 7. Classic Pool Factory 49 | const classicFactory = await deployContract('classicPoolFactory', 'SyncSwapClassicPoolFactory', 50 | [master.address], 51 | ); 52 | 53 | console.log('Whitelisting classic factory...'); 54 | await master.setFactoryWhitelisted(classicFactory.address, true); 55 | console.log('Whitelisted classic factory.'); 56 | 57 | // 8. Stable Pool Factory 58 | const stableFactory = await deployContract('stablePoolFactory', 'SyncSwapStablePoolFactory', 59 | [master.address], 60 | ); 61 | 62 | console.log('Whitelisting stable factory...'); 63 | await master.setFactoryWhitelisted(stableFactory.address, true); 64 | console.log('Whitelisted stable factory.'); 65 | 66 | // 9. Router 67 | const router = await deployContract('router', 'SyncSwapRouter', 68 | [vault.address, wETHAddress], 69 | ); 70 | 71 | console.log('Adding router as forwarder...'); 72 | await forwarderRegistry.addForwarder(router.address); 73 | console.log('Added router as forwarder.'); 74 | } -------------------------------------------------------------------------------- /deploy/deployPolygonTestnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { ethers } from "hardhat"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | import { deployContractEth } from "../deploy-utils/helper"; 5 | 6 | async function main() { 7 | const wETHAddress: string = '0xee589e91401066068af129b0005ac3ef69e3fdb4'; // Polygon zkEVM Testnet WETH 8 | 9 | // 1. Vault 10 | const vault = await deployContractEth('vault', 'SyncSwapVault', 11 | [wETHAddress], 12 | ); 13 | 14 | // 2. Forwarder Registry 15 | const forwarderRegistry = await deployContractEth('forwarderRegistry', 'ForwarderRegistry', 16 | [] 17 | ); 18 | 19 | // 3. Pool Master 20 | const master = await deployContractEth('master', 'SyncSwapPoolMaster', 21 | [vault.address, forwarderRegistry.address, ZERO_ADDRESS], 22 | ); 23 | 24 | // 4. Fee Registry 25 | const feeRegistry = await deployContractEth('feeRegistry', 'FeeRegistry', 26 | [master.address] 27 | ); 28 | 29 | console.log('Adding vault as fee sender...'); 30 | await feeRegistry.setSenderWhitelisted(vault.address, true); 31 | console.log('Added vault as fee sender.'); 32 | 33 | // 5. Fee Recipient 34 | const feeRecipient = await deployContractEth('feeRecipient', 'SyncSwapFeeRecipient', 35 | [feeRegistry.address] 36 | ); 37 | 38 | // 6. Fee Manager 39 | const feeManager = await deployContractEth('feeManager', 'SyncSwapFeeManager', 40 | [feeRecipient.address] 41 | ); 42 | 43 | console.log('Initializing fee manager to master...'); 44 | await master.setFeeManager(feeManager.address); 45 | console.log('Initialized fee manager to master.'); 46 | 47 | // 7. Classic Pool Factory 48 | const classicFactory = await deployContractEth('classicPoolFactory', 'SyncSwapClassicPoolFactory', 49 | [master.address], 50 | ); 51 | 52 | console.log('Whitelisting classic factory...'); 53 | await master.setFactoryWhitelisted(classicFactory.address, true); 54 | console.log('Whitelisted classic factory.'); 55 | 56 | // 8. Stable Pool Factory 57 | const stableFactory = await deployContractEth('stablePoolFactory', 'SyncSwapStablePoolFactory', 58 | [master.address], 59 | ); 60 | 61 | console.log('Whitelisting stable factory...'); 62 | await master.setFactoryWhitelisted(stableFactory.address, true); 63 | console.log('Whitelisted stable factory.'); 64 | 65 | // 9. Router 66 | const router = await deployContractEth('router', 'SyncSwapRouter', 67 | [vault.address, wETHAddress], 68 | ); 69 | 70 | console.log('Adding router as forwarder...'); 71 | await forwarderRegistry.addForwarder(router.address); 72 | console.log('Added router as forwarder.'); 73 | } 74 | 75 | // We recommend this pattern to be able to use async/await everywhere 76 | // and properly handle errors. 77 | main().catch((error) => { 78 | console.error(error); 79 | process.exitCode = 1; 80 | }); -------------------------------------------------------------------------------- /deploy/deployPoolMasterMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | 5 | export default async function (hre: HardhatRuntimeEnvironment) { 6 | initializeZkSyncWalletAndDeployer(hre); 7 | 8 | const vaultAddress = '0x621425a1Ef6abE91058E9712575dcc4258F8d091'; // zkSync Mainnet Vault 9 | const forwarderRegistryAddress = '0xF09B5EB4aa68Af47a8522155f8F73E93FB91F9d2'; // zkSync Mainnet Forwarder Registry 10 | 11 | await deployContract('master', 'SyncSwapPoolMaster', 12 | [vaultAddress, forwarderRegistryAddress, ZERO_ADDRESS], 13 | ); 14 | } -------------------------------------------------------------------------------- /deploy/deployScrollTestnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { ethers } from "hardhat"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | import { deployContractEth } from "../deploy-utils/helper"; 5 | 6 | async function main() { 7 | const wETHAddress: string = '0x7160570BB153Edd0Ea1775EC2b2Ac9b65F1aB61B'; // Scroll Testnet WETH 8 | 9 | // 1. Vault 10 | const vault = await deployContractEth('vault', 'SyncSwapVault', 11 | [wETHAddress], 12 | ); 13 | 14 | // 2. Forwarder Registry 15 | const forwarderRegistry = await deployContractEth('forwarderRegistry', 'ForwarderRegistry', 16 | [] 17 | ); 18 | 19 | // 3. Pool Master 20 | const master = await deployContractEth('master', 'SyncSwapPoolMaster', 21 | [vault.address, forwarderRegistry.address, ZERO_ADDRESS], 22 | ); 23 | 24 | // 4. Fee Registry 25 | const feeRegistry = await deployContractEth('feeRegistry', 'FeeRegistry', 26 | [master.address] 27 | ); 28 | 29 | console.log('Adding vault as fee sender...'); 30 | await feeRegistry.setSenderWhitelisted(vault.address, true); 31 | console.log('Added vault as fee sender.'); 32 | 33 | // 5. Fee Recipient 34 | const feeRecipient = await deployContractEth('feeRecipient', 'SyncSwapFeeRecipient', 35 | [feeRegistry.address] 36 | ); 37 | 38 | // 6. Fee Manager 39 | const feeManager = await deployContractEth('feeManager', 'SyncSwapFeeManager', 40 | [feeRecipient.address] 41 | ); 42 | 43 | console.log('Initializing fee manager to master...'); 44 | await master.setFeeManager(feeManager.address); 45 | console.log('Initialized fee manager to master.'); 46 | 47 | // 7. Classic Pool Factory 48 | const classicFactory = await deployContractEth('classicPoolFactory', 'SyncSwapClassicPoolFactory', 49 | [master.address], 50 | ); 51 | 52 | console.log('Whitelisting classic factory...'); 53 | await master.setFactoryWhitelisted(classicFactory.address, true); 54 | console.log('Whitelisted classic factory.'); 55 | 56 | // 8. Stable Pool Factory 57 | const stableFactory = await deployContractEth('stablePoolFactory', 'SyncSwapStablePoolFactory', 58 | [master.address], 59 | ); 60 | 61 | console.log('Whitelisting stable factory...'); 62 | await master.setFactoryWhitelisted(stableFactory.address, true); 63 | console.log('Whitelisted stable factory.'); 64 | 65 | // 9. Router 66 | const router = await deployContractEth('router', 'SyncSwapRouter', 67 | [vault.address, wETHAddress], 68 | ); 69 | 70 | console.log('Adding router as forwarder...'); 71 | await forwarderRegistry.addForwarder(router.address); 72 | console.log('Added router as forwarder.'); 73 | } 74 | 75 | // We recommend this pattern to be able to use async/await everywhere 76 | // and properly handle errors. 77 | main().catch((error) => { 78 | console.error(error); 79 | process.exitCode = 1; 80 | }); -------------------------------------------------------------------------------- /deploy/deployTemp.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | 5 | export default async function (hre: HardhatRuntimeEnvironment) { 6 | initializeZkSyncWalletAndDeployer(hre); 7 | 8 | const wETHAddress: string = '0x20b28B1e4665FFf290650586ad76E977EAb90c5D'; 9 | //const feeRecipientAddress: string = createArgument(ArgumentTypes.ACCOUNT); 10 | 11 | // 1. Vault 12 | const vaultAddress = '0x29B46ca1f2610019B01484d1aDf83a4f51bBCD9c' 13 | 14 | // 2. Forwarder Registry 15 | const forwarderRegistryAddress = '0xA878b73Fb0fd1863D8EE5d2d30120E3F5a548993' 16 | 17 | // 3. Pool Master 18 | const masterAddress = '0x716a8c2d07288EEeEf2151efC67a9b62F808b34a' 19 | 20 | // 4. Fee Registry 21 | const feeRegistryAddress = '0xCd89A32562df49A09B05f30F727B855BFC42661e' 22 | 23 | // 5. Fee Recipient 24 | const feeRecipientAddress = '0x7C14dd69165B786D7040446A43088bD122505f83' 25 | 26 | // Done 27 | //console.log('Adding vault as fee sender...'); 28 | //await feeRegistry.setSenderWhitelisted(vaultAddress, true); 29 | //console.log('Added vault as fee sender.'); 30 | 31 | // 6. Fee Manager 32 | const feeManager = await deployContract('feeManager', 'SyncSwapFeeManager', 33 | [feeRecipientAddress] 34 | ); 35 | 36 | //console.log('Initializing fee manager to master...'); 37 | //await master.setFeeManager(feeManager.address); 38 | //console.log('Initialized fee manager to master.'); 39 | 40 | // 7. Classic Pool Factory 41 | const classicFactory = await deployContract('classicPoolFactory', 'SyncSwapClassicPoolFactory', 42 | [masterAddress], 43 | ); 44 | 45 | //console.log('Whitelisting classic factory...'); 46 | //await master.setFactoryWhitelisted(classicFactory.address, true); 47 | //console.log('Whitelisted classic factory.'); 48 | 49 | // 8. Stable Pool Factory 50 | const stableFactory = await deployContract('stablePoolFactory', 'SyncSwapStablePoolFactory', 51 | [masterAddress], 52 | ); 53 | 54 | //console.log('Whitelisting stable factory...'); 55 | //await master.setFactoryWhitelisted(stableFactory.address, true); 56 | //console.log('Whitelisted stable factory.'); 57 | 58 | // 9. Router 59 | const router = await deployContract('router', 'SyncSwapRouter', 60 | [vaultAddress, wETHAddress], 61 | ); 62 | 63 | //console.log('Adding router as forwarder...'); 64 | //await forwarderRegistry.addForwarder(router.address); 65 | //console.log('Added router as forwarder.'); 66 | } -------------------------------------------------------------------------------- /deploy/deployTestnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | import { ZERO_ADDRESS } from "../test/shared/utilities"; 4 | 5 | export default async function (hre: HardhatRuntimeEnvironment) { 6 | initializeZkSyncWalletAndDeployer(hre); 7 | 8 | const wETHAddress: string = '0x20b28B1e4665FFf290650586ad76E977EAb90c5D'; // zkSync Testnet WETH 9 | 10 | // 1. Vault 11 | const vault = await deployContract('vault', 'SyncSwapVault', 12 | [wETHAddress], 13 | ); 14 | 15 | // 2. Forwarder Registry 16 | const forwarderRegistry = await deployContract('forwarderRegistry', 'ForwarderRegistry', 17 | [] 18 | ); 19 | 20 | // 3. Pool Master 21 | const master = await deployContract('master', 'SyncSwapPoolMaster', 22 | [vault.address, forwarderRegistry.address, ZERO_ADDRESS], 23 | ); 24 | 25 | // 4. Fee Registry 26 | const feeRegistry = await deployContract('feeRegistry', 'FeeRegistry', 27 | [master.address] 28 | ); 29 | 30 | console.log('Adding vault as fee sender...'); 31 | await feeRegistry.setSenderWhitelisted(vault.address, true); 32 | console.log('Added vault as fee sender.'); 33 | 34 | // 5. Fee Recipient 35 | const feeRecipient = await deployContract('feeRecipient', 'SyncSwapFeeRecipient', 36 | [feeRegistry.address] 37 | ); 38 | 39 | // 6. Fee Manager 40 | const feeManager = await deployContract('feeManager', 'SyncSwapFeeManager', 41 | [feeRecipient.address] 42 | ); 43 | 44 | console.log('Initializing fee manager to master...'); 45 | await master.setFeeManager(feeManager.address); 46 | console.log('Initialized fee manager to master.'); 47 | 48 | // 7. Classic Pool Factory 49 | const classicFactory = await deployContract('classicPoolFactory', 'SyncSwapClassicPoolFactory', 50 | [master.address], 51 | ); 52 | 53 | console.log('Whitelisting classic factory...'); 54 | await master.setFactoryWhitelisted(classicFactory.address, true); 55 | console.log('Whitelisted classic factory.'); 56 | 57 | // 8. Stable Pool Factory 58 | const stableFactory = await deployContract('stablePoolFactory', 'SyncSwapStablePoolFactory', 59 | [master.address], 60 | ); 61 | 62 | console.log('Whitelisting stable factory...'); 63 | await master.setFactoryWhitelisted(stableFactory.address, true); 64 | console.log('Whitelisted stable factory.'); 65 | 66 | // 9. Router 67 | const router = await deployContract('router', 'SyncSwapRouter', 68 | [vault.address, wETHAddress], 69 | ); 70 | 71 | console.log('Adding router as forwarder...'); 72 | await forwarderRegistry.addForwarder(router.address); 73 | console.log('Added router as forwarder.'); 74 | } -------------------------------------------------------------------------------- /deploy/deployVaultMainnet.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 2 | import { deployContract, initializeZkSyncWalletAndDeployer } from "../deploy-utils/helper"; 3 | 4 | export default async function (hre: HardhatRuntimeEnvironment) { 5 | initializeZkSyncWalletAndDeployer(hre); 6 | 7 | const wETHAddress: string = '0x5aea5775959fbc2557cc8789bc1bf90a239d9a91'; // zkSync Mainnet WETH 8 | 9 | await deployContract('vault', 'SyncSwapVault', 10 | [wETHAddress], 11 | ); 12 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@matterlabs/hardhat-zksync-deploy"; 2 | import "@matterlabs/hardhat-zksync-verify"; 3 | import "@matterlabs/hardhat-zksync-solc"; 4 | import "@nomiclabs/hardhat-waffle"; 5 | import "@nomiclabs/hardhat-solhint"; 6 | import "hardhat-gas-reporter"; 7 | require('dotenv').config() 8 | 9 | module.exports = { 10 | // hardhat-zksync-solc 11 | // The compiler configuration for zkSync artifacts. 12 | zksolc: { 13 | version: "latest", 14 | compilerSource: "binary", 15 | settings: { 16 | compilerPath: "./zksolc-linux-amd64-musl-v1.3.5", 17 | }, 18 | }, 19 | 20 | // The compiler configuration for default artifacts. 21 | solidity: { 22 | version: "0.8.15", 23 | settings: { 24 | optimizer: { 25 | enabled: true, 26 | runs: 200, 27 | details: { 28 | yul: false 29 | } 30 | }, 31 | } 32 | }, 33 | 34 | paths: { 35 | sources: "./contracts", 36 | tests: "./test", 37 | cache: "./cache", 38 | artifacts: "./artifacts" 39 | }, 40 | 41 | defaultNetwork: 'hardhat', 42 | networks: { 43 | hardhat: { 44 | chainId: 280, 45 | gasMultiplier: 0, 46 | initialBaseFeePerGas: 0, 47 | }, 48 | 49 | goerli: { 50 | url: "https://endpoints.omniatech.io/v1/eth/goerli/public" 51 | }, 52 | 53 | mainnet: { 54 | url: "https://eth.llamarpc.com" 55 | }, 56 | 57 | zkSyncTestnet: { 58 | zksync: true, 59 | // URL of the Ethereum Web3 RPC, or the identifier of the network (e.g. `mainnet` or `goerli`) 60 | ethNetwork: "goerli", 61 | // URL of the zkSync network RPC 62 | url: 'https://zksync2-testnet.zksync.dev', 63 | // Verification endpoint for Goerli 64 | verifyURL: 'https://testnet-explorer.zksync.dev/contract_verification' 65 | }, 66 | 67 | zkSyncMainnet: { 68 | zksync: true, 69 | ethNetwork: "mainnet", 70 | url: 'https://zksync2-mainnet.zksync.io', 71 | verifyURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification' 72 | }, 73 | 74 | baseGoerli: { 75 | url: "https://goerli.base.org", 76 | accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 77 | }, 78 | 79 | scrollTestnet: { 80 | url: "https://alpha-rpc.scroll.io/l2", 81 | accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 82 | }, 83 | 84 | polygonTestnet: { 85 | url: "https://rpc.public.zkevm-test.net", 86 | accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 87 | } 88 | }, 89 | mocha: { 90 | timeout: 40000 91 | }, 92 | }; -------------------------------------------------------------------------------- /nethereum-gen.settings: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Unisolid", 3 | "namespace": "Unisolid.Contracts", 4 | "lang": 0, 5 | "autoCodeGen": true, 6 | "projectPath": "../" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@syncswap/core-contracts", 3 | "description": "Core smart contracts of the SyncSwap protocol", 4 | "version": "1.0.0", 5 | "homepage": "https://syncswap.xyz", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/syncswap/core-contracts" 9 | }, 10 | "keywords": [ 11 | "syncswap", 12 | "zksync", 13 | "ethereum" 14 | ], 15 | "license": "AGPL-3.0-or-later", 16 | "devDependencies": { 17 | "@matterlabs/hardhat-zksync-chai-matchers": "^0.1.0", 18 | "@matterlabs/hardhat-zksync-deploy": "^0.6.2", 19 | "@matterlabs/hardhat-zksync-solc": "^0.3.14", 20 | "@matterlabs/hardhat-zksync-toolbox": "^0.1.0", 21 | "@matterlabs/hardhat-zksync-verify": "^0.1.2", 22 | "@matterlabs/hardhat-zksync-vyper": "^0.1.7", 23 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", 24 | "@nomiclabs/hardhat-ethers": "^2.2.2", 25 | "@nomiclabs/hardhat-etherscan": "^3.1.6", 26 | "@nomiclabs/hardhat-solhint": "^2.0.1", 27 | "@nomiclabs/hardhat-waffle": "^2.0.3", 28 | "@types/chai": "^4.3.1", 29 | "@types/mocha": "^9.1.1", 30 | "chai": "^4.3.6", 31 | "ethereum-waffle": "^3.4.4", 32 | "ethereumjs-util": "^7.1.4", 33 | "ethers": "^5.6.1", 34 | "hardhat": "^2.12.7", 35 | "hardhat-gas-reporter": "^1.0.9", 36 | "mocha": "^10.0.0", 37 | "ts-node": "^10.7.0", 38 | "typescript": "^4.6.2", 39 | "zksync-web3": "^0.14.3" 40 | }, 41 | "scripts": { 42 | "build": "yarn hardhat compile", 43 | "build-zk": "yarn hardhat compile --network zkSyncTestnet", 44 | "deploy-fee-manager-zksync-testnet": "yarn hardhat deploy-zksync --script deployFeeManagerTestnet.ts --network zkSyncTestnet", 45 | "deploy-fee-manager-zksync-mainnet": "yarn hardhat deploy-zksync --script deployFeeManagerMainnet.ts --network zkSyncMainnet", 46 | "deploy-fee-recipient-zksync-testnet": "yarn hardhat deploy-zksync --script deployFeeRecipientTestnet.ts --network zkSyncTestnet", 47 | "deploy-fee-recipient-zksync-mainnet": "yarn hardhat deploy-zksync --script deployFeeRecipientMainnet.ts --network zkSyncMainnet", 48 | "deploy-zksync-testnet": "yarn hardhat deploy-zksync --script deployTestnet.ts --network zkSyncTestnet", 49 | "deploy-zksync-mainnet": "yarn hardhat deploy-zksync --script deployMainnet.ts --network zkSyncMainnet", 50 | "deploy-scroll-testnet": "yarn hardhat run --network scrollTestnet deploy/deployScrollTestnet.ts", 51 | "deploy-polygon-testnet": "yarn hardhat run --network polygonTestnet deploy/deployPolygonTestnet.ts", 52 | "compile": "yarn hardhat compile", 53 | "test": "yarn hardhat test" 54 | }, 55 | "dependencies": { 56 | "dotenv": "^16.0.3", 57 | "hardhat-deploy": "^0.11.25" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/ClassicPoolFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { Contract } from 'ethers'; 3 | import { solidity } from 'ethereum-waffle'; 4 | import { expandTo18Decimals } from './shared/utilities'; 5 | import { deployClassicPoolFactory, deployPoolMaster, deployTestERC20, deployVault, deployWETH9 } from './shared/fixtures'; 6 | import { Artifact, HardhatRuntimeEnvironment } from 'hardhat/types'; 7 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 8 | import { ZERO_ADDRESS } from './shared/utilities'; 9 | import { defaultAbiCoder } from 'ethers/lib/utils'; 10 | 11 | const hre: HardhatRuntimeEnvironment = require('hardhat'); 12 | const ethers = require("hardhat").ethers; 13 | chai.use(solidity); 14 | 15 | describe('SyncSwapClassicPoolFactory', () => { 16 | let wallets: SignerWithAddress[]; 17 | let testTokens: [string, string]; 18 | 19 | before(async () => { 20 | wallets = await ethers.getSigners(); 21 | 22 | const tokenA = await deployTestERC20(expandTo18Decimals(10000), 18); 23 | const tokenB = await deployTestERC20(expandTo18Decimals(10000), 18); 24 | testTokens = [tokenA.address, tokenB.address]; 25 | }); 26 | 27 | let weth: Contract; 28 | let vault: Contract; 29 | let master: Contract; 30 | let feeManager: Contract; 31 | let factory: Contract; 32 | 33 | beforeEach(async () => { 34 | weth = await deployWETH9(); 35 | vault = await deployVault(weth.address); 36 | [master, feeManager] = await deployPoolMaster(vault.address); 37 | factory = await deployClassicPoolFactory(master); 38 | }); 39 | 40 | /* 41 | it('INIT_CODE_PAIR_HASH', async () => { 42 | expect(await factory.INIT_CODE_PAIR_HASH()).to.eq('0x0a44d25bd998b8cce3bec356e00044787b55feabe1b89cb62eba44ef25855128') 43 | }) 44 | */ 45 | 46 | async function createClassicPool(tokenA: string, tokenB: string) { 47 | const [token0, token1]: [string, string] = ( 48 | tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA] 49 | ); 50 | 51 | const data: string = defaultAbiCoder.encode( 52 | ["address", "address"], [tokenA, tokenB] 53 | ); 54 | await expect(master.createPool(factory.address, data)) 55 | .to.emit(factory, 'PoolCreated') 56 | .to.emit(master, 'RegisterPool'); 57 | 58 | await expect(master.createPool(factory.address, data)).to.be.reverted; 59 | await expect(master.createPool(factory.address, data)).to.be.reverted; 60 | 61 | const poolAddress: string = await factory.getPool(tokenA, tokenB); 62 | expect(await factory.getPool(tokenB, tokenA)).to.eq(poolAddress); 63 | expect(await master.isPool(poolAddress)).to.eq(true); 64 | //expect(await factory.pools(0)).to.eq(poolAddress); 65 | //expect(await factory.poolsLength()).to.eq(1); 66 | 67 | const poolArtifact: Artifact = await hre.artifacts.readArtifact('SyncSwapClassicPool'); 68 | const pool = new Contract(poolAddress, poolArtifact.abi, ethers.provider); 69 | expect(await pool.poolType()).to.eq(1); 70 | expect(await pool.master()).to.eq(master.address); 71 | expect(await pool.vault()).to.eq(vault.address); 72 | expect(await pool.token0()).to.eq(token0); 73 | expect(await pool.token1()).to.eq(token1); 74 | }; 75 | 76 | it('Should create a classic pool', async () => { 77 | await createClassicPool(testTokens[0], testTokens[1]); 78 | }); 79 | 80 | it('Should create a classic pool in reverse tokens', async () => { 81 | await createClassicPool(testTokens[1], testTokens[0]); 82 | }); 83 | 84 | it('Should use expected gas on creating classic pool', async () => { 85 | const data = defaultAbiCoder.encode( 86 | ["address", "address"], [testTokens[0], testTokens[1]] 87 | ); 88 | const tx = await master.createPool(factory.address, data); 89 | const receipt = await tx.wait(); 90 | expect(receipt.gasUsed).to.eq(3751041); // 2512920 for Uniswap V2 91 | }); 92 | 93 | /* 94 | it('Should set a new fee recipient', async () => { 95 | // Set fee recipient using a wrong account. 96 | await expect(factory.connect(wallets[1]).setFeeRecipient(wallets[1].address)).to.be.reverted; 97 | 98 | // Set a new fee recipient. 99 | await factory.setFeeRecipient(wallets[0].address); 100 | 101 | // Expect new fee recipient. 102 | expect(await factory.feeRecipient()).to.eq(wallets[0].address); 103 | }); 104 | 105 | it('Should set a new protocol fee', async () => { 106 | // Expect current protocol fee. 107 | expect(await factory.getProtocolFee()).to.eq(30000); 108 | 109 | // Set protocol fee using wrong account. 110 | await expect(factory.connect(wallets[1]).setProtocolFee(50000)).to.be.reverted; 111 | 112 | // Set a new protocol fee. 113 | await factory.setProtocolFee(50000); 114 | 115 | // Expect new protocol fee. 116 | expect(await factory.getProtocolFee()).to.eq(50000); 117 | }); 118 | */ 119 | }); -------------------------------------------------------------------------------- /test/ERC20Permit2.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { BigNumber, Contract } from 'ethers'; 3 | import { solidity } from 'ethereum-waffle'; 4 | import { expandTo18Decimals, getPermitSignature, getSplittedPermitSignature, MAX_UINT256 } from './shared/utilities'; 5 | import { hexlify } from 'ethers/lib/utils'; 6 | import { deployERC20Permit2 } from './shared/fixtures'; 7 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 8 | 9 | chai.use(solidity) 10 | 11 | const hre = require('hardhat'); 12 | 13 | const TOTAL_SUPPLY = expandTo18Decimals(10000); 14 | const TEST_AMOUNT = expandTo18Decimals(10); 15 | 16 | describe('ERC20Permit2', () => { 17 | let wallet: SignerWithAddress; 18 | let other: SignerWithAddress; 19 | before(async () => { 20 | const accounts = await hre.ethers.getSigners(); 21 | wallet = accounts[0]; 22 | other = accounts[1]; 23 | }) 24 | 25 | let token: Contract; 26 | beforeEach(async () => { 27 | token = await deployERC20Permit2(TOTAL_SUPPLY); 28 | }) 29 | 30 | it('Should return expected token metadata', async () => { 31 | expect(await token.name()).to.eq(''); 32 | expect(await token.symbol()).to.eq(''); 33 | expect(await token.decimals()).to.eq(18); 34 | expect(await token.totalSupply()).to.eq(TOTAL_SUPPLY); 35 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY); 36 | }) 37 | 38 | it('Should approve some tokens', async () => { 39 | await expect(token.approve(other.address, TEST_AMOUNT)) 40 | .to.emit(token, 'Approval') 41 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 42 | 43 | expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT); 44 | }) 45 | 46 | it('Should transfer some tokens', async () => { 47 | await expect(token.transfer(other.address, TEST_AMOUNT)) 48 | .to.emit(token, 'Transfer') 49 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 50 | 51 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)); 52 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT); 53 | }) 54 | 55 | it('Should fail to transfer tokens', async () => { 56 | await expect(token.transfer(other.address, TOTAL_SUPPLY.add(1))).to.be.reverted; // undeflow 57 | await expect(token.connect(other).transfer(wallet.address, 1)).to.be.reverted; // overflow 58 | }) 59 | 60 | it('Should transfer some tokens from other wallet', async () => { 61 | await token.approve(other.address, TEST_AMOUNT); 62 | 63 | await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT)) 64 | .to.emit(token, 'Transfer') 65 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 66 | 67 | expect(await token.allowance(wallet.address, other.address)).to.eq(0); 68 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)); 69 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT); 70 | }) 71 | 72 | it('Should transfer all tokens from other wallet', async () => { 73 | await token.approve(other.address, MAX_UINT256); 74 | 75 | await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT)) 76 | .to.emit(token, 'Transfer') 77 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 78 | 79 | expect(await token.allowance(wallet.address, other.address)).to.eq(MAX_UINT256); 80 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)); 81 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT); 82 | }) 83 | 84 | it('Should permit some tokens by splitted signature', async () => { 85 | const nonce = await token.nonces(wallet.address); 86 | const deadline = MAX_UINT256; 87 | 88 | const { v, r, s } = await getSplittedPermitSignature( 89 | wallet, 90 | token, 91 | { owner: wallet.address, spender: other.address, value: TEST_AMOUNT }, 92 | nonce, 93 | deadline 94 | ); 95 | 96 | await expect(token.permit(wallet.address, other.address, TEST_AMOUNT, deadline, v, hexlify(r), hexlify(s))) 97 | .to.emit(token, 'Approval') 98 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 99 | 100 | expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT); 101 | expect(await token.nonces(wallet.address)).to.eq(BigNumber.from(1)); 102 | }) 103 | 104 | it('Should permit some tokens by array signature', async () => { 105 | const nonce = await token.nonces(wallet.address); 106 | const deadline = MAX_UINT256; 107 | 108 | const signature = await getPermitSignature( 109 | wallet, 110 | token, 111 | { owner: wallet.address, spender: other.address, value: TEST_AMOUNT }, 112 | nonce, 113 | deadline 114 | ); 115 | 116 | await expect(token.permit2(wallet.address, other.address, TEST_AMOUNT, deadline, signature)) 117 | .to.emit(token, 'Approval') 118 | .withArgs(wallet.address, other.address, TEST_AMOUNT); 119 | 120 | expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT); 121 | expect(await token.nonces(wallet.address)).to.eq(BigNumber.from(1)); 122 | }) 123 | }) -------------------------------------------------------------------------------- /test/StablePoolFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { Contract } from 'ethers'; 3 | import { solidity } from 'ethereum-waffle'; 4 | import { expandTo18Decimals } from './shared/utilities'; 5 | import { deployPoolMaster, deployStablePoolFactory, deployTestERC20, deployVault, deployWETH9 } from './shared/fixtures'; 6 | import { Artifact, HardhatRuntimeEnvironment } from 'hardhat/types'; 7 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 8 | import { defaultAbiCoder } from 'ethers/lib/utils'; 9 | 10 | const hre: HardhatRuntimeEnvironment = require('hardhat'); 11 | const ethers = require("hardhat").ethers; 12 | chai.use(solidity); 13 | 14 | describe('SyncSwapStablePoolFactory', () => { 15 | let wallets: SignerWithAddress[]; 16 | let testTokens: [string, string]; 17 | 18 | before(async () => { 19 | wallets = await ethers.getSigners(); 20 | 21 | const tokenA = await deployTestERC20(expandTo18Decimals(10000), 18); 22 | const tokenB = await deployTestERC20(expandTo18Decimals(10000), 18); 23 | testTokens = [tokenA.address, tokenB.address]; 24 | }); 25 | 26 | let weth: Contract; 27 | let vault: Contract; 28 | let master: Contract; 29 | let feeManager: Contract; 30 | let factory: Contract; 31 | 32 | beforeEach(async () => { 33 | weth = await deployWETH9(); 34 | vault = await deployVault(weth.address); 35 | [master, feeManager] = await deployPoolMaster(vault.address); 36 | factory = await deployStablePoolFactory(master); 37 | }); 38 | 39 | /* 40 | it('INIT_CODE_PAIR_HASH', async () => { 41 | expect(await factory.INIT_CODE_PAIR_HASH()).to.eq('0x0a44d25bd998b8cce3bec356e00044787b55feabe1b89cb62eba44ef25855128') 42 | }) 43 | */ 44 | 45 | async function createStablePool(tokenA: string, tokenB: string) { 46 | const [token0, token1]: [string, string] = ( 47 | tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA] 48 | ); 49 | 50 | const data = defaultAbiCoder.encode( 51 | ["address", "address"], [tokenA, tokenB] 52 | ); 53 | await expect(master.createPool(factory.address, data)) 54 | .to.emit(factory, 'PoolCreated') 55 | .to.emit(master, 'RegisterPool'); 56 | 57 | await expect(master.createPool(factory.address, data)).to.be.reverted; 58 | await expect(master.createPool(factory.address, data)).to.be.reverted; 59 | 60 | const poolAddress = await factory.getPool(tokenA, tokenB); 61 | expect(await factory.getPool(tokenB, tokenA)).to.eq(poolAddress); 62 | expect(await master.isPool(poolAddress)).to.eq(true); 63 | //expect(await factory.pools(0)).to.eq(poolAddress); 64 | //expect(await factory.poolsLength()).to.eq(1); 65 | 66 | const poolArtifact: Artifact = await hre.artifacts.readArtifact('SyncSwapStablePool'); 67 | const pool = new Contract(poolAddress, poolArtifact.abi, ethers.provider); 68 | expect(await pool.poolType()).to.eq(2); 69 | expect(await pool.master()).to.eq(master.address); 70 | expect(await pool.vault()).to.eq(vault.address); 71 | expect(await pool.token0()).to.eq(token0); 72 | expect(await pool.token1()).to.eq(token1); 73 | }; 74 | 75 | it('Should create a stable pool', async () => { 76 | await createStablePool(testTokens[0], testTokens[1]); 77 | }); 78 | 79 | it('Should create a stable pool in reverse tokens', async () => { 80 | await createStablePool(testTokens[1], testTokens[0]); 81 | }); 82 | 83 | it('Should use expected gas on creating stable pool', async () => { 84 | const data = defaultAbiCoder.encode( 85 | ["address", "address"], [testTokens[0], testTokens[1]] 86 | ); 87 | const tx = await master.createPool(factory.address, data); 88 | const receipt = await tx.wait(); 89 | expect(receipt.gasUsed).to.eq(4042148); // 2512920 for Uniswap V2 90 | }); 91 | 92 | /* 93 | it('Should set a new fee recipient', async () => { 94 | // Set fee recipient using a wrong account. 95 | await expect(factory.connect(wallets[1]).setFeeRecipient(wallets[1].address)).to.be.reverted; 96 | 97 | // Set a new fee recipient. 98 | await factory.setFeeRecipient(wallets[0].address); 99 | 100 | // Expect new fee recipient. 101 | expect(await factory.feeRecipient()).to.eq(wallets[0].address); 102 | }); 103 | 104 | it('Should set a new protocol fee', async () => { 105 | // Expect current protocol fee. 106 | expect(await factory.getProtocolFee()).to.eq(50000); 107 | 108 | // Set protocol fee using wrong account. 109 | await expect(factory.connect(wallets[1]).setProtocolFee(30000)).to.be.reverted; 110 | 111 | // Set a new protocol fee. 112 | await factory.setProtocolFee(30000); 113 | 114 | // Expect new protocol fee. 115 | expect(await factory.getProtocolFee()).to.eq(30000); 116 | }); 117 | */ 118 | }); -------------------------------------------------------------------------------- /test/SyncSwapVault.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { BigNumber, Contract } from 'ethers'; 3 | import { solidity } from 'ethereum-waffle'; 4 | import { expandTo18Decimals, ZERO, ZERO_ADDRESS } from './shared/utilities'; 5 | import { deployERC20Permit2, deployVault, deployWETH9 } from './shared/fixtures'; 6 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 7 | import { HardhatEthersHelpers } from '@nomiclabs/hardhat-ethers/types'; 8 | 9 | chai.use(solidity); 10 | 11 | const hre = require('hardhat'); 12 | const ethers: HardhatEthersHelpers = hre.ethers; 13 | 14 | const TOTAL_SUPPLY = expandTo18Decimals(10000); 15 | const TEST_AMOUNT = expandTo18Decimals(10); 16 | const NATIVE_ETH = ZERO_ADDRESS; 17 | 18 | describe('SyncSwapVault', () => { 19 | let wallet: SignerWithAddress; 20 | let other: SignerWithAddress; 21 | before(async () => { 22 | const accounts = await ethers.getSigners(); 23 | wallet = accounts[0]; 24 | other = accounts[1]; 25 | }) 26 | 27 | let token: Contract; 28 | let weth: Contract; 29 | let vault: Contract; 30 | beforeEach(async () => { 31 | weth = await deployWETH9(); 32 | vault = await deployVault(weth.address); 33 | token = await deployERC20Permit2(TOTAL_SUPPLY); 34 | }) 35 | 36 | it('Should deposit some ERC20 tokens', async () => { 37 | await token.transfer(vault.address, TEST_AMOUNT); 38 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(0); 39 | 40 | await vault.deposit(token.address, wallet.address); 41 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT); 42 | }); 43 | 44 | it('Should receive and deposit some ERC20 tokens', async () => { 45 | expect(vault.transferAndDeposit(token.address, wallet.address, TEST_AMOUNT)) 46 | .to.be.revertedWith('TransferFromFailed()'); 47 | 48 | const balanceBefore = await token.balanceOf(wallet.address); 49 | 50 | await token.approve(vault.address, TEST_AMOUNT); 51 | await vault.transferAndDeposit(token.address, wallet.address, TEST_AMOUNT); 52 | 53 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT); 54 | expect(await token.balanceOf(wallet.address)).to.eq(balanceBefore.sub(TEST_AMOUNT)); 55 | 56 | expect(vault.transferAndDeposit(token.address, wallet.address, '1')) 57 | .to.be.revertedWith('TransferFromFailed()'); 58 | }); 59 | 60 | it('Should deposit some ETH', async () => { 61 | await vault.deposit(NATIVE_ETH, wallet.address, { 62 | value: TEST_AMOUNT, 63 | }); 64 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT); 65 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT); 66 | }); 67 | 68 | it('Should deposit some ETH via receive', async () => { 69 | await wallet.sendTransaction({ 70 | to: vault.address, 71 | value: TEST_AMOUNT, 72 | }); 73 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT); 74 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT); 75 | }); 76 | 77 | it('Should deposit some wETH', async () => { 78 | // Wrap ETH to wETH. 79 | await weth.deposit({ 80 | value: TEST_AMOUNT, 81 | }); 82 | 83 | await weth.transfer(vault.address, TEST_AMOUNT); 84 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(0); 85 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(0); 86 | 87 | await vault.deposit(weth.address, wallet.address); 88 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT); 89 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT); 90 | }); 91 | 92 | it('Should receive and deposit some wETH', async () => { 93 | // Wrap ETH to wETH. 94 | await weth.deposit({ 95 | value: TEST_AMOUNT, 96 | }); 97 | 98 | await weth.approve(vault.address, TEST_AMOUNT); 99 | await vault.transferAndDeposit(weth.address, wallet.address, TEST_AMOUNT); 100 | 101 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT); 102 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT); 103 | }); 104 | 105 | it('Should transfer some ERC20 tokens', async () => { 106 | // Deposit tokens. 107 | await token.transfer(vault.address, TEST_AMOUNT); 108 | await vault.deposit(token.address, wallet.address); 109 | 110 | expect(vault.connect(other).transfer(token.address, other.address, 1)).to.be.reverted; 111 | 112 | await vault.connect(wallet).transfer(token.address, other.address, 10000); 113 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 114 | expect(await vault.balanceOf(token.address, other.address)).to.eq(10000); 115 | }); 116 | 117 | it('Should transfer all ERC20 tokens', async () => { 118 | // Deposit tokens. 119 | await token.transfer(vault.address, TEST_AMOUNT); 120 | await vault.deposit(token.address, wallet.address); 121 | 122 | expect(vault.transfer(token.address, other.address, TEST_AMOUNT.add(1))).to.be.reverted; 123 | expect(vault.connect(other).transfer(token.address, other.address, TEST_AMOUNT)).to.be.reverted; 124 | 125 | await vault.connect(wallet).transfer(token.address, other.address, TEST_AMOUNT); 126 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(0); 127 | expect(await vault.balanceOf(token.address, other.address)).to.eq(TEST_AMOUNT); 128 | }); 129 | 130 | it('Should transfer zero ERC20 tokens', async () => { 131 | expect(vault.transfer(token.address, other.address, 0)).to.be.not.reverted; 132 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(0); 133 | expect(await vault.balanceOf(token.address, other.address)).to.eq(0); 134 | 135 | // Deposit tokens. 136 | await token.transfer(vault.address, TEST_AMOUNT); 137 | await vault.deposit(token.address, wallet.address); 138 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT); 139 | 140 | expect(vault.transfer(token.address, other.address, 0)).to.be.not.reverted; 141 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT); 142 | expect(await vault.balanceOf(token.address, other.address)).to.eq(0); 143 | }); 144 | 145 | it('Should transfer some ETH', async () => { 146 | // Deposit ETH. 147 | await vault.deposit(NATIVE_ETH, wallet.address, { 148 | value: TEST_AMOUNT, 149 | }); 150 | 151 | await vault.transfer(NATIVE_ETH, other.address, 10000); 152 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 153 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 154 | expect(await vault.balanceOf(weth.address, other.address)).to.eq(10000); 155 | expect(await vault.balanceOf(NATIVE_ETH, other.address)).to.eq(10000); 156 | 157 | await vault.connect(wallet).transfer(weth.address, other.address, 10000); 158 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT.sub(20000)); 159 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT.sub(20000)); 160 | expect(await vault.balanceOf(weth.address, other.address)).to.eq(20000); 161 | expect(await vault.balanceOf(NATIVE_ETH, other.address)).to.eq(20000); 162 | }); 163 | 164 | it('Should transfer all ETH', async () => { 165 | // Deposit ETH. 166 | await vault.deposit(NATIVE_ETH, wallet.address, { 167 | value: TEST_AMOUNT, 168 | }); 169 | 170 | await vault.transfer(NATIVE_ETH, other.address, TEST_AMOUNT); 171 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(0); 172 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(0); 173 | expect(await vault.balanceOf(weth.address, other.address)).to.eq(TEST_AMOUNT); 174 | expect(await vault.balanceOf(NATIVE_ETH, other.address)).to.eq(TEST_AMOUNT); 175 | 176 | // Deposit wETH. 177 | await weth.deposit({ 178 | value: TEST_AMOUNT, 179 | }); 180 | await weth.transfer(vault.address, TEST_AMOUNT); 181 | await vault.deposit(weth.address, wallet.address); 182 | 183 | await vault.connect(wallet).transfer(weth.address, other.address, TEST_AMOUNT); 184 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(0); 185 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(0); 186 | expect(await vault.balanceOf(weth.address, other.address)).to.eq(TEST_AMOUNT.add(TEST_AMOUNT)); 187 | expect(await vault.balanceOf(NATIVE_ETH, other.address)).to.eq(TEST_AMOUNT.add(TEST_AMOUNT)); 188 | }); 189 | 190 | it('Should withdraw some ERC20 tokens', async () => { 191 | // Deposit tokens. 192 | await token.transfer(vault.address, TEST_AMOUNT); 193 | await vault.deposit(token.address, wallet.address); 194 | 195 | const balanceBefore = await token.balanceOf(wallet.address); 196 | 197 | await vault.withdraw(token.address, wallet.address, 10000); 198 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 199 | expect(await token.balanceOf(wallet.address)).to.eq(balanceBefore.add(10000)); 200 | 201 | await vault.withdraw(token.address, other.address, 10000); 202 | expect(await vault.balanceOf(token.address, wallet.address)).to.eq(TEST_AMOUNT.sub(20000)); 203 | expect(await vault.balanceOf(token.address, other.address)).to.eq(0); 204 | expect(await token.balanceOf(other.address)).to.eq(10000); 205 | }); 206 | 207 | async function getGasFees(response: any): Promise { 208 | const receipt = await response.wait(); 209 | return receipt.gasUsed.mul(receipt.effectiveGasPrice); 210 | } 211 | 212 | it('Should withdraw some ETH', async () => { 213 | // Deposit ETH. 214 | await vault.deposit(NATIVE_ETH, wallet.address, { 215 | value: TEST_AMOUNT, 216 | }); 217 | 218 | const balanceBefore = await wallet.getBalance(); 219 | 220 | let fees = ZERO; 221 | fees = fees.add(await getGasFees(await vault.withdraw(NATIVE_ETH, wallet.address, TEST_AMOUNT))); 222 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(0); 223 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(0); 224 | expect(await wallet.getBalance()).to.eq(balanceBefore.add(TEST_AMOUNT).sub(fees)); 225 | 226 | // Deposit wETH. 227 | fees = fees.add(await getGasFees(await weth.deposit({ 228 | value: 20000, 229 | }))); 230 | fees = fees.add(await getGasFees(await weth.transfer(vault.address, 20000))); 231 | fees = fees.add(await getGasFees(await vault.deposit(weth.address, wallet.address))); 232 | 233 | fees = fees.add(await getGasFees(await vault.withdraw(NATIVE_ETH, wallet.address, 10000))); 234 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(10000); 235 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(10000); 236 | expect(await wallet.getBalance()).to.eq(balanceBefore.add(TEST_AMOUNT).sub(10000).sub(fees)); 237 | }); 238 | 239 | it('Should withdraw some wETH', async () => { 240 | // Deposit ETH. 241 | await vault.deposit(NATIVE_ETH, wallet.address, { 242 | value: TEST_AMOUNT, 243 | }); 244 | 245 | await vault.withdraw(weth.address, wallet.address, 10000); 246 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 247 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 248 | expect(await weth.balanceOf(wallet.address)).to.eq(10000); 249 | 250 | // Deposit wETH. 251 | await weth.deposit({ 252 | value: 10000, 253 | }); 254 | await weth.transfer(vault.address, 10000); 255 | await vault.deposit(weth.address, wallet.address); 256 | 257 | await vault.withdraw(weth.address, wallet.address, 10000); 258 | expect(await vault.balanceOf(weth.address, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 259 | expect(await vault.balanceOf(NATIVE_ETH, wallet.address)).to.eq(TEST_AMOUNT.sub(10000)); 260 | expect(await weth.balanceOf(wallet.address)).to.eq(20000); 261 | }); 262 | 263 | }); -------------------------------------------------------------------------------- /test/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | import { 3 | keccak256, 4 | toUtf8Bytes, 5 | } from 'ethers/lib/utils' 6 | 7 | export abstract class Constants { 8 | public static PERMIT_TYPEHASH = keccak256( 9 | toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') 10 | ); 11 | 12 | public static CHAIN_ID = 280; 13 | 14 | public static UINT256_MAX = BigNumber.from(2).pow(256).sub(1); 15 | 16 | public static ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; 17 | 18 | public static ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 19 | } -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract } from 'ethers'; 2 | import { expandTo18Decimals, MAX_UINT256, ZERO, ZERO_ADDRESS } from './utilities'; 3 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 4 | import { HardhatEthersHelpers } from '@nomiclabs/hardhat-ethers/types'; 5 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 6 | import { defaultAbiCoder } from 'ethers/lib/utils'; 7 | 8 | const hre: HardhatRuntimeEnvironment = require("hardhat"); 9 | const ethers: HardhatEthersHelpers = require("hardhat").ethers; 10 | 11 | export async function deployVault(weth: string): Promise { 12 | const contractFactory = await ethers.getContractFactory('SyncSwapVault'); 13 | const contract = await contractFactory.deploy(weth); 14 | await contract.deployed(); 15 | return contract; 16 | } 17 | 18 | export async function deployForwarderRegistry(): Promise { 19 | const contractFactory = await ethers.getContractFactory('ForwarderRegistry'); 20 | const contract = await contractFactory.deploy(); 21 | await contract.deployed(); 22 | return contract; 23 | } 24 | 25 | export async function deployFeeManager(feeRecipient: string): Promise { 26 | const contractFactory = await ethers.getContractFactory('SyncSwapFeeManager'); 27 | const contract = await contractFactory.deploy(feeRecipient); 28 | await contract.deployed(); 29 | return contract; 30 | } 31 | 32 | export async function deployFeeRegistry(master: string): Promise { 33 | const contractFactory = await ethers.getContractFactory('FeeRegistry'); 34 | const contract = await contractFactory.deploy(master); 35 | await contract.deployed(); 36 | return contract; 37 | } 38 | 39 | export async function deployFeeRecipient(feeRegistry: string): Promise { 40 | const contractFactory = await ethers.getContractFactory('SyncSwapFeeRecipient'); 41 | const contract = await contractFactory.deploy(feeRegistry); 42 | await contract.deployed(); 43 | return contract; 44 | } 45 | 46 | export async function deployPoolMaster(vault: string): Promise<[Contract, Contract]> { 47 | const forwarderRegistry = await deployForwarderRegistry(); 48 | 49 | const contractFactory = await ethers.getContractFactory('SyncSwapPoolMaster'); 50 | const master = await contractFactory.deploy(vault, forwarderRegistry.address, ZERO_ADDRESS); 51 | 52 | const feeRegistry = await deployFeeRegistry(master.address); 53 | const feeRecipient = await deployFeeRecipient(feeRegistry.address); 54 | const feeManager = await deployFeeManager(feeRecipient.address); 55 | await master.setFeeManager(feeManager.address); 56 | 57 | await master.deployed(); 58 | return [master, feeManager]; 59 | } 60 | 61 | export async function deployClassicPoolFactory(master: Contract): Promise { 62 | const contractFactory = await ethers.getContractFactory('SyncSwapClassicPoolFactory'); 63 | const contract = await contractFactory.deploy(master.address); 64 | await contract.deployed(); 65 | 66 | // whtielist factory 67 | await master.setFactoryWhitelisted(contract.address, true); 68 | return contract; 69 | } 70 | 71 | export async function deployStablePoolFactory(master: Contract): Promise { 72 | const contractFactory = await ethers.getContractFactory('SyncSwapStablePoolFactory'); 73 | const contract = await contractFactory.deploy(master.address); 74 | await contract.deployed(); 75 | 76 | // whtielist factory 77 | await master.setFactoryWhitelisted(contract.address, true); 78 | return contract; 79 | } 80 | 81 | export async function deployERC20Permit2(totalSupply: BigNumber): Promise { 82 | const contractFactory = await ethers.getContractFactory('TestERC20Permit2'); 83 | const contract = await contractFactory.deploy(totalSupply); 84 | await contract.deployed(); 85 | return contract; 86 | } 87 | 88 | interface PoolFixture { 89 | weth: Contract; 90 | vault: Contract; 91 | master: Contract; 92 | factory: Contract; 93 | token0: Contract; 94 | token1: Contract; 95 | pool: Contract; 96 | } 97 | 98 | export async function classicPoolFixture( 99 | wallet: SignerWithAddress, 100 | deflating0: boolean, 101 | deflating1: boolean, 102 | ): Promise { 103 | const weth = await deployWETH9(); 104 | const vault = await deployVault(weth.address); 105 | 106 | const [master, feeManager] = await deployPoolMaster(vault.address); 107 | feeManager.setDefaultSwapFee(1, 300); // Set fee to 0.3% for testing 108 | const factory = await deployClassicPoolFactory(master); 109 | 110 | const tokenA = deflating0 ? await deployDeflatingERC20(MAX_UINT256) : await deployTestERC20(MAX_UINT256, 18); 111 | const tokenB = deflating1 ? await deployDeflatingERC20(MAX_UINT256) : await deployTestERC20(MAX_UINT256, 18); 112 | const data = defaultAbiCoder.encode( 113 | ["address", "address"], [tokenA.address, tokenB.address] 114 | ); 115 | await master.createPool(factory.address, data); 116 | 117 | const poolArtifact = await hre.artifacts.readArtifact('SyncSwapClassicPool'); 118 | const poolAddress = await factory.getPool(tokenA.address, tokenB.address); 119 | const pool = new Contract(poolAddress, poolArtifact.abi, ethers.provider).connect(wallet); 120 | 121 | const [token0, token1] = Number(tokenA.address) < Number(tokenB.address) ? [tokenA, tokenB] : [tokenB, tokenA]; 122 | 123 | return { weth, vault, master, factory, token0, token1, pool }; 124 | } 125 | 126 | export async function stablePoolFixture( 127 | wallet: SignerWithAddress 128 | ): Promise { 129 | const weth = await deployWETH9(); 130 | const vault = await deployVault(weth.address); 131 | 132 | const [master, feeManager] = await deployPoolMaster(vault.address); 133 | feeManager.setDefaultSwapFee(2, 100); // Set fee to 0.1% for testing 134 | const factory = await deployStablePoolFactory(master); 135 | 136 | const tokenA = await deployTestERC20(MAX_UINT256, 18); 137 | const tokenB = await deployTestERC20(MAX_UINT256, 18); 138 | const data = defaultAbiCoder.encode( 139 | ["address", "address"], [tokenA.address, tokenB.address] 140 | ); 141 | await master.createPool(factory.address, data); 142 | 143 | const poolArtifact = await hre.artifacts.readArtifact('SyncSwapStablePool'); 144 | const poolAddress = await factory.getPool(tokenA.address, tokenB.address); 145 | const pool = new Contract(poolAddress, poolArtifact.abi, ethers.provider).connect(wallet); 146 | 147 | const [token0, token1] = Number(tokenA.address) < Number(tokenB.address) ? [tokenA, tokenB] : [tokenB, tokenA]; 148 | 149 | return { weth, vault, master, factory, token0, token1, pool }; 150 | } 151 | 152 | interface V2Fixture { 153 | token0: Contract; 154 | token1: Contract; 155 | WETH: Contract; 156 | WETHPartner: Contract; 157 | factory: Contract; 158 | router: Contract; 159 | routerEventEmitter: Contract; 160 | pair: Contract; 161 | WETHPair: Contract; 162 | } 163 | 164 | export async function deployTestERC20(totalSupply: BigNumber, decimals: number): Promise { 165 | const contractFactory = await ethers.getContractFactory('TestERC20'); 166 | const contract = await contractFactory.deploy(totalSupply, decimals); 167 | await contract.deployed(); 168 | return contract; 169 | } 170 | 171 | export async function deployDeflatingERC20(totalSupply: BigNumber): Promise { 172 | const contractFactory = await ethers.getContractFactory('DeflatingERC20'); 173 | const contract = await contractFactory.deploy(totalSupply); 174 | await contract.deployed(); 175 | return contract; 176 | } 177 | 178 | export async function deployWETH9(): Promise { 179 | const contractFactory = await ethers.getContractFactory('TestWETH9'); 180 | const contract = await contractFactory.deploy(); 181 | await contract.deployed(); 182 | return contract; 183 | } 184 | 185 | export async function deployRouter(factory: string, WETH: string): Promise { 186 | const contractFactory = await ethers.getContractFactory('SyncSwapRouter'); 187 | const contract = await contractFactory.deploy(factory, WETH); 188 | await contract.deployed(); 189 | return contract; 190 | } 191 | 192 | export async function deployRouterEventEmitter(factory: string, WETH: string): Promise { 193 | const contractFactory = await ethers.getContractFactory('RouterEventEmitter'); 194 | const contract = await contractFactory.deploy(factory, WETH); 195 | await contract.deployed(); 196 | return contract; 197 | } 198 | 199 | export async function routerFixture(): Promise { 200 | const accounts = await ethers.getSigners() 201 | const wallet = accounts[0]; 202 | 203 | // deploy tokens 204 | const tokenA = await deployTestERC20(expandTo18Decimals(10000), 18); 205 | const tokenB = await deployTestERC20(expandTo18Decimals(10000), 18); 206 | const WETH = await deployWETH9(); 207 | const WETHPartner = await deployTestERC20(expandTo18Decimals(10000), 18); 208 | 209 | // deploy core 210 | const vault = await deployVault(WETH.address); 211 | const [master, feeManager] = await deployPoolMaster(vault.address); 212 | const classicFactory = await deployClassicPoolFactory(master); 213 | const stableFactory = await deployStablePoolFactory(master); 214 | 215 | // deploy routers 216 | const router = await deployRouter(classicFactory.address, WETH.address); 217 | 218 | // event emitter for testing 219 | const routerEventEmitter = await deployRouterEventEmitter(classicFactory.address, WETH.address); 220 | 221 | const data = defaultAbiCoder.encode( 222 | ["address", "address"], [tokenA.address, tokenB.address] 223 | ); 224 | await classicFactory.createPool(data); 225 | const pairAddress = await classicFactory.getPool(tokenA.address, tokenB.address, false); 226 | const pairArtifact = await hre.artifacts.readArtifact('SyncSwapPool');; 227 | const pair = new Contract(pairAddress, pairArtifact.abi, ethers.provider).connect(wallet); 228 | 229 | const [token0, token1] = Number(tokenA.address) < Number(tokenB.address) ? [tokenA, tokenB] : [tokenB, tokenA]; 230 | 231 | await classicFactory.createPool(data); 232 | const WETHPairAddress = await classicFactory.getPool(WETH.address, WETHPartner.address, false); 233 | const WETHPair = new Contract(WETHPairAddress, pairArtifact.abi, ethers.provider).connect(wallet); 234 | 235 | return { 236 | token0, 237 | token1, 238 | WETH, 239 | WETHPartner, 240 | factory: classicFactory, 241 | router, 242 | routerEventEmitter, 243 | pair, 244 | WETHPair 245 | }; 246 | } -------------------------------------------------------------------------------- /test/shared/helper.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract } from "ethers"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { ZERO, ZERO_ADDRESS } from "./utilities"; 4 | 5 | const hre: HardhatRuntimeEnvironment = require("hardhat"); 6 | 7 | const ONE = BigNumber.from(1); 8 | const TWO = BigNumber.from(2); 9 | const MAX_LOOP_LIMIT = 256; 10 | 11 | function sqrt(value: BigNumber): BigNumber { 12 | const x = BigNumber.from(value); 13 | let z = x.add(ONE).div(TWO); 14 | let y = x; 15 | while (z.sub(y).isNegative()) { 16 | y = z; 17 | z = x.div(z).add(z).div(TWO); 18 | } 19 | return y; 20 | } 21 | 22 | const STABLE_A = BigNumber.from(1000); 23 | 24 | async function computeInvariant( 25 | pool: Contract, 26 | balance0: BigNumber, 27 | balance1: BigNumber 28 | ): Promise { 29 | const poolType = await pool.poolType(); 30 | if (poolType == 1) { 31 | return sqrt(balance0.mul(balance1)); 32 | } else { 33 | const adjustedReserve0 = balance0.mul(await pool.token0PrecisionMultiplier()); 34 | const adjustedReserve1 = balance1.mul(await pool.token1PrecisionMultiplier()); 35 | return computeDFromAdjustedBalances(STABLE_A, adjustedReserve0, adjustedReserve1); 36 | } 37 | } 38 | 39 | function computeDFromAdjustedBalances( 40 | A: BigNumber, 41 | xp0: BigNumber, 42 | xp1: BigNumber 43 | ): BigNumber { 44 | const s = xp0.add(xp1); 45 | 46 | if (s.isZero()) { 47 | return ZERO; 48 | } else { 49 | let prevD; 50 | let d = s; 51 | const nA = A.mul(2); 52 | for (let i = 0; i < MAX_LOOP_LIMIT; i++) { 53 | const dP = d.mul(d).div(xp0).mul(d).div(xp1).div(4); 54 | prevD = d; 55 | d = nA.mul(s).add(dP.mul(2)).mul(d).div( 56 | nA.sub(1).mul(d).add(dP.mul(3)) 57 | ); 58 | if (within1(d, prevD)) { 59 | return d; 60 | } 61 | } 62 | return d; 63 | } 64 | } 65 | 66 | function within1(a: BigNumber, b: BigNumber): boolean { 67 | if (a.gt(b)) { 68 | return a.sub(b).lte(1); 69 | } else { 70 | return b.sub(a).lte(1); 71 | } 72 | } 73 | 74 | const MAX_FEE = BigNumber.from(100000); // 1e5 75 | 76 | function unbalancedMintFee( 77 | swapFee: BigNumber, 78 | reserve0: BigNumber, 79 | reserve1: BigNumber, 80 | amount0: BigNumber, 81 | amount1: BigNumber 82 | ): [BigNumber, BigNumber] { 83 | if (reserve0.isZero() || reserve1.isZero()) { 84 | return [ZERO, ZERO]; 85 | } 86 | const amount1Optimal = amount0.mul(reserve1).div(reserve0); 87 | if (amount1.gte(amount1Optimal)) { 88 | return [ 89 | ZERO, 90 | swapFee.mul(amount1.sub(amount1Optimal)).div(MAX_FEE.mul(2)) 91 | ]; 92 | } else { 93 | const amount0Optimal = amount1.mul(reserve0).div(reserve1); 94 | return [ 95 | swapFee.mul(amount0.sub(amount0Optimal)).div(MAX_FEE.mul(2)), 96 | ZERO 97 | ]; 98 | } 99 | } 100 | 101 | async function calculateMintProtocolFee( 102 | pool: Contract, 103 | reserve0: BigNumber, 104 | reserve1: BigNumber 105 | ): Promise<{ 106 | totalSupply: BigNumber, 107 | invariant: BigNumber, 108 | protocolFee: BigNumber 109 | }> { 110 | const totalSupply = await pool.totalSupply(); 111 | const invariant = await computeInvariant(pool, reserve0, reserve1); 112 | 113 | const master = await getPoolMaster(await pool.master()); 114 | const feeTo = await master.getFeeRecipient(); 115 | 116 | if (feeTo == ZERO_ADDRESS) { 117 | return { 118 | totalSupply, 119 | invariant, 120 | protocolFee: ZERO 121 | }; 122 | } 123 | 124 | const lastInvariant = await pool.invariantLast(); 125 | if (!lastInvariant.isZero()) { 126 | if (invariant.gt(lastInvariant)) { 127 | const protocolFee = BigNumber.from(await master.getProtocolFee(pool.address)); 128 | const numerator = totalSupply.mul(invariant.sub(lastInvariant)).mul(protocolFee); 129 | const denominator = MAX_FEE.sub(protocolFee).mul(invariant).add(protocolFee.mul(lastInvariant)); 130 | const liquidity = numerator.div(denominator); 131 | return { 132 | totalSupply: totalSupply.add(liquidity), 133 | invariant, 134 | protocolFee: liquidity 135 | }; 136 | } 137 | } 138 | 139 | return { 140 | totalSupply, 141 | invariant, 142 | protocolFee: ZERO 143 | }; 144 | } 145 | 146 | async function getBasePoolFactory(factoryAddress: string): Promise { 147 | const artifact = await hre.artifacts.readArtifact('IBasePoolFactory'); 148 | return new Contract(factoryAddress, artifact.abi, hre.ethers.provider); 149 | } 150 | 151 | async function getPoolMaster(factoryAddress: string): Promise { 152 | const artifact = await hre.artifacts.readArtifact('IPoolMaster'); 153 | return new Contract(factoryAddress, artifact.abi, hre.ethers.provider); 154 | } 155 | 156 | async function getToken(tokenAddress: string): Promise { 157 | const tokenArtifact = await hre.artifacts.readArtifact('TestERC20'); 158 | return new Contract(tokenAddress, tokenArtifact.abi, hre.ethers.provider); 159 | } 160 | 161 | export async function getSwapFee(pool: Contract, sender: string): Promise { 162 | const master = await getPoolMaster(await pool.master()); 163 | return BigNumber.from(await master.getSwapFee(pool.address, sender, ZERO_ADDRESS, ZERO_ADDRESS, "0x")); 164 | } 165 | 166 | export async function calculateLiquidityToMint( 167 | sender: string, 168 | vault: Contract, 169 | pool: Contract, 170 | amount0In: BigNumber, 171 | amount1In: BigNumber 172 | ): Promise<{ 173 | liquidity: BigNumber, 174 | fee0: BigNumber, 175 | fee1: BigNumber, 176 | protocolFee: BigNumber 177 | }> { 178 | let reserve0 = await pool.reserve0(); 179 | let reserve1 = await pool.reserve1(); 180 | 181 | const token0 = await getToken(await pool.token0()); 182 | const token1 = await getToken(await pool.token1()); 183 | const balance0 = await vault.balanceOf(token0.address, pool.address); 184 | const balance1 = await vault.balanceOf(token1.address, pool.address); 185 | 186 | const newInvariant = await computeInvariant(pool, balance0, balance1); 187 | const swapFee = await getSwapFee(pool, sender); 188 | 189 | const [fee0, fee1] = unbalancedMintFee(swapFee, reserve0, reserve1, amount0In, amount1In); 190 | 191 | // Add unbalanced fees. 192 | const calculated = await calculateMintProtocolFee(pool, reserve0.add(fee0), reserve1.add(fee1)); 193 | 194 | if (calculated.totalSupply.isZero()) { 195 | return { 196 | liquidity: newInvariant, 197 | fee0, 198 | fee1, 199 | protocolFee: ZERO 200 | }; 201 | } else { 202 | const oldInvariant = calculated.invariant; 203 | return { 204 | liquidity: newInvariant.sub(oldInvariant).mul(calculated.totalSupply).div(oldInvariant), 205 | fee0, 206 | fee1, 207 | protocolFee: calculated.protocolFee 208 | }; 209 | } 210 | } 211 | 212 | export function calculatePoolTokens( 213 | liquidity: BigNumber, 214 | balance: BigNumber, 215 | totalSupply: BigNumber 216 | ): BigNumber { 217 | return liquidity.mul(balance).div(totalSupply); 218 | } 219 | 220 | interface GetAmountOutParams { 221 | amountIn: BigNumber, 222 | reserveIn: BigNumber; 223 | reserveOut: BigNumber; 224 | swapFee: BigNumber; 225 | A?: BigNumber; 226 | tokenInPrecisionMultiplier?: BigNumber; 227 | tokenOutPrecisionMultiplier?: BigNumber; 228 | } 229 | 230 | function getY(A: BigNumber, x: BigNumber, d: BigNumber): BigNumber { 231 | let c = d.mul(d).div(x.mul(2)); 232 | const nA = A.mul(2); 233 | c = c.mul(d).div(nA.mul(2)); 234 | const b = x.add(d.div(nA)); 235 | let yPrev; 236 | let y = d; 237 | for (let i = 0; i < MAX_LOOP_LIMIT; i++) { 238 | yPrev = y; 239 | y = y.mul(y).add(c).div(y.mul(2).add(b).sub(d)); 240 | if (within1(y, yPrev)) { 241 | break; 242 | } 243 | } 244 | return y; 245 | } 246 | 247 | function getAmountOutClassic(params: GetAmountOutParams): BigNumber { 248 | const amountInWithFee = params.amountIn.mul(MAX_FEE.sub(params.swapFee)); 249 | return amountInWithFee.mul(params.reserveOut).div(params.reserveIn.mul(MAX_FEE).add(amountInWithFee)); 250 | } 251 | 252 | function getAmountOutStable(params: GetAmountOutParams): BigNumber { 253 | const adjustedReserveIn = params.reserveIn.mul(params.tokenInPrecisionMultiplier!); 254 | const adjustedReserveOut = params.reserveOut.mul(params.tokenOutPrecisionMultiplier!); 255 | const feeDeductedAmountIn = params.amountIn.sub(params.amountIn.mul(params.swapFee).div(MAX_FEE)); 256 | const d = computeDFromAdjustedBalances(params.A!, adjustedReserveIn, adjustedReserveOut); 257 | 258 | const x = adjustedReserveIn.add(feeDeductedAmountIn.mul(params.tokenInPrecisionMultiplier!)); 259 | const y = getY(params.A!, x, d); 260 | const dy = adjustedReserveOut.sub(y).sub(1); 261 | return dy.div(params.tokenOutPrecisionMultiplier!); 262 | } 263 | 264 | export function getAmountOut(params: GetAmountOutParams): BigNumber { 265 | if (params.amountIn.isZero()) { 266 | return ZERO; 267 | } else { 268 | if (params.A && !params.A.isZero()) { 269 | return getAmountOutStable(params); 270 | } else { 271 | return getAmountOutClassic(params); 272 | } 273 | } 274 | } -------------------------------------------------------------------------------- /test/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { BigNumber, Contract, ethers, Signature } from 'ethers'; 3 | import { 4 | getAddress, 5 | keccak256, 6 | solidityPack, 7 | splitSignature 8 | } from 'ethers/lib/utils'; 9 | 10 | const hre = require("hardhat"); 11 | 12 | const DECIMALS_BASE_18 = BigNumber.from(10).pow(18); 13 | 14 | export const MINIMUM_LIQUIDITY = BigNumber.from(1000); 15 | export const MAX_UINT256 = BigNumber.from(2).pow(256).sub(1); 16 | export const ZERO = BigNumber.from(0); 17 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 18 | 19 | export function expandTo18Decimals(n: number | string): BigNumber { 20 | return BigNumber.from(n).mul(DECIMALS_BASE_18) 21 | } 22 | 23 | export async function getPermitSignature( 24 | wallet: SignerWithAddress, 25 | token: Contract, 26 | approve: { 27 | owner: string 28 | spender: string 29 | value: BigNumber 30 | }, 31 | nonce: BigNumber, 32 | deadline: BigNumber 33 | ): Promise { 34 | const domain = { 35 | name: await token.name(), 36 | version: '1', 37 | chainId: 280, 38 | verifyingContract: token.address 39 | }; 40 | const types = { 41 | Permit: [ 42 | { name: 'owner', type: 'address' }, 43 | { name: 'spender', type: 'address' }, 44 | { name: 'value', type: 'uint256' }, 45 | { name: 'nonce', type: 'uint256' }, 46 | { name: 'deadline', type: 'uint256' } 47 | ] 48 | }; 49 | const values = { 50 | owner: approve.owner, 51 | spender: approve.spender, 52 | value: approve.value, 53 | nonce: nonce, 54 | deadline: deadline 55 | }; 56 | return await wallet._signTypedData(domain, types, values); 57 | } 58 | 59 | export async function getSplittedPermitSignature( 60 | wallet: SignerWithAddress, 61 | token: Contract, 62 | approve: { 63 | owner: string 64 | spender: string 65 | value: BigNumber 66 | }, 67 | nonce: BigNumber, 68 | deadline: BigNumber 69 | ): Promise { 70 | return splitSignature(await getPermitSignature(wallet, token, approve, nonce, deadline)); 71 | } 72 | 73 | export async function mineBlock(timestamp: number): Promise { 74 | await hre.network.provider.request({ 75 | method: "evm_mine", 76 | params: [timestamp], 77 | }); 78 | } 79 | 80 | export function encodePrice(reserve0: BigNumber, reserve1: BigNumber) { 81 | return [reserve1.mul(BigNumber.from(2).pow(112)).div(reserve0), reserve0.mul(BigNumber.from(2).pow(112)).div(reserve1)] 82 | } 83 | 84 | export async function getWallet(): Promise { 85 | const accounts = await hre.ethers.getSigners() 86 | return accounts[0]; 87 | } 88 | 89 | export async function getOther(): Promise { 90 | const accounts = await hre.ethers.getSigners() 91 | return accounts[1]; 92 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | } 9 | } -------------------------------------------------------------------------------- /zksolc-linux-amd64-musl-v1.3.5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncswap/core-contracts/5285a3a7b2b00ca8b7ffc5ae5ce6f6c6195e4aa7/zksolc-linux-amd64-musl-v1.3.5 -------------------------------------------------------------------------------- /zksolc-linux-amd64-musl-v1.3.8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncswap/core-contracts/5285a3a7b2b00ca8b7ffc5ae5ce6f6c6195e4aa7/zksolc-linux-amd64-musl-v1.3.8 --------------------------------------------------------------------------------