MakerDAO-Clip
MakerDAO Clip 合约分析
// SPDX-License-Identifier: AGPL-3.0-or-later
/// clip.sol -- Dai auction module 2.0
// Copyright (C) 2020-2022 Dai Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity ^0.6.12;
/// @notice 金库合约接口
interface VatLike {
/// @notice 用户之间转移稳定币
function move(address, address, uint256) external;
/// @notice 用户之间转移抵押品
function flux(bytes32, address, address, uint256) external;
/// @notice 返回资产的相关参数
function ilks(bytes32) external returns (uint256, uint256, uint256, uint256, uint256);
/// @notice 增加系统中的债务和不良债务,同时向特定用户增加 Dai
function suck(address, address, uint256) external;
}
/// @notice 价格预言机接口
interface PipLike {
function peek() external returns (bytes32, bool);
}
/// @notice 现货合约接口
interface SpotterLike {
function par() external returns (uint256);
function ilks(bytes32) external returns (PipLike, uint256);
}
/// @notice 清算合约接口
interface DogLike {
function chop(bytes32) external returns (uint256);
function digs(bytes32, uint256) external;
}
/// @notice 闪电贷合约接口
interface ClipperCallee {
function clipperCall(address, uint256, uint256, bytes calldata) external;
}
/// @notice Abacus 计算合约接口
interface AbacusLike {
function price(uint256, uint256) external view returns (uint256);
}
/**
* @title
* @author
* @notice 拍卖合约
*/
contract Clipper {
// --- Auth --- 权限控制部分
mapping(address => uint256) public wards;
function rely(address usr) external auth {
wards[usr] = 1;
emit Rely(usr);
}
function deny(address usr) external auth {
wards[usr] = 0;
emit Deny(usr);
}
modifier auth() {
require(wards[msg.sender] == 1, "Clipper/not-authorized");
_;
}
// --- Data ---
bytes32 public immutable ilk; // Collateral type of this Clipper 该拍卖合约的抵押品类型
VatLike public immutable vat; // Core CDP Engine 核心 CDP 引擎 (Vat)
DogLike public dog; // Liquidation module 清算模块
address public vow; // Recipient of dai raised in auctions 拍卖中筹集的 DAI 的接收者
SpotterLike public spotter; // Collateral price module 抵押品价格模块
AbacusLike public calc; // Current price calculator 当前价格计算器
/// @dev `buf`:增加起始价格的乘数 [ray]
uint256 public buf; // Multiplicative factor to increase starting price [ray]
/// @dev `tail`:拍卖重置前经历的时间 [seconds]
uint256 public tail; // Time elapsed before auction reset [seconds]
/// @dev `cusp`:拍卖重置前下降的百分比 [ray]
uint256 public cusp; // Percentage drop before auction reset [ray]
/// @dev `chip`:从 `vow` 中取出 `tab` 的百分比,用来激励 Keepper [wad]
uint64 public chip; // Percentage of tab to suck from vow to incentivize keepers [wad]
/// @dev `tip`:从 `vow` 中取出的固定费用,用来激励 keeppers [rad]
uint192 public tip; // Flat fee to suck from vow to incentivize keepers [rad]
/// @dev `chost`:缓存 `ilk dust` 乘以 `ilk chop`以防止过多的 SLOAD [rad]
uint256 public chost; // Cache the ilk dust times the ilk chop to prevent excessive SLOADs [rad]
/// @dev 总拍卖(包括已经结束的,不在活跃的拍卖)
/// @notice 该值只增不减(类似于 nonce)
uint256 public kicks; // Total auctions
/// @dev 活跃拍卖 ID 数组,其中存储目前正在活跃的拍卖的 id
uint256[] public active; // Array of active auction ids
struct Sale {
uint256 pos; // Index in active array 活跃数组序号
uint256 tab; // Dai to raise [rad] 要筹集的 Dai [rad]
uint256 lot; // collateral to sell [wad] 出售的抵押品数量 [wad]
address usr; // Liquidated CDP 清算的 CDP
uint96 tic; // Auction start time 拍卖开始时间
uint256 top; // Starting price [ray] 起始价格 [ray]
}
/// @notice sale 映射
/// @notice key:活跃拍卖 id(是活跃拍卖 id,不是活跃拍卖 id 数组的 index)
/// @notice value:Sale 结构体
/// @notice 当拍卖不再活跃时,通过 `_remove()` 函数 `delete` 对应条目的数据
mapping(uint256 => Sale) public sales;
uint256 internal locked;
// Levels for circuit breaker 断路器的四个级别
// 0: no breaker
// 1: no new kick()
// 2: no new kick() or redo()
// 3: no new kick(), redo(), or take()
uint256 public stopped = 0;
// --- Events ---
event Rely(address indexed usr);
event Deny(address indexed usr);
event File(bytes32 indexed what, uint256 data);
event File(bytes32 indexed what, address data);
/// @notice 拍卖启动时触发该事件
event Kick(
uint256 indexed id,
uint256 top,
uint256 tab,
uint256 lot,
address indexed usr,
address indexed kpr,
uint256 coin
);
/// @notice 用户参与拍卖时,触发该事件
event Take(
uint256 indexed id, uint256 max, uint256 price, uint256 owe, uint256 tab, uint256 lot, address indexed usr
);
/// @notice 拍卖重启时触发该事件
event Redo(
uint256 indexed id,
uint256 top,
uint256 tab,
uint256 lot,
address indexed usr,
address indexed kpr,
uint256 coin
);
/// @notice 紧急关闭拍卖时触发该事件
event Yank(uint256 id);
// --- Init ---
/// @notice 初始化拍卖合约
constructor(address vat_, address spotter_, address dog_, bytes32 ilk_) public {
vat = VatLike(vat_);
spotter = SpotterLike(spotter_);
dog = DogLike(dog_);
ilk = ilk_;
buf = RAY;
wards[msg.sender] = 1;
emit Rely(msg.sender);
}
// --- Synchronization ---
modifier lock() {
require(locked == 0, "Clipper/system-locked");
locked = 1;
_;
locked = 0;
}
modifier isStopped(uint256 level) {
require(stopped < level, "Clipper/stopped-incorrect");
_;
}
// --- Administration ---
/**
* @notice 修改系统参数 (权限控制)
* @param what 修改的参数名称
* @param data 修改的目标值
*/
function file(bytes32 what, uint256 data) external auth lock {
if (what == "buf") buf = data;
else if (what == "tail") tail = data; // Time elapsed before auction reset 拍卖重置前经历的时间
else if (what == "cusp") cusp = data; // Percentage drop before auction reset 拍卖重置前下降的百分比
/// @notice 对 keepper 的百分比 tab 激励 (最大值:2^64 - 1 => 18.xxx WAD = 18xx%
else if (what == "chip") chip = uint64(data); // Percentage of tab to incentivize (max: 2^64 - 1 => 18.xxx WAD = 18xx%)
/// @notice 对 keepper 的固定激励 (最大值:2^192 - 1 => 6.277T RAD)
else if (what == "tip") tip = uint192(data); // Flat fee to incentivize keepers (max: 2^192 - 1 => 6.277T RAD)
/// @notice 设置断路器
else if (what == "stopped") stopped = data; // Set breaker (0, 1, 2, or 3)
else revert("Clipper/file-unrecognized-param");
emit File(what, data);
}
/// @notice 修改特定配件合约的对应地址(权限控制)
function file(bytes32 what, address data) external auth lock {
if (what == "spotter") spotter = SpotterLike(data);
else if (what == "dog") dog = DogLike(data);
else if (what == "vow") vow = data;
else if (what == "calc") calc = AbacusLike(data);
else revert("Clipper/file-unrecognized-param");
emit File(what, data);
}
// --- Math ---
uint256 constant BLN = 10 ** 9;
uint256 constant WAD = 10 ** 18;
uint256 constant RAY = 10 ** 27;
function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x <= y ? x : y;
}
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x);
}
function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x - y) <= x);
}
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x);
}
function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, y) / WAD;
}
function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, y) / RAY;
}
function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, RAY) / y;
}
// --- Auction ---
// get the price directly from the OSM
// Could get this from rmul(Vat.ilks(ilk).spot, Spotter.mat()) instead, but
// if mat has changed since the last poke, the resulting value will be
// incorrect.
// 直接从 OSM 获取价格
// 也可以从 `rmul(Vat.ilks(ilk).spot, Spotter.mat())` 获取,但是
// 如果 mat 自上次 `poke` 后发生了变化,则结果值将
// 不正确。
function getFeedPrice() internal returns (uint256 feedPrice) {
(PipLike pip,) = spotter.ilks(ilk);
(bytes32 val, bool has) = pip.peek();
require(has, "Clipper/invalid-price");
feedPrice = rdiv(mul(uint256(val), BLN), spotter.par());
}
// start an auction
// note: trusts the caller to transfer collateral to the contract
// The starting price `top` is obtained as follows:
//
// top = val * buf / par
//
// Where `val` is the collateral's unitary value in USD, `buf` is a
// multiplicative factor to increase the starting price, and `par` is a
// reference per DAI.
/**
* @notice 开始拍卖
* @param tab 负债(要筹集的 Dai)
* @param lot 拍卖品数量
* @param usr 拍卖完成后,若有剩余的抵押品将被退还给 usr。usr 通常是被清算的用户。
* @param kpr 将接受激励的地址。keeper 是执行清算操作的实体,启动拍卖时,会收到一定的激励(tip 和 chip)。
* @notice
* - 注意:信任调用者将抵押品转移到合约
* - 起始价格 `top` 计算方法:
* top = val * buf / par
* - `val` 抵押品的美元单位价值
* - `buf` 增加起始价格的乘数
* - `par` 每个 DAI 的参考
* - 权限控制 `auth`,重入锁 `lock`,断路器满足 `level < 1` (允许开启新的拍卖)
* - 该函数只能由 `Dog` 合约调用。(keepper(清算实施者)--call--> `Dog` --call--> Clipper)
*/
function kick(
uint256 tab, // Debt [rad]
uint256 lot, // Collateral [wad]
address usr, // Address that will receive any leftover collateral
address kpr // Address that will receive incentives
) external auth lock isStopped(1) returns (uint256 id) {
// Input validation
require(tab > 0, "Clipper/zero-tab");
require(lot > 0, "Clipper/zero-lot");
require(usr != address(0), "Clipper/zero-usr");
id = ++kicks;
require(id > 0, "Clipper/overflow");
active.push(id);
sales[id].pos = active.length - 1;
sales[id].tab = tab;
sales[id].lot = lot;
sales[id].usr = usr;
sales[id].tic = uint96(block.timestamp);
uint256 top;
top = rmul(getFeedPrice(), buf);
require(top > 0, "Clipper/zero-top-price");
sales[id].top = top;
// incentive to kick auction 激励拍卖
uint256 _tip = tip;
uint256 _chip = chip;
uint256 coin;
if (_tip > 0 || _chip > 0) {
coin = add(_tip, wmul(tab, _chip)); // 计算 keepper 的激励
vat.suck(vow, kpr, coin);
}
emit Kick(id, top, tab, lot, usr, kpr, coin);
}
// Reset an auction
// See `kick` above for an explanation of the computation of `top`.
/**
* @notice 重置一个拍卖
* @param id 要进行重置的拍卖的 id
* @param kpr 重置拍卖的 keepper (清算者)(将要接收激励的地址)
* @notice
* - `lock` 重入锁,`isStopped(2)` 断路器满足条件 < 2
*/
function redo(
uint256 id, // id of the auction to reset
address kpr // Address that will receive incentives
) external lock isStopped(2) {
// Read auction data 读取拍卖数据
address usr = sales[id].usr;
uint96 tic = sales[id].tic;
uint256 top = sales[id].top;
require(usr != address(0), "Clipper/not-running-auction");
// Check that auction needs reset 检查拍卖是否需要重置
// and compute current price [ray] 并计算当前价格 [ray]
(bool done,) = status(tic, top);
require(done, "Clipper/cannot-reset");
uint256 tab = sales[id].tab;
uint256 lot = sales[id].lot;
sales[id].tic = uint96(block.timestamp);
uint256 feedPrice = getFeedPrice();
top = rmul(feedPrice, buf);
require(top > 0, "Clipper/zero-top-price");
sales[id].top = top;
// incentive to redo auction
// 重置拍卖的激励计算
uint256 _tip = tip;
uint256 _chip = chip;
uint256 coin;
if (_tip > 0 || _chip > 0) {
uint256 _chost = chost;
if (tab >= _chost && mul(lot, feedPrice) >= _chost) {
coin = add(_tip, wmul(tab, _chip));
vat.suck(vow, kpr, coin);
}
}
emit Redo(id, top, tab, lot, usr, kpr, coin);
}
// Buy up to `amt` of collateral from the auction indexed by `id`.
//
// Auctions will not collect more DAI than their assigned DAI target,`tab`;
// thus, if `amt` would cost more DAI than `tab` at the current price, the
// amount of collateral purchased will instead be just enough to collect `tab` DAI.
//
// To avoid partial purchases resulting in very small leftover auctions that will
// never be cleared, any partial purchase must leave at least `Clipper.chost`
// remaining DAI target. `chost` is an asynchronously updated value equal to
// (Vat.dust * Dog.chop(ilk) / WAD) where the values are understood to be determined
// by whatever they were when Clipper.upchost() was last called. Purchase amounts
// will be minimally decreased when necessary to respect this limit; i.e., if the
// specified `amt` would leave `tab < chost` but `tab > 0`, the amount actually
// purchased will be such that `tab == chost`.
//
// If `tab <= chost`, partial purchases are no longer possible; that is, the remaining
// collateral can only be purchased entirely, or not at all.
// 从由 `id` 索引的拍卖中购买最多 `amt` 的抵押品。
//
// 拍卖不会收集比其指定的 DAI 目标 `tab` 更多的 DAI;
// 因此,如果 `amt` 在当前价格下比 `tab` 花费更多的 DAI,则购买的抵押品数量将刚好足以收集 `tab` DAI。
//
// 为避免部分购买导致剩余拍卖非常少并且永远不会被清除,任何部分购买都必须至少留下
// `Clipper.chost` 剩余的 DAI 目标。`chost` 是一个异步更新的值,等于
// (Vat.dust * Dog.chop(ilk) / WAD),其中这些值被理解为由
// 上次调用 Clipper.upchost() 时的值决定。购买金额将在必要时最小程度地减少以遵守此限制;
// 即,如果指定的 `amt` 为 `tab < chost` 但 `tab > 0`,则实际购买的金额将为 `tab == chost`。
//
// 如果 `tab <= chost`,则不再可能进行部分购买;也就是说,剩余的
// 抵押品只能全部购买,或者根本无法购买。
/**
* @notice 拍卖函数(拍抵押品的函数)
* @param id 要拍的拍卖 id
* @param amt 购买抵押品数量的上限 [wad]
* @param max 最高可接受价格 (DAI / 抵押品) [ray]
* @param who 抵押品接收者和外部调用地址(拍卖的地址)
* @param data 传入的外部 calldata 数据,长度为 0 表示未进行调用
* @notice
* - `lock` 重入锁,`isStopped(3)` 断路器满足 < 3
*/
function take(
uint256 id, // Auction id
uint256 amt, // Upper limit on amount of collateral to buy [wad]
uint256 max, // Maximum acceptable price (DAI / collateral) [ray]
address who, // Receiver of collateral and external call address
bytes calldata data // Data to pass in external call; if length 0, no call is done
) external lock isStopped(3) {
address usr = sales[id].usr;
uint96 tic = sales[id].tic;
require(usr != address(0), "Clipper/not-running-auction");
uint256 price;
{
bool done;
(done, price) = status(tic, sales[id].top);
// Check that auction doesn't need reset
// 检测对应的拍卖是否需要重置
require(!done, "Clipper/needs-reset");
}
// Ensure price is acceptable to buyer
// 确保价格为买家所接受
require(max >= price, "Clipper/too-expensive");
uint256 lot = sales[id].lot;
uint256 tab = sales[id].tab;
uint256 owe;
{
// Purchase as much as possible, up to amt
// 尽可能多的买,最多为 `amt`
// 计算实际购买的抵押品数量,初始值为购买上限和拍卖中剩余抵押品数量中的最小值
uint256 slice = min(lot, amt); // slice <= lot
// DAI needed to buy a slice of this sale
// 购买这笔拍卖的 slice 需要的 DAI
owe = mul(slice, price);
// Don't collect more than tab of DAI
// 不收集超过 `tab` 目标的 DAI
if (owe > tab) {
// 如果所需 DAI 数量超过债务
owe = tab; // owe' <= owe 调整所需 DAI 数量为债务数量
// Adjust slice
// 调整实际购买的抵押品数量
slice = owe / price; // slice' = owe' / price <= owe / price == slice <= lot
} else if (owe < tab && slice < lot) {
// If slice == lot => auction completed => dust doesn't matter
uint256 _chost = chost; // 获取最小剩余债务目标
if (tab - owe < _chost) {
// safe as owe < tab 安全:因为 owe < tab
// If tab <= chost, buyers have to take the entire lot.
// 确保部分购买满足最小剩余债务目标
require(tab > _chost, "Clipper/no-partial-purchase");
// Adjust amount to pay
// 调整所需 DAI 数量
owe = tab - _chost; // owe' <= owe
// Adjust slice
// 调整实际购买抵押品数量
slice = owe / price; // slice' = owe' / price < owe / price == slice < lot
}
}
// Calculate remaining tab after operation
// 更新剩余债务
tab = tab - owe; // safe since owe <= tab 安全:因为 owe <= tab
// Calculate remaining lot after operation
// 更新剩余抵押品数量
lot = lot - slice;
// Send collateral to who
// 将抵押品发送给 `who`
vat.flux(ilk, address(this), who, slice);
// Do external call (if data is defined) but to be
// extremely careful we don't allow to do it to the two
// contracts which the Clipper needs to be authorized
// 若 data 被定义,执行外部调用。不允许 `who` 为 `vat` 和 `dog`
// vat 和 dog 对 Clipper 合约进行授权,所以限制用户,不能通过 low-level call
// 调用 vat 和 dog 合约避免出现非预期问题,避免出现:权限访问控制漏洞
DogLike dog_ = dog;
if (data.length > 0 && who != address(vat) && who != address(dog_)) {
ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);
}
// Get DAI from caller
// 从调用者处获取 DAI
vat.move(msg.sender, vow, owe);
// Removes Dai out for liquidation from accumulator
// 从累加器中移除要清算的 DAI
dog_.digs(ilk, lot == 0 ? tab + owe : owe);
}
if (lot == 0) {
// 所有抵押品全部拍卖
_remove(id); // 直接移除对应的拍卖
} else if (tab == 0) {
// lot != 0 => 抵押品剩余
// 并且收取了目标 `tab` 的 DAI
vat.flux(ilk, address(this), usr, lot); // 将剩余的抵押品转移回被清算者
_remove(id); // 移除对应的拍卖
} else {
// 都不满足,证明拍卖还需要继续
// 更新拍卖状态(剩余需要拍卖的目标,剩余的抵押品数量)
sales[id].tab = tab;
sales[id].lot = lot;
}
emit Take(id, max, price, owe, tab, lot, usr);
}
function _remove(uint256 id) internal {
uint256 _move = active[active.length - 1]; // 读出 active 活跃拍卖id动态数组中的最后一个元素(最后一个拍卖 `id`)
// 若所要删除的活跃拍卖 `id` 与 `active` 数组中的最后一个拍卖 `id`不等:执行 if 代码块的修改
// 若相等(二者是一个),直接 `pop` 弹出
if (id != _move) {
// 读取要移除的`id`对应的活跃拍卖id数组的 index
uint256 _index = sales[id].pos;
// 将最后一个元素(拍卖` id` )存到要删除的序号的位置
active[_index] = _move;
// 修改最后一个元素对应的拍卖(`sales[_move]`)在 `sales` mapping 中的 `pos`(对应活跃拍卖 id 数组中的 index)
sales[_move].pos = _index;
}
active.pop(); // 删除动态数组的最后一个元素
delete sales[id]; // 删除对应 `id` 的 `sales` mapping 中的对应条目
}
// The number of active auctions
/**
* @notice 返回目前活跃拍卖的数量
* @return 返回 active 数组的长度
*/
function count() external view returns (uint256) {
return active.length;
}
// Return the entire array of active auctions
/**
* @notice 返回全部活跃拍卖
* @return 整个 active 数组
*/
function list() external view returns (uint256[] memory) {
return active;
}
// Externally returns boolean for if an auction needs a redo and also the current price
/**
* @notice 外部返回布尔值,表示拍卖是否需要重新拍卖以及当前价格
* @param id 对应拍卖 id
* @return needsRedo 是否需要重新拍卖
* @return price 没有重新拍卖时的价格
* @return lot 没有重新拍卖时的总抵押品数量
* @return tab 没有重新拍卖时的所需筹集 DAI 的目标
*/
function getStatus(uint256 id) external view returns (bool needsRedo, uint256 price, uint256 lot, uint256 tab) {
// Read auction data
address usr = sales[id].usr;
uint96 tic = sales[id].tic;
bool done;
(done, price) = status(tic, sales[id].top);
needsRedo = usr != address(0) && done;
lot = sales[id].lot;
tab = sales[id].tab;
}
// Internally returns boolean for if an auction needs a redo
/**
* @dev 内部函数,计算拍卖是否需要重置
* @param tic 拍卖开始的时间
* @param top 拍卖开始时的金额 [ray]
* @return done 拍卖是否需要重置
* @return price 当前拍卖价格
* @notice
* - 只需要满足下面两个条件之一即可重置拍卖(返回值为 `true`)
* - 拍卖时间超过了最长时间 `tail`
* - 拍卖价格下降的百分比低于了 `cusp`
*/
function status(uint96 tic, uint256 top) internal view returns (bool done, uint256 price) {
price = calc.price(top, sub(block.timestamp, tic));
done = (sub(block.timestamp, tic) > tail || rdiv(price, top) < cusp);
}
// Public function to update the cached dust*chop value.
/// @notice 用于更新缓存的 dust*chop 值的公共函数。
function upchost() external {
(,,,, uint256 _dust) = VatLike(vat).ilks(ilk);
chost = wmul(_dust, dog.chop(ilk));
}
// Cancel an auction during ES or via governance action.
///
/**
* @dev 在 ES 期间或通过治理行动取消拍卖。
* @param id 要取消的拍卖的拍卖 id
* @notice 该函数只能由只在 `end` 合约中调用,`end` 合约负责系统的关停
* 函数 `End.snip` 负责在触发关闭时调用 `Clipper.yank` 来关闭任何正在运行的拍卖。
* 他将收到 `Clipper.yank` 的抵押品,并将其剩余债务一起发送回 Vault 所有者进行追偿。
* 注意:Vault 所有者将收到的债务包括已经收取的清算罚款部分。
*/
function yank(uint256 id) external auth lock {
require(sales[id].usr != address(0), "Clipper/not-running-auction");
// 从累加器中移除要清算的 DAI
dog.digs(ilk, sales[id].tab);
// 将抵押品转回 `msg.sender`
vat.flux(ilk, address(this), msg.sender, sales[id].lot);
_remove(id);
emit Yank(id);
}
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Q1ngying!