Skip to content
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

Open
wants to merge 43 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a3a34d7
Copy ILimitOrder -> IPionexContract
NIC619 May 17, 2023
0b1d587
Add makerTokenAmount to IPionex TraderParam
NIC619 May 17, 2023
e34635e
Copy LimitOrderLibEIP712 -> PionexContractLibEIP712
NIC619 May 17, 2023
59b0342
Add makerTokenAmount to Fill
NIC619 May 17, 2023
fc1e0cc
Copy LimitOrder -> PionexContract
NIC619 May 17, 2023
1f8b067
Check takerToken/makerToken ratio in fillLimitOrderByTrader
NIC619 May 17, 2023
3f7978a
Copy LimitOrder tests -> PionexContract tests
NIC619 May 17, 2023
23d32d2
Adjust tests
NIC619 May 17, 2023
a575739
Change to quote from maker token instead
NIC619 May 18, 2023
ca57e62
Add better/worse takerToken/makerToken ratio tests
NIC619 May 18, 2023
c53f662
Lint
NIC619 May 18, 2023
7da8bba
Skip takerTokenAmount in quoteOrder
NIC619 May 19, 2023
dfbec2a
Remove signing test
NIC619 May 19, 2023
f8b2d26
Remove fillLimitOrderByProtocol from PionexContract
NIC619 May 19, 2023
edd0382
Remove takerFeeFactor
NIC619 May 23, 2023
a95f407
Remove profitFeeFactor
NIC619 May 23, 2023
b0e4941
Add Pionex fee factors: gas fee and strategy fee
NIC619 May 23, 2023
17c4420
Rename user to pionex in tests
NIC619 May 23, 2023
d0781c8
Update comment for fee and balance change check
NIC619 May 23, 2023
928094c
Rename maker -> user; taker -> pionex
NIC619 May 25, 2023
4429fb1
Rename
NIC619 May 26, 2023
a37116d
Rename userFeeFactor -> tokenlonFeeFactor
NIC619 May 26, 2023
1a1093d
Rename
NIC619 May 26, 2023
3667792
Merge branch 'v5' into Pionex_contract
NIC619 May 26, 2023
e5f5046
Update after merge
NIC619 May 26, 2023
1a4bf65
Remove TraderSettlement.pionex param
NIC619 May 29, 2023
b1e9d4b
Rename Maker -> User
NIC619 May 29, 2023
273d472
Add minPionexTokenAmount check to protect user
NIC619 Jun 9, 2023
ed48d9b
Remove ratio check bc we check minPionexTokenAmount
NIC619 Jun 12, 2023
f92de42
Rename pionex -> dealer
NIC619 Jun 12, 2023
8bb9f0f
Rename Pionex -> SignalBuy
NIC619 Jun 12, 2023
471fbd8
Rename LimitOrder -> SignalBuy
NIC619 Jun 12, 2023
c7cb516
Turn SignalBuy into a standalone contract
NIC619 Jun 13, 2023
91938e2
Remove unused
NIC619 Jun 14, 2023
3323a1a
Handle ETH/WETH dealer token conversion for dealer
NIC619 Jun 15, 2023
bc2bf46
Add ETH/WETH settlement tests
NIC619 Jun 15, 2023
f8612ac
Fix ORDER_TYPEHASH
NIC619 Jul 7, 2023
9f777cc
Merge remote-tracking branch 'origin/v5' into Pionex_contract
charlesjhongc Aug 14, 2023
d3ce5d9
refactor SignalBuyContract 712 lib
charlesjhongc Aug 14, 2023
2cd05c2
fix solidity lint
charlesjhongc Aug 14, 2023
a014442
fix IWETH import path
charlesjhongc Aug 16, 2023
b4082ca
fix allowFill seen check
charlesjhongc Sep 11, 2023
0951616
fix replay sig tests and import style
charlesjhongc Sep 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 364 additions & 0 deletions contracts/PionexContract.sol
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(
Copy link
Contributor

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 if pionexTokenForUser < _settlement.minPionexTokenAmount even the ratio here is better?

Copy link
Contributor Author

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

_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),
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
})
);
}
}
Loading