Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 18 additions & 16 deletions script/interfaces/ICatFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@ interface ICatFactory {
// ============================================================================================

struct DeployParams {
address borrowToken;
address collateralToken;
address priceOracle;
address borrow_token;
address collateral_token;
address price_oracle;
address management;
address performanceFeeRecipient;
uint256 minimumDebt;
uint256 minimumCollateralRatio;
uint256 upfrontInterestPeriod;
uint256 interestRateAdjCooldown;
uint256 redemptionMinimumPriceBufferPercentage;
uint256 redemptionStartingPriceBufferPercentage;
uint256 redemptionReKickStartingPriceBufferPercentage;
uint256 liquidationMinimumPriceBufferPercentage;
uint256 liquidationStartingPriceBufferPercentage;
uint256 stepDuration;
uint256 stepDecayRate;
uint256 auctionLength;
address performance_fee_recipient;
uint256 minimum_debt;
uint256 safe_collateral_ratio;
uint256 minimum_collateral_ratio;
uint256 max_penalty_collateral_ratio;
uint256 min_liquidation_fee;
uint256 max_liquidation_fee;
uint256 upfront_interest_period;
uint256 interest_rate_adj_cooldown;
uint256 minimum_price_buffer_percentage;
uint256 starting_price_buffer_percentage;
uint256 re_kick_starting_price_buffer_percentage;
uint256 step_duration;
uint256 step_decay_rate;
uint256 auction_length;
bytes32 salt;
}

Expand Down
63 changes: 17 additions & 46 deletions src/auction.vy
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ struct AuctionInfo:
minimum_price: uint256
receiver: address
surplus_receiver: address
is_liquidation: bool


struct InitializeParams:
Expand Down Expand Up @@ -94,7 +93,6 @@ step_decay_rate: public(uint256)
auction_length: public(uint256)

# Accounting
liquidation_auctions: public(uint256) # count of active liquidation auctions
auctions: public(HashMap[uint256, AuctionInfo]) # auction ID --> AuctionInfo


Expand Down Expand Up @@ -236,16 +234,6 @@ def is_active(auction_id: uint256) -> bool:
return self._is_active(auction)


@external
@view
def is_ongoing_liquidation_auction() -> bool:
"""
@notice Check if there's at least one ongoing liquidation auction
@return Whether there's at least one ongoing liquidation auction
"""
return self.liquidation_auctions > 0


# ============================================================================================
# Kick
# ============================================================================================
Expand All @@ -260,7 +248,6 @@ def kick(
minimum_price: uint256,
receiver: address,
surplus_receiver: address,
is_liquidation: bool,
):
"""
@notice Kick off an auction
Expand All @@ -273,7 +260,6 @@ def kick(
@param minimum_price The minimum price for the auction, WAD scaled in buy token
@param receiver The address that will receive the auction proceeds
@param surplus_receiver The address that will receive any surplus proceeds above maximum_amount
@param is_liquidation Whether this auction is selling liquidated collateral
"""
# Make sure caller is Papi
assert msg.sender == self.papi, "!papi"
Expand All @@ -299,10 +285,6 @@ def kick(
# Make sure auction is not already active
assert not self._is_active(auction), "active"

# If liquidation auction, increment counter
if is_liquidation:
self.liquidation_auctions += 1

# Update storage
self.auctions[auction_id] = AuctionInfo(
kick_timestamp=block.timestamp,
Expand All @@ -314,7 +296,6 @@ def kick(
minimum_price=minimum_price,
receiver=receiver,
surplus_receiver=surplus_receiver,
is_liquidation=is_liquidation,
)

# Pull the tokens from Papi
Expand Down Expand Up @@ -417,16 +398,12 @@ def take(
# Reset kick timestamp to mark auction as inactive
auction.kick_timestamp = 0

# If it was a liquidation auction, decrement counter
if auction.is_liquidation:
self.liquidation_auctions -= 1

# Send the token being sold to the take receiver
assert extcall self.sell_token.transfer(receiver, take_amount, default_return_value=True)

# If the caller provided data, perform the callback
if len(data) != 0:
extcall ITaker(receiver).auctionTakeCallback(
extcall ITaker(receiver).takeCallback(
auction_id,
msg.sender,
take_amount,
Expand All @@ -437,30 +414,24 @@ def take(
# Cache the buy token contract
buy_token: IERC20 = self.buy_token

# If liquidation auction, all proceeds goes to the Lender contract.
# Otherwise, make sure the receiver does not get more than the maximum and transfer the surplus to the surplus receiver
if auction.is_liquidation:
# Liquidation: all to the Lender contract
# How much the receiver still needs
receiver_remaining: uint256 = auction.maximum_amount - auction.amount_received

# If the bought amount is less than the receiver's maximum amount, transfer him all of it.
# Otherwise, cover the receiver first, then transfer the surplus to the surplus receiver
if needed_amount <= receiver_remaining:
# Entire amount to the receiver
auction.amount_received += needed_amount
assert extcall buy_token.transferFrom(msg.sender, auction.receiver, needed_amount, default_return_value=True)
else:
# How much the receiver still needs
receiver_remaining: uint256 = auction.maximum_amount - auction.amount_received

# If the bought amount is less than the receiver's maximum amount, transfer him all of it.
# Otherwise, cover the receiver first, then transfer the surplus to the surplus receiver
if needed_amount <= receiver_remaining:
# Entire amount to the receiver
auction.amount_received += needed_amount
assert extcall buy_token.transferFrom(msg.sender, auction.receiver, needed_amount, default_return_value=True)
else:
# Cover the receiver first
if receiver_remaining > 0:
auction.amount_received = auction.maximum_amount
assert extcall buy_token.transferFrom(msg.sender, auction.receiver, receiver_remaining, default_return_value=True)

# Transfer the surplus to the Lender contract
surplus: uint256 = needed_amount - receiver_remaining
assert extcall buy_token.transferFrom(msg.sender, auction.surplus_receiver, surplus, default_return_value=True)
# Cover the receiver first
if receiver_remaining > 0:
auction.amount_received = auction.maximum_amount
assert extcall buy_token.transferFrom(msg.sender, auction.receiver, receiver_remaining, default_return_value=True)

# Transfer the surplus to the surplus receiver
surplus: uint256 = needed_amount - receiver_remaining
assert extcall buy_token.transferFrom(msg.sender, auction.surplus_receiver, surplus, default_return_value=True)

# Update storage. No need to worry about re-entrancy since non-reentrant pragma is enabled
self.auctions[auction_id] = auction
Expand Down
78 changes: 24 additions & 54 deletions src/dutch_desk.vy
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@title Dutch Desk
@license MIT
@author Flex
@notice Handles liquidations and redemptions through Dutch Auctions
@notice Handles redemptions through Dutch Auctions
"""

from ethereum.ercs import IERC20
Expand Down Expand Up @@ -38,11 +38,9 @@ collateral_token: public(IERC20)
collateral_token_precision: public(uint256)

# Parameters
redemption_minimum_price_buffer_percentage: public(uint256)
redemption_starting_price_buffer_percentage: public(uint256)
redemption_re_kick_starting_price_buffer_percentage: public(uint256)
liquidation_minimum_price_buffer_percentage: public(uint256)
liquidation_starting_price_buffer_percentage: public(uint256)
minimum_price_buffer_percentage: public(uint256)
starting_price_buffer_percentage: public(uint256)
re_kick_starting_price_buffer_percentage: public(uint256)

# Accounting
nonce: public(uint256)
Expand All @@ -60,11 +58,9 @@ struct InitializeParams:
auction: address
borrow_token: address
collateral_token: address
redemption_minimum_price_buffer_percentage: uint256
redemption_starting_price_buffer_percentage: uint256
redemption_re_kick_starting_price_buffer_percentage: uint256
liquidation_minimum_price_buffer_percentage: uint256
liquidation_starting_price_buffer_percentage: uint256
minimum_price_buffer_percentage: uint256
starting_price_buffer_percentage: uint256
re_kick_starting_price_buffer_percentage: uint256


# ============================================================================================
Expand All @@ -91,11 +87,9 @@ def initialize(params: InitializeParams):
self.collateral_token = IERC20(params.collateral_token)

# Set parameters
self.redemption_minimum_price_buffer_percentage = params.redemption_minimum_price_buffer_percentage
self.redemption_starting_price_buffer_percentage = params.redemption_starting_price_buffer_percentage
self.redemption_re_kick_starting_price_buffer_percentage = params.redemption_re_kick_starting_price_buffer_percentage
self.liquidation_minimum_price_buffer_percentage = params.liquidation_minimum_price_buffer_percentage
self.liquidation_starting_price_buffer_percentage = params.liquidation_starting_price_buffer_percentage
self.minimum_price_buffer_percentage = params.minimum_price_buffer_percentage
self.starting_price_buffer_percentage = params.starting_price_buffer_percentage
self.re_kick_starting_price_buffer_percentage = params.re_kick_starting_price_buffer_percentage

# Get collateral token decimals
collateral_token_decimals: uint256 = convert(staticcall IERC20Detailed(params.collateral_token).decimals(), uint256)
Expand All @@ -113,21 +107,14 @@ def initialize(params: InitializeParams):


@external
def kick(
kick_amount: uint256,
maximum_amount: uint256 = 0,
receiver: address = empty(address),
is_liquidation: bool = True,
):
def kick(kick_amount: uint256, maximum_amount: uint256, receiver: address):
"""
@notice Kicks an auction of collateral tokens for borrow tokens
@dev Only callable by the Trove Manager contract
@dev Will use the Lender contract as receiver of auction proceeds if `receiver` is zero address
@dev Caller must approve this contract to transfer collateral tokens on its behalf before calling
@param kick_amount Amount of collateral tokens to auction
@param maximum_amount The maximum amount borrow tokens to be received
@param receiver Address to receive the auction proceeds in borrow tokens
@param is_liquidation Whether this auction is for liquidated collateral
"""
# Make sure caller is the Trove Manager contract
assert msg.sender == self.trove_manager, "!trove_manager"
Expand All @@ -139,15 +126,18 @@ def kick(
# Get the starting and minimum prices
starting_price: uint256 = 0
minimum_price: uint256 = 0
starting_price, minimum_price = self._get_prices(kick_amount, is_liquidation)
starting_price, minimum_price = self._get_prices(
kick_amount,
self.starting_price_buffer_percentage,
)

# Use the nonce as auction identifier
auction_id: uint256 = self.nonce

# Increment the nonce
self.nonce = auction_id + 1

# Pull the collateral tokens from the Trove Manager
# Pull the collateral tokens from the Trove Manager contract
assert extcall self.collateral_token.transferFrom(self.trove_manager, self, kick_amount, default_return_value=True)

# Kick the auction
Expand All @@ -157,9 +147,8 @@ def kick(
maximum_amount,
starting_price,
minimum_price,
receiver if receiver != empty(address) else self.lender,
receiver,
self.lender, # surplus receiver
is_liquidation,
)


Expand All @@ -169,23 +158,22 @@ def re_kick(auction_id: uint256):
@notice Re-kick an inactive auction with new starting and minimum prices
@dev Will revert if the auction is not kickable
@dev An auction may need to be re-kicked if its price has fallen below its minimum price
@dev Uses a higher starting price buffer percentage to allow for takers to regroup
@dev May use a higher starting price buffer percentage to allow for takers to regroup
@dev Does not set the receiver nor transfer collateral as those are already ready in the auction
@param auction_id Identifier of the auction to re-kick
"""
# Cache the Auction contract
auction: IAuction = self.auction

# Cache the auction info
# Get the auction info
auction_info: IAuction.AuctionInfo = staticcall auction.auctions(auction_id)

# Get new starting and minimum prices
starting_price: uint256 = 0
minimum_price: uint256 = 0
starting_price, minimum_price = self._get_prices(
auction_info.currentAmount,
auction_info.isLiquidation,
True, # is_rekick
auction_info.current_amount,
self.re_kick_starting_price_buffer_percentage,
)

# Re-kick with new prices
Expand All @@ -199,32 +187,14 @@ def re_kick(auction_id: uint256):

@internal
@view
def _get_prices(
kick_amount: uint256,
is_liquidation: bool,
is_rekick: bool = False,
) -> (uint256, uint256):
def _get_prices(kick_amount: uint256, starting_price_buffer_pct: uint256) -> (uint256, uint256):
"""
@notice Gets the starting and minimum prices for an auction
@param kick_amount Amount of collateral tokens to auction
@param is_liquidation Whether this is a liquidation auction
@param is_rekick Whether this is a re-kick of an existing auction
@param starting_price_buffer_pct The buffer percentage to apply to the starting price
@return starting_price The calculated starting price
@return minimum_price The calculated minimum price
"""
# Determine the minimum and starting price buffer percentages
minimum_price_buffer_pct: uint256 = 0
starting_price_buffer_pct: uint256 = 0
if is_liquidation:
minimum_price_buffer_pct = self.liquidation_minimum_price_buffer_percentage
starting_price_buffer_pct = self.liquidation_starting_price_buffer_percentage
else:
minimum_price_buffer_pct = self.redemption_minimum_price_buffer_percentage
if is_rekick:
starting_price_buffer_pct = self.redemption_re_kick_starting_price_buffer_percentage
else:
starting_price_buffer_pct = self.redemption_starting_price_buffer_percentage

# Get the collateral price
collateral_price: uint256 = staticcall self.price_oracle.get_price(False) # price in WAD format

Expand All @@ -234,6 +204,6 @@ def _get_prices(

# Calculate the minimum price with buffer to the collateral price
# Minimum price is per token and is scaled to WAD
minimum_price: uint256 = collateral_price * minimum_price_buffer_pct // _WAD
minimum_price: uint256 = collateral_price * self.minimum_price_buffer_percentage // _WAD

return starting_price, minimum_price
Loading