diff --git a/contracts/SignalBuyContract.sol b/contracts/SignalBuyContract.sol new file mode 100644 index 00000000..6798ee5b --- /dev/null +++ b/contracts/SignalBuyContract.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import { Math } from "@openzeppelin/contracts/math/Math.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import { ISignalBuyContract } from "./interfaces/ISignalBuyContract.sol"; +import { IWETH } from "./interfaces/IWETH.sol"; +import { Asset } from "./utils/Asset.sol"; +import { BaseLibEIP712 } from "./utils/BaseLibEIP712.sol"; +import { LibConstant } from "./utils/LibConstant.sol"; +import { LibSignalBuyContractOrderStorage } from "./utils/LibSignalBuyContractOrderStorage.sol"; +import { Ownable } from "./utils/Ownable.sol"; +import { Order, getOrderStructHash, Fill, getFillStructHash, AllowFill, getAllowFillStructHash } from "./utils/SignalBuyContractLibEIP712.sol"; +import { SignatureValidator } from "./utils/SignatureValidator.sol"; + +/// @title SignalBuy Contract +/// @notice Order can be filled as long as the provided dealerToken/userToken ratio is better than or equal to user's specfied dealerToken/userToken ratio. +/// @author imToken Labs +contract SignalBuyContract is ISignalBuyContract, BaseLibEIP712, SignatureValidator, ReentrancyGuard, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + using Asset for address; + + IWETH public immutable weth; + uint256 public immutable factorActivateDelay; + + // Below are the variables which consume storage slots. + address public coordinator; + address public feeCollector; + + // Factors + uint256 public factorsTimeLock; + uint16 public tokenlonFeeFactor = 0; + uint16 public pendingTokenlonFeeFactor; + + mapping(bytes32 => uint256) public filledAmount; + + /// @notice Emitted when allowing another account to spend assets + /// @param spender The address that is allowed to transfer tokens + event AllowTransfer(address indexed spender, address token); + + /// @notice Emitted when disallowing an account to spend assets + /// @param spender The address that is removed from allow list + event DisallowTransfer(address indexed spender, address token); + + /// @notice Emitted when ETH converted to WETH + /// @param amount The amount of converted ETH + event DepositETH(uint256 amount); + + constructor( + address _owner, + address _weth, + address _coordinator, + uint256 _factorActivateDelay, + address _feeCollector + ) Ownable(_owner) { + weth = IWETH(_weth); + coordinator = _coordinator; + factorActivateDelay = _factorActivateDelay; + feeCollector = _feeCollector; + } + + receive() external payable {} + + /// @notice Set allowance of tokens to an address + /// @notice Only owner can call + /// @param _tokenList The list of tokens + /// @param _spender The address that will be allowed + function setAllowance(address[] calldata _tokenList, address _spender) external onlyOwner { + for (uint256 i = 0; i < _tokenList.length; ++i) { + IERC20(_tokenList[i]).safeApprove(_spender, LibConstant.MAX_UINT); + + emit AllowTransfer(_spender, _tokenList[i]); + } + } + + /// @notice Clear allowance of tokens to an address + /// @notice Only owner can call + /// @param _tokenList The list of tokens + /// @param _spender The address that will be cleared + function closeAllowance(address[] calldata _tokenList, address _spender) external onlyOwner { + for (uint256 i = 0; i < _tokenList.length; ++i) { + IERC20(_tokenList[i]).safeApprove(_spender, 0); + + emit DisallowTransfer(_spender, _tokenList[i]); + } + } + + /// @notice Convert ETH in this contract to WETH + /// @notice Only owner can call + function depositETH() external onlyOwner { + uint256 balance = address(this).balance; + if (balance > 0) { + weth.deposit{ value: balance }(); + + emit DepositETH(balance); + } + } + + /// @notice Only owner can call + /// @param _newCoordinator The new address of coordinator + function upgradeCoordinator(address _newCoordinator) external onlyOwner { + require(_newCoordinator != address(0), "SignalBuyContract: coordinator can not be zero address"); + coordinator = _newCoordinator; + + emit UpgradeCoordinator(_newCoordinator); + } + + /// @notice Only owner can call + /// @param _tokenlonFeeFactor The new fee factor for user + function setFactors(uint16 _tokenlonFeeFactor) external onlyOwner { + require(_tokenlonFeeFactor <= LibConstant.BPS_MAX, "SignalBuyContract: Invalid user fee factor"); + + pendingTokenlonFeeFactor = _tokenlonFeeFactor; + + factorsTimeLock = block.timestamp + factorActivateDelay; + } + + /// @notice Only owner can call + function activateFactors() external onlyOwner { + require(factorsTimeLock != 0, "SignalBuyContract: no pending fee factors"); + require(block.timestamp >= factorsTimeLock, "SignalBuyContract: fee factors timelocked"); + factorsTimeLock = 0; + tokenlonFeeFactor = pendingTokenlonFeeFactor; + pendingTokenlonFeeFactor = 0; + + emit FactorsUpdated(tokenlonFeeFactor); + } + + /// @notice Only owner can call + /// @param _newFeeCollector The new address of fee collector + function setFeeCollector(address _newFeeCollector) external onlyOwner { + require(_newFeeCollector != address(0), "SignalBuyContract: fee collector can not be zero address"); + feeCollector = _newFeeCollector; + + emit SetFeeCollector(_newFeeCollector); + } + + /// @inheritdoc ISignalBuyContract + function fillSignalBuy( + Order calldata _order, + bytes calldata _orderUserSig, + TraderParams calldata _params, + CoordinatorParams calldata _crdParams + ) external payable override nonReentrant returns (uint256, uint256) { + bytes32 orderHash = getEIP712Hash(getOrderStructHash(_order)); + + _validateOrder(_order, orderHash, _orderUserSig); + bytes32 allowFillHash = _validateFillPermission(orderHash, _params.dealerTokenAmount, _params.dealer, _crdParams); + _validateOrderTaker(_order, _params.dealer); + + // Check gas fee factor and dealer strategy fee factor do not exceed limit + require( + (_params.gasFeeFactor <= LibConstant.BPS_MAX) && + (_params.dealerStrategyFeeFactor <= LibConstant.BPS_MAX) && + (_params.gasFeeFactor + _params.dealerStrategyFeeFactor <= LibConstant.BPS_MAX - tokenlonFeeFactor), + "SignalBuyContract: Invalid dealer fee factor" + ); + + { + Fill memory fill = Fill({ + orderHash: orderHash, + dealer: _params.dealer, + recipient: _params.recipient, + userTokenAmount: _params.userTokenAmount, + dealerTokenAmount: _params.dealerTokenAmount, + dealerSalt: _params.salt, + expiry: _params.expiry + }); + _validateTraderFill(fill, _params.dealerSig); + } + + (uint256 userTokenAmount, uint256 remainingUserTokenAmount) = _quoteOrderFromUserToken(_order, orderHash, _params.userTokenAmount); + // Calculate dealerTokenAmount according to the provided dealerToken/userToken ratio + uint256 dealerTokenAmount = userTokenAmount.mul(_params.dealerTokenAmount).div(_params.userTokenAmount); + // Calculate minimum dealerTokenAmount according to the offer's dealerToken/userToken ratio + uint256 minDealerTokenAmount = userTokenAmount.mul(_order.minDealerTokenAmount).div(_order.userTokenAmount); + + _settleForTrader( + TraderSettlement({ + orderHash: orderHash, + allowFillHash: allowFillHash, + trader: _params.dealer, + recipient: _params.recipient, + user: _order.user, + userToken: _order.userToken, + dealerToken: _order.dealerToken, + userTokenAmount: userTokenAmount, + dealerTokenAmount: dealerTokenAmount, + minDealerTokenAmount: minDealerTokenAmount, + remainingUserTokenAmount: remainingUserTokenAmount, + gasFeeFactor: _params.gasFeeFactor, + dealerStrategyFeeFactor: _params.dealerStrategyFeeFactor + }) + ); + + _recordUserTokenFilled(orderHash, userTokenAmount); + + return (dealerTokenAmount, userTokenAmount); + } + + function _validateTraderFill(Fill memory _fill, bytes memory _fillTakerSig) internal { + require(_fill.expiry > uint64(block.timestamp), "SignalBuyContract: Fill request is expired"); + require(_fill.recipient != address(0), "SignalBuyContract: recipient can not be zero address"); + + bytes32 fillHash = getEIP712Hash(getFillStructHash(_fill)); + require(!LibSignalBuyContractOrderStorage.getStorage().fillSeen[fillHash], "SignalBuyContract: Fill seen before"); + require(isValidSignature(_fill.dealer, fillHash, bytes(""), _fillTakerSig), "SignalBuyContract: Fill is not signed by dealer"); + + // Set fill seen to avoid replay attack. + LibSignalBuyContractOrderStorage.getStorage().fillSeen[fillHash] = true; + } + + function _validateFillPermission( + bytes32 _orderHash, + uint256 _fillAmount, + address _executor, + CoordinatorParams memory _crdParams + ) internal returns (bytes32) { + require(_crdParams.expiry > uint64(block.timestamp), "SignalBuyContract: Fill permission is expired"); + + bytes32 allowFillHash = getEIP712Hash( + getAllowFillStructHash( + AllowFill({ orderHash: _orderHash, executor: _executor, fillAmount: _fillAmount, salt: _crdParams.salt, expiry: _crdParams.expiry }) + ) + ); + require(!LibSignalBuyContractOrderStorage.getStorage().allowFillSeen[allowFillHash], "SignalBuyContract: AllowFill seen before"); + require(isValidSignature(coordinator, allowFillHash, bytes(""), _crdParams.sig), "SignalBuyContract: AllowFill is not signed by coordinator"); + + // Set allow fill seen to avoid replay attack + LibSignalBuyContractOrderStorage.getStorage().allowFillSeen[allowFillHash] = true; + + return allowFillHash; + } + + struct TraderSettlement { + bytes32 orderHash; + bytes32 allowFillHash; + address trader; + address recipient; + address user; + IERC20 userToken; + IERC20 dealerToken; + uint256 userTokenAmount; + uint256 dealerTokenAmount; + uint256 minDealerTokenAmount; + uint256 remainingUserTokenAmount; + uint16 gasFeeFactor; + uint16 dealerStrategyFeeFactor; + } + + function _settleForTrader(TraderSettlement memory _settlement) internal { + // memory cache + address _feeCollector = feeCollector; + + // Calculate user fee (user receives dealer token so fee is charged in dealer token) + // 1. Fee for Tokenlon + uint256 tokenlonFee = _mulFactor(_settlement.dealerTokenAmount, tokenlonFeeFactor); + // 2. Fee for SignalBuy, including gas fee and strategy fee + uint256 dealerFee = _mulFactor(_settlement.dealerTokenAmount, _settlement.gasFeeFactor + _settlement.dealerStrategyFeeFactor); + uint256 dealerTokenForUserAndTokenlon = _settlement.dealerTokenAmount.sub(dealerFee); + uint256 dealerTokenForUser = dealerTokenForUserAndTokenlon.sub(tokenlonFee); + require(dealerTokenForUser >= _settlement.minDealerTokenAmount, "SignalBuyContract: dealer token amount not enough"); + + // trader -> user + address _weth = address(weth); // cache + if (address(_settlement.dealerToken).isETH()) { + if (msg.value > 0) { + // User wants ETH and dealer pays in ETH + require(msg.value == dealerTokenForUserAndTokenlon, "SignalBuyContract: mismatch dealer token (ETH) amount"); + } else { + // User wants ETH but dealer pays in WETH + IERC20(_weth).safeTransferFrom(_settlement.trader, address(this), dealerTokenForUserAndTokenlon); + weth.withdraw(dealerTokenForUserAndTokenlon); + } + // Send ETH to user + LibConstant.ETH_ADDRESS.transferTo(payable(_settlement.user), dealerTokenForUser); + } else if (address(_settlement.dealerToken) == _weth) { + if (msg.value > 0) { + // User wants WETH but dealer pays in ETH + require(msg.value == dealerTokenForUserAndTokenlon, "SignalBuyContract: mismatch dealer token (ETH) amount"); + weth.deposit{ value: dealerTokenForUserAndTokenlon }(); + weth.transfer(_settlement.user, dealerTokenForUser); + } else { + // User wants WETH and dealer pays in WETH + IERC20(_weth).safeTransferFrom(_settlement.trader, _settlement.user, dealerTokenForUser); + } + } else { + _settlement.dealerToken.safeTransferFrom(_settlement.trader, _settlement.user, dealerTokenForUser); + } + + // user -> recipient + _settlement.userToken.safeTransferFrom(_settlement.user, _settlement.recipient, _settlement.userTokenAmount); + + // Collect user fee (charged in dealer token) + if (tokenlonFee > 0) { + if (address(_settlement.dealerToken).isETH()) { + LibConstant.ETH_ADDRESS.transferTo(payable(_feeCollector), tokenlonFee); + } else if (address(_settlement.dealerToken) == _weth) { + if (msg.value > 0) { + weth.transfer(_feeCollector, tokenlonFee); + } else { + weth.transferFrom(_settlement.trader, _feeCollector, tokenlonFee); + } + } else { + _settlement.dealerToken.safeTransferFrom(_settlement.trader, _feeCollector, tokenlonFee); + } + } + + // bypass stack too deep error + _emitSignalBuyFilledByTrader( + SignalBuyFilledByTraderParams({ + orderHash: _settlement.orderHash, + user: _settlement.user, + dealer: _settlement.trader, + allowFillHash: _settlement.allowFillHash, + recipient: _settlement.recipient, + userToken: address(_settlement.userToken), + dealerToken: address(_settlement.dealerToken), + userTokenFilledAmount: _settlement.userTokenAmount, + dealerTokenFilledAmount: _settlement.dealerTokenAmount, + remainingUserTokenAmount: _settlement.remainingUserTokenAmount, + tokenlonFee: tokenlonFee, + dealerFee: dealerFee + }) + ); + } + + /// @inheritdoc ISignalBuyContract + function cancelSignalBuy(Order calldata _order, bytes calldata _cancelOrderUserSig) external override nonReentrant { + require(_order.expiry > uint64(block.timestamp), "SignalBuyContract: Order is expired"); + bytes32 orderHash = getEIP712Hash(getOrderStructHash(_order)); + bool isCancelled = LibSignalBuyContractOrderStorage.getStorage().orderHashToCancelled[orderHash]; + require(!isCancelled, "SignalBuyContract: Order is cancelled already"); + { + Order memory cancelledOrder = _order; + cancelledOrder.minDealerTokenAmount = 0; + + bytes32 cancelledOrderHash = getEIP712Hash(getOrderStructHash(cancelledOrder)); + require( + isValidSignature(_order.user, cancelledOrderHash, bytes(""), _cancelOrderUserSig), + "SignalBuyContract: Cancel request is not signed by user" + ); + } + + // Set cancelled state to storage + LibSignalBuyContractOrderStorage.getStorage().orderHashToCancelled[orderHash] = true; + emit OrderCancelled(orderHash, _order.user); + } + + /* order utils */ + + function _validateOrder( + Order memory _order, + bytes32 _orderHash, + bytes memory _orderUserSig + ) internal view { + require(_order.expiry > uint64(block.timestamp), "SignalBuyContract: Order is expired"); + bool isCancelled = LibSignalBuyContractOrderStorage.getStorage().orderHashToCancelled[_orderHash]; + require(!isCancelled, "SignalBuyContract: Order is cancelled"); + + require(isValidSignature(_order.user, _orderHash, bytes(""), _orderUserSig), "SignalBuyContract: Order is not signed by user"); + } + + function _validateOrderTaker(Order memory _order, address _dealer) internal pure { + if (_order.dealer != address(0)) { + require(_order.dealer == _dealer, "SignalBuyContract: Order cannot be filled by this dealer"); + } + } + + function _quoteOrderFromUserToken( + Order memory _order, + bytes32 _orderHash, + uint256 _userTokenAmount + ) internal view returns (uint256, uint256) { + uint256 userTokenFilledAmount = LibSignalBuyContractOrderStorage.getStorage().orderHashToUserTokenFilledAmount[_orderHash]; + + require(userTokenFilledAmount < _order.userTokenAmount, "SignalBuyContract: Order is filled"); + + uint256 userTokenFillableAmount = _order.userTokenAmount.sub(userTokenFilledAmount); + uint256 userTokenQuota = Math.min(_userTokenAmount, userTokenFillableAmount); + uint256 remainingAfterFill = userTokenFillableAmount.sub(userTokenQuota); + + require(userTokenQuota != 0, "SignalBuyContract: zero token amount"); + return (userTokenQuota, remainingAfterFill); + } + + function _recordUserTokenFilled(bytes32 _orderHash, uint256 _userTokenAmount) internal { + LibSignalBuyContractOrderStorage.Storage storage stor = LibSignalBuyContractOrderStorage.getStorage(); + uint256 userTokenFilledAmount = stor.orderHashToUserTokenFilledAmount[_orderHash]; + stor.orderHashToUserTokenFilledAmount[_orderHash] = userTokenFilledAmount.add(_userTokenAmount); + } + + /* math utils */ + + function _mulFactor(uint256 amount, uint256 factor) internal pure returns (uint256) { + return amount.mul(factor).div(LibConstant.BPS_MAX); + } + + /* event utils */ + + struct SignalBuyFilledByTraderParams { + bytes32 orderHash; + address user; + address dealer; + bytes32 allowFillHash; + address recipient; + address userToken; + address dealerToken; + uint256 userTokenFilledAmount; + uint256 dealerTokenFilledAmount; + uint256 remainingUserTokenAmount; + uint256 tokenlonFee; + uint256 dealerFee; + } + + function _emitSignalBuyFilledByTrader(SignalBuyFilledByTraderParams memory _params) internal { + emit SignalBuyFilledByTrader( + _params.orderHash, + _params.user, + _params.dealer, + _params.allowFillHash, + _params.recipient, + FillReceipt({ + userToken: _params.userToken, + dealerToken: _params.dealerToken, + userTokenFilledAmount: _params.userTokenFilledAmount, + dealerTokenFilledAmount: _params.dealerTokenFilledAmount, + remainingUserTokenAmount: _params.remainingUserTokenAmount, + tokenlonFee: _params.tokenlonFee, + dealerFee: _params.dealerFee + }) + ); + } +} diff --git a/contracts/interfaces/ISignalBuyContract.sol b/contracts/interfaces/ISignalBuyContract.sol new file mode 100644 index 00000000..d4095c33 --- /dev/null +++ b/contracts/interfaces/ISignalBuyContract.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0; +pragma abicoder v2; + +import { Order } from "../utils/SignalBuyContractLibEIP712.sol"; + +/// @title ISignalBuyContract Interface +/// @author imToken Labs +interface ISignalBuyContract { + /// @notice Emitted when coordinator address is updated + /// @param newCoordinator The address of the new coordinator + event UpgradeCoordinator(address newCoordinator); + + /// @notice Emitted when fee factors are updated + /// @param userFeeFactor The new fee factor for user + event FactorsUpdated(uint16 userFeeFactor); + + /// @notice Emitted when fee collector address is updated + /// @param newFeeCollector The address of the new fee collector + event SetFeeCollector(address newFeeCollector); + + /// @notice Emitted when an order is filled by dealer + /// @param orderHash The EIP-712 hash of the target order + /// @param user The address of the user + /// @param dealer The address of the dealer + /// @param allowFillHash The EIP-712 hash of the fill permit granted by coordinator + /// @param recipient The address of the recipient which will receive tokens from user + /// @param fillReceipt Contains details of this single fill + event SignalBuyFilledByTrader( + bytes32 indexed orderHash, + address indexed user, + address indexed dealer, + bytes32 allowFillHash, + address recipient, + FillReceipt fillReceipt + ); + + /// @notice Emitted when order is cancelled + /// @param orderHash The EIP-712 hash of the target order + /// @param user The address of the user + event OrderCancelled(bytes32 orderHash, address user); + + struct FillReceipt { + address userToken; + address dealerToken; + uint256 userTokenFilledAmount; + uint256 dealerTokenFilledAmount; + uint256 remainingUserTokenAmount; + uint256 tokenlonFee; + uint256 dealerFee; + } + + struct CoordinatorParams { + bytes sig; + uint256 salt; + uint64 expiry; + } + + struct TraderParams { + address dealer; + address recipient; + uint256 userTokenAmount; + uint256 dealerTokenAmount; + uint16 gasFeeFactor; + uint16 dealerStrategyFeeFactor; + uint256 salt; + uint64 expiry; + bytes dealerSig; + } + + /// @notice Fill an order by a trader + /// @notice Only user proxy can call + /// @param _order The order that is going to be filled + /// @param _orderUserSig The signature of the order from user + /// @param _params Trader specific filling parameters + /// @param _crdParams Contains details of the fill permit + function fillSignalBuy( + Order calldata _order, + bytes calldata _orderUserSig, + TraderParams calldata _params, + CoordinatorParams calldata _crdParams + ) external payable returns (uint256, uint256); + + /// @notice Cancel an order + /// @notice Only user proxy can call + /// @param _order The order that is going to be cancelled + /// @param _cancelUserSig The cancelling signature signed by user + function cancelSignalBuy(Order calldata _order, bytes calldata _cancelUserSig) external; +} diff --git a/contracts/utils/LibSignalBuyContractOrderStorage.sol b/contracts/utils/LibSignalBuyContractOrderStorage.sol new file mode 100644 index 00000000..b7b7a5be --- /dev/null +++ b/contracts/utils/LibSignalBuyContractOrderStorage.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; + +library LibSignalBuyContractOrderStorage { + bytes32 private constant STORAGE_SLOT = 0x1360fb69f36f46eb45cf50ca3a6184b38e4ef3bde9e5aff734dccec027d7b9f7; + /// @dev Storage bucket for this feature. + struct Storage { + // Has the fill been executed. + mapping(bytes32 => bool) fillSeen; + // Has the allowFill been executed. + mapping(bytes32 => bool) allowFillSeen; + // How much maker token has been filled in order. + mapping(bytes32 => uint256) orderHashToUserTokenFilledAmount; + // Whether order is cancelled or not. + mapping(bytes32 => bool) orderHashToCancelled; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + assert(STORAGE_SLOT == bytes32(uint256(keccak256("signalbuycontract.order.storage")) - 1)); + + // Dip into assembly to change the slot pointed to by the local + // variable `stor`. + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { + stor.slot := STORAGE_SLOT + } + } +} diff --git a/contracts/utils/SignalBuyContractLibEIP712.sol b/contracts/utils/SignalBuyContractLibEIP712.sol new file mode 100644 index 00000000..d0b36676 --- /dev/null +++ b/contracts/utils/SignalBuyContractLibEIP712.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +struct Order { + IERC20 userToken; + IERC20 dealerToken; + uint256 userTokenAmount; + uint256 minDealerTokenAmount; + address user; + address dealer; + uint256 salt; + uint64 expiry; +} + +string constant ORDER_TYPESTRING = "Order(address userToken,address dealerToken,uint256 userTokenAmount,uint256 minDealerTokenAmount,address user,address dealer,uint256 salt,uint64 expiry)"; + +bytes32 constant ORDER_TYPEHASH = keccak256(bytes(ORDER_TYPESTRING)); + +// solhint-disable-next-line func-visibility +function getOrderStructHash(Order memory _order) pure returns (bytes32) { + return + keccak256( + abi.encode( + ORDER_TYPEHASH, + address(_order.userToken), + address(_order.dealerToken), + _order.userTokenAmount, + _order.minDealerTokenAmount, + _order.user, + _order.dealer, + _order.salt, + _order.expiry + ) + ); +} + +struct Fill { + bytes32 orderHash; // EIP712 hash + address dealer; + address recipient; + uint256 userTokenAmount; + uint256 dealerTokenAmount; + uint256 dealerSalt; + uint64 expiry; +} + +string constant FILL_TYPESTRING = "Fill(bytes32 orderHash,address dealer,address recipient,uint256 userTokenAmount,uint256 dealerTokenAmount,uint256 dealerSalt,uint64 expiry)"; + +bytes32 constant FILL_TYPEHASH = keccak256(bytes(FILL_TYPESTRING)); + +// solhint-disable-next-line func-visibility +function getFillStructHash(Fill memory _fill) pure returns (bytes32) { + return + keccak256( + abi.encode( + FILL_TYPEHASH, + _fill.orderHash, + _fill.dealer, + _fill.recipient, + _fill.userTokenAmount, + _fill.dealerTokenAmount, + _fill.dealerSalt, + _fill.expiry + ) + ); +} + +struct AllowFill { + bytes32 orderHash; // EIP712 hash + address executor; + uint256 fillAmount; + uint256 salt; + uint64 expiry; +} + +string constant ALLOW_FILL_TYPESTRING = "AllowFill(bytes32 orderHash,address executor,uint256 fillAmount,uint256 salt,uint64 expiry)"; + +bytes32 constant ALLOW_FILL_TYPEHASH = keccak256(bytes(ALLOW_FILL_TYPESTRING)); + +// solhint-disable-next-line func-visibility +function getAllowFillStructHash(AllowFill memory _allowFill) pure returns (bytes32) { + return keccak256(abi.encode(ALLOW_FILL_TYPEHASH, _allowFill.orderHash, _allowFill.executor, _allowFill.fillAmount, _allowFill.salt, _allowFill.expiry)); +} diff --git a/test/forkMainnet/SignalBuyContract.t.sol b/test/forkMainnet/SignalBuyContract.t.sol new file mode 100644 index 00000000..cb7ec435 --- /dev/null +++ b/test/forkMainnet/SignalBuyContract.t.sol @@ -0,0 +1,1380 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "contracts/SignalBuyContract.sol"; +import "contracts/interfaces/ISignalBuyContract.sol"; +import "contracts/utils/SignatureValidator.sol"; +import { Order, getOrderStructHash, Fill, getFillStructHash, AllowFill, getAllowFillStructHash } from "contracts/utils/SignalBuyContractLibEIP712.sol"; +import "contracts/utils/LibConstant.sol"; + +import "test/mocks/MockERC1271Wallet.sol"; +import "test/utils/BalanceSnapshot.sol"; +import "test/utils/BalanceUtil.sol"; +import { computeMainnetEIP712DomainSeparator, getEIP712Hash } from "test/utils/Sig.sol"; + +contract SignalBuyContractTest is BalanceUtil { + using SafeMath for uint256; + using SafeERC20 for IERC20; + using BalanceSnapshot for BalanceSnapshot.Snapshot; + + event SignalBuyFilledByTrader( + bytes32 indexed orderHash, + address indexed user, + address indexed dealer, + bytes32 allowFillHash, + address recipient, + ISignalBuyContract.FillReceipt fillReceipt + ); + + uint256 dealerPrivateKey = uint256(1); + uint256 userPrivateKey = uint256(2); + uint256 coordinatorPrivateKey = uint256(3); + + address dealer = vm.addr(dealerPrivateKey); + address user = vm.addr(userPrivateKey); + address coordinator = vm.addr(coordinatorPrivateKey); + address owner = makeAddr("owner"); + address feeCollector = makeAddr("feeCollector"); + address receiver = makeAddr("receiver"); + MockERC1271Wallet mockERC1271Wallet = new MockERC1271Wallet(dealer); + address[] wallet = [dealer, user, coordinator, address(mockERC1271Wallet)]; + address[] allowanceAddrs; + + address[] DEFAULT_AMM_PATH; + Order DEFAULT_ORDER; + bytes32 DEFAULT_ORDER_HASH; + bytes DEFAULT_ORDER_MAKER_SIG; + Fill DEFAULT_FILL; + AllowFill DEFAULT_ALLOW_FILL; + uint16 DEFAULT_GAS_FEE_FACTOR = 0; + uint16 DEFAULT_PIONEX_STRATEGY_FEE_FACTOR = 0; + ISignalBuyContract.TraderParams DEFAULT_TRADER_PARAMS; + ISignalBuyContract.CoordinatorParams DEFAULT_CRD_PARAMS; + + SignalBuyContract signalBuyContract; + uint64 DEADLINE = uint64(block.timestamp + 2 days); + uint256 FACTORSDEALY = 12 hours; + + // effectively a "beforeEach" block + function setUp() public { + // Setup + signalBuyContract = new SignalBuyContract(owner, address(weth), coordinator, FACTORSDEALY, feeCollector); + + DEFAULT_AMM_PATH = [address(dai), address(usdt)]; + allowanceAddrs = DEFAULT_AMM_PATH; + + // Default params + DEFAULT_ORDER = Order( + dai, // userToken + usdt, // dealerToken + 100 * 1e18, // userTokenAmount + 90 * 1e6, // minDealerTokenAmount + user, // user + address(0), // dealer + uint256(1001), // salt + DEADLINE // expiry + ); + DEFAULT_ORDER_HASH = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(DEFAULT_ORDER)); + DEFAULT_ORDER_MAKER_SIG = _signOrder(userPrivateKey, DEFAULT_ORDER, SignatureValidator.SignatureType.EIP712); + DEFAULT_FILL = Fill(DEFAULT_ORDER_HASH, dealer, receiver, DEFAULT_ORDER.userTokenAmount, DEFAULT_ORDER.minDealerTokenAmount, uint256(1002), DEADLINE); + DEFAULT_TRADER_PARAMS = ISignalBuyContract.TraderParams( + dealer, // dealer + receiver, // recipient + DEFAULT_FILL.userTokenAmount, // userTokenAmount + DEFAULT_FILL.dealerTokenAmount, // dealerTokenAmount + DEFAULT_GAS_FEE_FACTOR, // gas fee factor + DEFAULT_PIONEX_STRATEGY_FEE_FACTOR, // dealer strategy fee factor + DEFAULT_FILL.dealerSalt, // salt + DEADLINE, // expiry + _signFill(dealerPrivateKey, DEFAULT_FILL, SignatureValidator.SignatureType.EIP712) // dealerSig + ); + DEFAULT_ALLOW_FILL = AllowFill( + DEFAULT_ORDER_HASH, // orderHash + dealer, // executor + DEFAULT_FILL.dealerTokenAmount, // fillAmount + uint256(1003), // salt + DEADLINE // expiry + ); + DEFAULT_CRD_PARAMS = ISignalBuyContract.CoordinatorParams( + _signAllowFill(coordinatorPrivateKey, DEFAULT_ALLOW_FILL, SignatureValidator.SignatureType.EIP712), + DEFAULT_ALLOW_FILL.salt, + DEFAULT_ALLOW_FILL.expiry + ); + + // Deal 100 ETH to each account + dealWallet(wallet, 100 ether); + // Set token balance and approve + tokens = [weth, usdt, dai]; + setEOABalanceAndApprove(dealer, tokens, 10000); + setEOABalanceAndApprove(user, tokens, 10000); + setEOABalanceAndApprove(address(mockERC1271Wallet), tokens, 10000); + + // Label addresses for easier debugging + vm.label(dealer, "SignalBuy"); + vm.label(user, "User"); + vm.label(coordinator, "Coordinator"); + vm.label(receiver, "Receiver"); + vm.label(feeCollector, "FeeCollector"); + vm.label(address(this), "TestingContract"); + vm.label(address(signalBuyContract), "SignalBuyContract"); + vm.label(address(mockERC1271Wallet), "MockERC1271Wallet"); + } + + /********************************* + * Test: setup * + *********************************/ + + function testSetupSignalBuy() public { + assertEq(signalBuyContract.owner(), owner); + assertEq(signalBuyContract.coordinator(), coordinator); + assertEq(address(signalBuyContract.weth()), address(weth)); + + assertEq(uint256(signalBuyContract.tokenlonFeeFactor()), 0); + } + + /********************************* + * Test: transferOwnership * + *********************************/ + + function testCannotTransferOwnershipByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.nominateNewOwner(dealer); + } + + function testCannotAcceptOwnershipIfNotNominated() public { + vm.expectRevert("not nominated"); + vm.prank(dealer); + signalBuyContract.acceptOwnership(); + } + + function testTransferOwnership() public { + vm.prank(owner, owner); + signalBuyContract.nominateNewOwner(dealer); + vm.prank(dealer); + signalBuyContract.acceptOwnership(); + assertEq(signalBuyContract.owner(), dealer); + } + + /********************************* + * Test: upgradeCoordinator * + *********************************/ + + function testCannotUpgradeCoordinatorByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.upgradeCoordinator(dealer); + } + + function testCannotUpgradeCoordinatorToZeroAddr() public { + vm.expectRevert("SignalBuyContract: coordinator can not be zero address"); + vm.prank(owner, owner); + signalBuyContract.upgradeCoordinator(address(0)); + } + + function testUpgradeCoordinator() public { + vm.prank(owner, owner); + signalBuyContract.upgradeCoordinator(dealer); + assertEq(address(signalBuyContract.coordinator()), dealer); + } + + /********************************* + * Test: set/close allowance * + *********************************/ + + function testCannotSetAllowanceByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.setAllowance(allowanceAddrs, address(receiver)); + } + + function testCannotCloseAllowanceByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.closeAllowance(allowanceAddrs, address(receiver)); + } + + function testSetAndCloseAllowance() public { + // Set allowance + vm.prank(owner, owner); + signalBuyContract.setAllowance(allowanceAddrs, address(receiver)); + assertEq(usdt.allowance(address(signalBuyContract), address(receiver)), LibConstant.MAX_UINT); + assertEq(dai.allowance(address(signalBuyContract), address(receiver)), LibConstant.MAX_UINT); + + // Close allowance + vm.prank(owner, owner); + signalBuyContract.closeAllowance(allowanceAddrs, address(receiver)); + assertEq(usdt.allowance(address(signalBuyContract), address(receiver)), 0); + assertEq(dai.allowance(address(signalBuyContract), address(receiver)), 0); + } + + /********************************* + * Test: depoitETH * + *********************************/ + + function testCannotDepositETHByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.depositETH(); + } + + function testDepositETH() public { + // Send ether to limit order contract + uint256 amount = 1234 ether; + deal(address(signalBuyContract), amount); + vm.prank(owner, owner); + signalBuyContract.depositETH(); + assertEq(weth.balanceOf(address(signalBuyContract)), amount); + } + + /********************************* + * Test: setFactors * + *********************************/ + + function testCannotSetFactorsIfLargerThanBpsMax() public { + vm.expectRevert("SignalBuyContract: Invalid user fee factor"); + vm.prank(owner, owner); + signalBuyContract.setFactors(LibConstant.BPS_MAX + 1); + } + + function testSetFactors() public { + vm.startPrank(owner, owner); + signalBuyContract.setFactors(1); + // fee factors should stay same before new ones activate + assertEq(uint256(signalBuyContract.tokenlonFeeFactor()), 0); + vm.warp(block.timestamp + signalBuyContract.factorActivateDelay()); + + // fee factors should be updated now + signalBuyContract.activateFactors(); + vm.stopPrank(); + assertEq(uint256(signalBuyContract.tokenlonFeeFactor()), 1); + } + + /********************************* + * Test: setFeeCollector * + *********************************/ + + function testCannotSetFeeCollectorByNotOwner() public { + vm.expectRevert("not owner"); + vm.prank(dealer); + signalBuyContract.setFeeCollector(feeCollector); + } + + function testCannotSetFeeCollectorToZeroAddr() public { + vm.expectRevert("SignalBuyContract: fee collector can not be zero address"); + vm.prank(owner, owner); + signalBuyContract.setFeeCollector(address(0)); + } + + function testSetFeeCollector() public { + vm.prank(owner, owner); + signalBuyContract.setFeeCollector(dealer); + assertEq(address(signalBuyContract.feeCollector()), dealer); + } + + /********************************* + * Test: fillSignalBuy * + *********************************/ + + function testCannotFillFilledOrderByTrader() public { + // Fullly fill the default order first + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + + // Try to fill the default order, should fail + Fill memory fill = DEFAULT_FILL; + fill.dealerSalt = uint256(8001); + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + traderParams.salt = fill.dealerSalt; + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.salt = uint256(8002); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + crdParams.salt = allowFill.salt; + + vm.expectRevert("SignalBuyContract: Order is filled"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + } + + function testCannotFillExpiredOrderByTrader() public { + Order memory order = DEFAULT_ORDER; + order.expiry = uint64(block.timestamp - 1); + + bytes32 orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = orderHash; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = orderHash; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Order is expired"); + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + } + + function testCannotFillByTraderWithWrongMakerSig() public { + bytes memory wrongMakerSig = _signOrder(dealerPrivateKey, DEFAULT_ORDER, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Order is not signed by user"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, wrongMakerSig, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + } + + function testCannotFillByTraderWithWrongTakerSig() public { + ISignalBuyContract.TraderParams memory wrongTraderParams = DEFAULT_TRADER_PARAMS; + wrongTraderParams.dealerSig = _signFill(userPrivateKey, DEFAULT_FILL, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Fill is not signed by dealer"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, wrongTraderParams, DEFAULT_CRD_PARAMS); + } + + function testCannotFillByTraderWithTakerOtherThanOrderSpecified() public { + Order memory order = DEFAULT_ORDER; + // order specify dealer address + order.dealer = coordinator; + bytes32 orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = orderHash; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + // dealer try to fill this order + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = orderHash; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Order cannot be filled by this dealer"); + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + } + + function testCannotFillByTraderWithExpiredFill() public { + Fill memory fill = DEFAULT_FILL; + fill.expiry = uint64(block.timestamp - 1); + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + traderParams.expiry = fill.expiry; + + vm.expectRevert("SignalBuyContract: Fill request is expired"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, DEFAULT_CRD_PARAMS); + } + + function testCannotReplayFill() public { + // Fill with DEFAULT_FILL + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + + // Try to fill with same fill request with differnt allowFill (otherwise will revert by dup allowFill) + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.salt = uint256(9001); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + crdParams.salt = allowFill.salt; + + vm.expectRevert("SignalBuyContract: Fill seen before"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithAlteredTakerTokenAmount() public { + // Replace dealerTokenAmount in traderParams without corresponded signature + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = DEFAULT_TRADER_PARAMS.dealerTokenAmount.mul(2); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Fill is not signed by dealer"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + } + + function testCannotFillByTraderWithAlteredRecipient() public { + // Replace recipient in traderParams without corresponded signature + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.recipient = coordinator; + vm.expectRevert("SignalBuyContract: Fill is not signed by dealer"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, DEFAULT_CRD_PARAMS); + } + + function testCannotFillByTraderWithExpiredAllowFill() public { + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.expiry = uint64(block.timestamp - 1); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + crdParams.expiry = allowFill.expiry; + + vm.expectRevert("SignalBuyContract: Fill permission is expired"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithAlteredOrderHash() public { + // Replace orderHash in allowFill + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = bytes32(0); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: AllowFill is not signed by coordinator"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithAlteredExecutor() public { + // Set the executor to user (not dealer) + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.executor = user; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + // Fill order using dealer (not executor) + vm.expectRevert("SignalBuyContract: AllowFill is not signed by coordinator"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithAlteredFillAmount() public { + // Change fill amount in allow fill + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = DEFAULT_ALLOW_FILL.fillAmount.div(2); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: AllowFill is not signed by coordinator"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithAllowFillNotSignedByCoordinator() public { + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + // Sign allow fill using dealer's private key + crdParams.sig = _signAllowFill(dealerPrivateKey, DEFAULT_ALLOW_FILL, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: AllowFill is not signed by coordinator"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByTraderWithReplayedAllowFill() public { + // Fill with default allow fill + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + + vm.expectRevert("SignalBuyContract: AllowFill seen before"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + } + + function testCannotFillByTraderWithReplayedFill() public { + // Fill with default allow fill + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.salt = allowFill.salt + 1; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.salt = allowFill.salt; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Fill seen before"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, crdParams); + } + + function testCannotFillByZeroTrader() public { + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.recipient = address(0); + + vm.expectRevert("SignalBuyContract: recipient can not be zero address"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, DEFAULT_CRD_PARAMS); + } + + function testCannotFillByTraderWithWorseTakerMakerTokenRatio() public { + Fill memory fill = DEFAULT_FILL; + // Increase user token amount so the dealerToken/userToken ratio is worse than order's dealerToken/userToken ratio + fill.userTokenAmount = DEFAULT_FILL.userTokenAmount.add(1); + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.userTokenAmount = fill.userTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: dealer token amount not enough"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, DEFAULT_CRD_PARAMS); + } + + function testCannotFullyFillByTraderWithWorseTakerTokenAmountDueToFee() public { + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.gasFeeFactor = 50; // gasFeeFactor: 0.5% + traderParams.dealerStrategyFeeFactor = 250; // dealerStrategyFeeFactor: 2.5% + traderParams.dealerSig = _signFill(dealerPrivateKey, DEFAULT_FILL, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: dealer token amount not enough"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, DEFAULT_CRD_PARAMS); + } + + function testFullyFillByTraderWithNoFee() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcMakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcTakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.dealerToken)); + + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + DEFAULT_ORDER_HASH, + DEFAULT_ORDER.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(DEFAULT_ALLOW_FILL)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(DEFAULT_ORDER.userToken), + address(DEFAULT_ORDER.dealerToken), + DEFAULT_ORDER.userTokenAmount, + DEFAULT_ORDER.minDealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, // tokenlonFee = 0 + 0 // dealerStrategyFee = 0 + ) + ); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + + dealerTakerAsset.assertChange(-int256(DEFAULT_ORDER.minDealerTokenAmount)); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(DEFAULT_ORDER.minDealerTokenAmount)); + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + fcMakerAsset.assertChange(0); + fcTakerAsset.assertChange(0); + } + + function testFullyFillByTraderWithAddedTokenlonFee() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcMakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcTakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.dealerToken)); + + // tokenlonFeeFactor : 10% + vm.startPrank(owner, owner); + signalBuyContract.setFactors(1000); + vm.warp(block.timestamp + signalBuyContract.factorActivateDelay()); + signalBuyContract.activateFactors(); + vm.stopPrank(); + + Fill memory fill = DEFAULT_FILL; + // Increase dealer token amount so the dealerToken/userToken ratio is better than order's dealerToken/userToken ratio + // to account for tokenlon fee + fill.dealerTokenAmount = DEFAULT_FILL.dealerTokenAmount.mul(115).div(100); // 15% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + DEFAULT_ORDER_HASH, + DEFAULT_ORDER.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(DEFAULT_ORDER.userToken), + address(DEFAULT_ORDER.dealerToken), + DEFAULT_ORDER.userTokenAmount, + traderParams.dealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + traderParams.dealerTokenAmount.div(10), // tokenlonFee = 10% dealerTokenAmount + 0 // dealerStrategyFee = 0 + ) + ); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + + dealerTakerAsset.assertChange(-int256(traderParams.dealerTokenAmount)); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(traderParams.dealerTokenAmount.mul(9).div(10))); // 10% fee for Tokenlon + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + fcMakerAsset.assertChange(0); + fcTakerAsset.assertChange(int256(traderParams.dealerTokenAmount.div(10))); + } + + function testFullyFillByTraderWithAddedGasFeeAndStrategyFee() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcMakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcTakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.dealerToken)); + + Fill memory fill = DEFAULT_FILL; + // Increase dealer token amount so the dealerToken/userToken ratio is better than order's dealerToken/userToken ratio + // to account for gas fee and dealer strategy fee + fill.dealerTokenAmount = DEFAULT_FILL.dealerTokenAmount.mul(11).div(10); // 10% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.gasFeeFactor = 50; // gasFeeFactor: 0.5% + traderParams.dealerStrategyFeeFactor = 250; // dealerStrategyFeeFactor: 2.5% + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + DEFAULT_ORDER_HASH, + DEFAULT_ORDER.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(DEFAULT_ORDER.userToken), + address(DEFAULT_ORDER.dealerToken), + DEFAULT_ORDER.userTokenAmount, + traderParams.dealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, // tokenlonFee = 0 + traderParams.dealerTokenAmount.mul(3).div(100) // dealerStrategyFee = 0.5% + 2.5% = 3% dealerTokenAmount + ) + ); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + + dealerTakerAsset.assertChange(-int256(traderParams.dealerTokenAmount.mul(97).div(100))); // 3% fee for SignalBuy is deducted from dealerTokenAmount directly + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(traderParams.dealerTokenAmount.mul(97).div(100))); // 3% fee for SignalBuy + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + fcMakerAsset.assertChange(0); + fcTakerAsset.assertChange(0); + } + + function testFullyFillByTraderWithBetterTakerMakerTokenRatio() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcMakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory fcTakerAsset = BalanceSnapshot.take(feeCollector, address(DEFAULT_ORDER.dealerToken)); + + Fill memory fill = DEFAULT_FILL; + // Increase dealer token amount so the dealerToken/userToken ratio is better than order's dealerToken/userToken ratio + fill.dealerTokenAmount = DEFAULT_FILL.dealerTokenAmount.mul(11).div(10); // 10% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + DEFAULT_ORDER_HASH, + DEFAULT_ORDER.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + traderParams.recipient, + ISignalBuyContract.FillReceipt( + address(DEFAULT_ORDER.userToken), + address(DEFAULT_ORDER.dealerToken), + DEFAULT_ORDER.userTokenAmount, + fill.dealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, + 0 + ) + ); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + + dealerTakerAsset.assertChange(-int256(fill.dealerTokenAmount)); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(fill.dealerTokenAmount)); // 10% more + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + fcMakerAsset.assertChange(0); + fcTakerAsset.assertChange(0); + } + + function testFullyFillByContractWalletTrader() public { + // Contract mockERC1271Wallet as dealer which always return valid ERC-1271 magic value no matter what. + Fill memory fill = DEFAULT_FILL; + fill.dealer = address(mockERC1271Wallet); + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealer = address(mockERC1271Wallet); + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.WalletBytes32); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.executor = address(mockERC1271Wallet); + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + } + + function testFillBySpecificTaker() public { + Order memory order = DEFAULT_ORDER; + // order specify dealer address + order.dealer = dealer; + bytes32 orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = orderHash; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = orderHash; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + } + + function testFillBySpecificTakerWithOldEIP712Method() public { + Order memory order = DEFAULT_ORDER; + // order specify dealer address + order.dealer = dealer; + bytes32 orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + bytes memory orderMakerSig = _signOrderWithOldEIP712Method(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = orderHash; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerSig = _signFillWithOldEIP712Method(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = orderHash; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFillWithOldEIP712Method(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + } + + function testOverFillByTrader() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + + Fill memory fill = DEFAULT_FILL; + // set the fill amount to 2x of order quota + fill.userTokenAmount = DEFAULT_ORDER.userTokenAmount.mul(2); + fill.dealerTokenAmount = DEFAULT_ORDER.minDealerTokenAmount.mul(2); + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.userTokenAmount = fill.userTokenAmount; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = fill.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + + // Balance change should be bound by order amount (not affected by 2x fill amount) + dealerTakerAsset.assertChange(-int256(DEFAULT_ORDER.minDealerTokenAmount)); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(DEFAULT_ORDER.minDealerTokenAmount)); + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + } + + function testOverFillByTraderWithBetterTakerMakerTokenRatio() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + + Fill memory fill = DEFAULT_FILL; + // set the fill amount to 2x of order quota + fill.userTokenAmount = DEFAULT_ORDER.userTokenAmount.mul(2); + fill.dealerTokenAmount = DEFAULT_ORDER.minDealerTokenAmount.mul(2).mul(11).div(10); // 10% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.userTokenAmount = fill.userTokenAmount; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.fillAmount = fill.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams, crdParams); + + // Balance change should be bound by order amount (not affected by 2x fill amount) + dealerTakerAsset.assertChange(-int256(DEFAULT_ORDER.minDealerTokenAmount.mul(11).div(10))); // 10% more + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount)); + userTakerAsset.assertChange(int256(DEFAULT_ORDER.minDealerTokenAmount.mul(11).div(10))); // 10% more + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount)); + } + + function testFillByTraderMultipleTimes() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + + // First fill amount : 9 USDT + Fill memory fill1 = DEFAULT_FILL; + fill1.userTokenAmount = 10 * 1e18; + fill1.dealerTokenAmount = 9 * 1e6; + ISignalBuyContract.TraderParams memory traderParams1 = DEFAULT_TRADER_PARAMS; + traderParams1.userTokenAmount = fill1.userTokenAmount; + traderParams1.dealerTokenAmount = fill1.dealerTokenAmount; + traderParams1.dealerSig = _signFill(dealerPrivateKey, fill1, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill1 = DEFAULT_ALLOW_FILL; + allowFill1.fillAmount = fill1.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams1 = DEFAULT_CRD_PARAMS; + crdParams1.sig = _signAllowFill(coordinatorPrivateKey, allowFill1, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams1, crdParams1); + + // Second fill amount : 36 USDT + Fill memory fill2 = DEFAULT_FILL; + fill2.userTokenAmount = 40 * 1e18; + fill2.dealerTokenAmount = 36 * 1e6; + + ISignalBuyContract.TraderParams memory traderParams2 = DEFAULT_TRADER_PARAMS; + traderParams2.userTokenAmount = fill2.userTokenAmount; + traderParams2.dealerTokenAmount = fill2.dealerTokenAmount; + traderParams2.dealerSig = _signFill(dealerPrivateKey, fill2, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill2 = DEFAULT_ALLOW_FILL; + allowFill2.fillAmount = fill2.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams2 = DEFAULT_CRD_PARAMS; + crdParams2.sig = _signAllowFill(coordinatorPrivateKey, allowFill2, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams2, crdParams2); + + // Half of the order filled after 2 txs + dealerTakerAsset.assertChange(-int256(DEFAULT_ORDER.minDealerTokenAmount.div(2))); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount.div(2))); + userTakerAsset.assertChange(int256(DEFAULT_ORDER.minDealerTokenAmount.div(2))); + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount.div(2))); + } + + function testFillByTraderMultipleTimesWithBetterTakerMakerTokenRatio() public { + BalanceSnapshot.Snapshot memory dealerTakerAsset = BalanceSnapshot.take(dealer, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory receiverMakerAsset = BalanceSnapshot.take(receiver, address(DEFAULT_ORDER.userToken)); + BalanceSnapshot.Snapshot memory userTakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.dealerToken)); + BalanceSnapshot.Snapshot memory userMakerAsset = BalanceSnapshot.take(user, address(DEFAULT_ORDER.userToken)); + + // First fill amount : 9 USDT and same dealerToken/userToken ratio + Fill memory fill1 = DEFAULT_FILL; + fill1.userTokenAmount = 10 * 1e18; + fill1.dealerTokenAmount = 9 * 1e6; + ISignalBuyContract.TraderParams memory traderParams1 = DEFAULT_TRADER_PARAMS; + traderParams1.userTokenAmount = fill1.userTokenAmount; + traderParams1.dealerTokenAmount = fill1.dealerTokenAmount; + traderParams1.dealerSig = _signFill(dealerPrivateKey, fill1, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill1 = DEFAULT_ALLOW_FILL; + allowFill1.fillAmount = fill1.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams1 = DEFAULT_CRD_PARAMS; + crdParams1.sig = _signAllowFill(coordinatorPrivateKey, allowFill1, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams1, crdParams1); + + // Second fill amount : 36 USDT and better dealerToken/userToken ratio + Fill memory fill2 = DEFAULT_FILL; + fill2.userTokenAmount = 40 * 1e18; + fill2.dealerTokenAmount = uint256(36 * 1e6).mul(11).div(10); // 10% more + + ISignalBuyContract.TraderParams memory traderParams2 = DEFAULT_TRADER_PARAMS; + traderParams2.userTokenAmount = fill2.userTokenAmount; + traderParams2.dealerTokenAmount = fill2.dealerTokenAmount; + traderParams2.dealerSig = _signFill(dealerPrivateKey, fill2, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill2 = DEFAULT_ALLOW_FILL; + allowFill2.fillAmount = fill2.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams2 = DEFAULT_CRD_PARAMS; + crdParams2.sig = _signAllowFill(coordinatorPrivateKey, allowFill2, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, traderParams2, crdParams2); + + // Half of the order filled after 2 txs + dealerTakerAsset.assertChange(-int256(fill1.dealerTokenAmount.add(fill2.dealerTokenAmount))); + receiverMakerAsset.assertChange(int256(DEFAULT_ORDER.userTokenAmount.div(2))); + userTakerAsset.assertChange(int256(fill1.dealerTokenAmount.add(fill2.dealerTokenAmount))); + userMakerAsset.assertChange(-int256(DEFAULT_ORDER.userTokenAmount.div(2))); + } + + /********************************* + * ETH/WETH settlement * + *********************************/ + + struct ETHandWETHAssetSnapshot { + BalanceSnapshot.Snapshot dealerETHAsset; + BalanceSnapshot.Snapshot dealerWETHAsset; + BalanceSnapshot.Snapshot userETHAsset; + BalanceSnapshot.Snapshot userWETHAsset; + } + ETHandWETHAssetSnapshot assetSnapshots; + + function testSettlementETHToETHWithNoFee() public { + Order memory order = DEFAULT_ORDER; + order.dealerToken = IERC20(LibConstant.ETH_ADDRESS); + order.minDealerTokenAmount = 1e18; + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + fill.dealerTokenAmount = order.minDealerTokenAmount; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + assetSnapshots.dealerETHAsset = BalanceSnapshot.take(dealer, LibConstant.ETH_ADDRESS); + assetSnapshots.dealerWETHAsset = BalanceSnapshot.take(dealer, address(weth)); + assetSnapshots.userETHAsset = BalanceSnapshot.take(user, LibConstant.ETH_ADDRESS); + assetSnapshots.userWETHAsset = BalanceSnapshot.take(user, address(weth)); + + // Case 1: Tx failed due to mismatch msg.value + vm.expectRevert("SignalBuyContract: mismatch dealer token (ETH) amount"); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: fill.dealerTokenAmount - 1 }(order, orderMakerSig, traderParams, crdParams); + + // Case 2: Tx succeeded + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)), + order.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(order.userToken), + address(order.dealerToken), + order.userTokenAmount, + order.minDealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, // tokenlonFee = 0 + 0 // dealerStrategyFee = 0 + ) + ); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: fill.dealerTokenAmount }(order, orderMakerSig, traderParams, crdParams); + + assetSnapshots.dealerETHAsset.assertChange(-int256(order.minDealerTokenAmount)); + assetSnapshots.dealerWETHAsset.assertChange(0); + assetSnapshots.userETHAsset.assertChange(int256(order.minDealerTokenAmount)); + assetSnapshots.userWETHAsset.assertChange(0); + } + + function testSettlementWETHToWETHWithNoFee() public { + Order memory order = DEFAULT_ORDER; + order.dealerToken = weth; + order.minDealerTokenAmount = 1e18; + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + fill.dealerTokenAmount = order.minDealerTokenAmount; + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + assetSnapshots.dealerETHAsset = BalanceSnapshot.take(dealer, LibConstant.ETH_ADDRESS); + assetSnapshots.dealerWETHAsset = BalanceSnapshot.take(dealer, address(weth)); + assetSnapshots.userETHAsset = BalanceSnapshot.take(user, LibConstant.ETH_ADDRESS); + assetSnapshots.userWETHAsset = BalanceSnapshot.take(user, address(weth)); + + // Case 1: Tx failed due to invalid msg.value + vm.expectRevert("SignalBuyContract: mismatch dealer token (ETH) amount"); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: 1 }(order, orderMakerSig, traderParams, crdParams); + + // Case 2: Tx succeeded + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)), + order.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(order.userToken), + address(order.dealerToken), + order.userTokenAmount, + order.minDealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, // tokenlonFee = 0 + 0 // dealerStrategyFee = 0 + ) + ); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + + assetSnapshots.dealerETHAsset.assertChange(0); + assetSnapshots.dealerWETHAsset.assertChange(-int256(order.minDealerTokenAmount)); + assetSnapshots.userETHAsset.assertChange(0); + assetSnapshots.userWETHAsset.assertChange(int256(order.minDealerTokenAmount)); + } + + function testSettlementETHToWETHWithAddedTokenlonFee() public { + // tokenlonFeeFactor : 10% + vm.startPrank(owner, owner); + signalBuyContract.setFactors(1000); + vm.warp(block.timestamp + signalBuyContract.factorActivateDelay()); + signalBuyContract.activateFactors(); + vm.stopPrank(); + + Order memory order = DEFAULT_ORDER; + order.dealerToken = weth; + order.minDealerTokenAmount = 1e18; + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + // Increase dealer token amount so the dealerToken/userToken ratio is better than order's dealerToken/userToken ratio + // to account for tokenlon fee + fill.dealerTokenAmount = order.minDealerTokenAmount.mul(115).div(100); // 15% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + assetSnapshots.dealerETHAsset = BalanceSnapshot.take(dealer, LibConstant.ETH_ADDRESS); + assetSnapshots.dealerWETHAsset = BalanceSnapshot.take(dealer, address(weth)); + assetSnapshots.userETHAsset = BalanceSnapshot.take(user, LibConstant.ETH_ADDRESS); + assetSnapshots.userWETHAsset = BalanceSnapshot.take(user, address(weth)); + + // Case 1: Tx failed due to invalid msg.value + vm.expectRevert("SignalBuyContract: mismatch dealer token (ETH) amount"); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: 1 }(order, orderMakerSig, traderParams, crdParams); + + // Case 2: Tx succeeded + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)), + order.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(order.userToken), + address(order.dealerToken), + order.userTokenAmount, + fill.dealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + fill.dealerTokenAmount.div(10), // tokenlonFee = 10% dealerTokenAmount + 0 // dealerStrategyFee = 0 + ) + ); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: fill.dealerTokenAmount }(order, orderMakerSig, traderParams, crdParams); + + assetSnapshots.dealerETHAsset.assertChange(-int256(fill.dealerTokenAmount)); + assetSnapshots.dealerWETHAsset.assertChange(0); + assetSnapshots.userETHAsset.assertChange(0); + assetSnapshots.userWETHAsset.assertChange(int256(fill.dealerTokenAmount.mul(9).div(10))); // 10% fee for Tokenlon + } + + function testSettlementWETHToETHWithAddedGasFeeAndStrategyFee() public { + Order memory order = DEFAULT_ORDER; + order.dealerToken = IERC20(LibConstant.ETH_ADDRESS); + order.minDealerTokenAmount = 1e18; + bytes memory orderMakerSig = _signOrder(userPrivateKey, order, SignatureValidator.SignatureType.EIP712); + + Fill memory fill = DEFAULT_FILL; + fill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + // Increase dealer token amount so the dealerToken/userToken ratio is better than order's dealerToken/userToken ratio + // to account for gas fee and dealer strategy fee + fill.dealerTokenAmount = order.minDealerTokenAmount.mul(11).div(10); // 10% more + + ISignalBuyContract.TraderParams memory traderParams = DEFAULT_TRADER_PARAMS; + traderParams.gasFeeFactor = 50; // gasFeeFactor: 0.5% + traderParams.dealerStrategyFeeFactor = 250; // dealerStrategyFeeFactor: 2.5% + traderParams.dealerTokenAmount = fill.dealerTokenAmount; + traderParams.dealerSig = _signFill(dealerPrivateKey, fill, SignatureValidator.SignatureType.EIP712); + + AllowFill memory allowFill = DEFAULT_ALLOW_FILL; + allowFill.orderHash = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)); + allowFill.fillAmount = traderParams.dealerTokenAmount; + + ISignalBuyContract.CoordinatorParams memory crdParams = DEFAULT_CRD_PARAMS; + crdParams.sig = _signAllowFill(coordinatorPrivateKey, allowFill, SignatureValidator.SignatureType.EIP712); + + assetSnapshots.dealerETHAsset = BalanceSnapshot.take(dealer, LibConstant.ETH_ADDRESS); + assetSnapshots.dealerWETHAsset = BalanceSnapshot.take(dealer, address(weth)); + assetSnapshots.userETHAsset = BalanceSnapshot.take(user, LibConstant.ETH_ADDRESS); + assetSnapshots.userWETHAsset = BalanceSnapshot.take(user, address(weth)); + + // Case 1: Tx failed due to invalid msg.value + vm.expectRevert("SignalBuyContract: mismatch dealer token (ETH) amount"); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy{ value: 1 }(order, orderMakerSig, traderParams, crdParams); + + // Case 2: Tx succeeded + vm.expectEmit(true, true, true, true); + emit SignalBuyFilledByTrader( + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getOrderStructHash(order)), + order.user, + dealer, + getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), getAllowFillStructHash(allowFill)), + DEFAULT_TRADER_PARAMS.recipient, + ISignalBuyContract.FillReceipt( + address(order.userToken), + address(order.dealerToken), + order.userTokenAmount, + fill.dealerTokenAmount, + 0, // remainingUserTokenAmount should be zero after order fully filled + 0, // tokenlonFee = 0 + fill.dealerTokenAmount.mul(3).div(100) // dealerStrategyFee = 0.5% + 2.5% = 3% dealerTokenAmount + ) + ); + vm.prank(dealer, dealer); + signalBuyContract.fillSignalBuy(order, orderMakerSig, traderParams, crdParams); + + assetSnapshots.dealerETHAsset.assertChange(0); + assetSnapshots.dealerWETHAsset.assertChange(-int256(fill.dealerTokenAmount.mul(97).div(100))); // 3% fee for SignalBuy is deducted from dealerTokenAmount directly + assetSnapshots.userETHAsset.assertChange(int256(fill.dealerTokenAmount.mul(97).div(100))); // 3% fee for SignalBuy + assetSnapshots.userWETHAsset.assertChange(0); + } + + /********************************* + * cancelSignalBuy * + *********************************/ + + function testCannotFillCanceledOrder() public { + Order memory zeroOrder = DEFAULT_ORDER; + zeroOrder.minDealerTokenAmount = 0; + bytes memory cancelSig = _signOrder(userPrivateKey, zeroOrder, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.cancelSignalBuy(DEFAULT_ORDER, cancelSig); + + vm.expectRevert("SignalBuyContract: Order is cancelled"); + signalBuyContract.fillSignalBuy(DEFAULT_ORDER, DEFAULT_ORDER_MAKER_SIG, DEFAULT_TRADER_PARAMS, DEFAULT_CRD_PARAMS); + } + + function testCannotCancelIfNotMaker() public { + Order memory zeroOrder = DEFAULT_ORDER; + zeroOrder.minDealerTokenAmount = 0; + bytes memory cancelSig = _signOrder(dealerPrivateKey, zeroOrder, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Cancel request is not signed by user"); + signalBuyContract.cancelSignalBuy(DEFAULT_ORDER, cancelSig); + } + + function testCannotCancelExpiredOrder() public { + Order memory expiredOrder = DEFAULT_ORDER; + expiredOrder.expiry = 0; + bytes memory cancelSig = _signOrder(userPrivateKey, expiredOrder, SignatureValidator.SignatureType.EIP712); + + vm.expectRevert("SignalBuyContract: Order is expired"); + signalBuyContract.cancelSignalBuy(expiredOrder, cancelSig); + } + + function testCannotCancelTwice() public { + Order memory zeroOrder = DEFAULT_ORDER; + zeroOrder.minDealerTokenAmount = 0; + bytes memory cancelSig = _signOrder(userPrivateKey, zeroOrder, SignatureValidator.SignatureType.EIP712); + + signalBuyContract.cancelSignalBuy(DEFAULT_ORDER, cancelSig); + vm.expectRevert("SignalBuyContract: Order is cancelled already"); + signalBuyContract.cancelSignalBuy(DEFAULT_ORDER, cancelSig); + } + + function _signOrderEIP712( + address limitOrderAddr, + uint256 privateKey, + Order memory order + ) internal returns (bytes memory sig) { + bytes32 orderHash = getOrderStructHash(order); + bytes32 EIP712SignDigest = getEIP712Hash(computeMainnetEIP712DomainSeparator(limitOrderAddr), orderHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(2)); + } + + function _signFillEIP712( + address limitOrderAddr, + uint256 privateKey, + Fill memory fill + ) internal returns (bytes memory sig) { + bytes32 fillHash = getFillStructHash(fill); + bytes32 EIP712SignDigest = getEIP712Hash(computeMainnetEIP712DomainSeparator(limitOrderAddr), fillHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(2)); + } + + function _signAllowFillEIP712( + address limitOrderAddr, + uint256 privateKey, + AllowFill memory allowFill + ) internal returns (bytes memory sig) { + bytes32 allowFillHash = getAllowFillStructHash(allowFill); + bytes32 EIP712SignDigest = getEIP712Hash(computeMainnetEIP712DomainSeparator(limitOrderAddr), allowFillHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(2)); + } + + /********************************* + * Helpers * + *********************************/ + + function dealWallet(address[] memory _wallet, uint256 _amount) internal { + // Deal 100 ETH to each account + for (uint256 i = 0; i < _wallet.length; i++) { + deal(_wallet[i], _amount); + } + } + + function setEOABalanceAndApprove( + address eoa, + IERC20[] memory tokens, + uint256 amount + ) internal { + require(address(signalBuyContract) != address(0), "System contracts not setup yet"); + vm.startPrank(eoa); + for (uint256 i = 0; i < tokens.length; i++) { + setERC20Balance(address(tokens[i]), eoa, amount); + tokens[i].safeApprove(address(signalBuyContract), type(uint256).max); + } + vm.stopPrank(); + } + + function _signOrder( + uint256 privateKey, + Order memory order, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 orderHash = getOrderStructHash(order); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), orderHash); + + if (sigType == SignatureValidator.SignatureType.EIP712) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, uint8(sigType)); + } else if (sigType == SignatureValidator.SignatureType.Wallet) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ECDSA.toEthSignedMessageHash(EIP712SignDigest)); + sig = abi.encodePacked(v, r, s, uint8(sigType)); + } else { + revert("Invalid signature type"); + } + } + + function _signOrderWithOldEIP712Method( + uint256 privateKey, + Order memory order, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 orderHash = getOrderStructHash(order); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), orderHash); + require(sigType == SignatureValidator.SignatureType.EIP712, "Invalid signature type"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(sigType)); + } + + function _signFill( + uint256 privateKey, + Fill memory fill, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 fillHash = getFillStructHash(fill); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), fillHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, uint8(sigType)); + } + + function _signFillWithOldEIP712Method( + uint256 privateKey, + Fill memory fill, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 fillHash = getFillStructHash(fill); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), fillHash); + require(sigType == SignatureValidator.SignatureType.EIP712, "Invalid signature type"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(sigType)); + } + + function _signAllowFill( + uint256 privateKey, + AllowFill memory allowFill, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 allowFillHash = getAllowFillStructHash(allowFill); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), allowFillHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, uint8(sigType)); + } + + function _signAllowFillWithOldEIP712Method( + uint256 privateKey, + AllowFill memory allowFill, + SignatureValidator.SignatureType sigType + ) internal returns (bytes memory sig) { + bytes32 allowFillHash = getAllowFillStructHash(allowFill); + bytes32 EIP712SignDigest = getEIP712Hash(signalBuyContract.EIP712_DOMAIN_SEPARATOR(), allowFillHash); + require(sigType == SignatureValidator.SignatureType.EIP712, "Invalid signature type"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, EIP712SignDigest); + sig = abi.encodePacked(r, s, v, bytes32(0), uint8(sigType)); + } +}