-
Notifications
You must be signed in to change notification settings - Fork 24
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
SignalBuy contract #197
base: v5
Are you sure you want to change the base?
SignalBuy contract #197
Changes from 27 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,364 @@ | ||
// 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The last one check seems to imply the first two checks would be valid too. Maybe we could keep only the last one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to prevent the sum from overflow so I added the first two checks. |
||
"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); | ||
|
||
uint256 userTokenOut = _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, | ||
remainingUserTokenAmount: remainingUserTokenAmount, | ||
gasFeeFactor: _params.gasFeeFactor, | ||
pionexStrategyFeeFactor: _params.pionexStrategyFeeFactor | ||
}) | ||
); | ||
|
||
_recordUserTokenFilled(orderHash, userTokenAmount); | ||
|
||
return (pionexTokenAmount, userTokenOut); | ||
} | ||
|
||
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 remainingUserTokenAmount; | ||
uint16 gasFeeFactor; | ||
uint16 pionexStrategyFeeFactor; | ||
} | ||
|
||
function _settleForTrader(TraderSettlement memory _settlement) internal returns (uint256) { | ||
// 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); | ||
|
||
// 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 | ||
}) | ||
); | ||
|
||
return _settlement.userTokenAmount; | ||
} | ||
|
||
/// @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
minPionexTokenAmount
should be checked after fee deducted, may be this require can be removed since it'd be reverted ifpionexTokenForUser < _settlement.minPionexTokenAmount
even 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