├── doc ├── .gitkeep ├── AllowanceTarget.md ├── CoordinatedTaker.md ├── SmartOrderStrategy.md ├── TokenCollector.md ├── GenericSwap.md ├── RFQ.md └── LimitOrderSwap.md ├── .yarnrc ├── .solhintignore ├── Tokenlon-Architecture.png ├── remappings.txt ├── test ├── mocks │ ├── MockContract.sol │ ├── MockStrategy.sol │ ├── MockERC20.sol │ ├── MockERC20Permit.sol │ ├── MockWETH.sol │ ├── MockNoRevertERC20.sol │ ├── MockERC1271Wallet.sol │ ├── MockLimitOrderTaker.sol │ ├── MockNoReturnERC20.sol │ └── MockDeflationaryERC20.sol ├── utils │ ├── UniswapCommands.sol │ ├── ICurveFiV2.sol │ ├── IUniswapUniversalRouter.sol │ ├── payload │ │ ├── allowFill.json │ │ ├── rfqOffer.json │ │ ├── rfqTx.json │ │ ├── limitOrder.json │ │ └── genericSwapData.json │ ├── Sig.sol │ ├── config │ │ ├── local.json │ │ ├── goerli.json │ │ ├── mainnet.json │ │ ├── polygon.json │ │ └── arbitrumMainnet.json │ ├── Tokens.sol │ ├── IUniswapSwapRouter02.sol │ ├── BalanceUtil.sol │ ├── BalanceSnapshot.sol │ ├── IUniswapV2Router.sol │ ├── IUniswapV3SwapRouter.sol │ ├── IUniswapV3Quoter.sol │ ├── Addresses.sol │ ├── ICurveFi.sol │ ├── Permit2Helper.sol │ └── UniswapV2Library.sol ├── forkMainnet │ ├── LimitOrderSwap │ │ ├── Management.t.sol │ │ ├── CancelOrder.t.sol │ │ ├── FullOrKill.t.sol │ │ └── Setup.t.sol │ └── SmartOrderStrategy │ │ ├── Setup.t.sol │ │ └── Validation.t.sol ├── abstracts │ ├── EIP712.t.sol │ ├── AdminManagement.t.sol │ └── Ownable.t.sol └── libraries │ ├── Asset.t.sol │ └── SignatureValidator.t.sol ├── snapshots ├── AdminManagement.json ├── Ownable.json ├── EIP712.json ├── CoordinatedTaker.json ├── TokenCollector.json ├── AllowanceTarget.json ├── Asset.json ├── GenericSwap.json ├── SmartOrderStrategy.json ├── RFQ.json └── LimitOrderSwap.json ├── audits ├── PeckShield-Audit-TokenlonV5-v1.0.pdf ├── PeckShield-Audit-TokenlonV5-v1.0rc.pdf ├── PeckShield-Audit-Report-TokenlonV5.3-v1.0.pdf ├── PeckShield-Audit-Report-Tokenlonv5.2-v1.0.pdf ├── PeckShield-Audit-Report-Tokenlon-LimitOrder-v1.0.pdf ├── Tokenlon-v6.0.0-Smart-Contract-Audit-Report-Decurity.pdf └── README.md ├── .gitignore ├── .prettierignore ├── .gitmodules ├── contracts ├── libraries │ ├── Constant.sol │ ├── AllowFill.sol │ ├── RFQTx.sol │ ├── RFQOffer.sol │ ├── LimitOrder.sol │ ├── GenericSwapData.sol │ ├── SignatureValidator.sol │ └── Asset.sol ├── interfaces │ ├── IERC1271Wallet.sol │ ├── IStrategy.sol │ ├── IAllowanceTarget.sol │ ├── IWETH.sol │ ├── ISmartOrderStrategy.sol │ ├── ICoordinatedTaker.sol │ ├── IGenericSwap.sol │ ├── IRFQ.sol │ ├── IUniswapPermit2.sol │ └── ILimitOrderSwap.sol ├── abstracts │ ├── AdminManagement.sol │ ├── Ownable.sol │ ├── EIP712.sol │ └── TokenCollector.sol ├── AllowanceTarget.sol ├── SmartOrderStrategy.sol ├── GenericSwap.sol └── CoordinatedTaker.sol ├── .prettierrc ├── financial-statements ├── 2024_Q2.csv ├── 2025_Q2.csv ├── 2025_Q1.csv ├── 2024_Q1.csv ├── 2024_Q4.csv └── 2024_Q3.csv ├── foundry.toml ├── LICENSE.md ├── .solhint.json ├── .github └── workflows │ ├── tokenlon-contracts.yml │ └── gas-diff.yml ├── package.json ├── envrc └── README.md /doc/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # test folder 2 | test/ 3 | -------------------------------------------------------------------------------- /Tokenlon-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/Tokenlon-Architecture.png -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts@v5.0.2=lib/openzeppelin-contracts/contracts/ 2 | forge-std/=lib/forge-std/src 3 | -------------------------------------------------------------------------------- /test/mocks/MockContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract MockContract {} 5 | -------------------------------------------------------------------------------- /snapshots/AdminManagement.json: -------------------------------------------------------------------------------- 1 | { 2 | "approveTokens(): testApproveTokens": "133041", 3 | "rescueTokens(): testRescueTokens": "84308" 4 | } 5 | -------------------------------------------------------------------------------- /audits/PeckShield-Audit-TokenlonV5-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/PeckShield-Audit-TokenlonV5-v1.0.pdf -------------------------------------------------------------------------------- /audits/PeckShield-Audit-TokenlonV5-v1.0rc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/PeckShield-Audit-TokenlonV5-v1.0rc.pdf -------------------------------------------------------------------------------- /audits/PeckShield-Audit-Report-TokenlonV5.3-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/PeckShield-Audit-Report-TokenlonV5.3-v1.0.pdf -------------------------------------------------------------------------------- /audits/PeckShield-Audit-Report-Tokenlonv5.2-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/PeckShield-Audit-Report-Tokenlonv5.2-v1.0.pdf -------------------------------------------------------------------------------- /audits/PeckShield-Audit-Report-Tokenlon-LimitOrder-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/PeckShield-Audit-Report-Tokenlon-LimitOrder-v1.0.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # dependency 5 | node_modules/ 6 | 7 | # Coverage files 8 | coverage/ 9 | coverage.json 10 | 11 | # Foundry 12 | out/ 13 | cache/ 14 | -------------------------------------------------------------------------------- /audits/Tokenlon-v6.0.0-Smart-Contract-Audit-Report-Decurity.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consenlabs/tokenlon-contracts/HEAD/audits/Tokenlon-v6.0.0-Smart-Contract-Audit-Report-Decurity.pdf -------------------------------------------------------------------------------- /snapshots/Ownable.json: -------------------------------------------------------------------------------- 1 | { 2 | "acceptOwnership(): testAcceptOwnership": "28029", 3 | "nominateNewOwner(): testNominateNewOwner": "46965", 4 | "renounceOwnership(): testRenounceOwnership": "25105" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependency 2 | node_modules/ 3 | 4 | # Coverage files 5 | coverage/ 6 | coverage.json 7 | 8 | # Foundry 9 | out/ 10 | cache/ 11 | lib/ 12 | 13 | # Signing test payload 14 | test/signing/payload/ 15 | -------------------------------------------------------------------------------- /snapshots/EIP712.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP712_DOMAIN_SEPARATOR(): testDomainSeparatorOnChain": "337", 3 | "EIP712_DOMAIN_SEPARATOR(): testDomainSeparatorOnDifferentChain": "1074", 4 | "getEIP712Hash(): testGetEIP712Hash": "315" 5 | } 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/openzeppelin-contracts"] 2 | path = lib/openzeppelin-contracts 3 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 4 | [submodule "lib/forge-std"] 5 | path = lib/forge-std 6 | url = https://github.com/foundry-rs/forge-std 7 | -------------------------------------------------------------------------------- /snapshots/CoordinatedTaker.json: -------------------------------------------------------------------------------- 1 | { 2 | "approveTokens(): testApproveTokens": "53171", 3 | "setCoordinator(): testSetCoordinator": "30166", 4 | "submitLimitOrderFill(): testFillWithETH": "189977", 5 | "submitLimitOrderFill(): testFillWithPermission": "252405" 6 | } 7 | -------------------------------------------------------------------------------- /snapshots/TokenCollector.json: -------------------------------------------------------------------------------- 1 | { 2 | "collect(): testCollectByAllowanceTarget": "64565", 3 | "collect(): testCollectByPermit2AllowanceTransfer": "100762", 4 | "collect(): testCollectByPermit2SignatureTransfer": "94003", 5 | "collect(): testCollectByTokenApproval": "57170", 6 | "collect(): testCollectByTokenPermit": "91054" 7 | } 8 | -------------------------------------------------------------------------------- /audits/README.md: -------------------------------------------------------------------------------- 1 | # Tokenlon Security 2 | 3 | To keep our product safe and reliable we have "Tokenlon Security-vulnerabilities and Threat-intelligence Bounty Program" in conjunction with the SlowMist Team. 4 | 5 | More information can be found on the [website](https://tokenlon.gitbook.io/docs/v/docs.en/contribute-gong-xian/bounty-programme). 6 | -------------------------------------------------------------------------------- /test/utils/UniswapCommands.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library UniswapCommands { 5 | bytes1 internal constant FLAG_ALLOW_REVERT = 0x80; 6 | bytes1 internal constant COMMAND_TYPE_MASK = 0x3f; 7 | 8 | uint256 internal constant V3_SWAP_EXACT_IN = 0x00; 9 | uint256 internal constant V2_SWAP_EXACT_IN = 0x08; 10 | } 11 | -------------------------------------------------------------------------------- /doc/AllowanceTarget.md: -------------------------------------------------------------------------------- 1 | # AllowanceTarget 2 | 3 | The AllowanceTarget contract manages token allowances and authorizes specific spenders to transfer tokens on behalf of users. 4 | 5 | ## Security Considerations 6 | 7 | AllowanceTarget provides a pause mechanism to prevent unexpected situations. Only the contract owner can pause or unpause the contract. In most cases, the contract will not be paused. 8 | -------------------------------------------------------------------------------- /snapshots/AllowanceTarget.json: -------------------------------------------------------------------------------- 1 | { 2 | "pause(): testSpendFromUserToAfterUnpause": "27565", 3 | "spendFromUserTo(): testSpendFromUserTo": "61960", 4 | "spendFromUserTo(): testSpendFromUserToAfterUnpause": "61960", 5 | "spendFromUserTo(): testSpendFromUserToWithDeflationaryToken": "89862", 6 | "spendFromUserTo(): testSpendFromUserToWithNoReturnValueToken": "67278", 7 | "unpause(): testSpendFromUserToAfterUnpause": "27326" 8 | } 9 | -------------------------------------------------------------------------------- /test/utils/ICurveFiV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ICurveFiV2 { 5 | function get_dy(uint256 i, uint256 j, uint256 dx) external view returns (uint256 out); 6 | 7 | function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external payable; 8 | 9 | function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth) external payable; 10 | } 11 | -------------------------------------------------------------------------------- /snapshots/Asset.json: -------------------------------------------------------------------------------- 1 | { 2 | "getBalance(): testGetBalance": "5799", 3 | "getBalance(): testGetBalance(ETH_ADDRESS)": "510", 4 | "getBalance(): testGetBalance(ZERO_ADDRESS)": "517", 5 | "isETH(): testIsETH(ETH_ADDRESS)": "306", 6 | "isETH(): testIsETH(ZERO_ADDRESS)": "313", 7 | "transferTo(): testDoNothingIfTransferToSelf": "22182", 8 | "transferTo(): testDoNothingIfTransferWithZeroAmount": "22170", 9 | "transferTo(): testTransferETH": "56716", 10 | "transferTo(): testTransferToken": "50378" 11 | } 12 | -------------------------------------------------------------------------------- /snapshots/GenericSwap.json: -------------------------------------------------------------------------------- 1 | { 2 | "executeSwap(): testGenericSwapWithUniswap": "248141", 3 | "executeSwap(): testLeaveOneWeiWithMultipleUsers(the first deposit)": "248141", 4 | "executeSwap(): testLeaveOneWeiWithMultipleUsers(the second deposit)": "204495", 5 | "executeSwap(): testSwapWithETHInput": "97813", 6 | "executeSwap(): testSwapWithETHOutput": "127973", 7 | "executeSwap(): testSwapWithLessOutputButWithinTolerance": "157429", 8 | "executeSwapWithSig(): testGenericSwapRelayed": "279472" 9 | } 10 | -------------------------------------------------------------------------------- /contracts/libraries/Constant.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Constant Library 5 | /// @author imToken Labs 6 | /// @notice Library for defining constant values used across contracts 7 | library Constant { 8 | /// @dev Maximum value for basis points (BPS) 9 | uint16 internal constant BPS_MAX = 10000; 10 | address internal constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; 11 | address internal constant ZERO_ADDRESS = address(0); 12 | } 13 | -------------------------------------------------------------------------------- /doc/CoordinatedTaker.md: -------------------------------------------------------------------------------- 1 | # CoordinatedTaker 2 | 3 | CoordinatedTaker is a conditional taker contract of LimitOrderSwap. It adds a fill permission design on the top of it. A permission of fill is issued by a coordinator with signature. If a user wants to fill an order, he needs to apply for the fill permission and submit the fill with it. The coordinator will manage the available amount of each orders and only issue fill permission when the pending available amount is enough. It helps avoiding fill collision and makes off-chain order canceling possible. 4 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC1271Wallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC1271Wallet { 5 | /// @notice Checks if a signature is valid for a given hash. 6 | /// @param _hash The hash that was signed. 7 | /// @param _signature The signature bytes. 8 | /// @return magicValue The ERC-1271 magic value (0x1626ba7e) if the signature is valid, otherwise returns an error. 9 | function isValidSignature(bytes32 _hash, bytes calldata _signature) external view returns (bytes4 magicValue); 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": false, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "overrides": [ 9 | { 10 | "files": ["*.json", "*.yaml", "*.yml"], 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | }, 15 | { 16 | "files": ["*.sol"], 17 | "options": { 18 | "printWidth": 160, 19 | "semi": true 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /snapshots/SmartOrderStrategy.json: -------------------------------------------------------------------------------- 1 | { 2 | "executeStrategy(): testMultipleAMMs": "607558", 3 | "executeStrategy(): testUniswapV2WithWETHUnwrap": "140873", 4 | "executeStrategy(): testUniswapV3WithAmountReplace": "159842", 5 | "executeStrategy(): testUniswapV3WithMaxAmountReplace": "154811", 6 | "executeStrategy(): testUniswapV3WithoutAmountReplace": "152972", 7 | "executeStrategy(): testV6LOIntegration": "173538", 8 | "executeStrategy(): testV6RFQIntegration": "177215", 9 | "executeStrategy(): testV6RFQIntegrationWhenMakerTokenIsETH": "144660", 10 | "executeStrategy(): testV6RFQIntegrationWhenTakerTokenIsETH": "165683" 11 | } 12 | -------------------------------------------------------------------------------- /test/utils/IUniswapUniversalRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniversalRouter { 5 | error TransactionDeadlinePassed(); 6 | 7 | /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. 8 | /// @param commands A set of concatenated commands, each 1 byte in length 9 | /// @param inputs An array of byte strings containing abi encoded inputs for each command 10 | /// @param deadline The deadline by which the transaction must be executed 11 | function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/payload/allowFill.json: -------------------------------------------------------------------------------- 1 | { 2 | "orderHash": "0x0953626525aef0cff2a0a7078e4444cd0dcad46af0e0a58ddac72ff757235ada", 3 | "taker": "0x9d774D30019C5Fc766D032c1690b1087B8476AD4", 4 | "fillAmount": 100, 5 | "expiry": 3376656000, 6 | "salt": 123, 7 | "expectedSig": "0x5a085c54691b5092e2fbaac87a18c015c4ca6191e50d0a31a7ec835f12ab5a0048de3f1024ffeeabbf46b5bcd170594f11f7d166d3d8d1722262aa9a2a1d89491c", 8 | "signingKey": "0xb870b26aa28f19b6c1b196cad92e2e2c00d3b8f1d9c3cfe4cc83a1436edc689c", 9 | "chainId": 5, 10 | "verifyingContract": "0x90136D945bFFEAAe631bA2375E3996f083A65107", 11 | "typehash": "0xeccdf497641b27c43d174b4b41badb5c6cf370f3fd99e9a47c8fb62724bd0d49" 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/MockStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IStrategy } from "contracts/interfaces/IStrategy.sol"; 5 | import { Asset } from "contracts/libraries/Asset.sol"; 6 | 7 | contract MockStrategy is IStrategy { 8 | using Asset for address; 9 | 10 | uint256 public outputAmount; 11 | address payable public recipient; 12 | 13 | function setOutputAmountAndRecipient(uint256 amount, address payable rec) external { 14 | outputAmount = amount; 15 | recipient = rec; 16 | } 17 | 18 | function executeStrategy(address outputToken, bytes calldata) external payable override { 19 | outputToken.transferTo(recipient, outputAmount); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /snapshots/RFQ.json: -------------------------------------------------------------------------------- 1 | { 2 | "cancelRFQOffer(): testCancelRFQOffer": "49775", 3 | "fillRFQ(): testFillRFQ": "216172", 4 | "fillRFQ(): testFillRFQTakerGetRawETH": "236417", 5 | "fillRFQ(): testFillRFQWithRawETH": "156621", 6 | "fillRFQ(): testFillRFQWithRawETHAndReceiveWETH": "171907", 7 | "fillRFQ(): testFillRFQWithTakerApproveAllowanceTarget": "183936", 8 | "fillRFQ(): testFillRFQWithWETH": "218668", 9 | "fillRFQ(): testFillRFQWithWETHAndReceiveWETH": "204826", 10 | "fillRFQ(): testFillRFQWithZeroFee": "189744", 11 | "fillRFQ(): testFillWithContract": "219582", 12 | "fillRFQ(): testPartialFill": "216312", 13 | "fillRFQWithSig(): testFillRFQByTakerSig": "224772", 14 | "setFeeCollector(): testSetFeeCollector": "30134" 15 | } 16 | -------------------------------------------------------------------------------- /contracts/interfaces/IStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title IStrategy Interface 5 | /// @author imToken Labs 6 | /// @notice Interface for contract that implements a specific trading strategy. 7 | interface IStrategy { 8 | /// @notice Executes the trading strategy for the target token. 9 | /// @dev Implementations should handle the logic to trade tokens based on the provided parameters. 10 | /// @param targetToken The token to be received after executing the strategy. 11 | /// @param strategyData Encoded calldata that combines a sequence of instructions for trading the target token. 12 | function executeStrategy(address targetToken, bytes calldata strategyData) external payable; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/libraries/AllowFill.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | string constant ALLOWFILL_TYPESTRING = "AllowFill(bytes32 orderHash,address taker,uint256 fillAmount,uint256 expiry,uint256 salt)"; 5 | 6 | bytes32 constant ALLOWFILL_DATA_TYPEHASH = keccak256(bytes(ALLOWFILL_TYPESTRING)); 7 | 8 | struct AllowFill { 9 | bytes32 orderHash; 10 | address taker; 11 | uint256 fillAmount; 12 | uint256 expiry; 13 | uint256 salt; 14 | } 15 | 16 | // solhint-disable-next-line func-visibility 17 | function getAllowFillHash(AllowFill memory allowFill) pure returns (bytes32) { 18 | return keccak256(abi.encode(ALLOWFILL_DATA_TYPEHASH, allowFill.orderHash, allowFill.taker, allowFill.fillAmount, allowFill.expiry, allowFill.salt)); 19 | } 20 | -------------------------------------------------------------------------------- /test/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { ERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | uint8 private underlyingDecimals; 8 | 9 | constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol) { 10 | underlyingDecimals = _decimals; 11 | } 12 | 13 | function decimals() public view virtual override returns (uint8) { 14 | return underlyingDecimals; 15 | } 16 | 17 | function mint(address to, uint256 value) public virtual { 18 | _mint(to, value); 19 | } 20 | 21 | function burn(address from, uint256 value) public virtual { 22 | _burn(from, value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /doc/SmartOrderStrategy.md: -------------------------------------------------------------------------------- 1 | # SmartOrderStrategy 2 | 3 | `SmartOrderStrategy` is a strategy executor of a generic swap. It is designed to be called by the `GenericSwap` contract and performs swaps according to the provided payload. This contract should not hold any significant token balance or require token approvals, as it can execute arbitrary calls. Additionally, the `executeStrategy` function is restricted to being called only by the `GenericSwap` contract. 4 | 5 | ## Gas Saving Technique 6 | 7 | `SmartOrderStrategy` retains 1 wei of the maker token at the end of each swap transaction. This practice avoids repeatedly clearing the token balance to zero, as the EVM charges different gas fees for various storage states. By preventing frequent resets to zero, this approach effectively reduces gas consumption. 8 | -------------------------------------------------------------------------------- /test/utils/Sig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | function getEIP712Hash(bytes32 domainSeparator, bytes32 structHash) pure returns (bytes32) { 5 | string memory EIP191_HEADER = "\x19\x01"; 6 | return keccak256(abi.encodePacked(EIP191_HEADER, domainSeparator, structHash)); 7 | } 8 | 9 | function computeMainnetEIP712DomainSeparator(address verifyingContract) pure returns (bytes32) { 10 | uint256 CHAIN_ID = 1; 11 | bytes32 EIP712_DOMAIN_SEPARATOR = keccak256( 12 | abi.encode( 13 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 14 | keccak256(bytes("Tokenlon")), 15 | keccak256(bytes("v6")), 16 | CHAIN_ID, 17 | verifyingContract 18 | ) 19 | ); 20 | return EIP712_DOMAIN_SEPARATOR; 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/payload/rfqOffer.json: -------------------------------------------------------------------------------- 1 | { 2 | "taker": "0x7F12737B9404751510331E746b1401113CC4671A", 3 | "maker": "0x0fF215C8c01E3daC463E665bDf85325868c4760d", 4 | "takerToken": "0x9C6cEfD7F72e44356391f3246e90627d44CB57E1", 5 | "takerTokenAmount": 100, 6 | "makerToken": "0x3cBd481acc9FE145F6D8A52D37843ec92BE2BeD0", 7 | "makerTokenAmount": 100, 8 | "feeFactor": 100, 9 | "flags": 25, 10 | "expiry": 3376656000, 11 | "salt": 123, 12 | "expectedSig": "0xe6b347e3a230d9c4cae92581d4e38bf7e9a09b0a5ad201d73944a786e62df2cd73bd3ca06ba920826b8a0dcafd77b5ab4c36f2a3c3ffd8c7d5c27a880831cfb41c", 13 | "signingKey": "0x03d8876969800e604ad830604d4c5f55040072f68460ed8aed4e5ac698c12f19", 14 | "chainId": 5, 15 | "verifyingContract": "0xaa2Dde5557b0dCE0ea02236e5026056523a41e32", 16 | "typehash": "0x4b43f8e0f7a19a08c96469eb0679ca2da9fab62fb18a10e34e5e0c4ae0248a1c" 17 | } 18 | -------------------------------------------------------------------------------- /test/mocks/MockERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { ERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/ERC20.sol"; 5 | import { ERC20Permit } from "@openzeppelin/contracts@v5.0.2/token/ERC20/extensions/ERC20Permit.sol"; 6 | 7 | import { MockERC20 } from "./MockERC20.sol"; 8 | 9 | contract MockERC20Permit is ERC20Permit, MockERC20 { 10 | // solhint-disable-next-line var-name-mixedcase 11 | bytes32 public constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 12 | 13 | constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20Permit(_name) MockERC20(_name, _symbol, _decimals) {} 14 | 15 | function decimals() public view override(ERC20, MockERC20) returns (uint8) { 16 | return super.decimals(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/libraries/RFQTx.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { RFQOffer, RFQ_OFFER_TYPESTRING, getRFQOfferHash } from "./RFQOffer.sol"; 5 | 6 | string constant RFQ_TX_TYPESTRING = string(abi.encodePacked("RFQTx(RFQOffer rfqOffer,address recipient,uint256 takerRequestAmount)", RFQ_OFFER_TYPESTRING)); 7 | 8 | bytes32 constant RFQ_TX_TYPEHASH = keccak256(bytes(RFQ_TX_TYPESTRING)); 9 | 10 | struct RFQTx { 11 | RFQOffer rfqOffer; 12 | address payable recipient; 13 | uint256 takerRequestAmount; 14 | } 15 | 16 | // solhint-disable-next-line func-visibility 17 | function getRFQTxHash(RFQTx memory rfqTx) pure returns (bytes32 rfqOfferHash, bytes32 rfqTxHash) { 18 | rfqOfferHash = getRFQOfferHash(rfqTx.rfqOffer); 19 | rfqTxHash = keccak256(abi.encode(RFQ_TX_TYPEHASH, rfqOfferHash, rfqTx.recipient, rfqTx.takerRequestAmount)); 20 | } 21 | -------------------------------------------------------------------------------- /financial-statements/2024_Q2.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 14/5/2024,Q2 budget from Community Treasury,Community Budget,0x3557BD3d422300198719710Cc3f00194E1c20A46,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"50,000",USDT,,0xa9faf4d3bab79b784c981da411b8427cb5b4bf7cfdcf684c7008b1d919c756fc 3 | 4/6/2024,*BestBuy user testing reward,Community Budget,\,\,180,USDT,,\ 4 | ,,,,,,,, 5 | ,,,,,,,, 6 | ,,,,,,,, 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | Note: ,,,,,,,, 11 | 1. The community budget from 2024 Q1 balance will be settled in 2024 Q3.,,,,,,,, 12 | "2. Regarding the community budget for Q2, the funds will be transfered from the Tokenlon community wallet in Q3. The funds were allocated for the following purposes:",,,,,,,, 13 | ,,,,,,,, 14 | ,BestBuy user testing reward,https://discord.com/channels/749875871535595550/751387654570115072/1240923520658051152,,180 USDT,,,, -------------------------------------------------------------------------------- /test/mocks/MockWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { MockERC20 } from "test/mocks/MockERC20.sol"; 5 | 6 | contract MockWETH is MockERC20 { 7 | event Deposit(address indexed dst, uint256 wad); 8 | event Withdrawal(address indexed src, uint256 wad); 9 | 10 | constructor(string memory _name, string memory _symbol, uint8 _decimals) MockERC20(_name, _symbol, _decimals) {} 11 | 12 | receive() external payable { 13 | deposit(); 14 | } 15 | 16 | function deposit() public payable { 17 | _mint(msg.sender, msg.value); 18 | emit Deposit(msg.sender, msg.value); 19 | } 20 | 21 | function withdraw(uint256 wad) public { 22 | require(balanceOf(msg.sender) >= wad); 23 | _burn(msg.sender, wad); 24 | payable(msg.sender).transfer(wad); 25 | emit Withdrawal(msg.sender, wad); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' # the source directory 3 | cache = true # whether to cache builds or not 4 | force = false # whether to ignore the cache (clean build) 5 | evm_version = "shanghai" 6 | solc_version = "0.8.26" 7 | optimizer = true # enable or disable the solc optimizer 8 | optimizer_runs = 65536 # the number of optimizer runs 9 | via_ir = true # enable or disable the compilation pipeline for the new IR optimizer 10 | verbosity = 3 # The verbosity of tests 11 | isolate = true # enable or disable the isolate mode for calculating gas usage correctly 12 | fs_permissions = [ 13 | { access = "read", path = "./test/utils/config/" }, 14 | { access = "read", path = "./test/utils/payload/" }, 15 | ] 16 | 17 | [profile.ci] 18 | force = false # whether to ignore the cache (clean build) 19 | 20 | [fuzz] 21 | runs = 1000 # the number of fuzz runs for tests 22 | 23 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 24 | -------------------------------------------------------------------------------- /doc/TokenCollector.md: -------------------------------------------------------------------------------- 1 | # TokenCollector 2 | 3 | `TokenCollector` is an abstract contract designed to handle various token collection mechanisms. It supports different methods of token transfer across different token standards. Users have great flexibility in token authorization when interacting with Tokenlon. 4 | 5 | When interacting with Tokenlon, users can select one of the supported approval schemes and provide the corresponding parameters in the data field (encoded as type `bytes`). The first byte of this data indicates the type of the scheme, followed by the encoded data specific to that type. 6 | 7 | ``` 8 | // *********************************** 9 | // | 1 byte | n bytes | 10 | // ----------------------------------- 11 | // | type | encoded data | 12 | // *********************************** 13 | ``` 14 | 15 | Supported scheme: 16 | 17 | 1. Direct approve Tokenlon's contract 18 | 2. ERC-2612 permit 19 | 3. Tokenlon AllowanceTarget 20 | 4. Uniswap Permit2 21 | -------------------------------------------------------------------------------- /test/utils/payload/rfqTx.json: -------------------------------------------------------------------------------- 1 | { 2 | "recipient": "0xd81b10C4ac637F5f30792f072735a1641D6f17eA", 3 | "takerRequestAmount": 150, 4 | "rfqOffer": { 5 | "taker": "0x76f364AA90A9B07df04F4d3c421eC62193609437", 6 | "maker": "0xEA203F3514D3cd0a1ba4c8Cd4Edb547B2B712730", 7 | "takerToken": "0x3145a41B0f5d97c9c46ee1Fe05D2866DE3C03eC2", 8 | "takerTokenAmount": 100, 9 | "makerToken": "0x22ffAc760e03Ac246333851CC442de495640fC00", 10 | "makerTokenAmount": 100, 11 | "feeFactor": 100, 12 | "flags": 25, 13 | "expiry": 3376656000, 14 | "salt": 123 15 | }, 16 | "expectedSig": "0xa8811e02822fef427f734036fb78a8d56a95f5eea30d9d8ba871285676d76e1a02a8e3f073d18546760468218d7f350b6d2569eaeb08851f98accca64443b3e91c", 17 | "signingKey": "0x5cc7d299b2b71c5cacf7deb5f10080d328745db3e9f58d44d9006507e6d35406", 18 | "chainId": 5, 19 | "verifyingContract": "0xf8C41D071ABCCd22d7DDb22CBE3Bf3599ECfDc7C", 20 | "typehash": "0x97972dc666c2aa8c659018f15b9a82f6ef40f271eebb1ab163a310eca758f29f" 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/payload/limitOrder.json: -------------------------------------------------------------------------------- 1 | { 2 | "taker": "0x78DcE5A2DB5e1Dd8d32F70C2B4fab7Bd05Fd2bC9", 3 | "maker": "0xFccfF50598D10cfF2837a13528EE4122F6efDE33", 4 | "takerToken": "0x4f78Faa9730f09835119a40f799e44C7c15a4EF4", 5 | "takerTokenAmount": 100, 6 | "makerToken": "0xF9bDcEfa26A6d4BefD3D81a75a3cBB77884d2534", 7 | "makerTokenAmount": 100, 8 | "makerTokenPermit": "0x89029e70cabda8270807351cc02c47be30c6863500e607412c0ed20af991a73ce1a768b7910a68f947fc25a72cab6bc7923d536039b28d1d5aa182247b51a4b351563c16", 9 | "feeFactor": 10, 10 | "expiry": 3376656000, 11 | "salt": 123, 12 | "expectedSig": "0x75b69a6f4fa873e36403274be5f86fdf26da76155d05fda911f23cff3722c7673e9f0838536d6c449dddf8c74c0846d5e072c53100a676d6f64655b814dcd8d41c", 13 | "signingKey": "0x8c6c7cfaac6ec519b14b703bab7a6ebf80bb2139bfbdf898cdaf7d39f484a998", 14 | "chainId": 5, 15 | "verifyingContract": "0xA910B11B0Bff3b8A5d77e5361deCE447020B88e4", 16 | "typehash": "0x793a151d40717ec9661de4a7eba1a45e7313a08e4184d5383e3f9ff838fbcab6" 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/payload/genericSwapData.json: -------------------------------------------------------------------------------- 1 | { 2 | "maker": "0x15314FA3b8E8B99F0954bec851A1aA9ef63a1C44", 3 | "takerToken": "0xA2C3bf2C5C78f3822Ae2Cb7d5Edd14722E3BC88C", 4 | "takerTokenAmount": 100, 5 | "makerToken": "0xfD7391c3eA2792F4341E07A523CF9276AF1f215A", 6 | "makerTokenAmount": 100, 7 | "minMakerTokenAmount": 50, 8 | "expiry": 3376656000, 9 | "salt": 123, 10 | "recipient": "0xdf5621f59CcD4A58cF487E0C4E3fCDd6Db67E4dA", 11 | "strategyData": "0xcbe324dfbf8d48a7ad9ef3cb0c2bdd411129db5238371b8609272f14921736fb0ab28dcede4f2e1e41393d575ffec60002e565bfbf5eaf896bc490a965c8452d08d9b71b", 12 | "expectedSig": "0xdfe4e0414baa9b0ac4edcefc1d8ceb18dc21230f6b93a83c91188a105d1d7ece7809ca3c67db499fd497e35ba271a0a6689f7683d41b5d4602a5bb7895537a2d1c", 13 | "signingKey": "0x1dc4f07f92fa1783042350ffe685439f857a8fdd837589c00c5299e287c4afcb", 14 | "chainId": 5, 15 | "verifyingContract": "0x49eA36C04c109a8c62C9b48fd8C4d13b3EA98B27", 16 | "typehash": "0x1b6f9d7673107802b331a5ab52a40f7d942bdf74fa821744df8b69eead3d26c1" 17 | } 18 | -------------------------------------------------------------------------------- /doc/GenericSwap.md: -------------------------------------------------------------------------------- 1 | # GenericSwap 2 | 3 | GenericSwap is a general token swapping contract designed to integrate with various strategy executors (e.g. the `SmartOrderSwap` contract). The GenericSwap contract is responsible for ensuring a result of a swap is match the order. However, the actual swap is executed by a strategy executor. This design allows fulfilling an order with any combination of swapping protocols. Also, by adjusting payload in our off-chain system, it may support new protocol without upgrading contracts. 4 | 5 | ## Gas Saving Technique 6 | 7 | GenericSwap retains 1 wei of the maker token at the end of each swap transaction. This practice avoids repeatedly clearing the token balance to zero, as the EVM charges different gas fees for various storage states. By preventing frequent resets to zero, this approach effectively reduces gas consumption. 8 | 9 | ## Relayer 10 | 11 | The GenericSwap contract allows for trade submissions by a relayer with user's signatures. To prevent replay attacks, the hash of the relayed trade is recorded. 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IAllowanceTarget.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title IAllowanceTarget Interface 5 | /// @author imToken Labs 6 | /// @notice This interface defines the function for spending tokens on behalf of a user. 7 | /// @dev Only authorized addresses can call the spend function. 8 | interface IAllowanceTarget { 9 | /// @notice Error to be thrown when the caller is not authorized. 10 | /// @dev This error is used to ensure that only authorized addresses can spend tokens on behalf of a user. 11 | error NotAuthorized(); 12 | 13 | /// @notice Spend tokens on user's behalf. 14 | /// @dev Only an authorized address can call this function to spend tokens on behalf of a user. 15 | /// @param from The user to spend tokens from. 16 | /// @param token The address of the token. 17 | /// @param to The recipient of the transfer. 18 | /// @param amount The amount to spend. 19 | function spendFromUserTo(address from, address token, address to, uint256 amount) external; 20 | } 21 | -------------------------------------------------------------------------------- /test/utils/config/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH_ADDRESS": "0x0000000000000000000000000000000000000000", 3 | "USDT_ADDRESS": "0x0000000000000000000000000000000000000000", 4 | "USDC_ADDRESS": "0x0000000000000000000000000000000000000000", 5 | "CRV_ADDRESS": "0x0000000000000000000000000000000000000000", 6 | "TUSD_ADDRESS": "0x0000000000000000000000000000000000000000", 7 | "DAI_ADDRESS": "0x000000000000000000000000000000000000000", 8 | "LON_ADDRESS": "0x000000000000000000000000000000000000000", 9 | "WBTC_ADDRESS": "0x0000000000000000000000000000000000000000", 10 | 11 | "CURVE_TRICRYPTO2_POOL_ADDRESS": "0x000000000000000000000000000000000000000", 12 | "SUSHISWAP_ADDRESS": "0x000000000000000000000000000000000000000", 13 | "UNISWAP_V3_QUOTER_ADDRESS": "0x000000000000000000000000000000000000000", 14 | "UNISWAP_PERMIT2_ADDRESS": "0x000000000000000000000000000000000000000", 15 | "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x000000000000000000000000000000000000000", 16 | "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x000000000000000000000000000000000000000" 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/config/goerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH_ADDRESS": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", 3 | "USDT_ADDRESS": "0x0000000000000000000000000000000000000000", 4 | "USDC_ADDRESS": "0x0000000000000000000000000000000000000000", 5 | "CRV_ADDRESS": "0x0000000000000000000000000000000000000000", 6 | "TUSD_ADDRESS": "0x0000000000000000000000000000000000000000", 7 | "DAI_ADDRESS": "0x0000000000000000000000000000000000000000", 8 | "LON_ADDRESS": "0x6dA0e6ABd44175f50C563cd8b860DD988A7C3433", 9 | "WBTC_ADDRESS": "0x0000000000000000000000000000000000000000", 10 | 11 | "CURVE_TRICRYPTO2_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", 12 | "SUSHISWAP_ADDRESS": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", 13 | "UNISWAP_V3_QUOTER_ADDRESS": "0x0000000000000000000000000000000000000000", 14 | "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", 15 | "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 16 | "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x4648a43B2C14Da09FdF82B161150d3F634f40491" 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/config/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH_ADDRESS": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 3 | "USDT_ADDRESS": "0xdAC17F958D2ee523a2206206994597C13D831ec7", 4 | "USDC_ADDRESS": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 5 | "CRV_ADDRESS": "0xD533a949740bb3306d119CC777fa900bA034cd52", 6 | "TUSD_ADDRESS": "0x0000000000085d4780B73119b644AE5ecd22b376", 7 | "DAI_ADDRESS": "0x6B175474E89094C44Da98b954EedeAC495271d0F", 8 | "LON_ADDRESS": "0x0000000000095413afC295d19EDeb1Ad7B71c952", 9 | "WBTC_ADDRESS": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 10 | 11 | "CURVE_TRICRYPTO2_POOL_ADDRESS": "0x80466c64868E1ab14a1Ddf27A676C3fcBE638Fe5", 12 | "SUSHISWAP_ADDRESS": "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", 13 | "UNISWAP_V3_QUOTER_ADDRESS": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", 14 | "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", 15 | "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 16 | "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B" 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/config/polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH_ADDRESS": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", 3 | "USDT_ADDRESS": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", 4 | "USDC_ADDRESS": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 5 | "CRV_ADDRESS": "0x172370d5Cd63279eFa6d502DAB29171933a610AF", 6 | "TUSD_ADDRESS": "0x2e1AD108fF1D8C782fcBbB89AAd783aC49586756", 7 | "DAI_ADDRESS": "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", 8 | "LON_ADDRESS": "0x6f7C932e7684666C9fd1d44527765433e01fF61d", 9 | "WBTC_ADDRESS": "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", 10 | 11 | "CURVE_TRICRYPTO2_POOL_ADDRESS": "0xd51a44d3fae010294c616388b506acda1bfaae46", 12 | "SUSHISWAP_ADDRESS": "0x000000000000000000000000000000000000000", 13 | "UNISWAP_V3_QUOTER_ADDRESS": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", 14 | "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", 15 | "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 16 | "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x4C60051384bd2d3C01bfc845Cf5F4b44bcbE9de5" 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/config/arbitrumMainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH_ADDRESS": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", 3 | "USDT_ADDRESS": "0x0000000000000000000000000000000000000000", 4 | "USDC_ADDRESS": "0x0000000000000000000000000000000000000000", 5 | "CRV_ADDRESS": "0x0000000000000000000000000000000000000000", 6 | "TUSD_ADDRESS": "0x0000000000000000000000000000000000000000", 7 | "DAI_ADDRESS": "0x0000000000000000000000000000000000000000", 8 | "LON_ADDRESS": "0x0000000000000000000000000000000000000000", 9 | "WBTC_ADDRESS": "0x0000000000000000000000000000000000000000", 10 | 11 | "CURVE_TRICRYPTO2_POOL_ADDRESS": "0x0000000000000000000000000000000000000000", 12 | "SUSHISWAP_ADDRESS": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", 13 | "UNISWAP_V3_QUOTER_ADDRESS": "0x0000000000000000000000000000000000000000", 14 | "UNISWAP_PERMIT2_ADDRESS": "0x000000000022d473030f116ddee9f6b43ac78ba3", 15 | "UNISWAP_SWAP_ROUTER_02_ADDRESS": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 16 | "UNISWAP_UNIVERSAL_ROUTER_ADDRESS": "0x4C60051384bd2d3C01bfc845Cf5F4b44bcbE9de5" 17 | } 18 | -------------------------------------------------------------------------------- /test/mocks/MockNoRevertERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { MockERC20 } from "test/mocks/MockERC20.sol"; 5 | 6 | /** 7 | * @dev Return false instead of reverting when transfer allownace or balance is not enough. (ZRX) 8 | */ 9 | contract MockNoRevertERC20 is MockERC20 { 10 | constructor() MockERC20("MockNoRevertERC20", "MNRVT", 18) {} 11 | 12 | function transfer(address recipient, uint256 amount) public override returns (bool) { 13 | if (balanceOf(msg.sender) < amount) { 14 | return false; 15 | } 16 | _transfer(msg.sender, recipient, amount); 17 | return true; 18 | } 19 | 20 | function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { 21 | if (balanceOf(msg.sender) < amount || allowance(sender, msg.sender) < amount) { 22 | return false; 23 | } 24 | _transfer(sender, recipient, amount); 25 | _approve(sender, msg.sender, allowance(sender, msg.sender) - amount); 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ConsenLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "explicit-types": "error", 6 | "max-states-count": "error", 7 | "no-console": "error", 8 | "no-empty-blocks": "error", 9 | "no-global-import": "error", 10 | "no-unused-import": "error", 11 | "no-unused-vars": "error", 12 | "payable-fallback": "error", 13 | "constructor-syntax": "error", 14 | "comprehensive-interface": "warn", 15 | "func-named-parameters": "warn", 16 | "imports-on-top": "error", 17 | "ordering": "warn", 18 | "visibility-modifier-order": "error", 19 | "avoid-call-value": "error", 20 | "avoid-sha3": "error", 21 | "avoid-suicide": "error", 22 | "avoid-throw": "error", 23 | "avoid-tx-origin": "off", 24 | "check-send-result": "error", 25 | "compiler-version": ["error", ">=0.8.0"], 26 | "func-visibility": ["error", { "ignoreConstructors": true }], 27 | "multiple-sends": "error", 28 | "no-complex-fallback": "error", 29 | "no-inline-assembly": "warn", 30 | "not-rely-on-block-hash": "error", 31 | "not-rely-on-time": "warn", 32 | "reentrancy": "error", 33 | "state-visibility": "error" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/tokenlon-contracts.yml: -------------------------------------------------------------------------------- 1 | name: Tokenlon Contracts CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [16.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install 25 | run: yarn install --frozen-lockfile 26 | - name: Install Foundry and Setup 27 | uses: foundry-rs/foundry-toolchain@v1 28 | with: 29 | version: nightly 30 | - name: Format 31 | run: yarn run check-pretty 32 | - name: Lint 33 | run: | 34 | yarn run lint 35 | - name: Compile 36 | run: | 37 | yarn run compile 38 | - name: Test 39 | env: 40 | MAINNET_NODE_RPC_URL: ${{ secrets.MAINNET_NODE_RPC_URL }} 41 | FOUNDRY_PROFILE: CI 42 | run: | 43 | yarn run test-foundry-local 44 | yarn run test-foundry-fork 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tokenlon-contracts", 3 | "version": "6.0.0", 4 | "repository": "https://github.com/consenlabs/tokenlon-contracts.git", 5 | "author": "imToken Labs", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">=16.0.0 <18", 9 | "yarn": "^1.22.10" 10 | }, 11 | "scripts": { 12 | "setup": "yarn install --frozen-lockfile", 13 | "format": "prettier --write .", 14 | "check-pretty": "prettier --check .", 15 | "lint": "solhint \"contracts/**/*.sol\"", 16 | "compile": "forge build --force", 17 | "test-foundry-local": "DEPLOYED=false forge test --no-match-path 'test/forkMainnet/*.t.sol'", 18 | "test-foundry-fork": "DEPLOYED=false forge test --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 17900000 --match-path 'test/forkMainnet/*.t.sol'", 19 | "coverage": "DEPLOYED=false forge coverage --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 17900000 --report summary", 20 | "gas-report-local": "yarn test-foundry-local --gas-report", 21 | "gas-report-fork": "yarn test-foundry-fork --gas-report" 22 | }, 23 | "devDependencies": { 24 | "prettier": "^2.8.8", 25 | "prettier-plugin-solidity": "^1.1.3", 26 | "solhint": "^3.6.2", 27 | "solhint-plugin-prettier": "^0.0.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /snapshots/LimitOrderSwap.json: -------------------------------------------------------------------------------- 1 | { 2 | "cancelOrder(): testCancelOrder": "52957", 3 | "fillLimitOrder(): testFillLimitOrderWithETH": "155159", 4 | "fillLimitOrder(): testFillWithBetterTakingAmount": "212150", 5 | "fillLimitOrder(): testFillWithBetterTakingAmountButGetAdjusted": "212318", 6 | "fillLimitOrder(): testFillWithETHRefund": "162238", 7 | "fillLimitOrder(): testFillWithLargerVolumeAndSettleAsManyAsPossible": "212306", 8 | "fillLimitOrder(): testFillWithoutMakerSigForVerifiedOrder": "212150", 9 | "fillLimitOrder(): testFillWithoutMakerSigForVerifiedOrder(without makerSig)": "119214", 10 | "fillLimitOrder(): testFullyFillLimitOrder": "212150", 11 | "fillLimitOrder(): testFullyFillLimitOrderUsingAMM": "231466", 12 | "fillLimitOrder(): testPartiallyFillLimitOrder": "212150", 13 | "fillLimitOrderFullOrKill(): testFillWithFOK": "212327", 14 | "fillLimitOrderGroup(): testGroupFillRingTrade": "287118", 15 | "fillLimitOrderGroup(): testGroupFillWithPartialWETHUnwrap": "273557", 16 | "fillLimitOrderGroup(): testGroupFillWithTakerPrefundETH": "195696", 17 | "fillLimitOrderGroup(): testGroupFillWithWETHUnwrap": "195696", 18 | "fillLimitOrderGroup(): testPartialFillLargeOrderWithSmallOrders": "261176", 19 | "testGroupFillWithProfit: fillLimitOrderGroup()": "221442" 20 | } 21 | -------------------------------------------------------------------------------- /envrc: -------------------------------------------------------------------------------- 1 | export DEPLOYED=false 2 | 3 | export ALLOWANCE_TARGET_ADDRESS="0x62b546741A5D59733E1D7ee2f12dF3b790Fc1bb8" 4 | export SPENDER_ADDRESS="0x8090f7450023f0Cc7ccA35f18e2fFd818A0aa3E5" 5 | export USERPROXY_ADDRESS="0x45DB0f133E6dA3340363624A503a46f952570a29" 6 | export PERMANENTSTORAGE_ADDRESS="0x132aA80033ddBFFeeBd22A14Ee131cF7D4FdCB31" 7 | 8 | export WETH_ADDRESS="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 9 | export USDT_ADDRESS="0xdAC17F958D2ee523a2206206994597C13D831ec7" 10 | export USDC_ADDRESS="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 11 | export DAI_ADDRESS="0x6B175474E89094C44Da98b954EedeAC495271d0F" 12 | export WBTC_ADDRESS="0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 13 | export LON_ADDRESS="0x0000000000095413afC295d19EDeb1Ad7B71c952" 14 | 15 | export ARBITRUM_L1_GATEWAY_ROUTER_ADDRESS="0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef" 16 | export ARBITRUM_L1_BRIDGE_ADDRESS="0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a" 17 | export OPTIMISM_L1_STANDARD_BRIDGE_ADDRESS="0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1" 18 | 19 | export AMMWRAPPER_ADDRESS="0xCCaC49778C26bcC60e4648aB1E4C5F0276E82851" 20 | export AMMQUOTER_ADDRESS="0x77d8eeF8d15207b2228F76105cb46E558De7a8bD" 21 | export RFQ_ADDRESS="0x953CA2083aA94800d784ac37DEb3f15006bC6BD3" 22 | export LIMITORDER_ADDRESS="0x77d8eeF8d15207b2228F76105cb46E558De7a8bD" 23 | export L2DEPOSIT_ADDRESS="0x0B85B7d6bf392F665FBa4F4F5f42C645c7d9055B" 24 | -------------------------------------------------------------------------------- /financial-statements/2025_Q2.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 30/6/2025,imKey campaign + bitcoin pizza day campaign prize pool,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"3,525.00",LON,"https://x.com/tokenlon/status/1925016826716815853, https://x.com/imKeyOfficial/status/1924675909749244000",0xe90bc6992ce09bf95f6e136750f6d6ed33dc2785165fe0798639fa130073240a 3 | 30/6/2025,"Swap 2,494.43 USDT for 3,524.99 LON",Token swap,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"3,524.99",LON,,0xc621d5d8b7a17046c37d7d27dd65212f166465e47024bca0ae5fdb9e5bdecd7c 4 | ,,,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"2,494.43",USDT,,0xc621d5d8b7a17046c37d7d27dd65212f166465e47024bca0ae5fdb9e5bdecd7c 5 | 25/6/2025,"*Graph Studio reloads,daily LON staking data analysis gas fees",Subscription fees,,,7500,GRT,, 6 | 19/5/2025,"*Subscription fees, including Typefully for Twitter management",Subscription fees,,,149.99,USDT,, 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | ,,,,,,,, 18 | ,,,,,,,, 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,*to be reported in Q3 2025,,,,,,, 24 | ,,,,,,,, 25 | ,Q2 Total expenditure in USDT:,"3,274.42",,,,,, -------------------------------------------------------------------------------- /contracts/libraries/RFQOffer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | string constant RFQ_OFFER_TYPESTRING = "RFQOffer(address taker,address maker,address takerToken,uint256 takerTokenAmount,address makerToken,uint256 makerTokenAmount,uint256 feeFactor,uint256 flags,uint256 expiry,uint256 salt)"; 5 | 6 | bytes32 constant RFQ_OFFER_DATA_TYPEHASH = keccak256(bytes(RFQ_OFFER_TYPESTRING)); 7 | 8 | struct RFQOffer { 9 | address taker; 10 | address payable maker; 11 | address takerToken; 12 | uint256 takerTokenAmount; 13 | address makerToken; 14 | uint256 makerTokenAmount; 15 | uint256 feeFactor; 16 | uint256 flags; 17 | uint256 expiry; 18 | uint256 salt; 19 | } 20 | 21 | // solhint-disable-next-line func-visibility 22 | function getRFQOfferHash(RFQOffer memory rfqOffer) pure returns (bytes32) { 23 | return 24 | keccak256( 25 | abi.encode( 26 | RFQ_OFFER_DATA_TYPEHASH, 27 | rfqOffer.taker, 28 | rfqOffer.maker, 29 | rfqOffer.takerToken, 30 | rfqOffer.takerTokenAmount, 31 | rfqOffer.makerToken, 32 | rfqOffer.makerTokenAmount, 33 | rfqOffer.feeFactor, 34 | rfqOffer.flags, 35 | rfqOffer.expiry, 36 | rfqOffer.salt 37 | ) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /test/forkMainnet/LimitOrderSwap/Management.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Ownable } from "contracts/abstracts/Ownable.sol"; 5 | import { ILimitOrderSwap } from "contracts/interfaces/ILimitOrderSwap.sol"; 6 | 7 | import { LimitOrderSwapTest } from "test/forkMainnet/LimitOrderSwap/Setup.t.sol"; 8 | 9 | contract ManagementTest is LimitOrderSwapTest { 10 | function testCannotSetFeeCollectorByNotOwner() public { 11 | address newFeeCollector = makeAddr("newFeeCollector"); 12 | 13 | vm.startPrank(newFeeCollector); 14 | vm.expectRevert(Ownable.NotOwner.selector); 15 | limitOrderSwap.setFeeCollector(payable(newFeeCollector)); 16 | vm.stopPrank(); 17 | } 18 | 19 | function testCannotSetFeeCollectorToZero() public { 20 | vm.startPrank(limitOrderOwner); 21 | vm.expectRevert(ILimitOrderSwap.ZeroAddress.selector); 22 | limitOrderSwap.setFeeCollector(payable(address(0))); 23 | vm.stopPrank(); 24 | } 25 | 26 | function testSetFeeCollector() public { 27 | address newFeeCollector = makeAddr("newFeeCollector"); 28 | 29 | vm.expectEmit(false, false, false, true); 30 | emit SetFeeCollector(newFeeCollector); 31 | 32 | vm.startPrank(limitOrderOwner); 33 | limitOrderSwap.setFeeCollector(payable(newFeeCollector)); 34 | vm.stopPrank(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /financial-statements/2025_Q1.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 24/1/2025,Payment for CNY2025 gift sets,Community budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xdAC17F958D2ee523a2206206994597C13D831ec7,490.00,USDT,,0xc25aeb94e6d994f5383054dd67831e96f0ef538326c49b2db9a5dcd4bab764c2 3 | 26/1/2025,Quarterly USDT budget from Tokenlon Community Treasury multisig,Community budget,0x3557BD3d422300198719710Cc3f00194E1c20A46,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"50,000.00",USDT,,0x27e4f0507dfb1c2843d6d67681472cc930403e5cce4e34d38fd2ddbd6b93c842 4 | 20/2/2025,Swap 1706.73 USDT for 2147.99 LON,Token swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"1,706.73",USDT,,0x4e246ae485f563707d1d6ca7f48e8c93a9db9f319dfbf3c09d63db4b9c014094 5 | ,,,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"2,147.99",LON,, 6 | 20/12/2024,Chinese New Year & Lantern festival campaign rewards,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"2,148.00",LON,,0x7c39bca022a7c64b1ff04e5f3726fd1e5e260518186f54310797df42ec0f2d1e 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | ,,,,,,,, 18 | ,,,,,,,, 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,,,,,,,, 24 | ,,,,,,,, 25 | ,Q1 Total expenditure in USDT:,"2,196.73",,,,,, -------------------------------------------------------------------------------- /contracts/libraries/LimitOrder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | string constant LIMITORDER_TYPESTRING = "LimitOrder(address taker,address maker,address takerToken,uint256 takerTokenAmount,address makerToken,uint256 makerTokenAmount,bytes makerTokenPermit,uint256 feeFactor,uint256 expiry,uint256 salt)"; 5 | 6 | bytes32 constant LIMITORDER_DATA_TYPEHASH = keccak256(bytes(LIMITORDER_TYPESTRING)); 7 | 8 | struct LimitOrder { 9 | address taker; 10 | address payable maker; 11 | address takerToken; 12 | uint256 takerTokenAmount; 13 | address makerToken; 14 | uint256 makerTokenAmount; 15 | bytes makerTokenPermit; 16 | uint256 feeFactor; 17 | uint256 expiry; 18 | uint256 salt; 19 | } 20 | 21 | // solhint-disable-next-line func-visibility 22 | function getLimitOrderHash(LimitOrder memory limitOrder) pure returns (bytes32) { 23 | return 24 | keccak256( 25 | abi.encode( 26 | LIMITORDER_DATA_TYPEHASH, 27 | limitOrder.taker, 28 | limitOrder.maker, 29 | limitOrder.takerToken, 30 | limitOrder.takerTokenAmount, 31 | limitOrder.makerToken, 32 | limitOrder.makerTokenAmount, 33 | keccak256(limitOrder.makerTokenPermit), 34 | limitOrder.feeFactor, 35 | limitOrder.expiry, 36 | limitOrder.salt 37 | ) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /contracts/libraries/GenericSwapData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | string constant GS_DATA_TYPESTRING = string( 5 | "GenericSwapData(address maker,address takerToken,uint256 takerTokenAmount,address makerToken,uint256 makerTokenAmount,uint256 minMakerTokenAmount,uint256 expiry,uint256 salt,address recipient,bytes strategyData)" 6 | ); 7 | 8 | bytes32 constant GS_DATA_TYPEHASH = keccak256(bytes(GS_DATA_TYPESTRING)); 9 | 10 | struct GenericSwapData { 11 | address payable maker; 12 | address takerToken; 13 | uint256 takerTokenAmount; 14 | address makerToken; 15 | uint256 makerTokenAmount; 16 | uint256 minMakerTokenAmount; 17 | uint256 expiry; 18 | uint256 salt; 19 | address payable recipient; 20 | bytes strategyData; 21 | } 22 | 23 | // solhint-disable-next-line func-visibility 24 | function getGSDataHash(GenericSwapData memory gsData) pure returns (bytes32) { 25 | return 26 | keccak256( 27 | abi.encode( 28 | GS_DATA_TYPEHASH, 29 | gsData.maker, 30 | gsData.takerToken, 31 | gsData.takerTokenAmount, 32 | gsData.makerToken, 33 | gsData.makerTokenAmount, 34 | gsData.minMakerTokenAmount, 35 | gsData.expiry, 36 | gsData.salt, 37 | gsData.recipient, 38 | keccak256(gsData.strategyData) 39 | ) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /contracts/libraries/SignatureValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { ECDSA } from "@openzeppelin/contracts@v5.0.2/utils/cryptography/ECDSA.sol"; 5 | 6 | import { IERC1271Wallet } from "../interfaces/IERC1271Wallet.sol"; 7 | 8 | /// @title Signature Validator Library 9 | /// @author imToken Labs 10 | /// @notice Library for validating signatures using ECDSA and ERC1271 standards 11 | library SignatureValidator { 12 | // bytes4(keccak256("isValidSignature(bytes32,bytes)")) 13 | bytes4 internal constant ERC1271_MAGICVALUE = 0x1626ba7e; 14 | 15 | /// @notice Verifies that a hash has been signed by the given signer. 16 | /// @dev This function verifies signatures either through ERC1271 wallets or direct ECDSA recovery. 17 | /// @param _signerAddress Address that should have signed the given hash. 18 | /// @param _hash Hash of the EIP-712 encoded data. 19 | /// @param _signature Proof that the hash has been signed by signer. 20 | /// @return True if the address recovered from the provided signature matches the input signer address. 21 | function validateSignature(address _signerAddress, bytes32 _hash, bytes memory _signature) internal view returns (bool) { 22 | if (_signerAddress.code.length > 0) { 23 | return ERC1271_MAGICVALUE == IERC1271Wallet(_signerAddress).isValidSignature(_hash, _signature); 24 | } else { 25 | return _signerAddress == ECDSA.recover(_hash, _signature); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /financial-statements/2024_Q1.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 20/3/2024,Unclaimed genesis LON balance,TIP,0xcc734cebf6bd685e1d74ac6b09bf2fca867d7791,0x3557bd3d422300198719710cc3f00194e1c20a46,"9,883,293.57",LON,https://snapshot.org/#/tokenlon.eth/proposal/0x8170797ba1c854221fd0e3649bc6f8900a821d0d913006c622f667d739a72c5a,0xae20473613e159df7a39a83b971bf4713110019f1a4e8e30efdc0dc512c1f87a 3 | ,*Q1 community budget - LON,Community budget,,,4500,LON,, 4 | ,*Q1 community budget - USDT,Community budget,,,1830,USDT,, 5 | ,,,,,,,, 6 | ,,,,,,,, 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | Note:,"In accordance with TIP38, the Tokenlon team will designate a dedicated wallet address for the allocation of community and marketing funds, streamlining access to resources. This initiative will be implemented beginning the upcoming quarter, Q2 2024, accompanied by transaction hashes documenting budget expenditures.",,,,,,, 11 | ,"Regarding the community budget for Q1, the funds will be transfered to the Tokenlon community wallet in Q2. The funds were allocated for the following purposes:",,,,,,, 12 | ,,,,,,,, 13 | ,Tokenlon Xmas Surprise campaign,https://tokenlon.im/blog/22020266102420,,4500 LON,,,, 14 | ,Multi chain deployment online marketing,https://tokenlon.im/blog/21285160832788,,800 USDT,,,, 15 | ,imToken Chinese New Year red packet campaign,https://twitter.com/imTokenOfficial/status/1755927487450607622,,1000 USDT,,,, 16 | ,Tokenlon Lantern Festival campaign,https://twitter.com/tokenlon/status/1760492570830893270,,30 USDT,,,, -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title IWETH Interface 5 | interface IWETH { 6 | /// @notice Deposits ETH into the contract and wraps it into WETH. 7 | function deposit() external payable; 8 | 9 | /// @notice Withdraws a specified amount of WETH, unwraps it into ETH, and sends it to the caller. 10 | /// @param amount The amount of WETH to withdraw and unwrap. 11 | function withdraw(uint256 amount) external; 12 | 13 | /// @notice Transfers a specified amount of WETH to a destination address. 14 | /// @param dst The recipient address to which WETH will be transferred. 15 | /// @param wad The amount of WETH to transfer. 16 | /// @return True if the transfer is successful, false otherwise. 17 | function transfer(address dst, uint256 wad) external returns (bool); 18 | 19 | /// @notice Transfers a specified amount of WETH from a source address to a destination address. 20 | /// @param src The sender address from which WETH will be transferred. 21 | /// @param dst The recipient address to which WETH will be transferred. 22 | /// @param wad The amount of WETH to transfer. 23 | /// @return True if the transfer is successful, false otherwise. 24 | function transferFrom(address src, address dst, uint256 wad) external returns (bool); 25 | 26 | /// @notice Returns the balance of `account`. 27 | /// @param account The address for which to query the balance. 28 | /// @return The balance of `account`. 29 | function balanceOf(address account) external view returns (uint256); 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokenlon 2 | 3 | [![Node.js CI](https://github.com/consenlabs/tokenlon-contracts/actions/workflows/node.js.yml/badge.svg?branch=master)](https://github.com/consenlabs/tokenlon-contracts/actions/workflows/node.js.yml) 4 | [![Built-with openzeppelin](https://img.shields.io/badge/built%20with-OpenZeppelin-3677FF)](https://docs.openzeppelin.com/) 5 | 6 | Tokenlon is a decentralized exchange and payment settlement protocol based on blockchain technology. Visit [tokenlon.im](https://tokenlon.im/) 7 | 8 | > Notice: This repository may contain changes that are under development. Make sure the correct commit is referenced when reviewing specific deployed contract. 9 | 10 | ## Architecture 11 | 12 | ![Tokenlon Architecture](./Tokenlon-Architecture.png) 13 | 14 | ## Deployed contracts 15 | 16 | Under construction 17 | 18 | ## Prerequisite 19 | 20 | - node (>=16.0.0 <18) 21 | - yarn (^1.22.10) 22 | - [foundry](https://github.com/foundry-rs/foundry) 23 | - Environment Variables (Used for foundry fork tests) 24 | - `MAINNET_NODE_RPC_URL`: The RPC URL for accessing forked states. 25 | 26 | ### Example 27 | 28 | ```bash 29 | MAINNET_NODE_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/#####__YOUR_SECRET__##### 30 | ``` 31 | 32 | ## Installation 33 | 34 | ```bash 35 | $ git submodule update --init --recursive 36 | $ yarn run setup 37 | ``` 38 | 39 | ## Compile contracts 40 | 41 | ```bash 42 | # Compile contracts 43 | $ yarn run compile 44 | ``` 45 | 46 | ## Run unit test 47 | 48 | ```bash 49 | # Run unit tests with fresh states 50 | $ yarn run test-foundry-local 51 | 52 | # Run integration tests with forked states 53 | $ yarn run test-foundry-fork 54 | ``` 55 | -------------------------------------------------------------------------------- /test/utils/Tokens.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | 6 | import { MockERC20 } from "test/mocks/MockERC20.sol"; 7 | import { MockWETH } from "test/mocks/MockWETH.sol"; 8 | import { Addresses } from "test/utils/Addresses.sol"; 9 | 10 | contract Tokens is Addresses { 11 | IERC20 public weth; 12 | IERC20 public usdt; 13 | IERC20 public usdc; 14 | IERC20 public dai; 15 | IERC20 public wbtc; 16 | IERC20 public lon; 17 | IERC20[] public tokens; 18 | 19 | constructor() { 20 | uint256 chainId = getChainId(); 21 | 22 | if (chainId == 31337) { 23 | // local testnet, deploy new ERC20s 24 | weth = IERC20(address(new MockWETH("Wrapped ETH", "WETH", 18))); 25 | usdt = new MockERC20("USDT", "USDT", 6); 26 | usdc = new MockERC20("USDC", "USDC", 18); 27 | dai = new MockERC20("DAI", "DAI", 18); 28 | wbtc = new MockERC20("WBTC", "WBTC", 18); 29 | lon = new MockERC20("LON", "LON", 18); 30 | } else { 31 | // forked mainnet, load ERC20s using constant address 32 | weth = IERC20(WETH_ADDRESS); 33 | usdt = IERC20(USDT_ADDRESS); 34 | usdc = IERC20(USDC_ADDRESS); 35 | dai = IERC20(DAI_ADDRESS); 36 | wbtc = IERC20(WBTC_ADDRESS); 37 | lon = IERC20(LON_ADDRESS); 38 | } 39 | 40 | tokens = [weth, usdt, usdc, dai, wbtc, lon]; 41 | vm.label(address(weth), "WETH"); 42 | vm.label(address(usdt), "USDT"); 43 | vm.label(address(usdc), "USDC"); 44 | vm.label(address(dai), "DAI"); 45 | vm.label(address(wbtc), "WBTC"); 46 | vm.label(address(lon), "LON"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/utils/IUniswapSwapRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapSwapRouter02 { 5 | function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to) external payable returns (uint256 amountOut); 6 | 7 | function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to) external payable returns (uint256 amountIn); 8 | 9 | struct ExactInputSingleParams { 10 | address tokenIn; 11 | address tokenOut; 12 | uint24 fee; 13 | address recipient; 14 | uint256 amountIn; 15 | uint256 amountOutMinimum; 16 | uint160 sqrtPriceLimitX96; 17 | } 18 | 19 | function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); 20 | 21 | struct ExactInputParams { 22 | bytes path; 23 | address recipient; 24 | uint256 amountIn; 25 | uint256 amountOutMinimum; 26 | } 27 | 28 | function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); 29 | 30 | struct ExactOutputSingleParams { 31 | address tokenIn; 32 | address tokenOut; 33 | uint24 fee; 34 | address recipient; 35 | uint256 amountOut; 36 | uint256 amountInMaximum; 37 | uint160 sqrtPriceLimitX96; 38 | } 39 | 40 | function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); 41 | 42 | struct ExactOutputParams { 43 | bytes path; 44 | address recipient; 45 | uint256 amountOut; 46 | uint256 amountInMaximum; 47 | } 48 | 49 | function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); 50 | } 51 | -------------------------------------------------------------------------------- /doc/RFQ.md: -------------------------------------------------------------------------------- 1 | # RFQ 2 | 3 | The RFQ (Request For Quote) contract facilitates the settlement of trades between two parties: a market maker and an user. We provide an off-chain quoting system that handles the quoting process. Users can request quotes for specific trading pairs and sizes. Upon receiving a request, any interested maker can provide a quote to the user. If the user accepts the quote, both parties will sign the trading order and submit it ot the RFQ contract along with their signatures. The RFQ contract verifies the signatures and executes the token transfers between the user and the market maker. 4 | 5 | ## Order option flags 6 | 7 | The maker of an RFQ offer can specify certain options using a `uint256` field in the offer, referred to as option flags: 8 | 9 | - `FLG_ALLOW_CONTRACT_SENDER` : Determines whether an RFQ offer can be filled by a contract. This flag is intended to prevent arbitrageurs from using contracts to execute flash loans to arbitrage RFQ orders. 10 | - `FLG_ALLOW_PARTIAL_FILL`: Determines whether an RFQ offer can be partially filled. However, each RFQ order can only be filled once, regardless of whether it's fully or partially filled. 11 | - `FLG_MAKER_RECEIVES_WETH` : Specifies whether a market maker wants the RFQ contract to wrap the ETH he received into WETH for him. 12 | 13 | ## Relayer 14 | 15 | The RFQ contract allows for trade submissions by a relayer with user's signatures. To prevent replay attacks, the hash of the relayed trade is recorded. 16 | 17 | ## Fee 18 | 19 | A portion of the maker's asset in the order will be deducted as a protocol fee. This fee is transferred to the `feeCollector` during settlement. 20 | 21 | The fee factor is composed of two parts: 22 | 23 | 1. Protocol Fee 24 | 2. Gas Fee 25 | 26 | If a trade is submitted by a relayer, the relayer will adjust the gas fee according to the on-chain conditions at the time of the transaction. 27 | -------------------------------------------------------------------------------- /test/mocks/MockERC1271Wallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 6 | import { ECDSA } from "@openzeppelin/contracts@v5.0.2/utils/cryptography/ECDSA.sol"; 7 | 8 | import { IERC1271Wallet } from "contracts/interfaces/IERC1271Wallet.sol"; 9 | 10 | contract MockERC1271Wallet is IERC1271Wallet { 11 | using SafeERC20 for IERC20; 12 | 13 | // bytes4(keccak256("isValidSignature(bytes32,bytes)")) 14 | bytes4 internal constant ERC1271_MAGICVALUE = 0x1626ba7e; 15 | uint256 private constant MAX_UINT = 2 ** 256 - 1; 16 | 17 | address public operator; 18 | 19 | modifier onlyOperator() { 20 | require(operator == msg.sender, "MockERC1271Wallet: not the operator"); 21 | _; 22 | } 23 | 24 | constructor(address _operator) { 25 | operator = _operator; 26 | } 27 | 28 | receive() external payable {} 29 | 30 | function setAllowance(address[] memory _tokenList, address _spender) external onlyOperator { 31 | for (uint256 i; i < _tokenList.length; i++) { 32 | IERC20(_tokenList[i]).forceApprove(_spender, MAX_UINT); 33 | } 34 | } 35 | 36 | function closeAllowance(address[] memory _tokenList, address _spender) external onlyOperator { 37 | for (uint256 i; i < _tokenList.length; i++) { 38 | IERC20(_tokenList[i]).forceApprove(_spender, 0); 39 | } 40 | } 41 | 42 | function isValidSignature(bytes32 _hash, bytes calldata _signature) external view override returns (bytes4 magicValue) { 43 | if (operator == ECDSA.recover(_hash, _signature)) { 44 | return ERC1271_MAGICVALUE; 45 | } else { 46 | return bytes4(0x12345678); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/utils/BalanceUtil.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { ERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/ERC20.sol"; 5 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 6 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 7 | import { StdStorage, stdStorage } from "forge-std/StdStorage.sol"; 8 | import { Test } from "forge-std/Test.sol"; 9 | 10 | contract BalanceUtil is Test { 11 | using stdStorage for StdStorage; 12 | using SafeERC20 for IERC20; 13 | 14 | function externalDeal(address tokenAddr, address userAddr, uint256 amountInWei, bool updateTotalSupply) public { 15 | deal(tokenAddr, userAddr, amountInWei, updateTotalSupply); 16 | } 17 | 18 | function setTokenBalanceAndApprove(address user, address spender, IERC20[] memory tokens, uint256 amount) internal { 19 | for (uint256 i; i < tokens.length; i++) { 20 | setERC20Balance(address(tokens[i]), user, amount); 21 | approveERC20(address(tokens[i]), user, spender); 22 | } 23 | } 24 | 25 | function setERC20Balance(address token, address user, uint256 amount) internal { 26 | uint256 decimals = uint256(ERC20(token).decimals()); 27 | uint256 amountInWei = amount * (10 ** decimals); 28 | // First try to update `totalSupply` together, but this would fail with WETH because WETH does not store `totalSupply` in storage 29 | try this.externalDeal(token, user, amountInWei, true) {} catch { 30 | // If it fails, try again without update `totalSupply` 31 | deal(token, user, amountInWei, false); 32 | } 33 | } 34 | 35 | function approveERC20(address token, address user, address spender) internal { 36 | vm.startPrank(user); 37 | IERC20(token).forceApprove(spender, type(uint256).max); 38 | vm.stopPrank(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/interfaces/ISmartOrderStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IStrategy } from "./IStrategy.sol"; 5 | 6 | /// @title ISmartOrderStrategy Interface 7 | /// @author imToken Labs 8 | interface ISmartOrderStrategy is IStrategy { 9 | /// @title Operation 10 | /// @notice Struct containing parameters for the operation. 11 | /// @dev The encoded operation list should be passed as `data` when calling `IStrategy.executeStrategy` 12 | struct Operation { 13 | address dest; 14 | address inputToken; 15 | uint256 ratioNumerator; 16 | uint256 ratioDenominator; 17 | uint256 dataOffset; 18 | uint256 value; 19 | bytes data; 20 | } 21 | 22 | /// @notice Error thrown when the input is zero. 23 | /// @dev Thrown when an operation requires a non-zero input value that is not provided. 24 | error ZeroInput(); 25 | 26 | /// @notice Error thrown when the denominator is zero. 27 | /// @dev Thrown when an operation requires a non-zero denominator that is not provided. 28 | error ZeroDenominator(); 29 | 30 | /// @notice Error thrown when the operation list is empty. 31 | /// @dev Thrown when an operation list is required to be non-empty but is empty. 32 | error EmptyOps(); 33 | 34 | /// @notice Error thrown when the msg.value is invalid. 35 | /// @dev Thrown when an operation requires a specific msg.value that is not provided. 36 | error InvalidMsgValue(); 37 | 38 | /// @notice Error thrown when the input ratio is invalid. 39 | /// @dev Thrown when an operation requires a valid input ratio that is not provided or is invalid. 40 | error InvalidInputRatio(); 41 | 42 | /// @notice Error thrown when the operation is not from a Governance System (GS). 43 | /// @dev Thrown when an operation is attempted by an unauthorized caller that is not from a Governance System (GS). 44 | error NotFromGS(); 45 | } 46 | -------------------------------------------------------------------------------- /financial-statements/2024_Q4.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 27/10/2024,2173 LON for DevCon7 sponsorship with imToken,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"2,000.00",USDT,https://arbiscan.io/tx/0x6153f199b9815502f3b2be98513f1f5cebbf863bbf0da33a454101c7d17b789e,0x15af2f7336f902c0492281b4e0dd8d5e716fa4c3479eec9c231a2756cc98d28f 3 | 20/12/2024,5126.064735 USDT swapped for 1.4999865 ETH,Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"5,126.06",USDT,User product tests gas fees 1 ETH + current community treasury gas fees 0.5 ETH,https://etherscan.io/tx/0x5c19253d8376577d0c874f6cb4bb3aec40bd9d721c4a523cda4afc348940c078?locale=zh-CN&utm_source=imtoken 4 | ,,,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4031e602F315e37ec17415740fd24605372911B6,1.00,ETH,Product test gas fees,https://etherscan.io/tx/0xf380027246a6b0cc00c42ae05a110f34665e1ef18898306e50e9921dad632d80?locale=zh-CN&utm_source=imtoken 5 | 20/12/2024,7200.390516 USDT swapped for 8949.9999 LON,Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"7,200.39",USDT,"Devcon Twitter campaign 300 LON + BNB campaign 1150 LON + Xmas campaign 7500 LON, total 8950 LON",https://etherscan.io/tx/0x5c19253d8376577d0c874f6cb4bb3aec40bd9d721c4a523cda4afc348940c078?locale=zh-CN&utm_source=imtoken 6 | ,,,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"8,950.00",LON,"Rewards for DevCon, BNB, and Xmas campaign distribution",https://etherscan.io/tx/0x4652d57dca6c14b05e0595ab9bc350fb3dbc6c0b705a24d356b553299818afcf?locale=zh-CN&utm_source=imtoken 7 | ,,,,,,,, 8 | ,,,,,,,, 9 | ,,,,,,,, 10 | ,,,,,,,, 11 | ,,,,,,,, 12 | ,,,,,,,, 13 | ,,,,,,,, 14 | ,,,,,,,, 15 | ,,,,,,,, 16 | ,,,,,,,, 17 | ,,,,,,,, 18 | ,,,,,,,, 19 | ,,,,,,,, 20 | ,,,,,,,, 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,,,,,,,, 24 | ,Q4 Total expenditure in USDT:,"14,326.45",,,,,, -------------------------------------------------------------------------------- /doc/LimitOrderSwap.md: -------------------------------------------------------------------------------- 1 | # LimitOrder 2 | 3 | The `LimitOrder` contract provides order book style trading functionalities which is similar to what centralized exchange does. Orders can be queried from the off-chain order book system and then be filled on chain. Also, it supports order cancelling and partially fill. The `LimitOrder` contract shares the benefit from the infrastructure of Tokenlon which allow users to create an order without depositing tokens first. Tokens are transferred between taker and maker only when the order gets filled on chain. This design provides the certainty of price for maker while maintaining filling flexibilities for taker at the same time. 4 | 5 | A taker can optionally provide extra action parameter in payload which will be executed after maker token settlement. Given the ability to execute an external call, taker can leverage the liquidity of any AMM protocol to fulfill the order or validate the trade execute by external condition checking. 6 | 7 | ## FullOrKill 8 | 9 | The default `fillLimitOrder` function allows the settled taking amount is less than a taker requested. The reason is that it may not be sufficient for the whole request when a trade is actual executed and the rest available taking amount would be the actual taking amount in that case. If a taker wants a non-adjustable taking amount, then the `fillLimitOrderFullOrKill` function should be called instead. 10 | 11 | ## GroupFill 12 | 13 | If a group of orders can be fulfilled by each other, then no external liquidity is needed for settling those orders. A user can spot this kind of group with profits so it would be the incentive of searching and submitting it. In some cases, a user may need to add some external liquidity or create some orders so the group can be formed and settled. 14 | 15 | ## Fee 16 | 17 | Some portion of maker asset of an order will be deducted as protocol fee. The fee will be transferred to `feeCollector` during the settlement. Each order may have different fee factor, it depends on the characteristic of an order. 18 | -------------------------------------------------------------------------------- /test/mocks/MockLimitOrderTaker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | import { MockERC1271Wallet } from "./MockERC1271Wallet.sol"; 8 | 9 | import { IStrategy } from "contracts/interfaces/IStrategy.sol"; 10 | 11 | import { IUniswapSwapRouter02 } from "test/utils/IUniswapSwapRouter02.sol"; 12 | 13 | contract MockLimitOrderTaker is IStrategy, MockERC1271Wallet { 14 | using SafeERC20 for IERC20; 15 | 16 | IUniswapSwapRouter02 public immutable uniswapRouter02; 17 | 18 | constructor(address _operator, address _uniswapRouter02) MockERC1271Wallet(_operator) { 19 | uniswapRouter02 = IUniswapSwapRouter02(_uniswapRouter02); 20 | } 21 | 22 | function executeStrategy(address targetToken, bytes calldata strategyData) external payable override { 23 | (address routerAddr, address inputToken, uint256 inputAmount, bytes memory makerSpecificData) = abi.decode( 24 | strategyData, 25 | (address, address, uint256, bytes) 26 | ); 27 | require(routerAddr == address(uniswapRouter02), "non supported protocol"); 28 | 29 | address[] memory path = abi.decode(makerSpecificData, (address[])); 30 | _validateAMMPath(inputToken, targetToken, path); 31 | _tradeUniswapV2TokenToToken(inputAmount, path); 32 | } 33 | 34 | function _tradeUniswapV2TokenToToken(uint256 _inputAmount, address[] memory _path) internal returns (uint256) { 35 | return uniswapRouter02.swapExactTokensForTokens(_inputAmount, 0, _path, address(this)); 36 | } 37 | 38 | function _validateAMMPath(address _inputToken, address _outputToken, address[] memory _path) internal pure { 39 | require(_path.length >= 2, "path length must be at least two"); 40 | require(_path[0] == _inputToken, "invalid path"); 41 | require(_path[_path.length - 1] == _outputToken, "invalid path"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/abstracts/AdminManagement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | import { Asset } from "../libraries/Asset.sol"; 8 | 9 | import { Ownable } from "./Ownable.sol"; 10 | 11 | /// @title AdminManagement Contract 12 | /// @author imToken Labs 13 | /// @notice This contract provides administrative functions for token management. 14 | abstract contract AdminManagement is Ownable { 15 | using SafeERC20 for IERC20; 16 | 17 | /// @notice Sets the initial owner of the contract. 18 | /// @param _owner The address of the owner who can execute administrative functions. 19 | constructor(address _owner) Ownable(_owner) {} 20 | 21 | /// @notice Approves multiple tokens to multiple spenders with an unlimited allowance. 22 | /// @dev Only the owner can call this function. 23 | /// @param tokens The array of token addresses to approve. 24 | /// @param spenders The array of spender addresses to approve for each token. 25 | function approveTokens(address[] calldata tokens, address[] calldata spenders) external onlyOwner { 26 | for (uint256 i; i < tokens.length; ++i) { 27 | for (uint256 j; j < spenders.length; ++j) { 28 | IERC20(tokens[i]).forceApprove(spenders[j], type(uint256).max); 29 | } 30 | } 31 | } 32 | 33 | /// @notice Rescues multiple tokens held by this contract to the specified recipient. 34 | /// @dev Only the owner can call this function. 35 | /// @param tokens An array of token addresses to rescue. 36 | /// @param recipient The address to which rescued tokens will be transferred. 37 | function rescueTokens(address[] calldata tokens, address recipient) external onlyOwner { 38 | for (uint256 i; i < tokens.length; ++i) { 39 | uint256 selfBalance = Asset.getBalance(tokens[i], address(this)); 40 | Asset.transferTo(tokens[i], payable(recipient), selfBalance); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/AllowanceTarget.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 6 | import { Pausable } from "@openzeppelin/contracts@v5.0.2/utils/Pausable.sol"; 7 | 8 | import { Ownable } from "./abstracts/Ownable.sol"; 9 | 10 | import { IAllowanceTarget } from "./interfaces/IAllowanceTarget.sol"; 11 | 12 | /// @title AllowanceTarget Contract 13 | /// @author imToken Labs 14 | /// @notice This contract manages allowances and authorizes spenders to transfer tokens on behalf of users. 15 | contract AllowanceTarget is IAllowanceTarget, Pausable, Ownable { 16 | using SafeERC20 for IERC20; 17 | 18 | /// @notice Mapping of authorized addresses permitted to call spendFromUserTo. 19 | mapping(address trustedCaller => bool isAuthorized) public authorized; 20 | 21 | /// @notice Constructor to initialize the contract with the owner and trusted callers. 22 | /// @param _owner The address of the contract owner. 23 | /// @param trustedCaller An array of addresses that are initially authorized to call spendFromUserTo. 24 | constructor(address _owner, address[] memory trustedCaller) Ownable(_owner) { 25 | uint256 callerCount = trustedCaller.length; 26 | for (uint256 i; i < callerCount; ++i) { 27 | authorized[trustedCaller[i]] = true; 28 | } 29 | } 30 | 31 | /// @notice Pauses the contract, preventing the execution of spendFromUserTo. 32 | /// @dev Only the owner can call this function. 33 | function pause() external onlyOwner { 34 | _pause(); 35 | } 36 | 37 | /// @notice Unpauses the contract, allowing the execution of spendFromUserTo. 38 | /// @dev Only the owner can call this function. 39 | function unpause() external onlyOwner { 40 | _unpause(); 41 | } 42 | 43 | /// @inheritdoc IAllowanceTarget 44 | function spendFromUserTo(address from, address token, address to, uint256 amount) external whenNotPaused { 45 | if (!authorized[msg.sender]) revert NotAuthorized(); 46 | IERC20(token).safeTransferFrom(from, to, amount); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/utils/BalanceSnapshot.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | 6 | import { Constant } from "contracts/libraries/Constant.sol"; 7 | 8 | struct Snapshot { 9 | address owner; 10 | IERC20 token; 11 | int256 balanceBefore; // Assume max balance is type(int256).max 12 | } 13 | 14 | library BalanceSnapshot { 15 | function take(address owner, address token) internal view returns (Snapshot memory) { 16 | uint256 balanceBefore; 17 | if (token == Constant.ETH_ADDRESS || token == Constant.ZERO_ADDRESS) { 18 | balanceBefore = owner.balance; 19 | } else { 20 | balanceBefore = IERC20(token).balanceOf(owner); 21 | } 22 | return Snapshot(owner, IERC20(token), int256(balanceBefore)); 23 | } 24 | 25 | function _getBalanceAfter(Snapshot memory snapshot) internal view returns (int256) { 26 | if (address(snapshot.token) == Constant.ETH_ADDRESS || address(snapshot.token) == Constant.ZERO_ADDRESS) { 27 | return int256(snapshot.owner.balance); 28 | } else { 29 | return int256(snapshot.token.balanceOf(snapshot.owner)); 30 | } 31 | } 32 | 33 | function assertChange(Snapshot memory snapshot, int256 expectedChange) internal view { 34 | int256 balanceAfter = _getBalanceAfter(snapshot); 35 | require(balanceAfter - snapshot.balanceBefore == expectedChange, "Not expected balance change"); 36 | } 37 | 38 | function assertChangeGt(Snapshot memory snapshot, int256 expectedMinChange) internal view { 39 | int256 balanceAfter = _getBalanceAfter(snapshot); 40 | int256 balanceChange = balanceAfter - snapshot.balanceBefore; 41 | bool sameSign = (balanceChange >= int256(0) && expectedMinChange >= int256(0)) || (balanceChange <= int256(0) && expectedMinChange <= int256(0)); 42 | require(sameSign, "Actual and expected change do not have the same sign"); 43 | 44 | if (balanceChange > int256(0)) { 45 | require(balanceChange >= expectedMinChange, "Not expected balance change"); 46 | } else { 47 | require(balanceChange < expectedMinChange, "Not expected balance change"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/forkMainnet/LimitOrderSwap/CancelOrder.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { ILimitOrderSwap } from "contracts/interfaces/ILimitOrderSwap.sol"; 5 | import { getLimitOrderHash } from "contracts/libraries/LimitOrder.sol"; 6 | 7 | import { LimitOrderSwapTest } from "test/forkMainnet/LimitOrderSwap/Setup.t.sol"; 8 | 9 | contract CancelOrderTest is LimitOrderSwapTest { 10 | function testCancelOrder() public { 11 | vm.expectEmit(true, true, true, true); 12 | emit ILimitOrderSwap.OrderCanceled(getLimitOrderHash(defaultOrder), maker); 13 | 14 | vm.startPrank(maker); 15 | limitOrderSwap.cancelOrder(defaultOrder); 16 | vm.stopPrank(); 17 | vm.snapshotGasLastCall("LimitOrderSwap", "cancelOrder(): testCancelOrder"); 18 | 19 | assertEq(limitOrderSwap.isOrderCanceled(getLimitOrderHash(defaultOrder)), true); 20 | } 21 | 22 | function testCannotCancelOrderIfNotMaker() public { 23 | vm.startPrank(taker); 24 | vm.expectRevert(ILimitOrderSwap.NotOrderMaker.selector); 25 | limitOrderSwap.cancelOrder(defaultOrder); 26 | vm.stopPrank(); 27 | } 28 | 29 | function testCannotCancelExpiredOrder() public { 30 | vm.warp(defaultOrder.expiry + 1); 31 | 32 | vm.startPrank(maker); 33 | vm.expectRevert(ILimitOrderSwap.ExpiredOrder.selector); 34 | limitOrderSwap.cancelOrder(defaultOrder); 35 | vm.stopPrank(); 36 | } 37 | 38 | function testCannotCancelFilledOrder() public { 39 | vm.startPrank(taker); 40 | limitOrderSwap.fillLimitOrder({ order: defaultOrder, makerSignature: defaultMakerSig, takerParams: defaultTakerParams }); 41 | vm.stopPrank(); 42 | 43 | vm.startPrank(maker); 44 | vm.expectRevert(ILimitOrderSwap.FilledOrder.selector); 45 | limitOrderSwap.cancelOrder(defaultOrder); 46 | vm.stopPrank(); 47 | } 48 | 49 | function testCannotCancelCanceledOrder() public { 50 | vm.startPrank(maker); 51 | limitOrderSwap.cancelOrder(defaultOrder); 52 | vm.stopPrank(); 53 | 54 | vm.startPrank(maker); 55 | vm.expectRevert(ILimitOrderSwap.CanceledOrder.selector); 56 | limitOrderSwap.cancelOrder(defaultOrder); 57 | vm.stopPrank(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/utils/IUniswapV2Router.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapV2Router { 5 | function swapExactTokensForTokens( 6 | uint256 amountIn, 7 | uint256 amountOutMin, 8 | address[] calldata path, 9 | address to, 10 | uint256 deadline 11 | ) external returns (uint256[] memory amounts); 12 | 13 | function addLiquidity( 14 | address tokenA, 15 | address tokenB, 16 | uint256 amountADesired, 17 | uint256 amountBDesired, 18 | uint256 amountAMin, 19 | uint256 amountBMin, 20 | address to, 21 | uint256 deadline 22 | ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); 23 | 24 | function addLiquidityETH( 25 | address token, 26 | uint256 amountTokenDesired, 27 | uint256 amountTokenMin, 28 | uint256 amountETHMin, 29 | address to, 30 | uint256 deadline 31 | ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); 32 | 33 | function removeLiquidity( 34 | address tokenA, 35 | address tokenB, 36 | uint256 liquidity, 37 | uint256 amountAMin, 38 | uint256 amountBMin, 39 | address to, 40 | uint256 deadline 41 | ) external returns (uint256 amountA, uint256 amountB); 42 | 43 | function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); 44 | 45 | function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); 46 | 47 | function swapETHForExactTokens( 48 | uint256 amountOut, 49 | address[] calldata path, 50 | address to, 51 | uint256 deadline 52 | ) external payable returns (uint256[] memory amounts); 53 | 54 | function swapExactETHForTokens( 55 | uint256 amountOutMin, 56 | address[] calldata path, 57 | address to, 58 | uint256 deadline 59 | ) external payable returns (uint256[] memory amounts); 60 | 61 | function swapExactTokensForETH( 62 | uint256 amountIn, 63 | uint256 amountOutMin, 64 | address[] calldata path, 65 | address to, 66 | uint256 deadline 67 | ) external returns (uint256[] memory amounts); 68 | } 69 | -------------------------------------------------------------------------------- /test/forkMainnet/SmartOrderStrategy/Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { SmartOrderStrategy } from "contracts/SmartOrderStrategy.sol"; 7 | 8 | import { BalanceUtil } from "test/utils/BalanceUtil.sol"; 9 | import { IUniswapV3Quoter } from "test/utils/IUniswapV3Quoter.sol"; 10 | import { Tokens } from "test/utils/Tokens.sol"; 11 | import { UniswapV3 } from "test/utils/UniswapV3.sol"; 12 | 13 | contract SmartOrderStrategyTest is Test, Tokens, BalanceUtil { 14 | address strategyOwner = makeAddr("strategyOwner"); 15 | address genericSwap = makeAddr("genericSwap"); 16 | address defaultInputToken = USDC_ADDRESS; 17 | address defaultOutputToken = WETH_ADDRESS; 18 | uint256 defaultInputAmount = 1000; 19 | uint256 defaultInputRatio = 5000; 20 | uint256 defaultExpiry = block.timestamp + 100; 21 | bytes defaultOpsData; 22 | bytes encodedUniv3Path; 23 | address[] defaultUniV2Path = [defaultInputToken, defaultOutputToken]; 24 | address[] tokenList = [USDT_ADDRESS, USDC_ADDRESS, WETH_ADDRESS, WBTC_ADDRESS]; 25 | address[] ammList = [UNISWAP_SWAP_ROUTER_02_ADDRESS, CURVE_TRICRYPTO2_POOL_ADDRESS]; 26 | 27 | uint24 defaultFee = 3000; 28 | uint24[] v3Fees = [defaultFee]; 29 | 30 | SmartOrderStrategy smartOrderStrategy; 31 | IUniswapV3Quoter v3Quoter; 32 | 33 | function setUp() public virtual { 34 | // Deploy and setup SmartOrderStrategy 35 | smartOrderStrategy = new SmartOrderStrategy(strategyOwner, genericSwap, WETH_ADDRESS); 36 | vm.startPrank(strategyOwner); 37 | smartOrderStrategy.approveTokens(tokenList, ammList); 38 | vm.stopPrank(); 39 | 40 | // Make genericSwap rich to provide fund for strategy contract 41 | deal(genericSwap, 100 ether); 42 | for (uint256 i; i < tokenList.length; i++) { 43 | setERC20Balance(tokenList[i], genericSwap, 10000); 44 | } 45 | 46 | SmartOrderStrategy.Operation[] memory operations = new SmartOrderStrategy.Operation[](1); 47 | defaultOpsData = abi.encode(operations); 48 | 49 | v3Quoter = IUniswapV3Quoter(UNISWAP_V3_QUOTER_ADDRESS); 50 | encodedUniv3Path = UniswapV3.encodePath(defaultUniV2Path, v3Fees); 51 | 52 | vm.label(UNISWAP_UNIVERSAL_ROUTER_ADDRESS, "UniswapUniversalRouter"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/mocks/MockNoReturnERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev No return value on approve, transfer, transferFrom. (USDT) 6 | */ 7 | contract MockNoReturnERC20 { 8 | event Approval(address indexed owner, address indexed spender, uint256 value); 9 | event Transfer(address indexed from, address indexed to, uint256 value); 10 | 11 | mapping(address => uint256) private _balances; 12 | mapping(address => mapping(address => uint256)) private _allowances; 13 | 14 | uint256 public totalSupply; 15 | 16 | string public name = "MockNoReturnERC20"; 17 | string public symbol = "MNRT"; 18 | uint8 public decimals = 18; 19 | 20 | function allowance(address owner, address spender) public view returns (uint256) { 21 | return _allowances[owner][spender]; 22 | } 23 | 24 | function approve(address spender, uint256 amount) public { 25 | _approve(msg.sender, spender, amount); 26 | } 27 | 28 | function balanceOf(address account) public view returns (uint256) { 29 | return _balances[account]; 30 | } 31 | 32 | function transfer(address recipient, uint256 amount) public { 33 | _transfer(msg.sender, recipient, amount); 34 | } 35 | 36 | function transferFrom(address sender, address recipient, uint256 amount) public { 37 | require(_allowances[sender][msg.sender] >= amount, "ERC20: transfer amount exceeds allowance"); 38 | _transfer(sender, recipient, amount); 39 | _approve(sender, msg.sender, _allowances[sender][msg.sender] - amount); 40 | } 41 | 42 | function _approve(address owner, address spender, uint256 amount) internal { 43 | require(owner != address(0), "ERC20: approve from the zero address"); 44 | require(spender != address(0), "ERC20: approve to the zero address"); 45 | 46 | _allowances[owner][spender] = amount; 47 | 48 | emit Approval(owner, spender, amount); 49 | } 50 | 51 | function _transfer(address sender, address recipient, uint256 amount) internal { 52 | require(sender != address(0), "ERC20: transfer from the zero address"); 53 | require(recipient != address(0), "ERC20: transfer to the zero address"); 54 | require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance"); 55 | 56 | _balances[sender] = _balances[sender] - amount; 57 | _balances[recipient] = _balances[recipient] + amount; 58 | emit Transfer(sender, recipient, amount); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/abstracts/EIP712.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { EIP712 } from "contracts/abstracts/EIP712.sol"; 7 | 8 | contract EIP712Test is Test { 9 | EIP712Harness eip712Harness; 10 | 11 | // Dummy struct hash for testing 12 | bytes32 public constant DUMMY_STRUCT_HASH = keccak256("DummyStruct(string message)"); 13 | 14 | function setUp() public { 15 | eip712Harness = new EIP712Harness(); 16 | } 17 | 18 | function testOriginalChainId() public { 19 | uint256 chainId = block.chainid; 20 | assertEq(eip712Harness.originalChainId(), chainId); 21 | } 22 | 23 | function testOriginalDomainSeparator() public { 24 | bytes32 expectedDomainSeparator = eip712Harness.calculateDomainSeparator(); 25 | assertEq(eip712Harness.originalEIP712DomainSeparator(), expectedDomainSeparator); 26 | } 27 | 28 | function testGetEIP712Hash() public { 29 | bytes32 structHash = DUMMY_STRUCT_HASH; 30 | bytes32 domainSeparator = eip712Harness.calculateDomainSeparator(); 31 | bytes32 expectedEIP712Hash = keccak256(abi.encodePacked(hex"1901", domainSeparator, structHash)); 32 | 33 | assertEq(eip712Harness.exposedGetEIP712Hash(structHash), expectedEIP712Hash); 34 | vm.snapshotGasLastCall("EIP712", "getEIP712Hash(): testGetEIP712Hash"); 35 | } 36 | 37 | function testDomainSeparatorOnDifferentChain() public { 38 | uint256 chainId = block.chainid + 1234; 39 | vm.chainId(chainId); 40 | 41 | bytes32 newDomainSeparator = eip712Harness.calculateDomainSeparator(); 42 | assertEq(eip712Harness.EIP712_DOMAIN_SEPARATOR(), newDomainSeparator, "Domain separator should match the expected value on a different chain"); 43 | vm.snapshotGasLastCall("EIP712", "EIP712_DOMAIN_SEPARATOR(): testDomainSeparatorOnDifferentChain"); 44 | } 45 | 46 | function testDomainSeparatorOnChain() public { 47 | eip712Harness.EIP712_DOMAIN_SEPARATOR(); 48 | vm.snapshotGasLastCall("EIP712", "EIP712_DOMAIN_SEPARATOR(): testDomainSeparatorOnChain"); 49 | } 50 | } 51 | 52 | contract EIP712Harness is EIP712 { 53 | function calculateDomainSeparator() external view returns (bytes32) { 54 | return keccak256(abi.encode(EIP712_TYPE_HASH, keccak256(bytes(EIP712_NAME)), keccak256(bytes(EIP712_VERSION)), block.chainid, address(this))); 55 | } 56 | 57 | function exposedGetEIP712Hash(bytes32 structHash) public view returns (bytes32) { 58 | return getEIP712Hash(structHash); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/forkMainnet/SmartOrderStrategy/Validation.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { SmartOrderStrategyTest } from "./Setup.t.sol"; 5 | 6 | import { ISmartOrderStrategy } from "contracts/interfaces/ISmartOrderStrategy.sol"; 7 | 8 | contract ValidationTest is SmartOrderStrategyTest { 9 | function testCannotExecuteNotFromGenericSwap() public { 10 | vm.expectRevert(ISmartOrderStrategy.NotFromGS.selector); 11 | smartOrderStrategy.executeStrategy(defaultOutputToken, defaultOpsData); 12 | } 13 | 14 | function testCannotExecuteWithZeroRatioDenominatorWhenRatioNumeratorIsNonZero() public { 15 | ISmartOrderStrategy.Operation[] memory operations = new ISmartOrderStrategy.Operation[](1); 16 | operations[0].inputToken = USDC_ADDRESS; 17 | operations[0].ratioNumerator = 1; 18 | operations[0].ratioDenominator = 0; 19 | bytes memory opsData = abi.encode(operations); 20 | 21 | vm.startPrank(genericSwap); 22 | vm.expectRevert(ISmartOrderStrategy.ZeroDenominator.selector); 23 | smartOrderStrategy.executeStrategy(defaultOutputToken, opsData); 24 | vm.stopPrank(); 25 | } 26 | 27 | function testCannotExecuteWithFailDecodedData() public { 28 | vm.startPrank(genericSwap); 29 | vm.expectRevert(); 30 | smartOrderStrategy.executeStrategy(defaultOutputToken, bytes("random data")); 31 | vm.stopPrank(); 32 | } 33 | 34 | function testCannotExecuteWithEmptyOperation() public { 35 | ISmartOrderStrategy.Operation[] memory operations; 36 | bytes memory emptyOpsData = abi.encode(operations); 37 | 38 | vm.startPrank(genericSwap); 39 | vm.expectRevert(ISmartOrderStrategy.EmptyOps.selector); 40 | smartOrderStrategy.executeStrategy(defaultOutputToken, emptyOpsData); 41 | vm.stopPrank(); 42 | } 43 | 44 | function testCannotExecuteAnOperationWillFail() public { 45 | ISmartOrderStrategy.Operation[] memory operations = new ISmartOrderStrategy.Operation[](1); 46 | operations[0] = ISmartOrderStrategy.Operation({ 47 | dest: defaultInputToken, 48 | inputToken: defaultInputToken, 49 | ratioNumerator: 0, 50 | ratioDenominator: 0, 51 | dataOffset: 0, 52 | value: 0, 53 | data: abi.encode("invalid data") 54 | }); 55 | bytes memory opsData = abi.encode(operations); 56 | 57 | vm.startPrank(genericSwap); 58 | vm.expectRevert(); 59 | smartOrderStrategy.executeStrategy(defaultOutputToken, opsData); 60 | vm.stopPrank(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/mocks/MockDeflationaryERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | 6 | /** 7 | * @dev Burn a portion of tokens while transferring. (STA) 8 | */ 9 | contract MockDeflationaryERC20 is IERC20 { 10 | mapping(address => uint256) private _balances; 11 | mapping(address => mapping(address => uint256)) private _allowances; 12 | 13 | uint256 public override totalSupply; 14 | 15 | string public name = "MockDeflationaryERC20"; 16 | string public symbol = "MDT"; 17 | uint8 public decimals = 18; 18 | 19 | function allowance(address owner, address spender) public view override returns (uint256) { 20 | return _allowances[owner][spender]; 21 | } 22 | 23 | function approve(address spender, uint256 amount) public override returns (bool) { 24 | _approve(msg.sender, spender, amount); 25 | return true; 26 | } 27 | 28 | function balanceOf(address account) public view override returns (uint256) { 29 | return _balances[account]; 30 | } 31 | 32 | function transfer(address recipient, uint256 amount) public override returns (bool) { 33 | _transfer(msg.sender, recipient, amount); 34 | return true; 35 | } 36 | 37 | function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { 38 | require(_allowances[sender][msg.sender] >= amount, "ERC20: transfer amount exceeds allowance"); 39 | _transfer(sender, recipient, amount); 40 | _approve(sender, msg.sender, _allowances[sender][msg.sender] - amount); 41 | return true; 42 | } 43 | 44 | function _approve(address owner, address spender, uint256 amount) internal { 45 | require(owner != address(0), "ERC20: approve from the zero address"); 46 | require(spender != address(0), "ERC20: approve to the zero address"); 47 | 48 | _allowances[owner][spender] = amount; 49 | 50 | emit Approval(owner, spender, amount); 51 | } 52 | 53 | function _transfer(address sender, address recipient, uint256 amount) internal { 54 | require(sender != address(0), "ERC20: transfer from the zero address"); 55 | require(recipient != address(0), "ERC20: transfer to the zero address"); 56 | 57 | uint256 amountToBurn = (amount * 1) / 100; 58 | uint256 amountToTransfer = amount - amountToBurn; 59 | 60 | require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance"); 61 | _balances[sender] = _balances[sender] - amount; 62 | _balances[recipient] = _balances[recipient] + amountToTransfer; 63 | _balances[address(0)] = _balances[address(0)] + amountToBurn; 64 | 65 | emit Transfer(sender, recipient, amountToTransfer); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/libraries/Asset.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | import { Constant } from "./Constant.sol"; 8 | 9 | /// @title Asset Library 10 | /// @author imToken Labs 11 | /// @notice Library for handling asset operations, including ETH and ERC20 tokens 12 | library Asset { 13 | using SafeERC20 for IERC20; 14 | 15 | /// @notice Error thrown when there is insufficient balance for a transfer 16 | error InsufficientBalance(); 17 | 18 | /// @notice Error thrown when an ETH transfer fails 19 | error ETHTransferFailed(); 20 | 21 | /// @notice Checks if an address is ETH 22 | /// @dev ETH is identified by comparing the address to Constant.ETH_ADDRESS or Constant.ZERO_ADDRESS 23 | /// @param addr The address to check 24 | /// @return true if the address is ETH, false otherwise 25 | function isETH(address addr) internal pure returns (bool) { 26 | return (addr == Constant.ETH_ADDRESS || addr == Constant.ZERO_ADDRESS); 27 | } 28 | 29 | /// @notice Gets the balance of an asset for a specific owner 30 | /// @dev If the asset is ETH, retrieves the ETH balance of the owner; otherwise, retrieves the ERC20 balance 31 | /// @param asset The address of the asset (ETH or ERC20 token) 32 | /// @param owner The address of the owner 33 | /// @return The balance of the asset owned by the owner 34 | function getBalance(address asset, address owner) internal view returns (uint256) { 35 | if (isETH(asset)) { 36 | return owner.balance; 37 | } else { 38 | return IERC20(asset).balanceOf(owner); 39 | } 40 | } 41 | 42 | /// @notice Transfers an amount of asset to a recipient address 43 | /// @dev If the asset is ETH, transfers ETH using a low-level call; otherwise, uses SafeERC20 for ERC20 transfers 44 | /// @param asset The address of the asset (ETH or ERC20 token) 45 | /// @param to The address of the recipient 46 | /// @param amount The amount to transfer 47 | function transferTo(address asset, address payable to, uint256 amount) internal { 48 | if (amount > 0) { 49 | if (to != address(this)) { 50 | if (isETH(asset)) { 51 | // @dev Forward all available gas and may cause reentrancy 52 | if (address(this).balance < amount) revert InsufficientBalance(); 53 | (bool success, ) = to.call{ value: amount }(""); 54 | if (!success) revert ETHTransferFailed(); 55 | } else { 56 | IERC20(asset).safeTransfer(to, amount); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/utils/IUniswapV3SwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Router token swapping functionality 5 | /// @notice Functions for swapping tokens via Uniswap V3 6 | interface IUniswapV3SwapRouter { 7 | struct ExactInputSingleParams { 8 | address tokenIn; 9 | address tokenOut; 10 | uint24 fee; 11 | address recipient; 12 | uint256 deadline; 13 | uint256 amountIn; 14 | uint256 amountOutMinimum; 15 | uint160 sqrtPriceLimitX96; 16 | } 17 | 18 | /// @notice Swaps `amountIn` of one token for as much as possible of another token 19 | /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata 20 | /// @return amountOut The amount of the received token 21 | function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); 22 | 23 | struct ExactInputParams { 24 | bytes path; 25 | address recipient; 26 | uint256 deadline; 27 | uint256 amountIn; 28 | uint256 amountOutMinimum; 29 | } 30 | 31 | /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path 32 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata 33 | /// @return amountOut The amount of the received token 34 | function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); 35 | 36 | struct ExactOutputSingleParams { 37 | address tokenIn; 38 | address tokenOut; 39 | uint24 fee; 40 | address recipient; 41 | uint256 deadline; 42 | uint256 amountOut; 43 | uint256 amountInMaximum; 44 | uint160 sqrtPriceLimitX96; 45 | } 46 | 47 | /// @notice Swaps as little as possible of one token for `amountOut` of another token 48 | /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata 49 | /// @return amountIn The amount of the input token 50 | function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); 51 | 52 | struct ExactOutputParams { 53 | bytes path; 54 | address recipient; 55 | uint256 deadline; 56 | uint256 amountOut; 57 | uint256 amountInMaximum; 58 | } 59 | 60 | /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) 61 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata 62 | /// @return amountIn The amount of the input token 63 | function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/gas-diff.yml: -------------------------------------------------------------------------------- 1 | name: Report gas diff 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | compare_gas_reports: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install Foundry 30 | uses: onbjerg/foundry-toolchain@v1 31 | with: 32 | version: nightly 33 | 34 | - name: Run gas report on local and fork tests 35 | env: 36 | FOUNDRY_FUZZ_SEED: 0x123 37 | MAINNET_NODE_RPC_URL: ${{ secrets.MAINNET_NODE_RPC_URL }} 38 | FOUNDRY_PROFILE: CI 39 | # the report file name, e.g. gasreport-local.ansi, should be unique in your repository! 40 | run: | 41 | yarn compile 42 | yarn gas-report-local > gasreport-local.ansi 43 | yarn gas-report-fork > gasreport-fork.ansi 44 | echo "### Local tests gas report" >> $GITHUB_STEP_SUMMARY 45 | cat gasreport-local.ansi >> $GITHUB_STEP_SUMMARY 46 | echo "### Fork tests gas report" >> $GITHUB_STEP_SUMMARY 47 | cat gasreport-fork.ansi >> $GITHUB_STEP_SUMMARY 48 | 49 | - name: Compare gas reports of local tests 50 | uses: Rubilmax/foundry-gas-diff@v3 51 | with: 52 | report: gasreport-local.ansi 53 | ignore: test/**/* 54 | id: gas_diff_local 55 | 56 | - name: Compare gas reports of fork tests 57 | uses: Rubilmax/foundry-gas-diff@v3 58 | with: 59 | report: gasreport-fork.ansi 60 | ignore: test/**/* 61 | id: gas_diff_fork 62 | 63 | - name: Add gas diff local to sticky comment 64 | if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' 65 | uses: marocchino/sticky-pull-request-comment@v2 66 | with: 67 | header: gasrepost-local 68 | # delete the comment in case changes no longer impact gas costs 69 | delete: ${{ !steps.gas_diff_local.outputs.markdown }} 70 | message: ${{ steps.gas_diff_local.outputs.markdown }} 71 | 72 | - name: Add gas diff fork to sticky comment 73 | if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' 74 | uses: marocchino/sticky-pull-request-comment@v2 75 | with: 76 | header: gasrepost-fork 77 | # delete the comment in case changes no longer impact gas costs 78 | delete: ${{ !steps.gas_diff_fork.outputs.markdown }} 79 | message: ${{ steps.gas_diff_fork.outputs.markdown }} 80 | -------------------------------------------------------------------------------- /contracts/interfaces/ICoordinatedTaker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { LimitOrder } from "../libraries/LimitOrder.sol"; 5 | 6 | /// @title ICoordinatedTaker Interface 7 | /// @author imToken Labs 8 | interface ICoordinatedTaker { 9 | /// @title Coordinator Parameters 10 | /// @dev Contains the signature, salt, and expiry for coordinator authorization. 11 | struct CoordinatorParams { 12 | bytes sig; 13 | uint256 salt; 14 | uint256 expiry; 15 | } 16 | 17 | /// @notice Emitted when a limit order is filled by the coordinator. 18 | /// @param user The address of the user. 19 | /// @param orderHash The hash of the order. 20 | /// @param allowFillHash The hash of the allowed fill. 21 | event CoordinatorFill(address indexed user, bytes32 indexed orderHash, bytes32 indexed allowFillHash); 22 | 23 | /// @notice Emitted when the coordinator address is updated. 24 | /// @param newCoordinator The address of the new coordinator. 25 | event SetCoordinator(address newCoordinator); 26 | 27 | /// @notice Error to be thrown when a permission is reused. 28 | /// @dev This error is used to prevent the reuse of permissions. 29 | error ReusedPermission(); 30 | 31 | /// @notice Error to be thrown when the msg.value is invalid. 32 | /// @dev This error is used to ensure that the correct msg.value is sent with the transaction. 33 | error InvalidMsgValue(); 34 | 35 | /// @notice Error to be thrown when a signature is invalid. 36 | /// @dev This error is used to ensure that the provided signature is valid. 37 | error InvalidSignature(); 38 | 39 | /// @notice Error to be thrown when a permission has expired. 40 | /// @dev This error is used to ensure that the permission has not expired. 41 | error ExpiredPermission(); 42 | 43 | /// @notice Error to be thrown when an address is zero. 44 | /// @dev This error is used to ensure that a valid address is provided. 45 | error ZeroAddress(); 46 | 47 | /// @notice Submits a limit order fill with additional coordination parameters.. 48 | /// @param order The limit order to be filled. 49 | /// @param makerSignature The signature of the maker. 50 | /// @param takerTokenAmount The amount of tokens to be taken by the taker. 51 | /// @param makerTokenAmount The amount of tokens to be given by the maker. 52 | /// @param extraAction Any extra action to be performed. 53 | /// @param userTokenPermit The user's token permit. 54 | /// @param crdParams The coordinator parameters. 55 | function submitLimitOrderFill( 56 | LimitOrder calldata order, 57 | bytes calldata makerSignature, 58 | uint256 takerTokenAmount, 59 | uint256 makerTokenAmount, 60 | bytes calldata extraAction, 61 | bytes calldata userTokenPermit, 62 | CoordinatorParams calldata crdParams 63 | ) external payable; 64 | } 65 | -------------------------------------------------------------------------------- /test/utils/IUniswapV3Quoter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Quoter Interface 5 | /// @notice Supports quoting the calculated amounts from exact input or exact output swaps 6 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 7 | /// to compute the result. They are also not gas efficient and should not be called on-chain. 8 | interface IUniswapV3Quoter { 9 | /// @notice Returns the amount out received for a given exact input swap without executing the swap 10 | /// @param path The path of the swap, i.e. each token pair and the pool fee 11 | /// @param amountIn The amount of the first token to swap 12 | /// @return amountOut The amount of the last token that would be received 13 | function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut); 14 | 15 | /// @notice Returns the amount out received for a given exact input but for a swap of a single pool 16 | /// @param tokenIn The token being swapped in 17 | /// @param tokenOut The token being swapped out 18 | /// @param fee The fee of the token pool to consider for the pair 19 | /// @param amountIn The desired input amount 20 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 21 | /// @return amountOut The amount of `tokenOut` that would be received 22 | function quoteExactInputSingle( 23 | address tokenIn, 24 | address tokenOut, 25 | uint24 fee, 26 | uint256 amountIn, 27 | uint160 sqrtPriceLimitX96 28 | ) external returns (uint256 amountOut); 29 | 30 | /// @notice Returns the amount in required for a given exact output swap without executing the swap 31 | /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order 32 | /// @param amountOut The amount of the last token to receive 33 | /// @return amountIn The amount of first token required to be paid 34 | function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn); 35 | 36 | /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool 37 | /// @param tokenIn The token being swapped in 38 | /// @param tokenOut The token being swapped out 39 | /// @param fee The fee of the token pool to consider for the pair 40 | /// @param amountOut The desired output amount 41 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 42 | /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` 43 | function quoteExactOutputSingle( 44 | address tokenIn, 45 | address tokenOut, 46 | uint24 fee, 47 | uint256 amountOut, 48 | uint160 sqrtPriceLimitX96 49 | ) external returns (uint256 amountIn); 50 | } 51 | -------------------------------------------------------------------------------- /test/utils/Addresses.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { Test, Vm } from "forge-std/Test.sol"; 5 | 6 | contract Addresses is Test { 7 | string private file = readAddresses(vm); 8 | 9 | address WETH_ADDRESS = abi.decode(vm.parseJson(file, "$.WETH_ADDRESS"), (address)); 10 | address USDT_ADDRESS = abi.decode(vm.parseJson(file, "$.USDT_ADDRESS"), (address)); 11 | address USDC_ADDRESS = abi.decode(vm.parseJson(file, "$.USDC_ADDRESS"), (address)); 12 | address CRV_ADDRESS = abi.decode(vm.parseJson(file, "$.CRV_ADDRESS"), (address)); 13 | address TUSD_ADDRESS = abi.decode(vm.parseJson(file, "$.TUSD_ADDRESS"), (address)); 14 | address DAI_ADDRESS = abi.decode(vm.parseJson(file, "$.DAI_ADDRESS"), (address)); 15 | address LON_ADDRESS = abi.decode(vm.parseJson(file, "$.LON_ADDRESS"), (address)); 16 | address WBTC_ADDRESS = abi.decode(vm.parseJson(file, "$.WBTC_ADDRESS"), (address)); 17 | 18 | address CURVE_TRICRYPTO2_POOL_ADDRESS = abi.decode(vm.parseJson(file, "$.CURVE_TRICRYPTO2_POOL_ADDRESS"), (address)); 19 | address SUSHISWAP_ADDRESS = abi.decode(vm.parseJson(file, "$.SUSHISWAP_ADDRESS"), (address)); 20 | address UNISWAP_V3_QUOTER_ADDRESS = abi.decode(vm.parseJson(file, "$.UNISWAP_V3_QUOTER_ADDRESS"), (address)); 21 | address UNISWAP_PERMIT2_ADDRESS = abi.decode(vm.parseJson(file, "$.UNISWAP_PERMIT2_ADDRESS"), (address)); 22 | address UNISWAP_SWAP_ROUTER_02_ADDRESS = abi.decode(vm.parseJson(file, "$.UNISWAP_SWAP_ROUTER_02_ADDRESS"), (address)); 23 | address UNISWAP_UNIVERSAL_ROUTER_ADDRESS = abi.decode(vm.parseJson(file, "$.UNISWAP_UNIVERSAL_ROUTER_ADDRESS"), (address)); 24 | 25 | function getChainId() internal view returns (uint256 chainId) { 26 | assembly { 27 | chainId := chainid() 28 | } 29 | } 30 | } 31 | 32 | function readAddresses(Vm vm) view returns (string memory data) { 33 | uint256 chainId; 34 | assembly { 35 | chainId := chainid() 36 | } 37 | 38 | string memory fileName; 39 | if (chainId == 1) { 40 | fileName = "test/utils/config/mainnet.json"; 41 | } else if (chainId == 137) { 42 | fileName = "test/utils/config/polygon.json"; 43 | } else if (chainId == 5) { 44 | fileName = "test/utils/config/goerli.json"; 45 | } else if (chainId == 42161) { 46 | fileName = "test/utils/config/arbitrumMainnet.json"; 47 | } else if (chainId == 421613) { 48 | fileName = "test/utils/config/arbitrumGoerli.json"; 49 | } else if (chainId == 31337) { 50 | fileName = "test/utils/config/local.json"; 51 | } else { 52 | string memory errorMsg = string(abi.encodePacked("No address config support for network ", chainId)); 53 | revert(errorMsg); 54 | } 55 | 56 | return vm.readFile(fileName); 57 | } 58 | 59 | function computeContractAddress(address deployer, uint8 nonce) pure returns (address) { 60 | // TODO support nonce larger than uint8 max 61 | bytes memory rlpEncoded = abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, bytes1(uint8(nonce))); 62 | return address(uint160(uint256(keccak256(rlpEncoded)))); 63 | } 64 | -------------------------------------------------------------------------------- /contracts/abstracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Ownable Contract 5 | /// @author imToken Labs 6 | /// @notice This contract manages ownership and allows transfer and renouncement of ownership. 7 | /// @dev This contract uses a nomination system for ownership transfer. 8 | abstract contract Ownable { 9 | address public owner; 10 | address public nominatedOwner; 11 | 12 | /// @notice Event emitted when a new owner is nominated. 13 | /// @param newOwner The address of the new nominated owner. 14 | event OwnerNominated(address indexed newOwner); 15 | 16 | /// @notice Event emitted when ownership is transferred. 17 | /// @param oldOwner The address of the previous owner. 18 | /// @param newOwner The address of the new owner. 19 | event OwnerChanged(address indexed oldOwner, address indexed newOwner); 20 | 21 | /// @notice Error to be thrown when the caller is not the owner. 22 | /// @dev This error is used to ensure that only the owner can call certain functions. 23 | error NotOwner(); 24 | 25 | /// @notice Error to be thrown when the caller is not the nominated owner. 26 | /// @dev This error is used to ensure that only the nominated owner can accept ownership. 27 | error NotNominated(); 28 | 29 | /// @notice Error to be thrown when the provided owner address is zero. 30 | /// @dev This error is used to ensure a valid address is provided for the owner. 31 | error ZeroOwner(); 32 | 33 | /// @notice Error to be thrown when there is already a nominated owner. 34 | /// @dev This error is used to prevent nominating a new owner when one is already nominated. 35 | error NominationExists(); 36 | 37 | modifier onlyOwner() { 38 | if (msg.sender != owner) revert NotOwner(); 39 | _; 40 | } 41 | 42 | /// @notice Constructor to set the initial owner of the contract. 43 | /// @param _owner The address of the initial owner. 44 | constructor(address _owner) { 45 | if (_owner == address(0)) revert ZeroOwner(); 46 | owner = _owner; 47 | } 48 | 49 | /// @notice Accept the ownership transfer. 50 | /// @dev Only the nominated owner can call this function to accept the ownership. 51 | function acceptOwnership() external { 52 | if (msg.sender != nominatedOwner) revert NotNominated(); 53 | emit OwnerChanged(owner, nominatedOwner); 54 | 55 | owner = nominatedOwner; 56 | nominatedOwner = address(0); 57 | } 58 | 59 | /// @notice Renounce ownership of the contract. 60 | /// @dev Only the current owner can call this function to renounce ownership. Once renounced, ownership cannot be recovered. 61 | function renounceOwnership() external onlyOwner { 62 | if (nominatedOwner != address(0)) revert NominationExists(); 63 | emit OwnerChanged(owner, address(0)); 64 | owner = address(0); 65 | } 66 | 67 | /// @notice Nominate a new owner. 68 | /// @dev Only the current owner can call this function to nominate a new owner. 69 | /// @param newOwner The address of the new owner. 70 | function nominateNewOwner(address newOwner) external onlyOwner { 71 | nominatedOwner = newOwner; 72 | emit OwnerNominated(newOwner); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/abstracts/EIP712.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title EIP712 Contract 5 | /// @author imToken Labs 6 | /// @notice This contract implements the EIP-712 standard for structured data hashing and signing. 7 | /// @dev This contract provides functions to handle EIP-712 domain separator and hash calculation. 8 | abstract contract EIP712 { 9 | // EIP-712 Domain 10 | string public constant EIP712_NAME = "Tokenlon"; 11 | string public constant EIP712_VERSION = "v6"; 12 | bytes32 public constant EIP712_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 13 | bytes32 private constant EIP712_HASHED_NAME = keccak256(bytes(EIP712_NAME)); 14 | bytes32 private constant EIP712_HASHED_VERSION = keccak256(bytes(EIP712_VERSION)); 15 | 16 | uint256 public immutable originalChainId; 17 | bytes32 public immutable originalEIP712DomainSeparator; 18 | 19 | /// @notice Initialize the original chain ID and domain separator. 20 | constructor() { 21 | originalChainId = block.chainid; 22 | originalEIP712DomainSeparator = _buildDomainSeparator(); 23 | } 24 | 25 | /// @notice Internal function to build the EIP712 domain separator hash. 26 | /// @return The EIP712 domain separator hash. 27 | function _buildDomainSeparator() private view returns (bytes32) { 28 | return keccak256(abi.encode(EIP712_TYPE_HASH, EIP712_HASHED_NAME, EIP712_HASHED_VERSION, block.chainid, address(this))); 29 | } 30 | 31 | /// @notice Internal function to get the current EIP712 domain separator. 32 | /// @return The current EIP712 domain separator. 33 | function _getDomainSeparator() private view returns (bytes32) { 34 | if (block.chainid == originalChainId) { 35 | return originalEIP712DomainSeparator; 36 | } else { 37 | return _buildDomainSeparator(); 38 | } 39 | } 40 | 41 | /// @notice Calculate the EIP712 hash of a structured data hash. 42 | /// @param structHash The hash of the structured data. 43 | /// @return digest The EIP712 hash of the structured data. 44 | function getEIP712Hash(bytes32 structHash) internal view returns (bytes32 digest) { 45 | // return keccak256(abi.encodePacked("\x19\x01", _getDomainSeparator(), structHash)); 46 | 47 | digest = _getDomainSeparator(); 48 | 49 | // reference: 50 | // 1. solady: https://github.com/Vectorized/solady/blob/main/src/utils/EIP712.sol#L138-L147 51 | // 2. 1inch: https://etherscan.io/address/0x111111125421cA6dc452d289314280a0f8842A65#code (line 1204~1209) 52 | // solhint-disable no-inline-assembly 53 | assembly { 54 | // Compute the digest. 55 | mstore(0x00, 0x1901000000000000000000000000000000000000000000000000000000000000) // Store "\x19\x01". 56 | mstore(0x02, digest) // Store the domain separator. 57 | mstore(0x22, structHash) // Store the struct hash. 58 | digest := keccak256(0x0, 0x42) 59 | mstore(0x22, 0) // Restore the part of the free memory slot that was overwritten. 60 | } 61 | } 62 | 63 | /// @notice Get the current EIP712 domain separator. 64 | /// @return The current EIP712 domain separator. 65 | function EIP712_DOMAIN_SEPARATOR() external view returns (bytes32) { 66 | return _getDomainSeparator(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/abstracts/AdminManagement.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | 6 | import { AdminManagement } from "contracts/abstracts/AdminManagement.sol"; 7 | import { Ownable } from "contracts/abstracts/Ownable.sol"; 8 | 9 | import { MockERC20 } from "test/mocks/MockERC20.sol"; 10 | import { BalanceUtil } from "test/utils/BalanceUtil.sol"; 11 | 12 | contract ContractWithAdmin is AdminManagement { 13 | constructor(address _owner) AdminManagement(_owner) {} 14 | } 15 | 16 | contract AdminManagementTest is BalanceUtil { 17 | address owner = makeAddr("owner"); 18 | address rescueTarget = makeAddr("rescueTarget"); 19 | MockERC20 token1 = new MockERC20("TOKEN1", "TKN1", 18); 20 | MockERC20 token2 = new MockERC20("TOKEN2", "TKN2", 18); 21 | address[] tokens = [address(token1), address(token2)]; 22 | address[] spenders = [address(this), owner]; 23 | ContractWithAdmin contractWithAdmin; 24 | 25 | function setUp() public { 26 | contractWithAdmin = new ContractWithAdmin(owner); 27 | } 28 | 29 | function testCannotApproveTokensByNotOwner() public { 30 | vm.expectRevert(Ownable.NotOwner.selector); 31 | contractWithAdmin.approveTokens(tokens, spenders); 32 | } 33 | 34 | function testApproveTokens() public { 35 | for (uint256 i; i < tokens.length; ++i) { 36 | for (uint256 j; j < spenders.length; ++j) { 37 | assertEq(IERC20(tokens[i]).allowance(address(contractWithAdmin), spenders[j]), 0); 38 | } 39 | } 40 | 41 | vm.startPrank(owner); 42 | contractWithAdmin.approveTokens(tokens, spenders); 43 | vm.stopPrank(); 44 | vm.snapshotGasLastCall("AdminManagement", "approveTokens(): testApproveTokens"); 45 | 46 | for (uint256 i; i < tokens.length; ++i) { 47 | for (uint256 j; j < spenders.length; ++j) { 48 | assertEq(IERC20(tokens[i]).allowance(address(contractWithAdmin), spenders[j]), type(uint256).max); 49 | } 50 | } 51 | } 52 | 53 | function testCannotRescueTokensByNotOwner() public { 54 | vm.expectRevert(Ownable.NotOwner.selector); 55 | contractWithAdmin.rescueTokens(tokens, rescueTarget); 56 | } 57 | 58 | function testRescueTokens() public { 59 | uint256 amount1 = 1234; 60 | token1.mint(address(contractWithAdmin), amount1); 61 | uint256 amount2 = 6789; 62 | token2.mint(address(contractWithAdmin), amount2); 63 | 64 | assertEq(IERC20(tokens[0]).balanceOf(address(contractWithAdmin)), amount1); 65 | assertEq(IERC20(tokens[0]).balanceOf(rescueTarget), 0); 66 | assertEq(IERC20(tokens[1]).balanceOf(address(contractWithAdmin)), amount2); 67 | assertEq(IERC20(tokens[1]).balanceOf(rescueTarget), 0); 68 | 69 | vm.startPrank(owner); 70 | contractWithAdmin.rescueTokens(tokens, rescueTarget); 71 | vm.stopPrank(); 72 | vm.snapshotGasLastCall("AdminManagement", "rescueTokens(): testRescueTokens"); 73 | 74 | assertEq(IERC20(tokens[0]).balanceOf(address(contractWithAdmin)), 0); 75 | assertEq(IERC20(tokens[0]).balanceOf(rescueTarget), amount1); 76 | assertEq(IERC20(tokens[1]).balanceOf(address(contractWithAdmin)), 0); 77 | assertEq(IERC20(tokens[1]).balanceOf(rescueTarget), amount2); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/utils/ICurveFi.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ICurveFi { 5 | function get_virtual_price() external returns (uint256 out); 6 | 7 | function add_liquidity(uint256[2] calldata amounts, uint256 deadline) external; 8 | 9 | function add_liquidity( 10 | // sBTC pool 11 | uint256[3] calldata amounts, 12 | uint256 min_mint_amount 13 | ) external; 14 | 15 | function add_liquidity( 16 | // bUSD pool 17 | uint256[4] calldata amounts, 18 | uint256 min_mint_amount 19 | ) external; 20 | 21 | function get_dx(int128 i, int128 j, uint256 dy) external view returns (uint256 out); 22 | 23 | function get_dx_underlying(int128 i, int128 j, uint256 dy) external view returns (uint256 out); 24 | 25 | function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256 out); 26 | 27 | function get_dy_underlying(int128 i, int128 j, uint256 dx) external view returns (uint256 out); 28 | 29 | function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external payable; 30 | 31 | function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy, uint256 deadline) external payable; 32 | 33 | function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy) external payable; 34 | 35 | function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, uint256 deadline) external payable; 36 | 37 | function remove_liquidity(uint256 _amount, uint256 deadline, uint256[2] calldata min_amounts) external; 38 | 39 | function remove_liquidity_imbalance(uint256[2] calldata amounts, uint256 deadline) external; 40 | 41 | function remove_liquidity_imbalance(uint256[3] calldata amounts, uint256 max_burn_amount) external; 42 | 43 | function remove_liquidity(uint256 _amount, uint256[3] calldata amounts) external; 44 | 45 | function remove_liquidity_imbalance(uint256[4] calldata amounts, uint256 max_burn_amount) external; 46 | 47 | function remove_liquidity(uint256 _amount, uint256[4] calldata amounts) external; 48 | 49 | function commit_new_parameters(int128 amplification, int128 new_fee, int128 new_admin_fee) external; 50 | 51 | function apply_new_parameters() external; 52 | 53 | function revert_new_parameters() external; 54 | 55 | function commit_transfer_ownership(address _owner) external; 56 | 57 | function apply_transfer_ownership() external; 58 | 59 | function revert_transfer_ownership() external; 60 | 61 | function withdraw_admin_fees() external; 62 | 63 | function coins(int128 arg0) external returns (address out); 64 | 65 | function underlying_coins(int128 arg0) external returns (address out); 66 | 67 | function balances(int128 arg0) external returns (uint256 out); 68 | 69 | function A() external returns (int128 out); 70 | 71 | function fee() external returns (int128 out); 72 | 73 | function admin_fee() external returns (int128 out); 74 | 75 | function owner() external returns (address out); 76 | 77 | function admin_actions_deadline() external returns (uint256 out); 78 | 79 | function transfer_ownership_deadline() external returns (uint256 out); 80 | 81 | function future_A() external returns (int128 out); 82 | 83 | function future_fee() external returns (int128 out); 84 | 85 | function future_admin_fee() external returns (int128 out); 86 | 87 | function future_owner() external returns (address out); 88 | } 89 | -------------------------------------------------------------------------------- /contracts/interfaces/IGenericSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { GenericSwapData } from "../libraries/GenericSwapData.sol"; 5 | 6 | /// @title IGenericSwap Interface 7 | /// @author imToken Labs 8 | /// @notice Interface for a generic swap contract. 9 | /// @dev This interface defines functions and events related to executing swaps and handling swap errors. 10 | interface IGenericSwap { 11 | /// @notice Event emitted when a swap is executed. 12 | /// @param swapHash The hash of the swap data. 13 | /// @param maker The address of the maker initiating the swap. 14 | /// @param taker The address of the taker executing the swap. 15 | /// @param recipient The address receiving the output tokens. 16 | /// @param inputToken The address of the input token. 17 | /// @param inputAmount The amount of input tokens. 18 | /// @param outputToken The address of the output token. 19 | /// @param outputAmount The amount of output tokens received. 20 | /// @param salt The salt value used in the swap. 21 | event Swap( 22 | bytes32 indexed swapHash, 23 | address indexed maker, 24 | address indexed taker, 25 | address recipient, 26 | address inputToken, 27 | uint256 inputAmount, 28 | address outputToken, 29 | uint256 outputAmount, 30 | uint256 salt 31 | ); 32 | 33 | /// @notice Error to be thrown when a swap is already filled. 34 | /// @dev This error is used when attempting to fill a swap that has already been completed. 35 | error AlreadyFilled(); 36 | 37 | /// @notice Error to be thrown when the msg.value is invalid. 38 | /// @dev This error is used to ensure that the correct msg.value is sent with the transaction. 39 | error InvalidMsgValue(); 40 | 41 | /// @notice Error to be thrown when the output amount is insufficient. 42 | /// @dev This error is used when the output amount received from the swap is insufficient. 43 | error InsufficientOutput(); 44 | 45 | /// @notice Error to be thrown when a signature is invalid. 46 | /// @dev This error is used to ensure that the provided signature is valid. 47 | error InvalidSignature(); 48 | 49 | /// @notice Error to be thrown when an order has expired. 50 | /// @dev This error is used to ensure that the swap order has not expired. 51 | error ExpiredOrder(); 52 | 53 | /// @notice Error to be thrown when an address is zero. 54 | /// @dev This error is used to ensure that a valid address is provided. 55 | error ZeroAddress(); 56 | 57 | /// @notice Error to be thrown when a swap amount is zero. 58 | /// @dev This error is used to ensure that a valid, nonzero swap amount is provided. 59 | error SwapWithZeroAmount(); 60 | 61 | /// @notice Executes a swap using provided swap data and taker token permit. 62 | /// @param swapData The swap data containing details of the swap. 63 | /// @param takerTokenPermit The permit for spending taker's tokens. 64 | /// @return returnAmount The amount of tokens returned from the swap. 65 | function executeSwap(GenericSwapData calldata swapData, bytes calldata takerTokenPermit) external payable returns (uint256 returnAmount); 66 | 67 | /// @notice Executes a swap using provided swap data, taker token permit, taker address, and signature. 68 | /// @param swapData The swap data containing details of the swap. 69 | /// @param takerTokenPermit The permit for spending taker's tokens. 70 | /// @param taker The address of the taker initiating the swap. 71 | /// @param takerSig The signature of the taker authorizing the swap. 72 | /// @return returnAmount The amount of tokens returned from the swap. 73 | function executeSwapWithSig( 74 | GenericSwapData calldata swapData, 75 | bytes calldata takerTokenPermit, 76 | address taker, 77 | bytes calldata takerSig 78 | ) external payable returns (uint256 returnAmount); 79 | } 80 | -------------------------------------------------------------------------------- /test/abstracts/Ownable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { Ownable } from "contracts/abstracts/Ownable.sol"; 7 | 8 | contract OwnableTest is Test { 9 | OwnableTestContract ownable; 10 | 11 | address owner = makeAddr("owner"); 12 | address newOwner = makeAddr("newOwner"); 13 | address nominatedOwner = makeAddr("nominatedOwner"); 14 | address otherAccount = makeAddr("otherAccount"); 15 | 16 | function setUp() public { 17 | vm.startPrank(owner); 18 | ownable = new OwnableTestContract(owner); 19 | vm.stopPrank(); 20 | } 21 | 22 | function testOwnableInitialState() public { 23 | assertEq(ownable.owner(), owner); 24 | } 25 | 26 | function testCannotInitiateOwnerWithZeroAddress() public { 27 | vm.expectRevert(Ownable.ZeroOwner.selector); 28 | new OwnableTestContract(address(0)); 29 | } 30 | 31 | function testCannotAcceptOwnershipWithOtherAccount() public { 32 | vm.startPrank(owner); 33 | ownable.nominateNewOwner(newOwner); 34 | vm.stopPrank(); 35 | 36 | vm.startPrank(otherAccount); 37 | vm.expectRevert(Ownable.NotNominated.selector); 38 | ownable.acceptOwnership(); 39 | vm.stopPrank(); 40 | } 41 | 42 | function testCannotRenounceOwnershipWithNominatedOwner() public { 43 | vm.startPrank(owner); 44 | ownable.nominateNewOwner(newOwner); 45 | vm.stopPrank(); 46 | 47 | vm.startPrank(owner); 48 | vm.expectRevert(Ownable.NominationExists.selector); 49 | ownable.renounceOwnership(); 50 | vm.stopPrank(); 51 | } 52 | 53 | function testCannotRenounceOwnershipWithOtherAccount() public { 54 | vm.startPrank(otherAccount); 55 | vm.expectRevert(Ownable.NotOwner.selector); 56 | ownable.renounceOwnership(); 57 | vm.stopPrank(); 58 | } 59 | 60 | function testCannotNominateNewOwnerWithOtherAccount() public { 61 | vm.startPrank(otherAccount); 62 | vm.expectRevert(Ownable.NotOwner.selector); 63 | ownable.nominateNewOwner(newOwner); 64 | vm.stopPrank(); 65 | } 66 | 67 | function testAcceptOwnership() public { 68 | vm.startPrank(owner); 69 | ownable.nominateNewOwner(newOwner); 70 | vm.stopPrank(); 71 | 72 | assertEq(ownable.nominatedOwner(), newOwner); 73 | 74 | vm.expectEmit(true, true, false, false); 75 | emit Ownable.OwnerChanged(owner, newOwner); 76 | 77 | vm.startPrank(newOwner); 78 | ownable.acceptOwnership(); 79 | vm.stopPrank(); 80 | vm.snapshotGasLastCall("Ownable", "acceptOwnership(): testAcceptOwnership"); 81 | 82 | assertEq(ownable.owner(), newOwner); 83 | assertEq(ownable.nominatedOwner(), address(0)); 84 | } 85 | 86 | function testRenounceOwnership() public { 87 | vm.expectEmit(true, true, false, false); 88 | emit Ownable.OwnerChanged(owner, address(0)); 89 | 90 | vm.startPrank(owner); 91 | ownable.renounceOwnership(); 92 | vm.stopPrank(); 93 | vm.snapshotGasLastCall("Ownable", "renounceOwnership(): testRenounceOwnership"); 94 | 95 | assertEq(ownable.owner(), address(0)); 96 | } 97 | 98 | function testNominateNewOwner() public { 99 | vm.expectEmit(true, false, false, false); 100 | emit Ownable.OwnerNominated(newOwner); 101 | 102 | vm.startPrank(owner); 103 | ownable.nominateNewOwner(newOwner); 104 | vm.stopPrank(); 105 | vm.snapshotGasLastCall("Ownable", "nominateNewOwner(): testNominateNewOwner"); 106 | 107 | assertEq(ownable.nominatedOwner(), newOwner); 108 | } 109 | } 110 | 111 | contract OwnableTestContract is Ownable { 112 | constructor(address _owner) Ownable(_owner) {} 113 | } 114 | -------------------------------------------------------------------------------- /test/libraries/Asset.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { Asset } from "contracts/libraries/Asset.sol"; 7 | import { Constant } from "contracts/libraries/Constant.sol"; 8 | 9 | import { MockERC20 } from "test/mocks/MockERC20.sol"; 10 | 11 | contract AssetTest is Test { 12 | using Asset for address; 13 | 14 | MockERC20 token; 15 | AssetHarness assetHarness; 16 | 17 | address payable recipient = payable(makeAddr("recipient")); 18 | uint256 tokenBalance = 123; 19 | uint256 ethBalance = 456; 20 | 21 | function setUp() public { 22 | token = new MockERC20("TOKEN", "TKN", 18); 23 | assetHarness = new AssetHarness(); 24 | 25 | // set balance 26 | token.mint(address(assetHarness), tokenBalance); 27 | vm.deal(address(assetHarness), ethBalance); 28 | } 29 | 30 | function testIsETH() public { 31 | assertTrue(assetHarness.exposedIsETH(Constant.ETH_ADDRESS)); 32 | vm.snapshotGasLastCall("Asset", "isETH(): testIsETH(ETH_ADDRESS)"); 33 | assertTrue(assetHarness.exposedIsETH(Constant.ZERO_ADDRESS)); 34 | vm.snapshotGasLastCall("Asset", "isETH(): testIsETH(ZERO_ADDRESS)"); 35 | } 36 | 37 | function testGetBalance() public { 38 | assertEq(assetHarness.exposedGetBalance(address(token), address(assetHarness)), tokenBalance); 39 | vm.snapshotGasLastCall("Asset", "getBalance(): testGetBalance"); 40 | assertEq(assetHarness.exposedGetBalance(Constant.ETH_ADDRESS, address(assetHarness)), ethBalance); 41 | vm.snapshotGasLastCall("Asset", "getBalance(): testGetBalance(ETH_ADDRESS)"); 42 | assertEq(assetHarness.exposedGetBalance(Constant.ZERO_ADDRESS, address(assetHarness)), ethBalance); 43 | vm.snapshotGasLastCall("Asset", "getBalance(): testGetBalance(ZERO_ADDRESS)"); 44 | } 45 | 46 | function testDoNothingIfTransferWithZeroAmount() public { 47 | assetHarness.exposedTransferTo(address(token), recipient, 0); 48 | vm.snapshotGasLastCall("Asset", "transferTo(): testDoNothingIfTransferWithZeroAmount"); 49 | } 50 | 51 | function testDoNothingIfTransferToSelf() public { 52 | assetHarness.exposedTransferTo(address(token), payable(address(token)), 0); 53 | vm.snapshotGasLastCall("Asset", "transferTo(): testDoNothingIfTransferToSelf"); 54 | } 55 | 56 | function testCannotTransferETHWithInsufficientBalance() public { 57 | vm.expectRevert(); 58 | assetHarness.exposedTransferTo(Constant.ETH_ADDRESS, recipient, address(assetHarness).balance + 1); 59 | } 60 | 61 | function testCannotTransferETHToContractCannotReceiveETH() public { 62 | vm.expectRevert(); 63 | // mockERC20 cannot receive any ETH 64 | assetHarness.exposedTransferTo(Constant.ETH_ADDRESS, payable(address(token)), 1); 65 | } 66 | 67 | function testTransferETH() public { 68 | uint256 amount = address(assetHarness).balance; 69 | assetHarness.exposedTransferTo(Constant.ETH_ADDRESS, recipient, amount); 70 | vm.snapshotGasLastCall("Asset", "transferTo(): testTransferETH"); 71 | 72 | assertEq(address(recipient).balance, amount); 73 | assertEq(address(assetHarness).balance, 0); 74 | } 75 | 76 | function testTransferToken() public { 77 | uint256 amount = token.balanceOf(address(assetHarness)); 78 | assetHarness.exposedTransferTo(address(token), recipient, amount); 79 | vm.snapshotGasLastCall("Asset", "transferTo(): testTransferToken"); 80 | 81 | assertEq(token.balanceOf(recipient), amount); 82 | assertEq(token.balanceOf(address(assetHarness)), 0); 83 | } 84 | } 85 | 86 | contract AssetHarness { 87 | function exposedIsETH(address addr) external pure returns (bool) { 88 | return Asset.isETH(addr); 89 | } 90 | 91 | function exposedGetBalance(address asset, address owner) external view returns (uint256) { 92 | return Asset.getBalance(asset, owner); 93 | } 94 | 95 | function exposedTransferTo(address asset, address payable to, uint256 amount) external { 96 | Asset.transferTo(asset, to, amount); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/utils/Permit2Helper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { TokenCollector } from "contracts/abstracts/TokenCollector.sol"; 7 | import { IUniswapPermit2 } from "contracts/interfaces/IUniswapPermit2.sol"; 8 | 9 | import { getEIP712Hash } from "test/utils/Sig.sol"; 10 | 11 | contract Permit2Helper is Test { 12 | IUniswapPermit2 public constant permit2 = IUniswapPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); 13 | bytes32 public constant PERMIT_DETAILS_TYPEHASH = keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); 14 | bytes32 public constant PERMIT_SINGLE_TYPEHASH = 15 | keccak256( 16 | "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" 17 | ); 18 | bytes32 public constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); 19 | bytes32 public constant PERMIT_TRANSFER_FROM_TYPEHASH = 20 | keccak256( 21 | "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" 22 | ); 23 | 24 | function getPermitSingleHash(IUniswapPermit2.PermitSingle memory permit) public pure returns (bytes32) { 25 | bytes32 permitDetailsHash = keccak256(abi.encode(PERMIT_DETAILS_TYPEHASH, permit.details)); 26 | return keccak256(abi.encode(PERMIT_SINGLE_TYPEHASH, permitDetailsHash, permit.spender, permit.sigDeadline)); 27 | } 28 | 29 | function getPermitTransferFromHash(IUniswapPermit2.PermitTransferFrom memory permit, address spender) private pure returns (bytes32) { 30 | bytes32 structHashTokenPermissions = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)); 31 | return keccak256(abi.encode(PERMIT_TRANSFER_FROM_TYPEHASH, structHashTokenPermissions, spender, permit.nonce, permit.deadline)); 32 | } 33 | 34 | function signPermitSingle(uint256 privateKey, IUniswapPermit2.PermitSingle memory permitSingle) public view returns (bytes memory) { 35 | bytes32 permitSingleHash = getPermitSingleHash(permitSingle); 36 | bytes32 EIP712SignDigest = getEIP712Hash(permit2.DOMAIN_SEPARATOR(), permitSingleHash); 37 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); 38 | return abi.encodePacked(r, s, v); 39 | } 40 | 41 | function signPermitTransferFrom( 42 | uint256 privateKey, 43 | IUniswapPermit2.PermitTransferFrom memory permitTransferFrom, 44 | address spender 45 | ) public view returns (bytes memory) { 46 | bytes32 permitTransferFromHash = getPermitTransferFromHash(permitTransferFrom, spender); 47 | bytes32 EIP712SignDigest = getEIP712Hash(permit2.DOMAIN_SEPARATOR(), permitTransferFromHash); 48 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); 49 | return abi.encodePacked(r, s, v); 50 | } 51 | 52 | function encodeAllowanceTransfer(address owner, IUniswapPermit2.PermitSingle memory permit, bytes memory permitSig) public pure returns (bytes memory) { 53 | bytes memory permit2Calldata = abi.encode(owner, permit, permitSig); 54 | return abi.encodePacked(TokenCollector.Source.Permit2AllowanceTransfer, permit2Calldata); 55 | } 56 | 57 | function encodeSignatureTransfer(IUniswapPermit2.PermitTransferFrom memory permit, bytes memory permitSig) public pure returns (bytes memory) { 58 | return abi.encodePacked(TokenCollector.Source.Permit2SignatureTransfer, abi.encode(permit.nonce, permit.deadline, permitSig)); 59 | } 60 | 61 | // will return encoded AllownaceTransfer data 62 | function getTokenlonPermit2Data(address owner, uint256 ownerPrivateKey, address token, address spender) public view returns (bytes memory) { 63 | uint256 expiration = block.timestamp + 1 days; 64 | (, , uint48 nonce) = permit2.allowance(owner, token, spender); 65 | 66 | IUniswapPermit2.PermitSingle memory permit = IUniswapPermit2.PermitSingle({ 67 | details: IUniswapPermit2.PermitDetails({ token: token, amount: type(uint160).max, expiration: uint48(expiration), nonce: nonce }), 68 | spender: spender, 69 | sigDeadline: expiration 70 | }); 71 | bytes memory permitSig = signPermitSingle(ownerPrivateKey, permit); 72 | return encodeAllowanceTransfer(owner, permit, permitSig); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/abstracts/TokenCollector.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | import { IERC20Permit } from "@openzeppelin/contracts@v5.0.2/token/ERC20/extensions/IERC20Permit.sol"; 6 | import { SafeERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/utils/SafeERC20.sol"; 7 | 8 | import { IAllowanceTarget } from "../interfaces/IAllowanceTarget.sol"; 9 | import { IUniswapPermit2 } from "../interfaces/IUniswapPermit2.sol"; 10 | 11 | /// @title TokenCollector Contract 12 | /// @author imToken Labs 13 | /// @notice This contract handles the collection of tokens using various methods. 14 | /// @dev This contract supports multiple token collection mechanisms including allowance targets, direct transfers, and permit transfers. 15 | abstract contract TokenCollector { 16 | using SafeERC20 for IERC20; 17 | 18 | /// @title Token Collection Sources 19 | /// @notice Enumeration of possible token collection sources. 20 | /// @dev Represents the various methods for collecting tokens. 21 | enum Source { 22 | TokenlonAllowanceTarget, 23 | Token, 24 | TokenPermit, 25 | Permit2AllowanceTransfer, 26 | Permit2SignatureTransfer 27 | } 28 | 29 | address public immutable permit2; 30 | address public immutable allowanceTarget; 31 | 32 | /// @notice Error to be thrown when Permit2 data is empty. 33 | /// @dev This error is used to ensure Permit2 data is provided when required. 34 | error Permit2DataEmpty(); 35 | 36 | /// @notice Constructor to set the Permit2 and allowance target addresses. 37 | /// @param _permit2 The address of the Uniswap Permit2 contract. 38 | /// @param _allowanceTarget The address of the allowance target contract. 39 | constructor(address _permit2, address _allowanceTarget) { 40 | permit2 = _permit2; 41 | allowanceTarget = _allowanceTarget; 42 | } 43 | 44 | /// @notice Internal function to collect tokens using various methods. 45 | /// @dev Handles token collection based on the specified source. 46 | /// @param token The address of the token to be collected. 47 | /// @param from The address from which the tokens will be collected. 48 | /// @param to The address to which the tokens will be sent. 49 | /// @param amount The amount of tokens to be collected. 50 | /// @param data Additional data required for the token collection process. 51 | function _collect(address token, address from, address to, uint256 amount, bytes calldata data) internal { 52 | Source src = Source(uint8(data[0])); 53 | 54 | if (src == Source.TokenlonAllowanceTarget) { 55 | return IAllowanceTarget(allowanceTarget).spendFromUserTo(from, token, to, amount); 56 | } else if (src == Source.Token) { 57 | return IERC20(token).safeTransferFrom(from, to, amount); 58 | } else if (src == Source.TokenPermit) { 59 | (bool success, bytes memory result) = token.call(abi.encodePacked(IERC20Permit.permit.selector, data[1:])); 60 | if (!success) { 61 | assembly { 62 | revert(add(result, 32), returndatasize()) 63 | } 64 | } 65 | return IERC20(token).safeTransferFrom(from, to, amount); 66 | } else if (src == Source.Permit2AllowanceTransfer) { 67 | bytes memory permit2Data = data[1:]; 68 | if (permit2Data.length > 0) { 69 | (bool success, bytes memory result) = permit2.call(abi.encodePacked(IUniswapPermit2.permit.selector, permit2Data)); 70 | if (!success) { 71 | assembly { 72 | revert(add(result, 32), returndatasize()) 73 | } 74 | } 75 | } 76 | return IUniswapPermit2(permit2).transferFrom(from, to, uint160(amount), token); 77 | } else if (src == Source.Permit2SignatureTransfer) { 78 | bytes memory permit2Data = data[1:]; 79 | if (permit2Data.length == 0) revert Permit2DataEmpty(); 80 | (uint256 nonce, uint256 deadline, bytes memory permitSig) = abi.decode(permit2Data, (uint256, uint256, bytes)); 81 | IUniswapPermit2.PermitTransferFrom memory permit = IUniswapPermit2.PermitTransferFrom({ 82 | permitted: IUniswapPermit2.TokenPermissions({ token: token, amount: amount }), 83 | nonce: nonce, 84 | deadline: deadline 85 | }); 86 | IUniswapPermit2.SignatureTransferDetails memory detail = IUniswapPermit2.SignatureTransferDetails({ to: to, requestedAmount: amount }); 87 | return IUniswapPermit2(permit2).permitTransferFrom(permit, detail, from, permitSig); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /financial-statements/2024_Q3.csv: -------------------------------------------------------------------------------- 1 | date,description,type,from,to,amount,currency,comment,tx_hash 2 | 3/7/2024,Q1 + Q2 USDT community budget repayment,Community budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"2,010.00",USDT,,0x46778f9342b603e36e346a5bea71457e177c7321118cbe00a40fbf2582272010 3 | 3/7/2024,Swap 4305 USDT for 4501 LON,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"4,305.13",USDT,for Q1 LON community budget repayment,0x6c5a619d4460d443eceda00ab8791b73ad486e7ade0d7a726908cdb46c097d64 4 | ,Swap 4305 USDT for 4501 LON,Token Swap,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"4,500.99",LON,Pay gas with tokens' creates 2 on-chain transactions for a single swap,0x6c5a619d4460d443eceda00ab8791b73ad486e7ade0d7a726908cdb46c097d64 5 | 3/7/2024,Q1 LON Community budget repayment,Community budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"4,500.00",LON,,0xe8b0fe9b2db06a7ce70b89a1e3b60d0b2bf7d15f9783f62c00e92e64941f5328 6 | 25/7/2024,Tokenlon 5th anniversary merchandise,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xf0d53D32C5cd4996d50EB6cc360f490fC791013D,"6,000.00",USDT,,0xc6569f7b8794a685f8f699f7b4d81942663b46f0879d093603b66df39df00922 7 | 26/7/2024,Swap 619 USDT for 684 LON,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,619.26,USDT,for Tokenlon x imToken ETH ETF campaign,0x1043ef7fc76d9fb3faee0b77f910d550978011c8e0b4e103f730fab8ebe264ac 8 | ,Swap 619 USDT for 684 LON,Token Swap,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,683.99,LON,Pay gas with tokens' creates 2 on-chain transactions for a single swap,0x1043ef7fc76d9fb3faee0b77f910d550978011c8e0b4e103f730fab8ebe264ac 9 | 26/7/2024,Tokenlon x imToken ETH ETF celebration campaign,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,684.00,LON,https://x.com/imTokenCN/status/1815363906984120482,0xe98b9e0fb4a4945db98e22f667a9885ed7d44ef8bc261584b4313b776ec47ad5 10 | 6/9/2024,Swap 2204 USDT for 2504.9 LON,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,"2,204.40",USDT,for Tokenlon 5th anniversary prize pool,0x99cf67612bed33c265aef800a1835b2c4667014c20c4e4b2f52a1012c02bdb44 11 | ,Swap 2204 USDT for 2504.9 LON,Token Swap,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,"2,504.99",LON,Pay gas with tokens' creates 2 on-chain transactions for a single swap,0x99cf67612bed33c265aef800a1835b2c4667014c20c4e4b2f52a1012c02bdb44 12 | 6/9/2024,Swap 10713.1249 USDT for 4.48081998 ETH,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xfD6C2d2499b1331101726A8AC68CCc9Da3fAB54F,"10,713.12",USDT,for 6th August user gas refunds,0x2a6fa8de22a0d6f543c1916b7bc9f1fc9190d3ca49c5b2aa06bb8eef832a3dd1 13 | 6/9/2024,Swap 2751.0135 USDT for 0.04846959 WBTC,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x8D90113A1e286a5aB3e496fbD1853F265e5913c6,"2,751.01",USDT,for 6th August user gas refunds,0x6d824ea6f565db1f29df61a7c42e725c1e86b4f18d5ebe9374f7d3de1da4f302 14 | ,Swap 2751.0135 USDT for 0.04846959 WBTC,Token Swap,0x8D90113A1e286a5aB3e496fbD1853F265e5913c6,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0.0484,WBTC,,0x6d824ea6f565db1f29df61a7c42e725c1e86b4f18d5ebe9374f7d3de1da4f302 15 | 6/9/2024,Tokenlon 5th anniversary prize pool,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,"2,505.00",LON,https://tokenlon.im/campaign,0x6b730cc794d4784e53ca0c667abe6ce92a070ca30554ea955b899fc4d38c3db8 16 | 9/9/2024,6th August user gas refunds,Gas refund,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xb9E29984Fe50602E7A619662EBED4F90D93824C7,0.0484,WBTC,https://tokenlon.im/blog/29467460061460,0xe372884d9ae0e280d31963e334c74c765296c62a2b7d1f052cee0596833e1874 17 | 9/9/2024,6th August user gas refunds,Gas refund,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xb9E29984Fe50602E7A619662EBED4F90D93824C7,4.4809,ETH,,0x357bb888164c634da56138a634bfb319d3d4bf1c26007af331ce6134f712ff16 18 | 12/9/2024,Swap 91.79 USDT for 99.99 LON,Token Swap,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0x4a14347083B80E5216cA31350a2D21702aC3650d,91.79,USDT,for Mid-autumn festival campaign,0x266def802ea0f708c215abc7f71de2e87c1577aee77afed2dda003fdb312ffad 19 | 12/9/2024,Swap 91.79 USDT for 99.99 LON,Token Swap,0x4a14347083B80E5216cA31350a2D21702aC3650d,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,99.99,LON,,0x266def802ea0f708c215abc7f71de2e87c1577aee77afed2dda003fdb312ffad 20 | 12/9/2024,Mid-autumn Festival campaign,Campaign budget,0xc5B2f3e81c9B4FB643190A060Fbc5653542505dF,0xc79b9fB4828FF3987cc318d4EbaCf0d26305F14c,100.00,LON,https://x.com/tokenlon/status/1834165573980488075,0xe9b4e0f478775770bc3f0fb0f2a039e7a21f065c4d43ace6a1f7708a7da077ba 21 | ,,,,,,,, 22 | ,,,,,,,, 23 | ,,,,,,,, 24 | ,Q3 Total expenditure in USDT:,"28,694.71",,,,,, -------------------------------------------------------------------------------- /contracts/SmartOrderStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { IERC20 } from "@openzeppelin/contracts@v5.0.2/token/ERC20/IERC20.sol"; 5 | 6 | import { AdminManagement } from "./abstracts/AdminManagement.sol"; 7 | 8 | import { ISmartOrderStrategy } from "./interfaces/ISmartOrderStrategy.sol"; 9 | import { IStrategy } from "./interfaces/IStrategy.sol"; 10 | import { IWETH } from "./interfaces/IWETH.sol"; 11 | 12 | import { Asset } from "./libraries/Asset.sol"; 13 | 14 | /// @title SmartOrderStrategy Contract 15 | /// @author imToken Labs 16 | /// @notice This contract allows users to execute complex token swap operations. 17 | contract SmartOrderStrategy is ISmartOrderStrategy, AdminManagement { 18 | address public immutable weth; 19 | address public immutable genericSwap; 20 | 21 | /// @notice Receive function to receive ETH. 22 | receive() external payable {} 23 | 24 | /// @notice Constructor to initialize the contract with the owner, generic swap contract address, and WETH contract address. 25 | /// @param _owner The address of the contract owner. 26 | /// @param _genericSwap The address of the generic swap contract that interacts with this strategy. 27 | /// @param _weth The address of the WETH contract. 28 | constructor(address _owner, address _genericSwap, address _weth) AdminManagement(_owner) { 29 | genericSwap = _genericSwap; 30 | weth = _weth; 31 | } 32 | 33 | /// @dev Modifier to restrict access to the function only to the generic swap contract. 34 | modifier onlyGenericSwap() { 35 | if (msg.sender != genericSwap) revert NotFromGS(); 36 | _; 37 | } 38 | 39 | /// @inheritdoc IStrategy 40 | function executeStrategy(address targetToken, bytes calldata strategyData) external payable onlyGenericSwap { 41 | Operation[] memory ops = abi.decode(strategyData, (Operation[])); 42 | if (ops.length == 0) revert EmptyOps(); 43 | 44 | uint256 opsCount = ops.length; 45 | for (uint256 i; i < opsCount; ++i) { 46 | Operation memory op = ops[i]; 47 | _call(op.dest, op.inputToken, op.ratioNumerator, op.ratioDenominator, op.dataOffset, op.value, op.data); 48 | } 49 | 50 | // unwrap WETH to ETH if targetToken is ETH 51 | if (Asset.isETH(targetToken)) { 52 | // the if statement is not fully covered by the tests even replacing `makerToken.isETH()` with `makerToken == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` 53 | // and crafting some cases where targetToken is ETH and non-ETH 54 | uint256 wethBalance = IWETH(weth).balanceOf(address(this)); 55 | 56 | if (wethBalance > 0) { 57 | // this if statement is not be fully covered because WETH withdraw will always succeed as wethBalance > 0 58 | IWETH(weth).withdraw(wethBalance); 59 | } 60 | } 61 | 62 | uint256 selfBalance = Asset.getBalance(targetToken, address(this)); 63 | if (selfBalance > 1) { 64 | unchecked { 65 | --selfBalance; 66 | } 67 | } 68 | 69 | // transfer output tokens back to the generic swap contract 70 | Asset.transferTo(targetToken, payable(genericSwap), selfBalance); 71 | } 72 | 73 | /// @dev This function adjusts the input amount based on a ratio if specified, then calls the destination contract with data. 74 | /// @param _dest The destination address to call. 75 | /// @param _inputToken The address of the input token for the call. 76 | /// @param _ratioNumerator The numerator used for ratio calculation. 77 | /// @param _ratioDenominator The denominator used for ratio calculation. 78 | /// @param _dataOffset The offset in the data where the input amount is located. 79 | /// @param _value The amount of ETH to send with the call. 80 | /// @param _data Additional data to be passed with the call. 81 | function _call( 82 | address _dest, 83 | address _inputToken, 84 | uint256 _ratioNumerator, 85 | uint256 _ratioDenominator, 86 | uint256 _dataOffset, 87 | uint256 _value, 88 | bytes memory _data 89 | ) internal { 90 | // adjust amount if ratio != 0 91 | if (_ratioNumerator != 0) { 92 | uint256 inputTokenBalance = IERC20(_inputToken).balanceOf(address(this)); 93 | 94 | // calculate input amount if ratio should be applied 95 | if (_ratioNumerator != _ratioDenominator) { 96 | if (_ratioDenominator == 0) revert ZeroDenominator(); 97 | inputTokenBalance = (inputTokenBalance * _ratioNumerator) / _ratioDenominator; 98 | } 99 | assembly { 100 | mstore(add(_data, _dataOffset), inputTokenBalance) 101 | } 102 | } 103 | 104 | (bool success, bytes memory result) = _dest.call{ value: _value }(_data); 105 | if (!success) { 106 | assembly { 107 | revert(add(result, 32), mload(result)) 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/forkMainnet/LimitOrderSwap/FullOrKill.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { ILimitOrderSwap } from "contracts/interfaces/ILimitOrderSwap.sol"; 5 | import { Constant } from "contracts/libraries/Constant.sol"; 6 | import { getLimitOrderHash } from "contracts/libraries/LimitOrder.sol"; 7 | 8 | import { LimitOrderSwapTest } from "test/forkMainnet/LimitOrderSwap/Setup.t.sol"; 9 | import { BalanceSnapshot, Snapshot } from "test/utils/BalanceSnapshot.sol"; 10 | 11 | contract FullOrKillTest is LimitOrderSwapTest { 12 | using BalanceSnapshot for Snapshot; 13 | 14 | function testFillWithFOK() public { 15 | Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); 16 | Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); 17 | Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: defaultOrder.maker, token: defaultOrder.takerToken }); 18 | Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: defaultOrder.maker, token: defaultOrder.makerToken }); 19 | Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.takerToken }); 20 | Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); 21 | Snapshot memory fcMakerToken = BalanceSnapshot.take({ owner: feeCollector, token: defaultOrder.makerToken }); 22 | 23 | // fill FOK default order with 1/10 amount 24 | uint256 traderMakingAmount = defaultOrder.makerTokenAmount / 10; 25 | uint256 traderTakingAmount = defaultOrder.takerTokenAmount / 10; 26 | uint256 fee = (traderMakingAmount * defaultFeeFactor) / Constant.BPS_MAX; 27 | 28 | vm.expectEmit(true, true, true, true); 29 | emit LimitOrderFilled( 30 | getLimitOrderHash(defaultOrder), 31 | taker, 32 | defaultOrder.maker, 33 | defaultOrder.takerToken, 34 | traderTakingAmount, 35 | defaultOrder.makerToken, 36 | traderMakingAmount - fee, 37 | fee, 38 | recipient 39 | ); 40 | 41 | vm.startPrank(taker); 42 | limitOrderSwap.fillLimitOrderFullOrKill({ 43 | order: defaultOrder, 44 | makerSignature: defaultMakerSig, 45 | takerParams: ILimitOrderSwap.TakerParams({ 46 | takerTokenAmount: traderTakingAmount, 47 | makerTokenAmount: traderMakingAmount, 48 | recipient: recipient, 49 | extraAction: bytes(""), 50 | takerTokenPermit: defaultTakerPermit 51 | }) 52 | }); 53 | vm.stopPrank(); 54 | vm.snapshotGasLastCall("LimitOrderSwap", "fillLimitOrderFullOrKill(): testFillWithFOK"); 55 | 56 | takerTakerToken.assertChange(-int256(traderTakingAmount)); 57 | takerMakerToken.assertChange(int256(0)); 58 | makerTakerToken.assertChange(int256(traderTakingAmount)); 59 | makerMakerToken.assertChange(-int256(traderMakingAmount)); 60 | recTakerToken.assertChange(int256(0)); 61 | recMakerToken.assertChange(int256(traderMakingAmount - fee)); 62 | fcMakerToken.assertChange(int256(fee)); 63 | } 64 | 65 | function testCannotFillFOKIfNotEnough() public { 66 | // fill FOK default order with larger volume 67 | uint256 traderMakingAmount = defaultOrder.makerTokenAmount * 2; 68 | uint256 traderTakingAmount = defaultOrder.takerTokenAmount * 2; 69 | 70 | vm.startPrank(taker); 71 | vm.expectRevert(ILimitOrderSwap.NotEnoughForFill.selector); 72 | limitOrderSwap.fillLimitOrderFullOrKill({ 73 | order: defaultOrder, 74 | makerSignature: defaultMakerSig, 75 | takerParams: ILimitOrderSwap.TakerParams({ 76 | takerTokenAmount: traderTakingAmount, 77 | makerTokenAmount: traderMakingAmount, 78 | recipient: recipient, 79 | extraAction: bytes(""), 80 | takerTokenPermit: defaultTakerPermit 81 | }) 82 | }); 83 | vm.stopPrank(); 84 | } 85 | 86 | function testCannotFillFOKIfNotEnoughEvenPriceIsBetter() public { 87 | // fill FOK default order with larger volume, also provide better price (takingAmount is 20x) 88 | uint256 traderMakingAmount = defaultOrder.makerTokenAmount * 2; 89 | uint256 traderTakingAmount = defaultOrder.takerTokenAmount * 20; 90 | 91 | vm.startPrank(taker); 92 | vm.expectRevert(ILimitOrderSwap.NotEnoughForFill.selector); 93 | limitOrderSwap.fillLimitOrderFullOrKill({ 94 | order: defaultOrder, 95 | makerSignature: defaultMakerSig, 96 | takerParams: ILimitOrderSwap.TakerParams({ 97 | takerTokenAmount: traderTakingAmount, 98 | makerTokenAmount: traderMakingAmount, 99 | recipient: recipient, 100 | extraAction: bytes(""), 101 | takerTokenPermit: defaultTakerPermit 102 | }) 103 | }); 104 | vm.stopPrank(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /contracts/GenericSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { EIP712 } from "./abstracts/EIP712.sol"; 5 | import { TokenCollector } from "./abstracts/TokenCollector.sol"; 6 | 7 | import { IGenericSwap } from "./interfaces/IGenericSwap.sol"; 8 | import { IStrategy } from "./interfaces/IStrategy.sol"; 9 | 10 | import { Asset } from "./libraries/Asset.sol"; 11 | import { GenericSwapData, getGSDataHash } from "./libraries/GenericSwapData.sol"; 12 | import { SignatureValidator } from "./libraries/SignatureValidator.sol"; 13 | 14 | /// @title GenericSwap Contract 15 | /// @author imToken Labs 16 | /// @notice This contract facilitates token swaps using SmartOrderStrategy strategies. 17 | contract GenericSwap is IGenericSwap, TokenCollector, EIP712 { 18 | using Asset for address; 19 | 20 | /// @notice Mapping to keep track of filled swaps. 21 | /// @dev Stores the status of swaps to ensure they are not filled more than once. 22 | mapping(bytes32 swapHash => bool isFilled) public filledSwap; 23 | 24 | /// @notice Constructor to initialize the contract with the permit2 and allowance target. 25 | /// @param _uniswapPermit2 The address for Uniswap permit2. 26 | /// @param _allowanceTarget The address for the allowance target. 27 | constructor(address _uniswapPermit2, address _allowanceTarget) TokenCollector(_uniswapPermit2, _allowanceTarget) {} 28 | 29 | /// @notice Receive function to receive ETH. 30 | receive() external payable {} 31 | 32 | /// @inheritdoc IGenericSwap 33 | function executeSwap(GenericSwapData calldata swapData, bytes calldata takerTokenPermit) external payable returns (uint256 returnAmount) { 34 | returnAmount = _executeSwap(swapData, msg.sender, takerTokenPermit); 35 | 36 | _emitGSExecuted(getGSDataHash(swapData), swapData, msg.sender, returnAmount); 37 | } 38 | 39 | /// @inheritdoc IGenericSwap 40 | function executeSwapWithSig( 41 | GenericSwapData calldata swapData, 42 | bytes calldata takerTokenPermit, 43 | address taker, 44 | bytes calldata takerSig 45 | ) external payable returns (uint256 returnAmount) { 46 | bytes32 swapHash = getGSDataHash(swapData); 47 | bytes32 gs712Hash = getEIP712Hash(swapHash); 48 | if (filledSwap[swapHash]) revert AlreadyFilled(); 49 | filledSwap[swapHash] = true; 50 | if (!SignatureValidator.validateSignature(taker, gs712Hash, takerSig)) revert InvalidSignature(); 51 | 52 | returnAmount = _executeSwap(swapData, taker, takerTokenPermit); 53 | 54 | _emitGSExecuted(swapHash, swapData, taker, returnAmount); 55 | } 56 | 57 | /// @notice Executes a generic swap. 58 | /// @param _swapData The swap data containing details of the swap. 59 | /// @param _authorizedUser The address authorized to execute the swap. 60 | /// @param _takerTokenPermit The permit for the taker token. 61 | /// @return returnAmount The output amount of the swap. 62 | function _executeSwap( 63 | GenericSwapData calldata _swapData, 64 | address _authorizedUser, 65 | bytes calldata _takerTokenPermit 66 | ) private returns (uint256 returnAmount) { 67 | if (_swapData.expiry < block.timestamp) revert ExpiredOrder(); 68 | if (_swapData.recipient == address(0)) revert ZeroAddress(); 69 | if (_swapData.takerTokenAmount == 0) revert SwapWithZeroAmount(); 70 | 71 | address _inputToken = _swapData.takerToken; 72 | address _outputToken = _swapData.makerToken; 73 | 74 | if (_inputToken.isETH()) { 75 | if (msg.value != _swapData.takerTokenAmount) revert InvalidMsgValue(); 76 | } else { 77 | if (msg.value != 0) revert InvalidMsgValue(); 78 | _collect(_inputToken, _authorizedUser, _swapData.maker, _swapData.takerTokenAmount, _takerTokenPermit); 79 | } 80 | 81 | IStrategy(_swapData.maker).executeStrategy{ value: msg.value }(_outputToken, _swapData.strategyData); 82 | 83 | returnAmount = _outputToken.getBalance(address(this)); 84 | if (returnAmount > 1) { 85 | unchecked { 86 | --returnAmount; 87 | } 88 | } 89 | if (returnAmount < _swapData.minMakerTokenAmount) revert InsufficientOutput(); 90 | 91 | _outputToken.transferTo(_swapData.recipient, returnAmount); 92 | } 93 | 94 | /// @notice Emits the Swap event after executing a generic swap. 95 | /// @param _gsOfferHash The hash of the generic swap offer. 96 | /// @param _swapData The swap data containing details of the swap. 97 | /// @param _taker The address of the taker. 98 | /// @param returnAmount The output amount of the swap. 99 | function _emitGSExecuted(bytes32 _gsOfferHash, GenericSwapData calldata _swapData, address _taker, uint256 returnAmount) internal { 100 | emit Swap( 101 | _gsOfferHash, 102 | _swapData.maker, 103 | _taker, 104 | _swapData.recipient, 105 | _swapData.takerToken, 106 | _swapData.takerTokenAmount, 107 | _swapData.makerToken, 108 | returnAmount, 109 | _swapData.salt 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /contracts/CoordinatedTaker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { AdminManagement } from "./abstracts/AdminManagement.sol"; 5 | import { EIP712 } from "./abstracts/EIP712.sol"; 6 | import { TokenCollector } from "./abstracts/TokenCollector.sol"; 7 | 8 | import { ICoordinatedTaker } from "./interfaces/ICoordinatedTaker.sol"; 9 | import { ILimitOrderSwap } from "./interfaces/ILimitOrderSwap.sol"; 10 | import { IWETH } from "./interfaces/IWETH.sol"; 11 | 12 | import { AllowFill, getAllowFillHash } from "./libraries/AllowFill.sol"; 13 | import { Asset } from "./libraries/Asset.sol"; 14 | import { LimitOrder, getLimitOrderHash } from "./libraries/LimitOrder.sol"; 15 | import { SignatureValidator } from "./libraries/SignatureValidator.sol"; 16 | 17 | /// @title CoordinatedTaker Contract 18 | /// @author imToken Labs 19 | /// @notice This contract is a taker contract for the LimitOrderSwap. 20 | /// @dev It helps users avoid collisions when filling a limit order and provides an off-chain order canceling mechanism. 21 | /// For more details, check the reference: https://github.com/consenlabs/tokenlon-contracts/blob/v6.0.1/doc/CoordinatedTaker.md 22 | contract CoordinatedTaker is ICoordinatedTaker, AdminManagement, TokenCollector, EIP712 { 23 | using Asset for address; 24 | 25 | IWETH public immutable weth; 26 | ILimitOrderSwap public immutable limitOrderSwap; 27 | address public coordinator; 28 | 29 | /// @notice Mapping to keep track of used allow fill hashes. 30 | mapping(bytes32 allowFillHash => bool isUsed) public allowFillUsed; 31 | 32 | /// @notice Constructor to initialize the contract with the owner, Uniswap permit2, allowance target, WETH, coordinator and LimitOrderSwap contract. 33 | /// @param _owner The address of the contract owner. 34 | /// @param _uniswapPermit2 The address for Uniswap permit2. 35 | /// @param _allowanceTarget The address for the allowance target. 36 | /// @param _weth The WETH contract instance. 37 | /// @param _coordinator The initial coordinator address. 38 | /// @param _limitOrderSwap The LimitOrderSwap contract address. 39 | constructor( 40 | address _owner, 41 | address _uniswapPermit2, 42 | address _allowanceTarget, 43 | IWETH _weth, 44 | address _coordinator, 45 | ILimitOrderSwap _limitOrderSwap 46 | ) AdminManagement(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) { 47 | weth = _weth; 48 | coordinator = _coordinator; 49 | limitOrderSwap = _limitOrderSwap; 50 | } 51 | 52 | /// @notice Receive function to receive ETH. 53 | receive() external payable {} 54 | 55 | /// @notice Sets a new coordinator address. 56 | /// @dev Only the owner can call this function. 57 | /// @param _newCoordinator The address of the new coordinator. 58 | function setCoordinator(address _newCoordinator) external onlyOwner { 59 | if (_newCoordinator == address(0)) revert ZeroAddress(); 60 | coordinator = _newCoordinator; 61 | 62 | emit SetCoordinator(_newCoordinator); 63 | } 64 | 65 | /// @inheritdoc ICoordinatedTaker 66 | function submitLimitOrderFill( 67 | LimitOrder calldata order, 68 | bytes calldata makerSignature, 69 | uint256 takerTokenAmount, 70 | uint256 makerTokenAmount, 71 | bytes calldata extraAction, 72 | bytes calldata userTokenPermit, 73 | CoordinatorParams calldata crdParams 74 | ) external payable { 75 | // validate fill permission 76 | { 77 | if (crdParams.expiry < block.timestamp) revert ExpiredPermission(); 78 | 79 | bytes32 orderHash = getLimitOrderHash(order); 80 | bytes32 allowFillHash = getEIP712Hash( 81 | getAllowFillHash( 82 | AllowFill({ orderHash: orderHash, taker: msg.sender, fillAmount: makerTokenAmount, salt: crdParams.salt, expiry: crdParams.expiry }) 83 | ) 84 | ); 85 | 86 | if (!SignatureValidator.validateSignature(coordinator, allowFillHash, crdParams.sig)) revert InvalidSignature(); 87 | if (allowFillUsed[allowFillHash]) revert ReusedPermission(); 88 | 89 | allowFillUsed[allowFillHash] = true; 90 | 91 | emit CoordinatorFill({ user: msg.sender, orderHash: orderHash, allowFillHash: allowFillHash }); 92 | } 93 | 94 | // collect taker token from user (forward to LO contract without validation if taker token is ETH) 95 | if (!order.takerToken.isETH()) { 96 | if (msg.value != 0) revert InvalidMsgValue(); 97 | _collect(order.takerToken, msg.sender, address(this), takerTokenAmount, userTokenPermit); 98 | } 99 | 100 | // send order to limit order contract 101 | // use fillLimitOrderFullOrKill since coordinator should manage fill amount distribution 102 | limitOrderSwap.fillLimitOrderFullOrKill{ value: msg.value }( 103 | order, 104 | makerSignature, 105 | ILimitOrderSwap.TakerParams({ 106 | takerTokenAmount: takerTokenAmount, 107 | makerTokenAmount: makerTokenAmount, 108 | recipient: msg.sender, 109 | extraAction: extraAction, 110 | takerTokenPermit: abi.encodePacked(TokenCollector.Source.Token) 111 | }) 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contracts/interfaces/IRFQ.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { RFQOffer } from "../libraries/RFQOffer.sol"; 5 | import { RFQTx } from "../libraries/RFQTx.sol"; 6 | 7 | /// @title IRFQ Interface 8 | /// @author imToken Labs 9 | /// @notice Interface for an RFQ (Request for Quote) contract. 10 | /// @dev This interface defines functions and events related to handling RFQ offers and transactions. 11 | interface IRFQ { 12 | /// @notice Emitted when the fee collector address is updated. 13 | /// @param newFeeCollector The address of the new fee collector. 14 | event SetFeeCollector(address newFeeCollector); 15 | 16 | /// @notice Emitted when an RFQ offer is successfully filled. 17 | /// @param rfqOfferHash The hash of the RFQ offer. 18 | /// @param user The address of the user filling the RFQ offer. 19 | /// @param maker The address of the maker who created the RFQ offer. 20 | /// @param takerToken The address of the token taken by the taker. 21 | /// @param takerTokenUserAmount The amount of taker tokens taken by the user. 22 | /// @param makerToken The address of the token provided by the maker. 23 | /// @param makerTokenUserAmount The amount of maker tokens received by the user. 24 | /// @param recipient The address receiving the tokens. 25 | /// @param fee The fee amount paid for the RFQ transaction. 26 | event FilledRFQ( 27 | bytes32 indexed rfqOfferHash, 28 | address indexed user, 29 | address indexed maker, 30 | address takerToken, 31 | uint256 takerTokenUserAmount, 32 | address makerToken, 33 | uint256 makerTokenUserAmount, 34 | address recipient, 35 | uint256 fee 36 | ); 37 | 38 | /// @notice Emitted when an RFQ offer is canceled. 39 | /// @param rfqOfferHash The hash of the canceled RFQ offer. 40 | /// @param maker The address of the maker who canceled the RFQ offer. 41 | event CancelRFQOffer(bytes32 indexed rfqOfferHash, address indexed maker); 42 | 43 | /// @notice Error to be thrown when an RFQ offer has expired. 44 | /// @dev Thrown when attempting to fill an RFQ offer that has expired. 45 | error ExpiredRFQOffer(); 46 | 47 | /// @notice Error to be thrown when an RFQ offer is already filled. 48 | /// @dev Thrown when attempting to fill an RFQ offer that has already been filled. 49 | error FilledRFQOffer(); 50 | 51 | /// @notice Error to be thrown when an address is zero. 52 | /// @dev Thrown when an operation requires a non-zero address. 53 | error ZeroAddress(); 54 | 55 | /// @notice Error to be thrown when the fee factor is invalid. 56 | /// @dev Thrown when an operation requires a valid fee factor that is not provided. 57 | error InvalidFeeFactor(); 58 | 59 | /// @notice Error to be thrown when the msg.value is invalid. 60 | /// @dev Thrown when an operation requires a specific msg.value that is not provided. 61 | error InvalidMsgValue(); 62 | 63 | /// @notice Error to be thrown when a signature is invalid. 64 | /// @dev Thrown when an operation requires a valid cryptographic signature that is not provided or is invalid. 65 | error InvalidSignature(); 66 | 67 | /// @notice Error to be thrown when the taker amount is invalid. 68 | /// @dev Thrown when an operation requires a valid taker amount that is not provided or is invalid. 69 | error InvalidTakerAmount(); 70 | 71 | /// @notice Error to be thrown when the maker amount is invalid. 72 | /// @dev Thrown when an operation requires a valid maker amount that is not provided or is invalid. 73 | error InvalidMakerAmount(); 74 | 75 | /// @notice Error to be thrown when interaction with contracts is forbidden. 76 | /// @dev Thrown when an operation is attempted with a contract address where only EOA (Externally Owned Account) is allowed. 77 | error ForbidContract(); 78 | 79 | /// @notice Error to be thrown when partial fill is forbidden. 80 | /// @dev Thrown when attempting to partially fill an RFQ offer that does not allow partial fills. 81 | error ForbidPartialFill(); 82 | 83 | /// @notice Error to be thrown when the caller is not the maker of the RFQ offer. 84 | /// @dev Thrown when an operation is attempted by an unauthorized caller who is not the maker of the RFQ offer. 85 | error NotOfferMaker(); 86 | 87 | /// @notice Fills an RFQ offer. 88 | /// @param rfqTx The RFQ transaction details. 89 | /// @param makerSignature The signature of the maker authorizing the fill. 90 | /// @param makerTokenPermit The permit for spending maker's tokens. 91 | /// @param takerTokenPermit The permit for spending taker's tokens. 92 | function fillRFQ(RFQTx calldata rfqTx, bytes calldata makerSignature, bytes calldata makerTokenPermit, bytes calldata takerTokenPermit) external payable; 93 | 94 | /// @notice Fills an RFQ offer using a taker signature. 95 | /// @param rfqTx The RFQ transaction details. 96 | /// @param makerSignature The signature of the maker authorizing the fill. 97 | /// @param makerTokenPermit The permit for spending maker's tokens. 98 | /// @param takerTokenPermit The permit for spending taker's tokens. 99 | /// @param takerSignature The cryptographic signature of the taker authorizing the fill. 100 | function fillRFQWithSig( 101 | RFQTx calldata rfqTx, 102 | bytes calldata makerSignature, 103 | bytes calldata makerTokenPermit, 104 | bytes calldata takerTokenPermit, 105 | bytes calldata takerSignature 106 | ) external; 107 | 108 | /// @notice Cancels an RFQ offer. 109 | /// @param rfqOffer The RFQ offer to be canceled. 110 | function cancelRFQOffer(RFQOffer calldata rfqOffer) external; 111 | } 112 | -------------------------------------------------------------------------------- /test/utils/UniswapV2Library.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapV2Pair { 5 | event Approval(address indexed owner, address indexed spender, uint256 value); 6 | event Transfer(address indexed from, address indexed to, uint256 value); 7 | 8 | function name() external pure returns (string memory); 9 | 10 | function symbol() external pure returns (string memory); 11 | 12 | function decimals() external pure returns (uint8); 13 | 14 | function totalSupply() external view returns (uint256); 15 | 16 | function balanceOf(address owner) external view returns (uint256); 17 | 18 | function allowance(address owner, address spender) external view returns (uint256); 19 | 20 | function approve(address spender, uint256 value) external returns (bool); 21 | 22 | function transfer(address to, uint256 value) external returns (bool); 23 | 24 | function transferFrom(address from, address to, uint256 value) external returns (bool); 25 | 26 | function DOMAIN_SEPARATOR() external view returns (bytes32); 27 | 28 | function PERMIT_TYPEHASH() external pure returns (bytes32); 29 | 30 | function nonces(address owner) external view returns (uint256); 31 | 32 | function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; 33 | 34 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 35 | event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); 36 | event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to); 37 | event Sync(uint112 reserve0, uint112 reserve1); 38 | 39 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 40 | 41 | function factory() external view returns (address); 42 | 43 | function token0() external view returns (address); 44 | 45 | function token1() external view returns (address); 46 | 47 | function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 48 | 49 | function price0CumulativeLast() external view returns (uint256); 50 | 51 | function price1CumulativeLast() external view returns (uint256); 52 | 53 | function kLast() external view returns (uint256); 54 | 55 | function mint(address to) external returns (uint256 liquidity); 56 | 57 | function burn(address to) external returns (uint256 amount0, uint256 amount1); 58 | 59 | function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; 60 | 61 | function skim(address to) external; 62 | 63 | function sync() external; 64 | 65 | function initialize(address, address) external; 66 | } 67 | 68 | library UniswapV2Library { 69 | address constant factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; 70 | 71 | // returns sorted token addresses, used to handle return values from pairs sorted in this order 72 | function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { 73 | require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); 74 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 75 | require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); 76 | } 77 | 78 | // calculates the CREATE2 address for a pair without making any external calls 79 | function pairFor(address tokenA, address tokenB) internal pure returns (address pair) { 80 | (address token0, address token1) = sortTokens(tokenA, tokenB); 81 | pair = address( 82 | uint160( 83 | uint256( 84 | keccak256( 85 | abi.encodePacked( 86 | hex"ff", 87 | factory, 88 | keccak256(abi.encodePacked(token0, token1)), 89 | hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" // init code hash 90 | ) 91 | ) 92 | ) 93 | ) 94 | ); 95 | } 96 | 97 | // fetches and sorts the reserves for a pair 98 | function getReserves(address tokenA, address tokenB) internal view returns (uint256 reserveA, uint256 reserveB) { 99 | (address token0, ) = sortTokens(tokenA, tokenB); 100 | (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(tokenA, tokenB)).getReserves(); 101 | (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 102 | } 103 | 104 | // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset 105 | function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountOut) { 106 | require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); 107 | require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); 108 | uint256 amountInWithFee = amountIn * 997; 109 | uint256 numerator = amountInWithFee * reserveOut; 110 | uint256 denominator = (reserveIn * 1000) + amountInWithFee; 111 | amountOut = numerator / denominator; 112 | } 113 | 114 | // performs chained getAmountOut calculations on any number of pairs 115 | function getAmountsOut(uint256 amountIn, address[] memory path) internal view returns (uint256[] memory amounts) { 116 | require(path.length >= 2, "UniswapV2Library: INVALID_PATH"); 117 | amounts = new uint256[](path.length); 118 | amounts[0] = amountIn; 119 | for (uint256 i; i < path.length - 1; ++i) { 120 | (uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]); 121 | amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/forkMainnet/LimitOrderSwap/Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | import { AllowanceTarget } from "contracts/AllowanceTarget.sol"; 7 | import { LimitOrderSwap } from "contracts/LimitOrderSwap.sol"; 8 | import { TokenCollector } from "contracts/abstracts/TokenCollector.sol"; 9 | import { ILimitOrderSwap } from "contracts/interfaces/ILimitOrderSwap.sol"; 10 | import { IUniswapPermit2 } from "contracts/interfaces/IUniswapPermit2.sol"; 11 | import { IWETH } from "contracts/interfaces/IWETH.sol"; 12 | import { LimitOrder } from "contracts/libraries/LimitOrder.sol"; 13 | 14 | import { MockLimitOrderTaker } from "test/mocks/MockLimitOrderTaker.sol"; 15 | import { computeContractAddress } from "test/utils/Addresses.sol"; 16 | import { BalanceUtil } from "test/utils/BalanceUtil.sol"; 17 | import { Permit2Helper } from "test/utils/Permit2Helper.sol"; 18 | import { SigHelper } from "test/utils/SigHelper.sol"; 19 | import { Tokens } from "test/utils/Tokens.sol"; 20 | 21 | contract LimitOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper { 22 | event SetFeeCollector(address newFeeCollector); 23 | event LimitOrderFilled( 24 | bytes32 indexed offerHash, 25 | address indexed taker, 26 | address indexed maker, 27 | address takerToken, 28 | uint256 takerTokenFilledAmount, 29 | address makerToken, 30 | uint256 makerTokenSettleAmount, 31 | uint256 fee, 32 | address recipient 33 | ); 34 | 35 | address limitOrderOwner = makeAddr("limitOrderOwner"); 36 | address allowanceTargetOwner = makeAddr("allowanceTargetOwner"); 37 | uint256 makerPrivateKey = uint256(1); 38 | address payable maker = payable(vm.addr(makerPrivateKey)); 39 | uint256 takerPrivateKey = uint256(2); 40 | address taker = vm.addr(takerPrivateKey); 41 | address payable recipient = payable(makeAddr("recipient")); 42 | address payable feeCollector = payable(makeAddr("feeCollector")); 43 | address walletOwner = makeAddr("walletOwner"); 44 | uint256 defaultExpiry = block.timestamp + 1; 45 | uint256 defaultSalt = 1234; 46 | uint256 defaultFeeFactor = 100; 47 | LimitOrder defaultOrder; 48 | bytes defaultMakerSig; 49 | bytes directApprovePermit = abi.encodePacked(TokenCollector.Source.Token); 50 | bytes allowanceTransferPermit = abi.encodePacked(TokenCollector.Source.Permit2AllowanceTransfer); 51 | bytes defaultMakerPermit = allowanceTransferPermit; 52 | bytes defaultTakerPermit; 53 | ILimitOrderSwap.TakerParams defaultTakerParams; 54 | MockLimitOrderTaker mockLimitOrderTaker; 55 | LimitOrderSwap limitOrderSwap; 56 | AllowanceTarget allowanceTarget; 57 | 58 | function setUp() public virtual { 59 | // deploy allowance target 60 | address[] memory trusted = new address[](1); 61 | // pre-compute LimitOrderSwap address since the whitelist of allowance target is immutable 62 | // NOTE: this assumes LimitOrderSwap is deployed right next to Allowance Target 63 | trusted[0] = computeContractAddress(address(this), uint8(vm.getNonce(address(this)) + 1)); 64 | allowanceTarget = new AllowanceTarget(allowanceTargetOwner, trusted); 65 | 66 | limitOrderSwap = new LimitOrderSwap(limitOrderOwner, UNISWAP_PERMIT2_ADDRESS, address(allowanceTarget), IWETH(WETH_ADDRESS), feeCollector); 67 | mockLimitOrderTaker = new MockLimitOrderTaker(walletOwner, UNISWAP_SWAP_ROUTER_02_ADDRESS); 68 | 69 | deal(maker, 100 ether); 70 | setTokenBalanceAndApprove(maker, UNISWAP_PERMIT2_ADDRESS, tokens, 100000); 71 | deal(taker, 100 ether); 72 | setTokenBalanceAndApprove(taker, UNISWAP_PERMIT2_ADDRESS, tokens, 100000); 73 | deal(address(mockLimitOrderTaker), 100 ether); 74 | // mockLimitOrderTaker approve LO contract directly for convenience 75 | setTokenBalanceAndApprove(address(mockLimitOrderTaker), address(limitOrderSwap), tokens, 100000); 76 | 77 | address[] memory tokenList = new address[](2); 78 | tokenList[0] = DAI_ADDRESS; 79 | tokenList[1] = USDT_ADDRESS; 80 | vm.startPrank(walletOwner); 81 | mockLimitOrderTaker.setAllowance(tokenList, UNISWAP_SWAP_ROUTER_02_ADDRESS); 82 | vm.stopPrank(); 83 | 84 | defaultOrder = LimitOrder({ 85 | taker: address(0), 86 | maker: maker, 87 | takerToken: USDT_ADDRESS, 88 | takerTokenAmount: 10 * 1e6, 89 | makerToken: DAI_ADDRESS, 90 | makerTokenAmount: 10 ether, 91 | makerTokenPermit: defaultMakerPermit, 92 | feeFactor: defaultFeeFactor, 93 | expiry: defaultExpiry, 94 | salt: defaultSalt 95 | }); 96 | 97 | // maker should call permit2 first independently 98 | vm.startPrank(maker); 99 | IUniswapPermit2(UNISWAP_PERMIT2_ADDRESS).approve(defaultOrder.makerToken, address(limitOrderSwap), type(uint160).max, uint48(block.timestamp + 1 days)); 100 | vm.stopPrank(); 101 | 102 | defaultTakerPermit = getTokenlonPermit2Data(taker, takerPrivateKey, defaultOrder.takerToken, address(limitOrderSwap)); 103 | 104 | defaultTakerParams = ILimitOrderSwap.TakerParams({ 105 | takerTokenAmount: defaultOrder.takerTokenAmount, 106 | makerTokenAmount: defaultOrder.makerTokenAmount, 107 | recipient: recipient, 108 | extraAction: bytes(""), 109 | takerTokenPermit: defaultTakerPermit 110 | }); 111 | 112 | defaultMakerSig = signLimitOrder(makerPrivateKey, defaultOrder, address(limitOrderSwap)); 113 | 114 | vm.label(address(limitOrderSwap), "limitOrderSwap"); 115 | vm.label(taker, "taker"); 116 | vm.label(maker, "maker"); 117 | } 118 | 119 | function testLimitOrderSwapInitialState() public virtual { 120 | limitOrderSwap = new LimitOrderSwap(limitOrderOwner, UNISWAP_PERMIT2_ADDRESS, address(allowanceTarget), IWETH(WETH_ADDRESS), feeCollector); 121 | 122 | assertEq(limitOrderSwap.owner(), limitOrderOwner); 123 | assertEq(limitOrderSwap.permit2(), UNISWAP_PERMIT2_ADDRESS); 124 | assertEq(limitOrderSwap.allowanceTarget(), address(allowanceTarget)); 125 | assertEq(address(limitOrderSwap.weth()), WETH_ADDRESS); 126 | assertEq(limitOrderSwap.feeCollector(), feeCollector); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/libraries/SignatureValidator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.26; 3 | 4 | import { ECDSA } from "@openzeppelin/contracts@v5.0.2/utils/cryptography/ECDSA.sol"; 5 | import { Test } from "forge-std/Test.sol"; 6 | 7 | import { IERC1271Wallet } from "contracts/interfaces/IERC1271Wallet.sol"; 8 | import { SignatureValidator } from "contracts/libraries/SignatureValidator.sol"; 9 | 10 | import { MockERC1271Wallet } from "test/mocks/MockERC1271Wallet.sol"; 11 | 12 | contract SignatureValidatorTest is Test { 13 | uint256 userPrivateKey = 1234; 14 | uint256 walletAdminPrivateKey = 5678; 15 | bytes32 digest = keccak256("EIP-712 data"); 16 | MockERC1271Wallet mockERC1271Wallet; 17 | 18 | function setUp() public { 19 | mockERC1271Wallet = new MockERC1271Wallet(vm.addr(walletAdminPrivateKey)); 20 | } 21 | 22 | // this is a workaround for library contract tests 23 | // assertion may not working for library internal functions 24 | // https://github.com/foundry-rs/foundry/issues/4405 25 | function validateSignatureWrap(address _signerAddress, bytes32 _hash, bytes memory _signature) public view returns (bool) { 26 | return SignatureValidator.validateSignature(_signerAddress, _hash, _signature); 27 | } 28 | 29 | function testEIP712Signature() public { 30 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 31 | bytes memory signature = abi.encodePacked(r, s, v); 32 | assertTrue(SignatureValidator.validateSignature(vm.addr(userPrivateKey), digest, signature)); 33 | } 34 | 35 | function testEIP712WithDifferentSigner() public { 36 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 37 | bytes memory signature = abi.encodePacked(r, s, v); 38 | assertFalse(SignatureValidator.validateSignature(vm.addr(walletAdminPrivateKey), digest, signature)); 39 | } 40 | 41 | function testEIP712WithWrongHash() public { 42 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 43 | bytes memory signature = abi.encodePacked(r, s, v); 44 | bytes32 otherDigest = keccak256("other data other data"); 45 | assertFalse(SignatureValidator.validateSignature(vm.addr(walletAdminPrivateKey), otherDigest, signature)); 46 | } 47 | 48 | /// forge-config: default.allow_internal_expect_revert = true 49 | function testEIP712WithWrongSignatureLength() public { 50 | uint256 v = 1; 51 | uint256 r = 2; 52 | uint256 s = 3; 53 | // should have 96 bytes signature 54 | bytes memory signature = abi.encodePacked(r, s, v); 55 | // will be reverted in OZ ECDSA lib 56 | vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureLength.selector, signature.length)); 57 | SignatureValidator.validateSignature(vm.addr(userPrivateKey), digest, signature); 58 | } 59 | 60 | /// forge-config: default.allow_internal_expect_revert = true 61 | function testEIP712WithEmptySignature() public { 62 | bytes memory signature; 63 | // will be reverted in OZ ECDSA lib 64 | vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureLength.selector, signature.length)); 65 | SignatureValidator.validateSignature(vm.addr(userPrivateKey), digest, signature); 66 | } 67 | 68 | function testEIP1271Signature() public { 69 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(walletAdminPrivateKey, digest); 70 | bytes memory signature = abi.encodePacked(r, s, v); 71 | assertTrue(SignatureValidator.validateSignature(address(mockERC1271Wallet), digest, signature)); 72 | } 73 | 74 | function testEIP1271WithWrongSignatureLength() public { 75 | uint256 v = 1; 76 | uint256 r = 2; 77 | uint256 s = 3; 78 | // should have 96 bytes signature 79 | bytes memory signature = abi.encodePacked(r, s, v); 80 | // will be reverted in OZ ECDSA lib 81 | vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureLength.selector, signature.length)); 82 | SignatureValidator.validateSignature(address(mockERC1271Wallet), digest, signature); 83 | } 84 | 85 | function testEIP1271WithDifferentSigner() public { 86 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 87 | bytes memory signature = abi.encodePacked(r, s, v); 88 | assertFalse(SignatureValidator.validateSignature(address(mockERC1271Wallet), digest, signature)); 89 | } 90 | 91 | function testEIP1271WithInvalidSignatureS() public { 92 | (uint8 v, bytes32 r, ) = vm.sign(userPrivateKey, digest); 93 | bytes memory signature = abi.encodePacked(r, r, v); 94 | 95 | vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignatureS.selector, r)); 96 | SignatureValidator.validateSignature(address(mockERC1271Wallet), digest, signature); 97 | } 98 | 99 | function testEIP1271WithZeroAddressSigner() public { 100 | (, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 101 | // change the value of v so ecrecover will return address(0) 102 | bytes memory signature = abi.encodePacked(r, s, uint8(10)); 103 | // OZ ECDSA lib will handle the zero address case and throw error instead 104 | // so the zero address will never be matched 105 | vm.expectRevert(ECDSA.ECDSAInvalidSignature.selector); 106 | this.validateSignatureWrap(address(mockERC1271Wallet), digest, signature); 107 | } 108 | 109 | function testEIP1271WithWrongReturnValue() public { 110 | NonStandard1271Wallet nonWallet = new NonStandard1271Wallet(); 111 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(walletAdminPrivateKey, digest); 112 | bytes memory signature = abi.encodePacked(r, s, v); 113 | assertFalse(SignatureValidator.validateSignature(address(nonWallet), digest, signature)); 114 | } 115 | 116 | function testEIP1271WithNon1271Wallet() public { 117 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(walletAdminPrivateKey, digest); 118 | bytes memory signature = abi.encodePacked(r, s, v); 119 | vm.expectRevert(); 120 | SignatureValidator.validateSignature(address(this), digest, signature); 121 | } 122 | } 123 | 124 | contract NonStandard1271Wallet is IERC1271Wallet { 125 | function isValidSignature(bytes32 _hash, bytes calldata _signature) external pure override returns (bytes4) { 126 | ECDSA.recover(_hash, _signature); 127 | return 0x12345678; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapPermit2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title IUniswapPermit2 Interface 5 | interface IUniswapPermit2 { 6 | /// @notice Thrown when an allowance on a token has expired. 7 | /// @param deadline The timestamp at which the allowed amount is no longer valid 8 | error AllowanceExpired(uint256 deadline); 9 | 10 | /// @notice Thrown when an allowance on a token has been depleted. 11 | /// @param amount The maximum amount allowed 12 | error InsufficientAllowance(uint256 amount); 13 | 14 | /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount 15 | /// @param maxAmount The maximum amount a spender can request to transfer 16 | error InvalidAmount(uint256 maxAmount); 17 | 18 | /// @notice Thrown when validating that the inputted nonce has not been used 19 | error InvalidNonce(); 20 | 21 | /// @notice Thrown when the recovered signer does not equal the claimedSigner 22 | error InvalidSigner(); 23 | 24 | /// @notice Thrown when validating an inputted signature that is stale 25 | /// @param signatureDeadline The timestamp at which a signature is no longer valid 26 | error SignatureExpired(uint256 signatureDeadline); 27 | 28 | /* 29 | * Allowance Transfer 30 | */ 31 | 32 | /// @notice The permit data for a token 33 | struct PermitDetails { 34 | // ERC20 token address 35 | address token; 36 | // the maximum amount allowed to spend 37 | uint160 amount; 38 | // timestamp at which a spender's token allowances become invalid 39 | uint48 expiration; 40 | // an incrementing value indexed per owner,token,and spender for each signature 41 | uint48 nonce; 42 | } 43 | 44 | /// @notice The permit message signed for a single token allownce 45 | struct PermitSingle { 46 | // the permit data for a single token alownce 47 | PermitDetails details; 48 | // address permissioned on the allowed tokens 49 | address spender; 50 | // deadline on the permit signature 51 | uint256 sigDeadline; 52 | } 53 | 54 | /// @notice Returns the domain separator for the current chain. 55 | /// @dev Uses cached version if chainid and address are unchanged from construction. 56 | function DOMAIN_SEPARATOR() external view returns (bytes32); 57 | 58 | /// @notice A mapping from owner address to token address to spender address to PackedAllowance struct, which contains details and conditions of the approval. 59 | /// @notice The mapping is indexed in the above order see: allowance[ownerAddress][tokenAddress][spenderAddress] 60 | /// @dev The packed slot holds the allowed amount, expiration at which the allowed amount is no longer valid, and current nonce thats updated on any signature based approvals. 61 | function allowance(address user, address token, address spender) external view returns (uint160 amount, uint48 expiration, uint48 nonce); 62 | 63 | /// @notice Permit a spender to a given amount of the owners token via the owner's EIP-712 signature 64 | /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce 65 | /// @param owner The owner of the tokens being approved 66 | /// @param permitSingle Data signed over by the owner specifying the terms of approval 67 | /// @param signature The owner's signature over the permit data 68 | function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external; 69 | 70 | /// @notice Transfer approved tokens from one address to another 71 | /// @param from The address to transfer from 72 | /// @param to The address of the recipient 73 | /// @param amount The amount of the token to transfer 74 | /// @param token The token address to transfer 75 | /// @dev Requires the from address to have approved at least the desired amount 76 | /// of tokens to msg.sender. 77 | function transferFrom(address from, address to, uint160 amount, address token) external; 78 | 79 | /// @notice Approves the spender to use up to amount of the specified token up until the expiration 80 | /// @param token The token to approve 81 | /// @param spender The spender address to approve 82 | /// @param amount The approved amount of the token 83 | /// @param expiration The timestamp at which the approval is no longer valid 84 | /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve 85 | /// @dev Setting amount to type(uint160).max sets an unlimited approval 86 | function approve(address token, address spender, uint160 amount, uint48 expiration) external; 87 | 88 | /* 89 | * Signature Transfer 90 | */ 91 | 92 | /// @notice The token and amount details for a transfer signed in the permit transfer signature 93 | struct TokenPermissions { 94 | // ERC20 token address 95 | address token; 96 | // the maximum amount that can be spent 97 | uint256 amount; 98 | } 99 | 100 | /// @notice The signed permit message for a single token transfer 101 | struct PermitTransferFrom { 102 | TokenPermissions permitted; 103 | // a unique value for every token owner's signature to prevent signature replays 104 | uint256 nonce; 105 | // deadline on the permit signature 106 | uint256 deadline; 107 | } 108 | 109 | /// @notice Specifies the recipient address and amount for batched transfers. 110 | /// @dev Recipients and amounts correspond to the index of the signed token permissions array. 111 | /// @dev Reverts if the requested amount is greater than the permitted signed amount. 112 | struct SignatureTransferDetails { 113 | // recipient address 114 | address to; 115 | // spender requested amount 116 | uint256 requestedAmount; 117 | } 118 | 119 | /// @notice Transfers a token using a signed permit message 120 | /// @dev Reverts if the requested amount is greater than the permitted signed amount 121 | /// @param permit The permit data signed over by the owner 122 | /// @param owner The owner of the tokens to transfer 123 | /// @param transferDetails The spender's requested transfer details for the permitted token 124 | /// @param signature The signature to verify 125 | function permitTransferFrom( 126 | PermitTransferFrom memory permit, 127 | SignatureTransferDetails calldata transferDetails, 128 | address owner, 129 | bytes calldata signature 130 | ) external; 131 | } 132 | -------------------------------------------------------------------------------- /contracts/interfaces/ILimitOrderSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { LimitOrder } from "../libraries/LimitOrder.sol"; 5 | 6 | /// @title ILimitOrderSwap Interface 7 | /// @author imToken Labs 8 | /// @notice Interface for a limit order swap contract. 9 | /// @dev This interface defines functions and events related to executing and managing limit orders. 10 | interface ILimitOrderSwap { 11 | /// @notice Struct containing parameters for the taker. 12 | /// @dev This struct encapsulates the parameters necessary for a taker to fill a limit order. 13 | struct TakerParams { 14 | uint256 takerTokenAmount; // Amount of tokens taken by the taker. 15 | uint256 makerTokenAmount; // Amount of tokens provided by the maker. 16 | address recipient; // Address to receive the tokens. 17 | bytes extraAction; // Additional action to be performed. 18 | bytes takerTokenPermit; // Permit for spending taker's tokens. 19 | } 20 | 21 | /// @notice Emitted when the fee collector address is updated. 22 | /// @param newFeeCollector The address of the new fee collector. 23 | event SetFeeCollector(address newFeeCollector); 24 | 25 | /// @notice Emitted when a limit order is successfully filled. 26 | /// @param orderHash The hash of the limit order. 27 | /// @param taker The address of the taker filling the order. 28 | /// @param maker The address of the maker who created the order. 29 | /// @param takerToken The address of the token taken by the taker. 30 | /// @param takerTokenFilledAmount The amount of taker tokens filled. 31 | /// @param makerToken The address of the token received by the maker. 32 | /// @param makerTokenSettleAmount The amount of maker tokens settled. 33 | /// @param fee The fee amount paid for the order. 34 | /// @param recipient The address receiving the tokens. 35 | event LimitOrderFilled( 36 | bytes32 indexed orderHash, 37 | address indexed taker, 38 | address indexed maker, 39 | address takerToken, 40 | uint256 takerTokenFilledAmount, 41 | address makerToken, 42 | uint256 makerTokenSettleAmount, 43 | uint256 fee, 44 | address recipient 45 | ); 46 | 47 | /// @notice Emitted when an order is canceled. 48 | /// @param orderHash The hash of the canceled order. 49 | /// @param maker The address of the maker who canceled the order. 50 | event OrderCanceled(bytes32 orderHash, address maker); 51 | 52 | /// @notice Error to be thrown when an order has expired. 53 | /// @dev Thrown when attempting to fill an order that has already expired. 54 | error ExpiredOrder(); 55 | 56 | /// @notice Error to be thrown when an order is canceled. 57 | /// @dev Thrown when attempting to fill or interact with a canceled order. 58 | error CanceledOrder(); 59 | 60 | /// @notice Error to be thrown when an order is already filled. 61 | /// @dev Thrown when attempting to fill an order that has already been fully filled. 62 | error FilledOrder(); 63 | 64 | /// @notice Error to be thrown when an address is zero. 65 | /// @dev Thrown when an operation requires a non-zero address. 66 | error ZeroAddress(); 67 | 68 | /// @notice Error to be thrown when the taker token amount is zero. 69 | /// @dev Thrown when filling an order with zero taker token amount. 70 | error ZeroTakerTokenAmount(); 71 | 72 | /// @notice Error to be thrown when the maker token amount is zero. 73 | /// @dev Thrown when filling an order with zero maker token amount. 74 | error ZeroMakerTokenAmount(); 75 | 76 | /// @notice Error to be thrown when the taker spending amount is zero. 77 | /// @dev Thrown when an action requires a non-zero taker spending amount. 78 | error ZeroTakerSpendingAmount(); 79 | 80 | /// @notice Error to be thrown when the maker spending amount is zero. 81 | /// @dev Thrown when an action requires a non-zero maker spending amount. 82 | error ZeroMakerSpendingAmount(); 83 | 84 | /// @notice Error to be thrown when there are not enough tokens to fill the order. 85 | /// @dev Thrown when attempting to fill an order with insufficient tokens available. 86 | error NotEnoughForFill(); 87 | 88 | /// @notice Error to be thrown when the msg.value is invalid. 89 | /// @dev Thrown when an operation requires a specific msg.value that is not provided. 90 | error InvalidMsgValue(); 91 | 92 | /// @notice Error to be thrown when a signature is invalid. 93 | /// @dev Thrown when an operation requires a valid cryptographic signature that is not provided or is invalid. 94 | error InvalidSignature(); 95 | 96 | /// @notice Error to be thrown when the taker address is invalid. 97 | /// @dev Thrown when an operation requires a valid taker address that is not provided or is invalid. 98 | error InvalidTaker(); 99 | 100 | /// @notice Error to be thrown when the taking amount is invalid. 101 | /// @dev Thrown when an operation requires a valid taking amount that is not provided or is invalid. 102 | error InvalidTakingAmount(); 103 | 104 | /// @notice Error to be thrown when the parameters provided are invalid. 105 | /// @dev Thrown when an operation receives invalid parameters that prevent execution. 106 | error InvalidParams(); 107 | 108 | /// @notice Error to be thrown when the caller is not the maker of the order. 109 | /// @dev Thrown when an operation is attempted by an unauthorized caller who is not the maker of the order. 110 | error NotOrderMaker(); 111 | 112 | /// @notice Fills a limit order. 113 | /// @param order The limit order to be filled. 114 | /// @param makerSignature The signature of the maker authorizing the fill. 115 | /// @param takerParams The parameters specifying how the order should be filled by the taker. 116 | function fillLimitOrder(LimitOrder calldata order, bytes calldata makerSignature, TakerParams calldata takerParams) external payable; 117 | 118 | /// @notice Fills a limit order fully or cancels it. 119 | /// @param order The limit order to be filled or canceled. 120 | /// @param makerSignature The signature of the maker authorizing the fill or cancel. 121 | /// @param takerParams The parameters specifying how the order should be filled by the taker. 122 | function fillLimitOrderFullOrKill(LimitOrder calldata order, bytes calldata makerSignature, TakerParams calldata takerParams) external payable; 123 | 124 | /// @notice Fills a group of limit orders atomically. 125 | /// @param orders The array of limit orders to be filled. 126 | /// @param makerSignatures The array of signatures of the makers authorizing the fills. 127 | /// @param makerTokenAmounts The array of amounts of tokens provided by the makers. 128 | /// @param profitTokens The array of addresses of tokens used for profit sharing. 129 | function fillLimitOrderGroup( 130 | LimitOrder[] calldata orders, 131 | bytes[] calldata makerSignatures, 132 | uint256[] calldata makerTokenAmounts, 133 | address[] calldata profitTokens 134 | ) external payable; 135 | 136 | /// @notice Cancels a limit order. 137 | /// @param order The limit order to be canceled. 138 | function cancelOrder(LimitOrder calldata order) external; 139 | 140 | /// @notice Checks if an order is canceled. 141 | /// @param orderHash The hash of the order to check. 142 | /// @return True if the order is canceled, otherwise false. 143 | function isOrderCanceled(bytes32 orderHash) external view returns (bool); 144 | } 145 | --------------------------------------------------------------------------------