Skip to content

Commit

Permalink
Feature/dutch auction cb bid (#1027)
Browse files Browse the repository at this point in the history
Co-authored-by: Taylor Brent <taylor.w.brent@gmail.com>
  • Loading branch information
jankjr and tbrent authored Jan 15, 2024
1 parent b507dd3 commit 5ade2e1
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 84 deletions.
45 changes: 45 additions & 0 deletions contracts/plugins/mocks/CallbackDutchTradeBidder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IDutchTradeCallee, DutchTrade } from "../trading/DutchTrade.sol";

contract CallbackDutchTraderBidder is IDutchTradeCallee {
function bid(DutchTrade trade) external {
trade.bidWithCallback(new bytes(0));
}

function dutchTradeCallback(
address buyToken,
uint256 buyAmount,
bytes calldata
) external {
IERC20(buyToken).transfer(msg.sender, buyAmount);
}
}

contract CallbackDutchTraderBidderLowBaller is IDutchTradeCallee {
function bid(DutchTrade trade) external {
trade.bidWithCallback(new bytes(0));
}

function dutchTradeCallback(
address buyToken,
uint256 buyAmount,
bytes calldata
) external {
IERC20(buyToken).transfer(msg.sender, buyAmount - 1);
}
}

contract CallbackDutchTraderBidderNoPayer is IDutchTradeCallee {
function bid(DutchTrade trade) external {
trade.bidWithCallback(new bytes(0));
}

function dutchTradeCallback(
address buyToken,
uint256 buyAmount,
bytes calldata
) external {}
}
81 changes: 73 additions & 8 deletions contracts/plugins/trading/DutchTrade.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ import "../../interfaces/IAsset.sol";
import "../../interfaces/IBroker.sol";
import "../../interfaces/ITrade.sol";

interface IDutchTradeCallee {
function dutchTradeCallback(
address buyToken,
// {qBuyTok}
uint256 buyAmount,
bytes calldata data
) external;
}

enum BidType {
NONE,
CALLBACK,
TRANSFER
}

