Skip to content

Commit

Permalink
Merge branch 'master' into feature/bump-hardhat-ethers
Browse files Browse the repository at this point in the history
# Conflicts:
#	contracts/extensions/ChainlinkCalculator.sol
#	package.json
#	test/ChainLinkExample.js
#	test/LimitOrderProtocol.js
#	yarn.lock
  • Loading branch information
zZoMROT committed Dec 19, 2023
2 parents 34d5574 + eeb1a00 commit d1b5fde
Show file tree
Hide file tree
Showing 17 changed files with 913 additions and 316 deletions.
24 changes: 13 additions & 11 deletions contracts/OrderMixin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ abstract contract OrderMixin is IOrderMixin, EIP712, OnlyWethReceiver, Predicate
using BitInvalidatorLib for BitInvalidatorLib.Data;
using RemainingInvalidatorLib for RemainingInvalidator;

uint256 private constant _RAW_CALL_GAS_LIMIT = 5000;
address private constant _PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

IWETH private immutable _WETH; // solhint-disable-line var-name-mixedcase
mapping(address => BitInvalidatorLib.Data) private _bitInvalidator;
mapping(address => mapping(bytes32 => RemainingInvalidator)) private _remainingInvalidator;
mapping(address maker => BitInvalidatorLib.Data data) private _bitInvalidator;
mapping(address maker => mapping(bytes32 orderHash => RemainingInvalidator remaining)) private _remainingInvalidator;

constructor(IWETH weth) OnlyWethReceiver(address(weth)) {
_WETH = weth;
}

