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);
    }
}