- 
                Notifications
    You must be signed in to change notification settings 
- Fork 30
SignalBuy contract #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v5
Are you sure you want to change the base?
SignalBuy contract #197
Changes from 28 commits
a3a34d7
              0b1d587
              e34635e
              59b0342
              fc1e0cc
              1f8b067
              3f7978a
              23d32d2
              a575739
              ca57e62
              c53f662
              7da8bba
              dfbec2a
              f8b2d26
              edd0382
              a95f407
              b0e4941
              17c4420
              d0781c8
              928094c
              4429fb1
              a37116d
              1a1093d
              3667792
              e5f5046
              1a4bf65
              b1e9d4b
              273d472
              ed48d9b
              f92de42
              8bb9f0f
              471fbd8
              c7cb516
              91938e2
              3323a1a
              bc2bf46
              f8612ac
              9f777cc
              d3ce5d9
              2cd05c2
              a014442
              b4082ca
              0951616
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,367 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.7.6; | ||
| pragma abicoder v2; | ||
|  | ||
| import "@openzeppelin/contracts/math/Math.sol"; | ||
| import "@openzeppelin/contracts/math/SafeMath.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; | ||
| import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; | ||
|  | ||
| import "./interfaces/IPionexContract.sol"; | ||
| import "./interfaces/IPermanentStorage.sol"; | ||
| import "./interfaces/ISpender.sol"; | ||
| import "./interfaces/IWeth.sol"; | ||
| import "./utils/StrategyBase.sol"; | ||
| import "./utils/BaseLibEIP712.sol"; | ||
| import "./utils/LibConstant.sol"; | ||
| import "./utils/LibPionexContractOrderStorage.sol"; | ||
| import "./utils/PionexContractLibEIP712.sol"; | ||
| import "./utils/SignatureValidator.sol"; | ||
|  | ||
| /// @title Pionex Contract | ||
| /// @notice Order can be filled as long as the provided pionexToken/userToken ratio is better than or equal to user's specfied pionexToken/userToken ratio. | ||
| /// @author imToken Labs | ||
| contract PionexContract is IPionexContract, StrategyBase, BaseLibEIP712, SignatureValidator, ReentrancyGuard { | ||
| using SafeMath for uint256; | ||
| using SafeERC20 for IERC20; | ||
|  | ||
| 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; | ||
|  | ||
| constructor( | ||
| address _owner, | ||
| address _userProxy, | ||
| address _weth, | ||
| address _permStorage, | ||
| address _spender, | ||
| address _coordinator, | ||
| uint256 _factorActivateDelay, | ||
| address _feeCollector | ||
| ) StrategyBase(_owner, _userProxy, _weth, _permStorage, _spender) { | ||
| coordinator = _coordinator; | ||
| factorActivateDelay = _factorActivateDelay; | ||
| feeCollector = _feeCollector; | ||
| } | ||
|  | ||
| receive() external payable {} | ||
|  | ||
| /// @notice Only owner can call | ||
| /// @param _newCoordinator The new address of coordinator | ||
| function upgradeCoordinator(address _newCoordinator) external onlyOwner { | ||
| require(_newCoordinator != address(0), "PionexContract: 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, "PionexContract: Invalid user fee factor"); | ||
|  | ||
| pendingTokenlonFeeFactor = _tokenlonFeeFactor; | ||
|  | ||
| factorsTimeLock = block.timestamp + factorActivateDelay; | ||
| } | ||
|  | ||
| /// @notice Only owner can call | ||
| function activateFactors() external onlyOwner { | ||
| require(factorsTimeLock != 0, "PionexContract: no pending fee factors"); | ||
| require(block.timestamp >= factorsTimeLock, "PionexContract: 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), "PionexContract: fee collector can not be zero address"); | ||
| feeCollector = _newFeeCollector; | ||
|  | ||
| emit SetFeeCollector(_newFeeCollector); | ||
| } | ||
|  | ||
| /// @inheritdoc IPionexContract | ||
| function fillLimitOrder( | ||
| PionexContractLibEIP712.Order calldata _order, | ||
| bytes calldata _orderUserSig, | ||
| TraderParams calldata _params, | ||
| CoordinatorParams calldata _crdParams | ||
| ) external override onlyUserProxy nonReentrant returns (uint256, uint256) { | ||
| bytes32 orderHash = getEIP712Hash(PionexContractLibEIP712._getOrderStructHash(_order)); | ||
|  | ||
| _validateOrder(_order, orderHash, _orderUserSig); | ||
| bytes32 allowFillHash = _validateFillPermission(orderHash, _params.pionexTokenAmount, _params.pionex, _crdParams); | ||
| _validateOrderTaker(_order, _params.pionex); | ||
|  | ||
| // Check provided pionexToken/userToken ratio is better than or equal to user's specfied pionexToken/userToken ratio | ||
| // -> _params.pionexTokenAmount/_params.userTokenAmount >= _order.pionexTokenAmount/_order.userTokenAmount | ||
| require( | ||
| _params.pionexTokenAmount.mul(_order.userTokenAmount) >= _order.minPionexTokenAmount.mul(_params.userTokenAmount), | ||
| "PionexContract: pionex/user token ratio not good enough" | ||
| ); | ||
| // Check gas fee factor and pionex strategy fee factor do not exceed limit | ||
| require( | ||
| (_params.gasFeeFactor <= LibConstant.BPS_MAX) && | ||
| (_params.pionexStrategyFeeFactor <= LibConstant.BPS_MAX) && | ||
| (_params.gasFeeFactor + _params.pionexStrategyFeeFactor <= LibConstant.BPS_MAX - tokenlonFeeFactor), | ||
|          | ||
| "PionexContract: Invalid pionex fee factor" | ||
| ); | ||
|  | ||
| { | ||
| PionexContractLibEIP712.Fill memory fill = PionexContractLibEIP712.Fill({ | ||
| orderHash: orderHash, | ||
| pionex: _params.pionex, | ||
| recipient: _params.recipient, | ||
| userTokenAmount: _params.userTokenAmount, | ||
| pionexTokenAmount: _params.pionexTokenAmount, | ||
| pionexSalt: _params.salt, | ||
| expiry: _params.expiry | ||
| }); | ||
| _validateTraderFill(fill, _params.pionexSig); | ||
| } | ||
|  | ||
| (uint256 userTokenAmount, uint256 remainingUserTokenAmount) = _quoteOrderFromUserToken(_order, orderHash, _params.userTokenAmount); | ||
| // Calculate pionexTokenAmount according to the provided pionexToken/userToken ratio | ||
| uint256 pionexTokenAmount = userTokenAmount.mul(_params.pionexTokenAmount).div(_params.userTokenAmount); | ||
| // Calculate minimum pionexTokenAmount according to the offer's pionexToken/userToken ratio | ||
| uint256 minPionexTokenAmount = userTokenAmount.mul(_order.minPionexTokenAmount).div(_order.userTokenAmount); | ||
|  | ||
| _settleForTrader( | ||
| TraderSettlement({ | ||
| orderHash: orderHash, | ||
| allowFillHash: allowFillHash, | ||
| trader: _params.pionex, | ||
| recipient: _params.recipient, | ||
| user: _order.user, | ||
| userToken: _order.userToken, | ||
| pionexToken: _order.pionexToken, | ||
| userTokenAmount: userTokenAmount, | ||
| pionexTokenAmount: pionexTokenAmount, | ||
| minPionexTokenAmount: minPionexTokenAmount, | ||
| remainingUserTokenAmount: remainingUserTokenAmount, | ||
| gasFeeFactor: _params.gasFeeFactor, | ||
| pionexStrategyFeeFactor: _params.pionexStrategyFeeFactor | ||
| }) | ||
| ); | ||
|  | ||
| _recordUserTokenFilled(orderHash, userTokenAmount); | ||
|  | ||
| return (pionexTokenAmount, userTokenAmount); | ||
| } | ||
|  | ||
| function _validateTraderFill(PionexContractLibEIP712.Fill memory _fill, bytes memory _fillTakerSig) internal { | ||
| require(_fill.expiry > uint64(block.timestamp), "PionexContract: Fill request is expired"); | ||
| require(_fill.recipient != address(0), "PionexContract: recipient can not be zero address"); | ||
|  | ||
| bytes32 fillHash = getEIP712Hash(PionexContractLibEIP712._getFillStructHash(_fill)); | ||
| require(isValidSignature(_fill.pionex, fillHash, bytes(""), _fillTakerSig), "PionexContract: Fill is not signed by pionex"); | ||
|  | ||
| // Set fill seen to avoid replay attack. | ||
| // PermanentStorage would throw error if fill is already seen. | ||
| permStorage.setLimitOrderTransactionSeen(fillHash); | ||
| } | ||
|  | ||
| function _validateFillPermission( | ||
| bytes32 _orderHash, | ||
| uint256 _fillAmount, | ||
| address _executor, | ||
| CoordinatorParams memory _crdParams | ||
| ) internal returns (bytes32) { | ||
| require(_crdParams.expiry > uint64(block.timestamp), "PionexContract: Fill permission is expired"); | ||
|  | ||
| bytes32 allowFillHash = getEIP712Hash( | ||
| PionexContractLibEIP712._getAllowFillStructHash( | ||
| PionexContractLibEIP712.AllowFill({ | ||
| orderHash: _orderHash, | ||
| executor: _executor, | ||
| fillAmount: _fillAmount, | ||
| salt: _crdParams.salt, | ||
| expiry: _crdParams.expiry | ||
| }) | ||
| ) | ||
| ); | ||
| require(isValidSignature(coordinator, allowFillHash, bytes(""), _crdParams.sig), "PionexContract: AllowFill is not signed by coordinator"); | ||
|  | ||
| // Set allow fill seen to avoid replay attack | ||
| // PermanentStorage would throw error if allow fill is already seen. | ||
| permStorage.setLimitOrderAllowFillSeen(allowFillHash); | ||
|  | ||
| return allowFillHash; | ||
| } | ||
|  | ||
| struct TraderSettlement { | ||
| bytes32 orderHash; | ||
| bytes32 allowFillHash; | ||
| address trader; | ||
| address recipient; | ||
| address user; | ||
| IERC20 userToken; | ||
| IERC20 pionexToken; | ||
| uint256 userTokenAmount; | ||
| uint256 pionexTokenAmount; | ||
| uint256 minPionexTokenAmount; | ||
| uint256 remainingUserTokenAmount; | ||
| uint16 gasFeeFactor; | ||
| uint16 pionexStrategyFeeFactor; | ||
| } | ||
|  | ||
| function _settleForTrader(TraderSettlement memory _settlement) internal { | ||
| // memory cache | ||
| ISpender _spender = spender; | ||
| address _feeCollector = feeCollector; | ||
|  | ||
| // Calculate user fee (user receives pionex token so fee is charged in pionex token) | ||
| // 1. Fee for Tokenlon | ||
| uint256 tokenlonFee = _mulFactor(_settlement.pionexTokenAmount, tokenlonFeeFactor); | ||
| // 2. Fee for Pionex, including gas fee and strategy fee | ||
| uint256 pionexFee = _mulFactor(_settlement.pionexTokenAmount, _settlement.gasFeeFactor + _settlement.pionexStrategyFeeFactor); | ||
| uint256 pionexTokenForUser = _settlement.pionexTokenAmount.sub(tokenlonFee).sub(pionexFee); | ||
| require(pionexTokenForUser >= _settlement.minPionexTokenAmount, "PionexContract: pionex token amount not enough"); | ||
|  | ||
| // trader -> user | ||
| _spender.spendFromUserTo(_settlement.trader, address(_settlement.pionexToken), _settlement.user, pionexTokenForUser); | ||
|  | ||
| // user -> recipient | ||
| _spender.spendFromUserTo(_settlement.user, address(_settlement.userToken), _settlement.recipient, _settlement.userTokenAmount); | ||
|  | ||
| // Collect user fee (charged in pionex token) | ||
| if (tokenlonFee > 0) { | ||
| _spender.spendFromUserTo(_settlement.trader, address(_settlement.pionexToken), _feeCollector, tokenlonFee); | ||
| } | ||
|  | ||
| // bypass stack too deep error | ||
| _emitLimitOrderFilledByTrader( | ||
| LimitOrderFilledByTraderParams({ | ||
| orderHash: _settlement.orderHash, | ||
| user: _settlement.user, | ||
| pionex: _settlement.trader, | ||
| allowFillHash: _settlement.allowFillHash, | ||
| recipient: _settlement.recipient, | ||
| userToken: address(_settlement.userToken), | ||
| pionexToken: address(_settlement.pionexToken), | ||
| userTokenFilledAmount: _settlement.userTokenAmount, | ||
| pionexTokenFilledAmount: _settlement.pionexTokenAmount, | ||
| remainingUserTokenAmount: _settlement.remainingUserTokenAmount, | ||
| tokenlonFee: tokenlonFee, | ||
| pionexFee: pionexFee | ||
| }) | ||
| ); | ||
| } | ||
|  | ||
| /// @inheritdoc IPionexContract | ||
| function cancelLimitOrder(PionexContractLibEIP712.Order calldata _order, bytes calldata _cancelOrderUserSig) external override onlyUserProxy nonReentrant { | ||
| require(_order.expiry > uint64(block.timestamp), "PionexContract: Order is expired"); | ||
| bytes32 orderHash = getEIP712Hash(PionexContractLibEIP712._getOrderStructHash(_order)); | ||
| bool isCancelled = LibPionexContractOrderStorage.getStorage().orderHashToCancelled[orderHash]; | ||
| require(!isCancelled, "PionexContract: Order is cancelled already"); | ||
| { | ||
| PionexContractLibEIP712.Order memory cancelledOrder = _order; | ||
| cancelledOrder.minPionexTokenAmount = 0; | ||
|  | ||
| bytes32 cancelledOrderHash = getEIP712Hash(PionexContractLibEIP712._getOrderStructHash(cancelledOrder)); | ||
| require(isValidSignature(_order.user, cancelledOrderHash, bytes(""), _cancelOrderUserSig), "PionexContract: Cancel request is not signed by user"); | ||
| } | ||
|  | ||
| // Set cancelled state to storage | ||
| LibPionexContractOrderStorage.getStorage().orderHashToCancelled[orderHash] = true; | ||
| emit OrderCancelled(orderHash, _order.user); | ||
| } | ||
|  | ||
| /* order utils */ | ||
|  | ||
| function _validateOrder( | ||
| PionexContractLibEIP712.Order memory _order, | ||
| bytes32 _orderHash, | ||
| bytes memory _orderUserSig | ||
| ) internal view { | ||
| require(_order.expiry > uint64(block.timestamp), "PionexContract: Order is expired"); | ||
| bool isCancelled = LibPionexContractOrderStorage.getStorage().orderHashToCancelled[_orderHash]; | ||
| require(!isCancelled, "PionexContract: Order is cancelled"); | ||
|  | ||
| require(isValidSignature(_order.user, _orderHash, bytes(""), _orderUserSig), "PionexContract: Order is not signed by user"); | ||
| } | ||
|  | ||
| function _validateOrderTaker(PionexContractLibEIP712.Order memory _order, address _pionex) internal pure { | ||
| if (_order.pionex != address(0)) { | ||
| require(_order.pionex == _pionex, "PionexContract: Order cannot be filled by this pionex"); | ||
| } | ||
| } | ||
|  | ||
| function _quoteOrderFromUserToken( | ||
| PionexContractLibEIP712.Order memory _order, | ||
| bytes32 _orderHash, | ||
| uint256 _userTokenAmount | ||
| ) internal view returns (uint256, uint256) { | ||
| uint256 userTokenFilledAmount = LibPionexContractOrderStorage.getStorage().orderHashToUserTokenFilledAmount[_orderHash]; | ||
|  | ||
| require(userTokenFilledAmount < _order.userTokenAmount, "PionexContract: Order is filled"); | ||
|  | ||
| uint256 userTokenFillableAmount = _order.userTokenAmount.sub(userTokenFilledAmount); | ||
| uint256 userTokenQuota = Math.min(_userTokenAmount, userTokenFillableAmount); | ||
| uint256 remainingAfterFill = userTokenFillableAmount.sub(userTokenQuota); | ||
|  | ||
| require(userTokenQuota != 0, "PionexContract: zero token amount"); | ||
| return (userTokenQuota, remainingAfterFill); | ||
| } | ||
|  | ||
| function _recordUserTokenFilled(bytes32 _orderHash, uint256 _userTokenAmount) internal { | ||
| LibPionexContractOrderStorage.Storage storage stor = LibPionexContractOrderStorage.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 LimitOrderFilledByTraderParams { | ||
| bytes32 orderHash; | ||
| address user; | ||
| address pionex; | ||
| bytes32 allowFillHash; | ||
| address recipient; | ||
| address userToken; | ||
| address pionexToken; | ||
| uint256 userTokenFilledAmount; | ||
| uint256 pionexTokenFilledAmount; | ||
| uint256 remainingUserTokenAmount; | ||
| uint256 tokenlonFee; | ||
| uint256 pionexFee; | ||
| } | ||
|  | ||
| function _emitLimitOrderFilledByTrader(LimitOrderFilledByTraderParams memory _params) internal { | ||
| emit LimitOrderFilledByTrader( | ||
| _params.orderHash, | ||
| _params.user, | ||
| _params.pionex, | ||
| _params.allowFillHash, | ||
| _params.recipient, | ||
| FillReceipt({ | ||
| userToken: _params.userToken, | ||
| pionexToken: _params.pionexToken, | ||
| userTokenFilledAmount: _params.userTokenFilledAmount, | ||
| pionexTokenFilledAmount: _params.pionexTokenFilledAmount, | ||
| remainingUserTokenAmount: _params.remainingUserTokenAmount, | ||
| tokenlonFee: _params.tokenlonFee, | ||
| pionexFee: _params.pionexFee | ||
| }) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the
minPionexTokenAmountshould be checked after fee deducted, may be this require can be removed since it'd be reverted ifpionexTokenForUser < _settlement.minPionexTokenAmounteven the ratio here is better?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. Fixed in ed48d9b