/**
* @notice See {IOrderMixin-permitAndCall}.
*/
function permitAndCall(bytes calldata permit, bytes calldata action) external {
IERC20(address(bytes20(permit))).tryPermit(msg.sender, address(this), permit[20:]);
IERC20(address(bytes20(permit))).tryPermit(permit[20:]);
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
let ptr := mload(0x40)
Expand Down Expand Up @@ -97,11 +97,12 @@ abstract contract OrderMixin is IOrderMixin, EIP712, OnlyWethReceiver, Predicate
*/
function cancelOrder(MakerTraits makerTraits, bytes32 orderHash) public {
if (makerTraits.useBitInvalidator()) {
_bitInvalidator[msg.sender].massInvalidate(makerTraits.nonceOrEpoch(), 0);
uint256 invalidator = _bitInvalidator[msg.sender].massInvalidate(makerTraits.nonceOrEpoch(), 0);
emit BitInvalidatorUpdated(msg.sender, makerTraits.nonceOrEpoch() >> 8, invalidator);
} else {
_remainingInvalidator[msg.sender][orderHash] = RemainingInvalidatorLib.fullyFilled();
emit OrderCancelled(orderHash);
}
emit OrderCancelled(orderHash);
}

/**
Expand All @@ -121,7 +122,8 @@ abstract contract OrderMixin is IOrderMixin, EIP712, OnlyWethReceiver, Predicate
*/
function bitsInvalidateForOrder(MakerTraits makerTraits, uint256 additionalMask) external {
if (!makerTraits.useBitInvalidator()) revert OrderIsNotSuitableForMassInvalidation();
_bitInvalidator[msg.sender].massInvalidate(makerTraits.nonceOrEpoch(), additionalMask);
uint256 invalidator = _bitInvalidator[msg.sender].massInvalidate(makerTraits.nonceOrEpoch(), additionalMask);
emit BitInvalidatorUpdated(msg.sender, makerTraits.nonceOrEpoch() >> 8, invalidator);
}

/**
Expand Down Expand Up @@ -404,14 +406,14 @@ abstract contract OrderMixin is IOrderMixin, EIP712, OnlyWethReceiver, Predicate
if (msg.value > takingAmount) {
unchecked {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = msg.sender.call{value: msg.value - takingAmount, gas: _RAW_CALL_GAS_LIMIT}("");
(bool success, ) = msg.sender.call{value: msg.value - takingAmount}("");
if (!success) revert Errors.ETHTransferFailed();
}
}

if (order.makerTraits.unwrapWeth()) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = order.getReceiver().call{value: takingAmount, gas: _RAW_CALL_GAS_LIMIT}("");
(bool success, ) = order.getReceiver().call{value: takingAmount}("");
if (!success) revert Errors.ETHTransferFailed();
} else {
_WETH.safeDeposit(takingAmount);
Expand Down
87 changes: 51 additions & 36 deletions contracts/extensions/ChainlinkCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "../interfaces/IOrderMixin.sol";
import "../interfaces/IAmountGetter.sol";

// solhint-disable not-rely-on-time

/// @title A helper contract for interactions with https://docs.chain.link
contract ChainlinkCalculator is IAmountGetter {
using SafeCast for int256;
Expand All @@ -16,6 +18,14 @@ contract ChainlinkCalculator is IAmountGetter {

uint256 private constant _SPREAD_DENOMINATOR = 1e9;
uint256 private constant _ORACLE_TTL = 4 hours;
bytes1 private constant _INVERSE_FLAG = 0x80;
bytes1 private constant _DOUBLE_PRICE_FLAG = 0x40;

/// @notice Calculates price of token A relative to token B. Note that order is important
/// @return result Token A relative price times amount
function doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, int256 decimalsScale, uint256 amount) external view returns(uint256 result) {
return _doublePrice(oracle1, oracle2, decimalsScale, amount);
}

function getMakingAmount(
IOrderMixin.Order calldata /* order */,
Expand All @@ -26,20 +36,7 @@ contract ChainlinkCalculator is IAmountGetter {
uint256 /* remainingMakingAmount */,
bytes calldata extraData
) external view returns (uint256) {
(
AggregatorV3Interface oracle,
uint256 spread
) = abi.decode(extraData, (AggregatorV3Interface, uint256));

/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// Lowest 254 bits specify spread amount. Spread is scaled by 1e9, i.e. 101% = 1.01e9, 99% = 0.99e9.
/// Highest bit is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// @return Amount * spread * oracle price
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice(); // solhint-disable-line not-rely-on-time
return takingAmount * spread * latestAnswer.toUint256() / (10 ** oracle.decimals()) / _SPREAD_DENOMINATOR;
return _getSpreadedAmount(takingAmount, extraData);
}

function getTakingAmount(
Expand All @@ -51,31 +48,46 @@ contract ChainlinkCalculator is IAmountGetter {
uint256 /* remainingMakingAmount */,
bytes calldata extraData
) external view returns (uint256) {
(
AggregatorV3Interface oracle,
uint256 spread
) = abi.decode(extraData, (AggregatorV3Interface, uint256));
return _getSpreadedAmount(makingAmount, extraData);
}

/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// Lowest 254 bits specify spread amount. Spread is scaled by 1e9, i.e. 101% = 1.01e9, 99% = 0.99e9.
/// Highest bit is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// @return Amount * spread * oracle price
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice(); // solhint-disable-line not-rely-on-time
return makingAmount * spread * (10 ** oracle.decimals()) / latestAnswer.toUint256() / _SPREAD_DENOMINATOR;
/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// The first byte of the blob contain inverse and useDoublePrice flags,
/// The inverse flag is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// The useDoublePrice flag is set when needs price for two custom tokens (other than ETH or USD)
/// @return Amount * spread * oracle price
function _getSpreadedAmount(uint256 amount, bytes calldata blob) internal view returns(uint256) {
bytes1 flags = bytes1(blob[:1]);
if (flags & _DOUBLE_PRICE_FLAG == _DOUBLE_PRICE_FLAG) {
AggregatorV3Interface oracle1 = AggregatorV3Interface(address(bytes20(blob[1:21])));
AggregatorV3Interface oracle2 = AggregatorV3Interface(address(bytes20(blob[21:41])));
int256 decimalsScale = int256(uint256(bytes32(blob[41:73])));
uint256 spread = uint256(bytes32(blob[73:105]));
return _doublePrice(oracle1, oracle2, decimalsScale, spread * amount) / _SPREAD_DENOMINATOR;
} else {
AggregatorV3Interface oracle = AggregatorV3Interface(address(bytes20(blob[1:21])));
uint256 spread = uint256(bytes32(blob[21:53]));
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
if (flags & _INVERSE_FLAG == _INVERSE_FLAG) {
return spread * amount * (10 ** oracle.decimals()) / latestAnswer.toUint256() / _SPREAD_DENOMINATOR;
} else {
return spread * amount * latestAnswer.toUint256() / (10 ** oracle.decimals()) / _SPREAD_DENOMINATOR;
}
}
}

/// @notice Calculates price of token A relative to token B. Note that order is important
/// @return result Token A relative price times amount
function doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, uint256 spread, int256 decimalsScale, uint256 amount) external view returns(uint256 result) {
function _doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, int256 decimalsScale, uint256 amount) internal view returns(uint256 result) {
if (oracle1.decimals() != oracle2.decimals()) revert DifferentOracleDecimals();

{
(, int256 latestAnswer1,, uint256 updatedAt,) = oracle1.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice(); // solhint-disable-line not-rely-on-time
result = amount * spread * latestAnswer1.toUint256();
(, int256 latestAnswer,, uint256 updatedAt,) = oracle1.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
result = amount * latestAnswer.toUint256();
}

if (decimalsScale > 0) {
Expand All @@ -85,9 +97,12 @@ contract ChainlinkCalculator is IAmountGetter {
}

{
(, int256 latestAnswer2,, uint256 updatedAt,) = oracle2.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice(); // solhint-disable-line not-rely-on-time
result /= latestAnswer2.toUint256() * _SPREAD_DENOMINATOR;
(, int256 latestAnswer,, uint256 updatedAt,) = oracle2.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
result /= latestAnswer.toUint256();
}
}
}

// solhint-enable not-rely-on-time
2 changes: 1 addition & 1 deletion contracts/extensions/ETHOrders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
address private immutable _limitOrderProtocol;
IWETH private immutable _WETH; // solhint-disable-line var-name-mixedcase
/// @notice Makers and their uint96 ETH balances in single mapping.
mapping(bytes32 => ETHOrder) public ordersMakersBalances;
mapping(bytes32 orderHash => ETHOrder data) public ordersMakersBalances;

event ETHDeposited(bytes32 orderHash, uint256 amount);
event ETHOrderCancelled(bytes32 orderHash, uint256 amount);
Expand Down
2 changes: 1 addition & 1 deletion contracts/extensions/OrderIdInvalidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract OrderIdInvalidator is IPreInteraction {
/// @notice Limit order protocol address.
address private immutable _limitOrderProtocol;
/// @notice Stores corresponding maker orders ids and hashes.
mapping(address => mapping(uint32 => bytes32)) private _ordersIdsHashes;
mapping(address maker => mapping(uint32 orderId => bytes32 orderHash)) private _ordersIdsHashes;

/// @notice Only limit order protocol can call this contract.
modifier onlyLimitOrderProtocol() {
Expand Down
18 changes: 9 additions & 9 deletions contracts/helpers/SeriesEpochManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity 0.8.19;
/// @title A helper contract to manage nonce with the series
contract SeriesEpochManager {
error AdvanceEpochFailed();
event EpochIncreased(address indexed maker, uint256 series, uint256 newNonce);
event EpochIncreased(address indexed maker, uint256 series, uint256 newEpoch);

// {
// 1: {
Expand All @@ -18,7 +18,7 @@ contract SeriesEpochManager {
// },
// ...
// }
mapping(uint256 => uint256) private _epochs;
mapping(uint256 seriesId => uint256 epoch) private _epochs;

/// @notice Returns nonce for `maker` and `series`
function epoch(address maker, uint96 series) public view returns(uint256) {
Expand All @@ -35,15 +35,15 @@ contract SeriesEpochManager {
if (amount == 0 || amount > 255) revert AdvanceEpochFailed();
unchecked {
uint256 key = uint160(msg.sender) | (uint256(series) << 160);
uint256 newNonce = _epochs[key] + amount;
_epochs[key] = newNonce;
emit EpochIncreased(msg.sender, series, newNonce);
uint256 newEpoch = _epochs[key] + amount;
_epochs[key] = newEpoch;
emit EpochIncreased(msg.sender, series, newEpoch);
}
}

/// @notice Checks if `maker` has specified `makerNonce` for `series`
/// @return Result True if `maker` has specified nonce. Otherwise, false
function epochEquals(address maker, uint256 series, uint256 makerNonce) public view returns(bool) {
return _epochs[uint160(maker) | (uint256(series) << 160)] == makerNonce;
/// @notice Checks if `maker` has specified `makerEpoch` for `series`
/// @return Result True if `maker` has specified epoch. Otherwise, false
function epochEquals(address maker, uint256 series, uint256 makerEpoch) public view returns(bool) {
return _epochs[uint160(maker) | (uint256(series) << 160)] == makerEpoch;
}
}
2 changes: 1 addition & 1 deletion contracts/helpers/SeriesNonceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract SeriesNonceManager {
// },
// ...
// }
mapping(uint256 => mapping(address => uint256)) public nonce;
mapping(uint256 series => mapping(address maker => uint256 nonce)) public nonce;

/// @notice Advances nonce by one
function increaseNonce(uint8 series) external {
Expand Down
23 changes: 21 additions & 2 deletions contracts/interfaces/IOrderMixin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,32 @@ interface IOrderMixin {
);

/**
* @notice Emitted when order gets filled
* @notice Emitted when order without `useBitInvalidator` gets cancelled
* @param orderHash Hash of the order
*/
event OrderCancelled(
bytes32 orderHash
);

/**
* @notice Emitted when order with `useBitInvalidator` gets cancelled
* @param maker Maker address
* @param slotIndex Slot index that was updated
* @param slotValue New slot value
*/
event BitInvalidatorUpdated(
address indexed maker,
uint256 slotIndex,
uint256 slotValue
);

/**
* @notice Executes a permit and then calls a specified action using delegatecall.
* @param permit The permit data, including the token address, permit parameters and permit signature.
* @param action The action data to be executed using delegatecall.
*/
function permitAndCall(bytes calldata permit, bytes calldata action) external;

/**
* @notice Returns bitmask for double-spend invalidators based on lowest byte of order.info and filled quotes
* @param maker Maker address
Expand All @@ -74,7 +93,7 @@ interface IOrderMixin {
/**
* @notice Returns bitmask for double-spend invalidators based on lowest byte of order.info and filled quotes
* @param orderHash Hash of the order
* @return remainingRaw Remaining amount of the order plus 1 if order was partially filled, otherwise 0
* @return remainingRaw Inverse of the remaining amount of the order if order was filled at least once, otherwise 0
*/
function rawRemainingInvalidatorForOrder(address maker, bytes32 orderHash) external view returns(uint256 remainingRaw);

Expand Down
8 changes: 5 additions & 3 deletions contracts/libraries/BitInvalidatorLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ library BitInvalidatorLib {
error BitInvalidatedOrder();

struct Data {
mapping(uint256 => uint256) _raw;
mapping(uint256 slotIndex => uint256 slotData) _raw;
}

/**
Expand Down Expand Up @@ -51,10 +51,12 @@ library BitInvalidatorLib {
* @param self The data structure.
* @param nonce The nonce identifying the slot.
* @param additionalMask A mask of bits to be invalidated.
* @return result Resulting validity status of entities in the slot as a uint256.
*/
function massInvalidate(Data storage self, uint256 nonce, uint256 additionalMask) internal {
function massInvalidate(Data storage self, uint256 nonce, uint256 additionalMask) internal returns(uint256 result) {
uint256 invalidatorSlot = nonce >> 8;
uint256 invalidatorBits = (1 << (nonce & 0xff)) | additionalMask;
self._raw[invalidatorSlot] |= invalidatorBits;
result = self._raw[invalidatorSlot] | invalidatorBits;
self._raw[invalidatorSlot] = result;
}
}
4 changes: 2 additions & 2 deletions contracts/libraries/ExtensionLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ library ExtensionLib {
* @dev The first 32 bytes of an extension calldata contain offsets to the end of each field within the calldata.
* @param extension The calldata from which the field is to be retrieved.
* @param field The specific dynamic field to retrieve from the extension.
* @return result A bytes calldata representing the requested field.
* @return calldata Bytes representing the requested field.
*/
function _get(bytes calldata extension, DynamicField field) private pure returns(bytes calldata result) {
function _get(bytes calldata extension, DynamicField field) private pure returns(bytes calldata) {
if (extension.length < 0x20) return msg.data[:0];

Offsets offsets;
Expand Down
1 change: 1 addition & 0 deletions contracts/libraries/MakerTraitsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type MakerTraits is uint256;
* High bits are used for flags
* 255 bit `NO_PARTIAL_FILLS_FLAG` - if set, the order does not allow partial fills
* 254 bit `ALLOW_MULTIPLE_FILLS_FLAG` - if set, the order permits multiple fills
* 253 bit - unused
* 252 bit `PRE_INTERACTION_CALL_FLAG` - if set, the order requires pre-interaction call
* 251 bit `POST_INTERACTION_CALL_FLAG` - if set, the order requires post-interaction call
* 250 bit `NEED_CHECK_EPOCH_MANAGER_FLAG` - if set, the order requires to check the epoch manager
Expand Down
4 changes: 2 additions & 2 deletions contracts/libraries/RemainingInvalidatorLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ type RemainingInvalidator is uint256;
* @title RemainingInvalidatorLib
* @notice The library provides a mechanism to invalidate order based on the remaining amount of the order.
* @dev The remaining amount is used as a nonce to invalidate the order.
* When order is created, the remaining invalidator is 0. When the order is filled, the remaining invalidator is type(uint256).max.
* When order is filled partially, the remaining invalidator is the negation of the remaining amount.
* When order is created, the remaining invalidator is 0.
* When order is filled, the remaining invalidator is the inverse of the remaining amount.
*/
library RemainingInvalidatorLib {

Expand Down
24 changes: 24 additions & 0 deletions contracts/mocks/TakerContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import { IOrderMixin } from "../interfaces/IOrderMixin.sol";
import { TakerTraits } from "../libraries/TakerTraitsLib.sol";

contract TakerContract {
IOrderMixin private immutable _swap;

constructor(IOrderMixin swap) {
_swap = swap;
}

function fillOrder(
IOrderMixin.Order calldata order,
bytes32 r,
bytes32 vs,
uint256 amount,
TakerTraits takerTraits
) external payable {
_swap.fillOrder {value: msg.value} (order, r, vs, amount, takerTraits);
}
}
Loading

0 comments on commit d1b5fde

Please sign in to comment.