Contract Name:
SecondaryMarket
Contract Source Code:
<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
/**
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2016-2019 zOS Global Limited
*
*/
pragma solidity >=0.8.0 <0.9.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP. Does not include
* the optional functions; to access them see `ERC20Detailed`.
*/
interface IERC20 {
// Optional functions
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a `Transfer` event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through `transferFrom`. This is
* zero by default.
*
* This value changes when `approve` or `transferFrom` are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* > Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an `Approval` event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a `Transfer` event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to `approve`. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import {IERC20} from "../ERC20/IERC20.sol";
/**
* @title Intent
* @author Luzius Meisser, luzius@aktionariat.com
* @author Murat Ögat, murat@aktionariat.com
*
* The struct to be signed for submitting orders to the TradeReactor contract and its hashing per EIP-712.
*/
struct Intent {
address owner;
address filler;
address tokenOut; // The ERC20 token sent out
uint256 amountOut; // The maximum amount
address tokenIn; // The ERC20 token received
uint256 amountIn; // The amount received in exchange for the maximum of the sent token
uint256 creation; // timestamp at which the intent was created
uint256 expiration; // timestamp at which the intent expires
bytes data;
}
library IntentHash {
bytes32 internal constant INTENT_TYPE_HASH = keccak256("Intent(address owner,address filler,address tokenOut,uint256 amountOut,address tokenIn,uint256 amountIn,uint256 creation,uint256 expiration,bytes data)");
function hash(Intent calldata intent) internal pure returns (bytes32) {
return
keccak256(
abi.encode(
INTENT_TYPE_HASH,
intent.owner,
intent.filler,
intent.tokenOut,
intent.amountOut,
intent.tokenIn,
intent.amountIn,
intent.creation,
intent.expiration,
keccak256(intent.data)
)
);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import {Intent} from "./IntentHash.sol";
interface IReactor {
function verify(Intent calldata intent, bytes calldata sig) external view;
function signalIntent(Intent calldata intent, bytes calldata signature) external;
function getFilledAmount(Intent calldata intent) external view returns (uint256);
function verifyPriceMatch(Intent calldata buyerIntent, Intent calldata sellerIntent) external pure;
function getTotalExecutionPrice(Intent calldata buyerIntent, Intent calldata sellerIntent, uint256 tradedAmount) external pure returns (uint256);
function getBid(Intent calldata intent, uint256 amount) external pure returns (uint256);
function process(Intent calldata sellerIntent, bytes calldata sellerSig, Intent calldata buyerIntent, bytes calldata buyerSig, uint256 tradedAmount, uint256 totalFee) external;
function cancelIntent(Intent calldata intent) external;
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "../ERC20/IERC20.sol";
import "../utils/Ownable.sol";
import {IReactor} from "./IReactor.sol";
import {Intent, IntentHash} from "./IntentHash.sol";
/**
* @title SecondaryMarket
*
* @author Luzius Meisser, luzius@aktionariat.com
* @author Murat Ögat, murat@aktionariat.com
*
* @notice This contract handles the secondary market transactions and is controlled by the issuer.
* @notice It works together with a TradeReactor contract to verify and process trade intents.
* @notice If a filler is specified when creating and signing an intent, only this contract can forward it to the reactor for processing.
* @notice If a router is specified in this contract, only that address can call the process function to execute trades.
* @notice This contract also collects and distributes trading fees, if any.
*/
contract SecondaryMarket is Ownable {
using IntentHash for Intent;
// Version
// 1: initial version
uint16 public constant VERSION = 1;
uint16 public constant ALL = 10000;
address public constant LICENSE_FEE_RECIPIENT = 0x29Fe8914e76da5cE2d90De98a64d0055f199d06D;
uint256 public constant CANCELLED = type(uint256).max;
address public immutable CURRENCY;
address public immutable TOKEN;
address public immutable REACTOR;
event TradingFeeCollected(address currency, uint256 actualFee, address spreadRecipient, uint256 returnedSpread);
event TradingFeeWithdrawn(address currency, address target, uint256 amount);
event LicenseFeePaid(address currency, address target, uint256 amount);
event MarketStatusChanged(bool isOpen, uint256 timestamp);
event Trade(address indexed seller, address indexed buyer, bytes32 sellIntentHash, bytes32 buyIntentHash, address token, uint256 tokenAmount, address currency, uint256 currencyAmount, uint256 fees);
error LargerSpreadNeeded(uint256 feesCollected, uint256 requiredMinimum);
error WrongFiller();
error WrongTokens();
error WrongRouter(address expected, address actual);
error InvalidConfiguration();
error MarketClosed();
error AlreadyFilled();
error UserCancelled();
address public router; // null for any, 20B
uint16 public tradingFeeBips; // 2B
uint16 public licenseShare; // Share of the trading fee that goes to the router in bips
bool public isOpen;
constructor(address owner, address currency, address token, address _reactor, address _router) Ownable(owner) {
CURRENCY = currency;
TOKEN = token;
REACTOR = _reactor;
tradingFeeBips = 190; // default trading fee is 1.9%
licenseShare = 5000; // default license share is 50% of trading fee
router = _router;
isOpen = true;
}
//// ADMINISTRATION ////
/**
* Opens the market.
*/
function open() onlyOwner external {
isOpen = true;
emit MarketStatusChanged(true, block.timestamp);
}
/**
* Closes the market.
*/
function close() onlyOwner external {
isOpen = false;
emit MarketStatusChanged(false, block.timestamp);
}
/**
* Configures the permissible router or the null address for any.
*
* Having a trusted router helps with the prevention of front-running attacks as no
* one else can front the router with a different matching of the submitted orders.
*/
function setRouter(address router_) onlyOwner external {
router = router_;
}
/**
* Configures the software license fee as agreed with the copyright owners.
*/
function setLicenseFee(uint16 licenseShare_) onlyOwner external {
if (uint256(licenseShare_) > ALL) revert InvalidConfiguration();
licenseShare = licenseShare_;
}
function setTradingFee(uint16 tradingFeeBips_) onlyOwner external {
if (tradingFeeBips_ > 500) revert InvalidConfiguration(); // commit to never set it above 5%
tradingFeeBips = tradingFeeBips_;
}
//// TRADING ////
/**
* Create an order intent that can be signed by the owner.
*/
function createBuyOrder(address owner, uint256 amountOut, uint256 amountIn, uint24 validitySeconds) public view returns (Intent memory) {
return Intent(owner, address(this), CURRENCY, amountOut, TOKEN, amountIn, block.timestamp, block.timestamp + validitySeconds, new bytes(0));
}
/**
* Create an order intent that can be signed by the owner.
* The tokenIn amount is reduced by the trading fee, which is always charged to the seller.
*/
function createSellOrder(address owner, uint256 amountOut, uint256 amountIn, uint24 validitySeconds) public view returns (Intent memory) {
return Intent(owner, address(this), TOKEN, amountOut, CURRENCY, amountIn, block.timestamp, block.timestamp + validitySeconds, new bytes(0));
}
function getIntentHash(Intent calldata intent) external pure returns (bytes32) {
return intent.hash();
}
/**
* Stores an order in the Ethereum blockchain as a publicly readable event, so any allowed router
* can pick it up and execute it against another valid order.
*
* In case the owner configured a specific router to be used, it is usually better to send the
* order to the configured router directly through a suitable API. Note that all partially filled
* orders and all filled orders are publicly recorded on-chain anyway, so taking the direct
* transmission shortcut does not effectively preserve privacy.
*
* To invalidate an order, the owner must call the invalidateNonce function on the SignatureTransfer
*
* contract found in this.ROUTER().TRANSFER().
*/
function placeOrder(Intent calldata intent, bytes calldata signature) external {
verifySignature(intent, signature);
IReactor(REACTOR).signalIntent(intent, signature);
}
/**
* Verify the signature of an order.
*/
function verifySignature(Intent calldata intent, bytes calldata sig) public view {
if (intent.filler != address(this)) revert WrongFiller();
IReactor(REACTOR).verify(intent, sig);
}
/**
* Check if an order can be executed and if yes, returns the maximum amount of the tokenOut.
*/
function validateOrder(Intent calldata intent, bytes calldata sig) external view returns (uint256 unfilled, uint256 balance, uint256 allowance) {
verifySignature(intent, sig);
require((intent.tokenOut == TOKEN && intent.tokenIn == CURRENCY) || (intent.tokenOut == CURRENCY && intent.tokenIn == TOKEN), WrongTokens());
balance = IERC20(intent.tokenOut).balanceOf(intent.owner);
allowance = IERC20(intent.tokenOut).allowance(intent.owner, REACTOR);
uint256 amountTokens = (intent.tokenOut == TOKEN) ? intent.amountOut : intent.amountIn;
uint256 alreadyFilled = IReactor(REACTOR).getFilledAmount(intent);
if (alreadyFilled == CANCELLED) revert UserCancelled();
if (amountTokens <= alreadyFilled) revert AlreadyFilled();
uint256 remaining = amountTokens - alreadyFilled;
return (remaining, balance, allowance);
}
/**
* Returns multiple order book entries for an array of intents, avoiding multiple calls.
* The returned numbers are to be used directly in the order book.
* Therefore, this function doesn't revert, it returns 0 for unexecutable entries instead.
*/
function executableAmounts(Intent[] calldata intents) public view returns (uint256[] memory) {
uint256[] memory available = new uint256[](intents.length);
for (uint256 i = 0; i < intents.length; i++) {
available[i] = executableAmount(intents[i]);
}
return available;
}
/**
* Check if an order can be executed and if yes, returns the maximum amount in TOKENS that can be executed immediately.
* Considers the unfilled amount, and also the actual balance and allowance of the intent owner.
* This is useful for user interfaces to show how much can be traded right now.
* This function should never return zero. It should either revert or return a non-zero value.
*/
function executableAmount(Intent calldata intent) public view returns (uint256) {
if (intent.tokenOut == TOKEN && intent.tokenIn == CURRENCY) {
return executableSellAmount(intent);
} else if (intent.tokenOut == CURRENCY && intent.tokenIn == TOKEN) {
return executableBuyAmount(intent);
} else {
revert WrongTokens();
}
}
/**
* Internal counterpart of getAvailableForExecution for selling.
* This is straightforward as we can directly check the token balance and allowance.
*/
function executableSellAmount(Intent calldata intent) internal view returns (uint256) {
uint256 alreadyFilled = IReactor(REACTOR).getFilledAmount(intent);
uint256 balance = IERC20(intent.tokenOut).balanceOf(intent.owner);
uint256 allowance = IERC20(intent.tokenOut).allowance(intent.owner, REACTOR);
if (intent.amountOut <= alreadyFilled) return 0;
uint256 unfilled = intent.amountOut - alreadyFilled;
uint256 availableInWallet = (balance < allowance) ? balance : allowance;
uint256 finalAvailable = (unfilled < availableInWallet) ? unfilled : availableInWallet;
return finalAvailable;
}
/**
* Internal counterpart of getAvailableForExecution for buying.
* This is slightly more tricky, as we need to check balance/allowance in CURRENCY
* but return available amount in TOKENS, so getBid() is used to get the conversion rate.
*/
function executableBuyAmount(Intent calldata intent) internal view returns (uint256) {
uint256 alreadyFilled = IReactor(REACTOR).getFilledAmount(intent);
uint256 bid = IReactor(REACTOR).getBid(intent, 1);
uint256 balanceInShares = IERC20(intent.tokenOut).balanceOf(intent.owner) / bid;
uint256 allowanceInShares = IERC20(intent.tokenOut).allowance(intent.owner, REACTOR) / bid;
if (intent.amountIn <= alreadyFilled) return 0;
uint256 unfilled = intent.amountIn - alreadyFilled;
uint256 availableInShares = (balanceInShares < allowanceInShares) ? balanceInShares : allowanceInShares;
uint256 finalAvailable = (unfilled < availableInShares) ? unfilled : availableInShares;
return finalAvailable;
}
/**
* Convenience method to check with 2 intents to get what can be executed right now.
* Takes into account unfilled amounts, balances and allowances of both sides.
* Also reverts if price is not matching, with OfferTooLow().
*/
function executableTrade(Intent calldata sellerIntent, Intent calldata buyerIntent) external view returns (uint256) {
IReactor(REACTOR).verifyPriceMatch(buyerIntent, sellerIntent);
uint256 executableSell = executableSellAmount(sellerIntent);
uint256 executableBuy = executableBuyAmount(buyerIntent);
return (executableSell < executableBuy) ? executableSell : executableBuy;
}
function process(Intent calldata seller, bytes calldata sellerSig, Intent calldata buyer, bytes calldata buyerSig, uint256 tradedAmount) external {
if (!isOpen) revert MarketClosed();
if (router != address(0) && msg.sender != router) revert WrongRouter(msg.sender, router);
uint256 totalExecutionPrice = IReactor(REACTOR).getTotalExecutionPrice(buyer, seller, tradedAmount);
uint256 totalFee = totalExecutionPrice * tradingFeeBips / 10000;
IReactor(REACTOR).process(seller, sellerSig, buyer, buyerSig, tradedAmount, totalFee);
emit Trade(seller.owner, buyer.owner, seller.hash(), buyer.hash(), seller.tokenOut, tradedAmount, seller.tokenIn, totalExecutionPrice, totalFee);
}
function cancelIntent(Intent calldata intent) external {
if (msg.sender != router && msg.sender != owner) revert WrongRouter(msg.sender, router);
IReactor(REACTOR).cancelIntent(intent);
}
/**
* Withdraw the accumulated fees applying a 50/50 split between the two addresses.
*
* The assumption is that this can be used to collect accumulated trading fees and to pay license fees
* to Aktionariat in the same transaction for convenience.
*/
function withdrawFees() external {
withdrawFees(CURRENCY, IERC20(CURRENCY).balanceOf(address(this)));
}
function withdrawFees(address currency, uint256 amount) public onlyOwner {
uint256 split = amount * licenseShare / 10000;
IERC20(currency).transfer(owner, amount - split); // rounded up
IERC20(currency).transfer(LICENSE_FEE_RECIPIENT, split); // rounded down
emit LicenseFeePaid(currency, LICENSE_FEE_RECIPIENT, split);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
//
// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol
//
// Modifications:
// - Replaced Context._msgSender() with msg.sender
// - Made leaner
// - Extracted interface
pragma solidity >=0.8.0 <0.9.0;
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
error Ownable_NotOwner(address sender);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor (address initialOwner) {
owner = initialOwner;
emit OwnershipTransferred(address(0), owner);
}
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) external onlyOwner {
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
function _checkOwner() internal view {
if (msg.sender != owner) {
revert Ownable_NotOwner(msg.sender);
}
}
}