// A dutch auction in 4 parts:
// 1. 0% - 20%: Geometric decay from 1000x the bestPrice to ~1.5x the bestPrice
// 2. 20% - 45%: Linear decay from ~1.5x the bestPrice to the bestPrice
Expand Down Expand Up @@ -77,6 +92,8 @@ contract DutchTrade is ITrade {
// solhint-disable-next-line var-name-mixedcase
uint48 public immutable ONE_BLOCK; // {s} 1 block based on network

BidType public bidType; // = BidType.NONE

TradeStatus public status; // reentrancy protection

IBroker public broker; // The Broker that cloned this contract into existence
Expand Down Expand Up @@ -183,7 +200,7 @@ contract DutchTrade is ITrade {
bestPrice = _bestPrice; // gas-saver
}

/// Bid for the auction lot at the current price; settling atomically via a callback
/// Bid for the auction lot at the current price; settle trade in protocol
/// @dev Caller must have provided approval
/// @return amountIn {qBuyTok} The quantity of tokens the bidder paid
function bid() external returns (uint256 amountIn) {
Expand All @@ -195,10 +212,48 @@ contract DutchTrade is ITrade {
// {qBuyTok}
amountIn = _bidAmount(price);

// Transfer in buy tokens
// Mark bidder
bidder = msg.sender;
bidType = BidType.TRANSFER;

// status must begin OPEN
assert(status == TradeStatus.OPEN);

// reportViolation if auction cleared in geometric phase
if (price > bestPrice.mul(ONE_POINT_FIVE, CEIL)) {
broker.reportViolation();
}

// Transfer in buy tokens from bidder
buy.safeTransferFrom(msg.sender, address(this), amountIn);

// settle() in core protocol
origin.settleTrade(sell);

// confirm .settleTrade() succeeded and .settle() has been called
assert(status == TradeStatus.CLOSED);
}

/// Bid with callback for the auction lot at the current price; settle trade in protocol
/// Sold funds are sent back to the callee first via callee.dutchTradeCallback(...)
/// Balance of buy token must increase by bidAmount(current block) after callback
///
/// @dev Caller must implement IDutchTradeCallee
/// @param data {bytes} The data to pass to the callback
/// @return amountIn {qBuyTok} The quantity of tokens the bidder paid
function bidWithCallback(bytes calldata data) external returns (uint256 amountIn) {
require(bidder == address(0), "bid already received");

// {buyTok/sellTok}
uint192 price = _price(block.number); // enforces auction ongoing

// {qBuyTok}
amountIn = _bidAmount(price);

// Mark bidder
bidder = msg.sender;
bidType = BidType.CALLBACK;

// status must begin OPEN
assert(status == TradeStatus.OPEN);

Expand All @@ -207,10 +262,20 @@ contract DutchTrade is ITrade {
broker.reportViolation();
}

// settle() via callback
// Transfer sell tokens to bidder
sell.safeTransfer(bidder, lot()); // {qSellTok}

uint256 balanceBefore = buy.balanceOf(address(this)); // {qBuyTok}
IDutchTradeCallee(bidder).dutchTradeCallback(address(buy), amountIn, data);
require(
amountIn <= buy.balanceOf(address(this)) - balanceBefore,
"insufficient buy tokens"
);

// settle() in core protocol
origin.settleTrade(sell);

// confirm callback succeeded
// confirm .settleTrade() succeeded and .settle() has been called
assert(status == TradeStatus.CLOSED);
}

Expand All @@ -223,13 +288,13 @@ contract DutchTrade is ITrade {
returns (uint256 soldAmt, uint256 boughtAmt)
{
require(msg.sender == address(origin), "only origin can settle");
require(bidder != address(0) || block.number > endBlock, "auction not over");

// Received bid
if (bidder != address(0)) {
if (bidType == BidType.CALLBACK) {
soldAmt = lot(); // {qSellTok}
} else if (bidType == BidType.TRANSFER) {
soldAmt = lot(); // {qSellTok}
sell.safeTransfer(bidder, soldAmt); // {qSellTok}
} else {
require(block.number > endBlock, "auction not over");
}

// Transfer remaining balances back to origin
Expand Down
113 changes: 113 additions & 0 deletions contracts/plugins/trading/DutchTradeRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IDutchTradeCallee, TradeStatus, DutchTrade } from "../trading/DutchTrade.sol";
import { IMain } from "../../interfaces/IMain.sol";

/** @title DutchTradeRouter
* @notice Utility contract for placing bids on DutchTrade auctions
* @dev This contract is needed as end user wallets cannot call DutchTrade.bid directly anymore,
tests and UI need to be updated to use this contract
*/
contract DutchTradeRouter is IDutchTradeCallee {
using SafeERC20 for IERC20;
struct Bid {
/// @notice The DutchTrade that was bid on
DutchTrade trade;
/// @notice The token sold to the protocol
IERC20 sellToken;
/// @notice The amount of tokenIn the protocol got {qSellAmt}
uint256 sellAmt;
/// @notice The token bought from the trade
IERC20 buyToken;
/// @notice The amount of tokenOut the we got {qBuyAmt}
uint256 buyAmt;
}

/// @notice Emitted when a bid is placed
/// @param main The main contract of the rToken
/// @param trade The DutchTrade that was bid on
/// @param bidder The address of the bidder
/// @param sellToken The token being sold by the protocol
/// @param soldAmt The amount of sellToken sold {qSellToken}
/// @param buyToken The token being bought by the protocol
/// @param boughtAmt The amount of buyToken bought {qBuyToken}
event BidPlaced(
IMain main,
DutchTrade trade,
address bidder,
IERC20 sellToken,
uint256 soldAmt,
IERC20 buyToken,
uint256 boughtAmt
);
DutchTrade private _currentTrade;

/// Place a bid on an OPEN dutch auction
/// @param trade The DutchTrade to bid on
/// @param recipient The recipient of the tokens out
/// @dev Requires msg.sender has sufficient approval on the tokenIn with router
/// @dev Requires msg.sender has sufficient balance on the tokenIn
function bid(DutchTrade trade, address recipient) external returns (Bid memory) {
Bid memory out = _placeBid(trade, msg.sender);
_sendBalanceTo(out.sellToken, recipient);
_sendBalanceTo(out.buyToken, recipient);
return out;
}

/// @notice Callback for DutchTrade
/// @param buyToken The token DutchTrade is expecting to receive
/// @param buyAmount The amt the DutchTrade is expecting to receive {qBuyToken}
/// @notice Data is not used here
function dutchTradeCallback(
address buyToken,
uint256 buyAmount,
bytes calldata
) external {
require(msg.sender == address(_currentTrade), "Incorrect callee");
IERC20(buyToken).safeTransfer(msg.sender, buyAmount); // {qBuyToken}
}

function _sendBalanceTo(IERC20 token, address to) internal {
uint256 bal = token.balanceOf(address(this));
token.safeTransfer(to, bal);
}

/// Helper for placing bid on DutchTrade
/// @notice pulls funds from 'bidder'
/// @notice Does not send proceeds anywhere, funds have to be transfered out after this call
/// @notice non-reentrant, uses _currentTrade to prevent reentrancy
function _placeBid(DutchTrade trade, address bidder) internal returns (Bid memory out) {
// Prevent reentrancy
require(_currentTrade == DutchTrade(address(0)), "already bidding");
require(trade.status() == TradeStatus.OPEN, "trade not open");
_currentTrade = trade;
out.trade = trade;
out.buyToken = IERC20(trade.buy());
out.sellToken = IERC20(trade.sell());
out.buyAmt = trade.bidAmount(block.number); // {qBuyToken}
out.buyToken.safeTransferFrom(bidder, address(this), out.buyAmt);

uint256 sellAmt = out.sellToken.balanceOf(address(this)); // {qSellToken}

uint256 expectedSellAmt = trade.lot(); // {qSellToken}
trade.bidWithCallback(new bytes(0));

sellAmt = out.sellToken.balanceOf(address(this)) - sellAmt; // {qSellToken}
require(sellAmt >= expectedSellAmt, "insufficient amount out");
out.sellAmt = sellAmt; // {qSellToken}

_currentTrade = DutchTrade(address(0));
emit BidPlaced(
IMain(address(out.trade.broker().main())),
out.trade,
bidder,
out.sellToken,
out.sellAmt,
out.buyToken,
out.buyAmt
);
}
}
6 changes: 4 additions & 2 deletions docs/mev.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Bidding instructions from the `DutchTrade` contract:
`DutchTrade` (relevant) interface:

```solidity
function bid() external; // execute a bid at the current block number
function bid(bytes memory data) external; // execute a bid at the current block number
function sell() external view returns (IERC20);
Expand All @@ -43,13 +43,15 @@ function bidAmount(uint256 blockNumber) external view returns (uint256); // {qBu

To participate:

Make sure calling contract implements the `IDutchTradeCallee` interface. It contains a single method function `dutchTradeCallbac(address buyToken,uint256 buyAmount,bytes calldata data) external;`. This method will be called by the `DutchTrade` as a callback after calling `bidWithCallback` and before the trade has been resolved. The trader is expected to pay for the trade during the callback. See `DutchTradeRouter.sol` for an example.

1. Call `status()` view; the auction is ongoing if return value is 1
2. Call `lot()` to see the number of tokens being sold
3. Call `bidAmount()` to see the number of tokens required to buy the lot, at various block numbers
4. After finding an attractive bidAmount, provide an approval for the `buy()` token. The spender should be the `DutchTrade` contract.
**Note**: it is very important to set tight approvals! Do not set more than the `bidAmount()` for the desired bidding block else reorgs present risk.
5. Wait until the desired block is reached (hopefully not in the first 40% of the auction)
6. Call `bid()`. If someone else completes the auction first, this will revert with the error message "bid already received". Approvals do not have to be revoked in the event that another MEV searcher wins the auction. (Though ideally the searcher includes the approval in the same tx they `bid()`)
6. Call `bidWithCallback()`. If someone else completes the auction first, this will revert with the error message "bid already received". Approvals do not have to be revoked in the event that another MEV searcher wins the auction. (Though ideally the searcher includes the approval in the same tx they `bid()`)

For a sample price curve, see [docs/system-design.md](./system-design.md#sample-price-curve)

Expand Down
20 changes: 12 additions & 8 deletions tasks/testing/upgrade-checker-utils/trades.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { QUEUE_START, TradeKind, TradeStatus } from '#/common/constants'
import { bn, fp } from '#/common/numbers'
import { whileImpersonating } from '#/utils/impersonation'
import {
advanceBlocks,
advanceTime,
getLatestBlockTimestamp,
getLatestBlockNumber,
getLatestBlockTimestamp,
} from '#/utils/time'
import { DutchTrade } from '@typechain/DutchTrade'
import { GnosisTrade } from '@typechain/GnosisTrade'
import { TestITrading } from '@typechain/TestITrading'
import { BigNumber, ContractTransaction } from 'ethers'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { QUEUE_START, TradeKind, TradeStatus } from '#/common/constants'
import { Interface, LogDescription } from 'ethers/lib/utils'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { collateralToUnderlying, whales } from './constants'
import { bn, fp } from '#/common/numbers'
import { logToken } from './logs'
import { GnosisTrade } from '@typechain/GnosisTrade'
import { DutchTrade } from '@typechain/DutchTrade'

export const runBatchTrade = async (
hre: HardhatRuntimeEnvironment,
Expand Down Expand Up @@ -86,6 +86,7 @@ export const runDutchTrade = async (
trader: TestITrading,
tradeToken: string
): Promise<[boolean, string]> => {
const router = await (await hre.ethers.getContractFactory('DutchTradeRouter')).deploy()
// NOTE:
// buy & sell are from the perspective of the auction-starter
// bid() flips it to be from the perspective of the trader
Expand Down Expand Up @@ -116,9 +117,12 @@ export const runDutchTrade = async (

await whileImpersonating(hre, whaleAddr, async (whale) => {
const sellToken = await hre.ethers.getContractAt('ERC20Mock', buyTokenAddress)
await sellToken.connect(whale).approve(trade.address, buyAmount)
// Bid
;[tradesRemain, newSellToken] = await callAndGetNextTrade(trade.connect(whale).bid(), trader)

;[tradesRemain, newSellToken] = await callAndGetNextTrade(
router.bid(trade.address, await router.signer.getAddress()),
trader
)
})

if (
Expand Down
Loading

0 comments on commit 5ade2e1

Please sign in to comment.