From c8d4c806691dacb903ff281b81f316bea974e4c7 Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Fri, 2 Apr 2021 12:40:50 -0300 Subject: [PATCH] Liq 2.0 (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * dapp init DutchOven * dapp install ds-test * paste in code from gist * make it build * tweak README * add license blurb * First draft for flash loan + removing loaf if lot is 0 + minor changes (#1) * cmooney-20200725: various bug fixes * done TODO checks and fixed trailing whitespace * ONE to RAY, removed slices, bakes public * chore: add TODO for max/min/pay variables * SC-4072: limit amount of Dai out for liquidation (#5) * SC-4072: limit amount of Dai out for liquidation * remove changes for another PR * added BLN * fix comment * change MILLION to MIl * fixed up the formula * fixed bug in MIL va. MLN * forgot rdiv() * missing closing ) * add comments on types * typo, needed param to be data * fix: update dog to fix stack to deep, compiles * feat: update dog to use LIQ-1.2 logic * fix: update comments, remove dunk, change bone to digs * fix: gas optimize dink > 0 require * fix: update comments, add spot to brace block, add cost var * fix: change cost to due * fix: formatting Co-authored-by: Lucas Manuel * fix dust check * feat: Add Auction Reset Functionality (SC-4635) (#7) * feat: add auction reset functionality * fix: add storage vars, fix compilation issues * feat: add require to warm, change to external * fix: update comments * fix: use safe sub, add dead auction check in warm and take * Price decrease function interface and sample implementations (#8) * define price decrease function interface and some trial implementations * commit sample price decrease functions * dapp install https://github.com/makerdao/dss.git * feat: add testing for linear/exp price dec functions * feat: add unit tests to stairstep, linear price decrease functions * fix: add comments about precision * fix: update exp test tolerance to 1e-22 * feat: tests working consistently with precision tolerance * fix: fix wrong comments * dapp uninstall dss * fix: update variables, comments Co-authored-by: Lucas Manuel * rely on oven in Dog.file (#11) * rename files (#13) * Add on-chain tracking of live auctions (#3) * Add on-chain tracking of live auctions * rename last to move for clarity * Add splice and count funtions for array management * Fix index shifting * Change _stopBaking to _remove * Clean up and fixes * remove list(uint256,uint256)(uint256[]) and replace with getId(uint256)(uint256) * Replace .length-- with .pop() for forward compatibility * Remove comment * fix: update memes, comments Co-authored-by: Lucas * feat: Add Unit Testing (SC-4636) (#9) * feat: start bake test * dapp install ds-value * feat: add take tests for over/under/at tab * feat: add take test for multiple bids, tests pass * fix: delete notes file * fix: remove empty lib/dss * dapp install dss * feat: add more testing, setup * feat: all tests pass except exp decrease * fix: fix exp decrease step and cut values * fix: remove rpow, change max/pay to pay/val, update dirt require * Fix import file and rename uint to uin256 * fix: update room require * fix: update val to price * fix: remove vat.flux(), grab straight to oven * fix: remove rely/deny/hope/nope from oven file in dog * fix: update imports * fix: update dog.digs() comment * fix: update vat helpers, hevm.warping, address(this) * fix: use defined CHEAT_CODE * feat: add pos to testing * fix: remove unused MLN, BLN vars in test * uint to uint256 * Compile with solc 0.5 and 0.6 Co-authored-by: Gonzalo Balabasquer * re-meme and minor cleanup (#15) * dapp uninstall ds-test * dapp uninstall ds-value * dapp uninstall dss * Remove stuff that will not be transferred * Minor changes to make clipper tests to work * Preliminary Circuit Breaker Added logic to `stop` and `start` the clipper. All major functions are frozen when `stopped == true`. * use current price, not bidder maximum * hole per ilk (#141) * Bit simpler tests * Implement hole per collateral * Fixing minor things * fix: update hole comment Co-authored-by: Lucas * Clipper cleanup (#146) * improve take arg comments * remove comment about returning collateral incrementally * rename warm to redo * rename resetting event * add Take event * fix: format Take event, comments Co-authored-by: Lucas * feat: Add Auction Reset Tests (SC-6413) (#148) * feat: add auction reset testing, tests pass * fix: move repeated setup code to function * feat: add try_redo() to clip tests * fix: update to camelcase, fix comment * Add license identifier to clip.sol * change to levels * Revert "Merge branch 'SC-4636' into liq-2.0" This reverts commit 1906a89cf6c907cbd3bfb350cd2ff83395460304, reversing changes made to 12d3254ab16debbd9b132c1254b0802db652d29d. * Add license identifier to dog.sol * Add tests * No breaker for yank * remove yank breaker tests * Add tests * No breaker for yank * remove yank breaker tests * fixing rebase issues * remove `which` * fix: read dust from vat (#151) * feat: add chop getter API * feat: Add Dusty `bite` Check (SC-6351) (#154) * feat: add dusty bite check * fix: use rate in dust revert calc * feat: add rate to dusty check test * fix: remove math import * fix: update comments * fix: get rid of jug * feat: Add LibNote events to dog.sol, clip.sol (SC-4645) (#156) * feat: add LibNote to dog, clip * feat: index ids, add top to Kick and Redo, add max/price/owe to Take * fix: remove note from functions with custom events in dog and clip * minor gas optimizations in Clipper (#152) * minor gas optimizations in Clipper * fix: add dust call back, resolve conflicts Co-authored-by: Lucas * feat: Use custom events (#157) * feat: add custom events across functions in dog and clip * fix: update public to external for rely/deny in clip * feat: update File events to differentiate types * fix: add rely events to constructors, index id in Bark * fix: change tab to due in Bark event * feat: Add Keeper Liquidation Incentive (SC-6597) (#153) * feat: add keeper liquidation incentive and testing, tests pass * fix: use ali to bark() * feat: add tip and chip as per ilk params for incentives * fix: fix test failures from rebase * fix: update wmul * feat: update tests to assert bob balance * fix: update comments * feat: add custom events for rely/deny in abaci.sol (#163) * change price API to accept seconds elapsed since auction start (#162) * change price API to accept seconds elapsed since auction start * reinstate 0.5.12 compatibility * fix: add Rely event to abaci constructors * fix: update liq incentive to use due (#164) * feat: Upgrade dog, clip to 0.6.7, Update CI (SC-7268) (#165) * feat: upgrade dog, clip to 0.6.7, update CI * fix: format immutable vars, use interface in abaci.sol * fix: update chip comment (#166) * feat: End Integration and `yank()` (SC-6352 + SC-6353) (#161) * draft, dog is in end w/ cat, and auction cancellation * committing from laptop so i can pull to desktop * committing latest changes to share * fix: fix interace and formatting * feat: add tests for yank and end integration feix: formatting * fix: use _remove in yank, remove vat.hope() in halt() * fix: update halt to skip * fix: update tests to use real rate, remove unnecessary testing * fix: use vat.fold in snip test for rate * feat: add clipper assertions from yank call * fix: update scope of tab and lot Co-authored-by: wilbarnes * separate price decrease function tests into their own file (#168) * feat: Add `needsRedo` Function (SC-7445) (#169) * feat: add view function for auction reset * feat: test for both tail and cusp * feat: add ripe function * fix: update ripe to done * fix: remove sale.tab check from done function * fix: update return bool * feat: minor gas optimization for dink calculation (#167) * fix: Input and state validation (#170) * add input validation to Clipper.kick w/tests * regularize checks for non-running auctions * more tests * Slither findings (#172) * make public funcs external in Dog * simplify cut param of StairstepExponentialDecrease * fix revert message in StairstepExponentialDecrease * gas optimization and test for Clipper.yank (#175) * Compile with solc 0.6.11 * Make LinearDecrease.tau public * fix: whitespace and BIL (#178) * LIQ-2.0 20210114 updates (#181) * some small changes and TODO notes * verbose testing * Add id to yank * rm max TODO Co-authored-by: Brian McMichael * Add extra function to pay rewards to external address (#180) * Add remaining art to dart to prevent unliquidatable vaults (#179) * Add remaining art to dart to prevent unliquidatable vaults * rm extra require * Reuse mart * Simplify dust check * Modify unit test in order to work with the dust behaviour change * rename test for clarity * Add test TODO Co-authored-by: Gonzalo Balabasquer * Liq 2.0 gas improvements (#184) * Optimize gas usage in some functions * Optimize take function and change done to status (which returns also the price) * Fix require message * Fix in needsRedo * now => block.timestamp * Add comment * Use id as in other functions * Fix shadow variable warning * Add missing SPDX License Identifier for some files * Liq 2.0: add tests for external calls (#185) * tests for external calls * add test for reentrancy * test redo reentrancy * more realistic flashsale test * avoid third-party names * Prevent take impersonation. (#189) Co-authored-by: Emilio Silva Schlenker * Instead of ending auction if tab < dust, recalculate amounts (#190) * Instead of ending auction if tab < dust, recalculate amounts * Instead of ending auction if tab < dust, recalculate amounts * Some changes * Avoid if block if tab == owe Co-authored-by: Emilio Silva Schlenker Co-authored-by: Brian McMichael * fix: move incentive to kick() in redo() (SC-8390) (#188) * fix: move incentive to kick() in redo() (SC-8390) * removed comment * prevent vat.suck() and move SLOAD * pedantic security * adding TODO comments * LIQ-2.0: dusty test cases around dog.bark() (#186) * create test file for dog * dog set up * add isDusty() function and a few tests - test_bark_basic() - testFail_bark_not_unsafe() - test_bark_unliquidatable_vault() - test_bark_dust_over_ilk_hole() - test_bark_dust_over_Hole() - test_bark_exactly_dust_over_Hole() * update code to recent changes * add some tests - test_bark_over_ilk_hole - test_bark_over_Hole - test_bark_dust_under_ilk_hole - test_bark_dust_under_Hole * remove duplicate lines * Redo some tests Co-authored-by: Gonzalo Balabasquer * Add lock modifier to redo just as extra safety measure (#192) * Change needsRedo function to getStatus which also includes the price (#193) * Change needsRedo function to getStatus which also includes the price * Fix comment * Clean up warnings (#195) * Make dog immutable and spotter manageable + check dog is not used as who in flash loan call * Not paying incentive in redo if the auction is dusty * Make chip and tip public + more tests (#196) * Test for not partial purchase when tab == dust * Make chip and tip public + adding a test to check redo incentives logic * Addressing comments * Fix spacing * Avoid extra SLOAD when using chip and tip (#2) * In case Hole < Dirt or milk.hole < milk.dirt also show revert message * Save SLOAD in kick * Save SLOAD in kick * move top * Clearer parameter name * Yank param uint256 * Tweak breaker docs * Custom events + int/uint with 256 + now -> block.timestamp * Custom events + int/uint with 256 + now -> block.timestamp * Add indexed to usr in Skip and Snip * Move math constant to the top * Rename all File events to just plain File name * getPrice function This function: * avoids replicated code * avoids stack-too-deep brackets * Fix comment take param * Add snip to End code documentation * Add snip to End code documentation * Text changes * Liq 2.0 formulas * Add formulas for `take` * add brief `top` formula for kick and redo * formulas for `bark` * made formulas for bark clearer * general equation for bark * added the case when `dart` creates a dusty vault * corrected typos * adding formulas for abaci linear and exponential decrease for price * Updating explanation of cut * New formulas for bark and take (#199) * New formulas for bark * New formulas for take * Fix bark formula * Format some text * Minor fixes Co-authored-by: Emilio Silva Schlenker Co-authored-by: tannr * Optimize deletion: don't shift if element is last * Optimize deletion: don't shift if element is last * Add test of element removal * Add more assertions and a test of out-of-range failure * document test * unused var * dig entire tab when full lot is purchased * Address PwC finding 6.6 * revamp dog partial liquidation logic * add tests for not creating dusty auctions * Update src/test/dog.t.sol Co-authored-by: Gonzalo Balabasquer * Update src/test/dog.t.sol Co-authored-by: Gonzalo Balabasquer * fix try_bark * improve clarity of dusty vault & dusty room test Co-authored-by: Gonzalo Balabasquer * Add lock modifier to every auth function + make SetBreaker part of th… * Add lock modifier to every auth function + make SetBreaker part of the file function * Update src/clip.sol Co-authored-by: Kurt Barry Co-authored-by: Kurt Barry * Make dog can be changed on the clipper * Save extra SLOAD calling kicks * Addp kpr to Kick and Redo events * Addp kpr to Kick and Redo events * Log amount is being paid to kpr * Rename kprAmt to coin * Use one storage slot for chip and tip * Use one storage slot for chip and tip * Fix for Kurt and Brian * Remove LibNote in order to be able to compile with solc 0.6 optimized * Remove LibNote in order to be able to compile with solc 0.6 optimized * Add runs to the Makefile * Require top > 0 for kicking and resetting (#205) * Don't set unstable default values in stairstep constructor (#204) * Update to 0.6.12 (#206) * changing wards uint to uint256 * Remove list(), use active() (#207) * validation for chop (#209) * Fix state mutability warning in one test contract (#210) * Add list(), remove getId() (#212) * add continuous exponential decrease (#211) * add continuous exponential decrease * Update src/test/abaci.t.sol Co-authored-by: Gonzalo Balabasquer * harmonize comments Co-authored-by: Gonzalo Balabasquer * Gas Optimization: Cache the dust value and allow public modification. (#214) * More efficient lock in case tx reverts or if in future there are not … (#213) * More efficient lock in case tx reverts or if in future there are not refunds * Fix spacing Co-authored-by: Brian McMichael * validate Clipper ilk when filing in Dog (#216) * Revert "More efficient lock in case tx reverts or if in future there are not … (#213)" (#217) This reverts commit 628d6f90b4735d99b4c777b6b356b8723c89a67d. * Return lot size with getStatus (#215) * Return lot size with getStatus * Add tab to getStatus * Add a breaker level that prevents kick and redo but not take (#220) * add a breaker level to disable kick and redo but not take * test re-enabling taking Co-authored-by: Gonzalo Balabasquer * fix tests * fix tests post-rebase Co-authored-by: Gonzalo Balabasquer * Dog and Clipper comment updates (#221) * update Dog comment * typos and phrasing * improve kick param comments * change kpr comments for kick and redo * rename getPrice() to getFeedPrice() and price to feedPrice (#224) * rename getPrice to getFeedPrice Note that this is inconsistent with daiwanese practices * rename the return value of getFeedPrice() * gas tests and don't use optimizations (#226) * gas tests and don't use optimizations * update shell.nix to not compile w/optimizations * properly account for chop in dust checks in the Clipper (#222) * properly account for chop in dust checks in the Clipper * cached chost, uniform checks, tests * update take comment * Cache chost * Remove unused variable * Change _ position * don't set upchost in constructor * cache ilk, fix comment * actually don't cache, ilk is immutable * remove underscore * remove extra comma Co-authored-by: Gonzalo Balabasquer * Rename abaci file event (#229) * remove extra newline Co-authored-by: Kurt Barry Co-authored-by: Christopher Mooney Co-authored-by: Lucas Manuel Co-authored-by: Kurt Barry Co-authored-by: Brian L. McMichael Co-authored-by: andy8052 Co-authored-by: wilbarnes Co-authored-by: Emilio Silva Schlenker Co-authored-by: tannr --- Makefile | 10 +- shell.nix | 5 +- src/abaci.sol | 260 +++++++ src/cat.sol | 22 +- src/clip.sol | 471 ++++++++++++ src/dai.sol | 10 +- src/dog.sol | 249 ++++++ src/end.sol | 212 ++++-- src/flap.sol | 22 +- src/flip.sol | 24 +- src/flop.sol | 22 +- src/join.sol | 28 +- src/jug.sol | 20 +- src/lib.sol | 45 -- src/pot.sol | 22 +- src/spot.sol | 18 +- src/test/abaci.t.sol | 213 ++++++ src/test/clip.t.sol | 1718 ++++++++++++++++++++++++++++++++++++++++++ src/test/dog.t.sol | 290 +++++++ src/test/end.t.sol | 132 +++- src/vat.sol | 66 +- src/vow.sol | 28 +- 22 files changed, 3625 insertions(+), 262 deletions(-) create mode 100644 src/abaci.sol create mode 100644 src/clip.sol create mode 100644 src/dog.sol delete mode 100644 src/lib.sol create mode 100644 src/test/abaci.t.sol create mode 100644 src/test/clip.t.sol create mode 100644 src/test/dog.t.sol diff --git a/Makefile b/Makefile index bd2f5260..5eef2ce8 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ -build :; dapp --use solc:0.5.12 build -clean :; dapp clean -test :; dapp --use solc:0.5.12 test -v ${TEST_FLAGS} +.PHONY: build clean test test-gas + +build :; DAPP_BUILD_OPTIMIZE=0 DAPP_BUILD_OPTIMIZE_RUNS=0 dapp --use solc:0.6.12 build +clean :; dapp clean +test :; DAPP_BUILD_OPTIMIZE=0 DAPP_BUILD_OPTIMIZE_RUNS=0 dapp --use solc:0.6.12 test -v ${TEST_FLAGS} +test-gas : build + LANG=C.UTF-8 hevm dapp-test --rpc="${ETH_RPC_URL}" --json-file=out/dapp.sol.json --dapp-root=. --verbose 2 --match "test_gas" diff --git a/shell.nix b/shell.nix index 0051c572..8af885ef 100644 --- a/shell.nix +++ b/shell.nix @@ -4,8 +4,9 @@ }: with dappPkgs; mkShell { - DAPP_SOLC = solc-static-versions.solc_0_5_12 + "/bin/solc-0.5.12"; - # SOLC_FLAGS = "--optimize --optimize-runs=200"; + DAPP_SOLC = solc-static-versions.solc_0_6_12 + "/bin/solc-0.6.12"; + # No optimizations + SOLC_FLAGS = ""; buildInputs = [ dapp ]; diff --git a/src/abaci.sol b/src/abaci.sol new file mode 100644 index 00000000..8aa7fb96 --- /dev/null +++ b/src/abaci.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. +// +// 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 . + +pragma solidity >=0.6.12; + +interface Abacus { + // 1st arg: initial price [ray] + // 2nd arg: seconds since auction start [seconds] + // returns: current auction price [ray] + function price(uint256, uint256) external view returns (uint256); +} + +contract LinearDecrease is Abacus { + + // --- 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, "LinearDecrease/not-authorized"); + _; + } + + // --- Data --- + uint256 public tau; // Seconds after auction start when the price reaches zero [seconds] + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + + event File(bytes32 indexed what, uint256 data); + + // --- Init --- + constructor() public { + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- Administration --- + function file(bytes32 what, uint256 data) external auth { + if (what == "tau") tau = data; + else revert("LinearDecrease/file-unrecognized-param"); + emit File(what, data); + } + + // --- Math --- + uint256 constant RAY = 10 ** 27; + function add(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 rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x * y; + require(y == 0 || z / y == x); + z = z / RAY; + } + + // Price calculation when price is decreased linearly in proportion to time: + // tau: The number of seconds after the start of the auction where the price will hit 0 + // top: Initial price + // dur: current seconds since the start of the auction + // + // Returns y = top * ((tau - dur) / tau) + // + // Note the internal call to mul multiples by RAY, thereby ensuring that the rmul calculation + // which utilizes top and tau (RAY values) is also a RAY value. + function price(uint256 top, uint256 dur) override external view returns (uint256) { + if (dur >= tau) return 0; + return rmul(top, mul(tau - dur, RAY) / tau); + } +} + +contract StairstepExponentialDecrease is Abacus { + + // --- 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, "StairstepExponentialDecrease/not-authorized"); + _; + } + + // --- Data --- + uint256 public step; // Length of time between price drops [seconds] + uint256 public cut; // Per-step multiplicative factor [ray] + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + + event File(bytes32 indexed what, uint256 data); + + // --- Init --- + // @notice: `cut` and `step` values must be correctly set for + // this contract to return a valid price + constructor() public { + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- Administration --- + function file(bytes32 what, uint256 data) external auth { + if (what == "cut") require((cut = data) <= RAY, "StairstepExponentialDecrease/cut-gt-RAY"); + else if (what == "step") step = data; + else revert("StairstepExponentialDecrease/file-unrecognized-param"); + emit File(what, data); + } + + // --- Math --- + uint256 constant RAY = 10 ** 27; + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x * y; + require(y == 0 || z / y == x); + z = z / RAY; + } + // optimized version from dss PR #78 + function rpow(uint256 x, uint256 n, uint256 b) internal pure returns (uint256 z) { + assembly { + switch n case 0 { z := b } + default { + switch x case 0 { z := 0 } + default { + switch mod(n, 2) case 0 { z := b } default { z := x } + let half := div(b, 2) // for rounding. + for { n := div(n, 2) } n { n := div(n,2) } { + let xx := mul(x, x) + if shr(128, x) { revert(0,0) } + let xxRound := add(xx, half) + if lt(xxRound, xx) { revert(0,0) } + x := div(xxRound, b) + if mod(n,2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0,0) } + let zxRound := add(zx, half) + if lt(zxRound, zx) { revert(0,0) } + z := div(zxRound, b) + } + } + } + } + } + } + + // top: initial price + // dur: seconds since the auction has started + // step: seconds between a price drop + // cut: cut encodes the percentage to decrease per step. + // For efficiency, the values is set as (1 - (% value / 100)) * RAY + // So, for a 1% decrease per step, cut would be (1 - 0.01) * RAY + // + // returns: top * (cut ^ dur) + // + // + function price(uint256 top, uint256 dur) override external view returns (uint256) { + return rmul(top, rpow(cut, dur / step, RAY)); + } +} + +// While an equivalent function can be obtained by setting step = 1 in StairstepExponentialDecrease, +// this continous (i.e. per-second) exponential decrease has be implemented as it is more gas-efficient +// than using the stairstep version with step = 1 (primarily due to 1 fewer SLOAD per price calculation). +contract ExponentialDecrease is Abacus { + + // --- 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, "ExponentialDecrease/not-authorized"); + _; + } + + // --- Data --- + uint256 public cut; // Per-second multiplicative factor [ray] + + // --- Events --- + event Rely(address indexed usr); + event Deny(address indexed usr); + + event File(bytes32 indexed what, uint256 data); + + // --- Init --- + // @notice: `cut` value must be correctly set for + // this contract to return a valid price + constructor() public { + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- Administration --- + function file(bytes32 what, uint256 data) external auth { + if (what == "cut") require((cut = data) <= RAY, "ExponentialDecrease/cut-gt-RAY"); + else revert("ExponentialDecrease/file-unrecognized-param"); + emit File(what, data); + } + + // --- Math --- + uint256 constant RAY = 10 ** 27; + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x * y; + require(y == 0 || z / y == x); + z = z / RAY; + } + // optimized version from dss PR #78 + function rpow(uint256 x, uint256 n, uint256 b) internal pure returns (uint256 z) { + assembly { + switch n case 0 { z := b } + default { + switch x case 0 { z := 0 } + default { + switch mod(n, 2) case 0 { z := b } default { z := x } + let half := div(b, 2) // for rounding. + for { n := div(n, 2) } n { n := div(n,2) } { + let xx := mul(x, x) + if shr(128, x) { revert(0,0) } + let xxRound := add(xx, half) + if lt(xxRound, xx) { revert(0,0) } + x := div(xxRound, b) + if mod(n,2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0,0) } + let zxRound := add(zx, half) + if lt(zxRound, zx) { revert(0,0) } + z := div(zxRound, b) + } + } + } + } + } + } + + // top: initial price + // dur: seconds since the auction has started + // cut: cut encodes the percentage to decrease per second. + // For efficiency, the values is set as (1 - (% value / 100)) * RAY + // So, for a 1% decrease per second, cut would be (1 - 0.01) * RAY + // + // returns: top * (cut ^ dur) + // + function price(uint256 top, uint256 dur) override external view returns (uint256) { + return rmul(top, rpow(cut, dur, RAY)); + } +} diff --git a/src/cat.sol b/src/cat.sol index 744db346..f359dbcc 100644 --- a/src/cat.sol +++ b/src/cat.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface Kicker { function kick(address urn, address gal, uint256 tab, uint256 lot, uint256 bid) @@ -47,11 +49,11 @@ interface VowLike { function fess(uint256) external; } -contract Cat is LibNote { +contract Cat { // --- Auth --- mapping (address => uint256) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Cat/not-authorized"); _; @@ -107,20 +109,20 @@ contract Cat is LibNote { } // --- Administration --- - function file(bytes32 what, address data) external note auth { + function file(bytes32 what, address data) external auth { if (what == "vow") vow = VowLike(data); else revert("Cat/file-unrecognized-param"); } - function file(bytes32 what, uint256 data) external note auth { + function file(bytes32 what, uint256 data) external auth { if (what == "box") box = data; else revert("Cat/file-unrecognized-param"); } - function file(bytes32 ilk, bytes32 what, uint256 data) external note auth { + function file(bytes32 ilk, bytes32 what, uint256 data) external auth { if (what == "chop") ilks[ilk].chop = data; else if (what == "dunk") ilks[ilk].dunk = data; else revert("Cat/file-unrecognized-param"); } - function file(bytes32 ilk, bytes32 what, address flip) external note auth { + function file(bytes32 ilk, bytes32 what, address flip) external auth { if (what == "flip") { vat.nope(ilks[ilk].flip); ilks[ilk].flip = flip; @@ -177,11 +179,11 @@ contract Cat is LibNote { emit Bite(ilk, urn, dink, dart, mul(dart, rate), milk.flip, id); } - function claw(uint256 rad) external note auth { + function claw(uint256 rad) external auth { litter = sub(litter, rad); } - function cage() external note auth { + function cage() external auth { live = 0; } } diff --git a/src/clip.sol b/src/clip.sol new file mode 100644 index 00000000..96a08584 --- /dev/null +++ b/src/clip.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. +// +// 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 . + +pragma solidity >=0.6.12; + +interface VatLike { + function move(address,address,uint256) external; + function flux(bytes32,address,address,uint256) external; + function ilks(bytes32) external returns (uint256, uint256, uint256, uint256, uint256); + function suck(address,address,uint256) external; +} + +interface PipLike { + function peek() external returns (bytes32, bool); +} + +interface SpotterLike { + function par() external returns (uint256); + function ilks(bytes32) external returns (PipLike, uint256); +} + +interface DogLike { + function chop(bytes32) external returns (uint256); + function digs(bytes32, uint256) external; +} + +interface ClipperCallee { + function clipperCall(address, uint256, uint256, bytes calldata) external; +} + +interface AbacusLike { + function price(uint256, uint256) external view returns (uint256); +} + +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 immutable public ilk; // Collateral type of this Clipper + VatLike immutable public vat; // Core CDP Engine + + DogLike public dog; // Liquidation module + address public vow; // Recipient of dai raised in auctions + SpotterLike public spotter; // Collateral price module + AbacusLike public calc; // Current price calculator + + uint256 public buf; // Multiplicative factor to increase starting price [ray] + uint256 public tail; // Time elapsed before auction reset [seconds] + uint256 public cusp; // Percentage drop before auction reset [ray] + uint64 public chip; // Percentage of tab to suck from vow to incentivize keepers [wad] + uint192 public tip; // Flat fee to suck from vow to incentivize keepers [rad] + uint256 public chost; // Cache the ilk dust times the ilk chop to prevent excessive SLOADs [rad] + + uint256 public kicks; // Total auctions + uint256[] public active; // Array of active auction ids + + struct Sale { + uint256 pos; // Index in active array + uint256 tab; // Dai to raise [rad] + uint256 lot; // collateral to sell [wad] + address usr; // Liquidated CDP + uint96 tic; // Auction start time + uint256 top; // Starting price [ray] + } + 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); + + event Kick( + uint256 indexed id, + uint256 top, + uint256 tab, + uint256 lot, + address indexed usr, + address indexed kpr, + uint256 coin + ); + event Take( + uint256 indexed id, + uint256 max, + uint256 price, + uint256 owe, + uint256 tab, + uint256 lot, + address indexed usr + ); + event Redo( + uint256 indexed id, + uint256 top, + uint256 tab, + uint256 lot, + address indexed usr, + address indexed kpr, + uint256 coin + ); + + event Yank(uint256 id); + + // --- Init --- + 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 --- + 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 + else if (what == "chip") chip = uint64(data); // Percentage of tab to incentivize (max: 2^64 - 1 => 18.xxx WAD = 18xx%) + else if (what == "tip") tip = uint192(data); // Flat fee to incentivize keepers (max: 2^192 - 1 => 6.277T RAD) + else if (what == "stopped") stopped = data; // Set breaker (0, 1, 2, or 3) + else revert("Clipper/file-unrecognized-param"); + emit File(what, data); + } + 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. + 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. + 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)); + 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`. + 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] + (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. + 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 + uint256 slice = min(lot, amt); // slice <= lot + + // DAI needed to buy a slice of this sale + owe = mul(slice, price); + + // Don't collect more than tab of DAI + if (owe > tab) { + // Total debt will be paid + owe = tab; // owe' <= owe + // 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 + // If tab <= chost, buyers have to take the entire lot. + require(tab > _chost, "Clipper/no-partial-purchase"); + // Adjust amount to pay + 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 + // Calculate remaining lot after operation + lot = lot - slice; + + // Send collateral to 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 + 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 + vat.move(msg.sender, vow, owe); + + // Removes Dai out for liquidation from accumulator + dog_.digs(ilk, lot == 0 ? tab + owe : owe); + } + + if (lot == 0) { + _remove(id); + } else if (tab == 0) { + 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]; + if (id != _move) { + uint256 _index = sales[id].pos; + active[_index] = _move; + sales[_move].pos = _index; + } + active.pop(); + delete sales[id]; + } + + // The number of active auctions + function count() external view returns (uint256) { + return active.length; + } + + // Return the entire array of active auctions + function list() external view returns (uint256[] memory) { + return active; + } + + // Externally returns boolean for if an auction needs a redo and also the current price + 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 + 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. + function upchost() external { + (,,,, uint256 _dust) = VatLike(vat).ilks(ilk); + chost = wmul(_dust, dog.chop(ilk)); + } + + // Cancel an auction during ES or via governance action. + function yank(uint256 id) external auth lock { + require(sales[id].usr != address(0), "Clipper/not-running-auction"); + dog.digs(ilk, sales[id].tab); + vat.flux(ilk, address(this), msg.sender, sales[id].lot); + _remove(id); + emit Yank(id); + } +} diff --git a/src/dai.sol b/src/dai.sol index 0ea11d77..dd50d1b3 100644 --- a/src/dai.sol +++ b/src/dai.sol @@ -17,13 +17,15 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). -contract Dai is LibNote { +contract Dai { // --- Auth --- mapping (address => uint) public wards; - function rely(address guy) external note auth { wards[guy] = 1; } - function deny(address guy) external note auth { wards[guy] = 0; } + function rely(address guy) external auth { wards[guy] = 1; } + function deny(address guy) external auth { wards[guy] = 0; } modifier auth { require(wards[msg.sender] == 1, "Dai/not-authorized"); _; diff --git a/src/dog.sol b/src/dog.sol new file mode 100644 index 00000000..7ded7318 --- /dev/null +++ b/src/dog.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// dog.sol -- Dai liquidation module 2.0 + +// Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC. +// +// 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 . + +pragma solidity >=0.6.12; + +interface ClipperLike { + function ilk() external view returns (bytes32); + function kick( + uint256 tab, + uint256 lot, + address usr, + address kpr + ) external returns (uint256); +} + +interface VatLike { + function ilks(bytes32) external view returns ( + uint256 Art, // [wad] + uint256 rate, // [ray] + uint256 spot, // [ray] + uint256 line, // [rad] + uint256 dust // [rad] + ); + function urns(bytes32,address) external view returns ( + uint256 ink, // [wad] + uint256 art // [wad] + ); + function grab(bytes32,address,address,address,int256,int256) external; + function hope(address) external; + function nope(address) external; +} + +interface VowLike { + function fess(uint256) external; +} + +contract Dog { + // --- 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, "Dog/not-authorized"); + _; + } + + // --- Data --- + struct Ilk { + address clip; // Liquidator + uint256 chop; // Liquidation Penalty [wad] + uint256 hole; // Max DAI needed to cover debt+fees of active auctions per ilk [rad] + uint256 dirt; // Amt DAI needed to cover debt+fees of active auctions per ilk [rad] + } + + VatLike immutable public vat; // CDP Engine + + mapping (bytes32 => Ilk) public ilks; + + VowLike public vow; // Debt Engine + uint256 public live; // Active Flag + uint256 public Hole; // Max DAI needed to cover debt+fees of active auctions [rad] + uint256 public Dirt; // Amt DAI needed to cover debt+fees of active auctions [rad] + + // --- 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); + event File(bytes32 indexed ilk, bytes32 indexed what, uint256 data); + event File(bytes32 indexed ilk, bytes32 indexed what, address clip); + + event Bark( + bytes32 indexed ilk, + address indexed urn, + uint256 ink, + uint256 art, + uint256 due, + address clip, + uint256 indexed id + ); + event Digs(bytes32 indexed ilk, uint256 rad); + event Cage(); + + // --- Init --- + constructor(address vat_) public { + vat = VatLike(vat_); + live = 1; + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + // --- Math --- + uint256 constant WAD = 10 ** 18; + + 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); + } + + // --- Administration --- + function file(bytes32 what, address data) external auth { + if (what == "vow") vow = VowLike(data); + else revert("Dog/file-unrecognized-param"); + emit File(what, data); + } + function file(bytes32 what, uint256 data) external auth { + if (what == "Hole") Hole = data; + else revert("Dog/file-unrecognized-param"); + emit File(what, data); + } + function file(bytes32 ilk, bytes32 what, uint256 data) external auth { + if (what == "chop") { + require(data >= WAD, "Dog/file-chop-lt-WAD"); + ilks[ilk].chop = data; + } else if (what == "hole") ilks[ilk].hole = data; + else revert("Dog/file-unrecognized-param"); + emit File(ilk, what, data); + } + function file(bytes32 ilk, bytes32 what, address clip) external auth { + if (what == "clip") { + require(ilk == ClipperLike(clip).ilk(), "Dog/file-ilk-neq-clip.ilk"); + ilks[ilk].clip = clip; + } else revert("Dog/file-unrecognized-param"); + emit File(ilk, what, clip); + } + + function chop(bytes32 ilk) external view returns (uint256) { + return ilks[ilk].chop; + } + + // --- CDP Liquidation: all bark and no bite --- + // + // Liquidate a Vault and start a Dutch auction to sell its collateral for DAI. + // + // The third argument is the address that will receive the liquidation reward, if any. + // + // The entire Vault will be liquidated except when the target amount of DAI to be raised in + // the resulting auction (debt of Vault + liquidation penalty) causes either Dirt to exceed + // Hole or ilk.dirt to exceed ilk.hole by an economically significant amount. In that + // case, a partial liquidation is performed to respect the global and per-ilk limits on + // outstanding DAI target. The one exception is if the resulting auction would likely + // have too little collateral to be interesting to Keepers (debt taken from Vault < ilk.dust), + // in which case the function reverts. Please refer to the code and comments within if + // more detail is desired. + function bark(bytes32 ilk, address urn, address kpr) external returns (uint256 id) { + require(live == 1, "Dog/not-live"); + + (uint256 ink, uint256 art) = vat.urns(ilk, urn); + Ilk memory milk = ilks[ilk]; + uint256 dart; + uint256 rate; + uint256 dust; + { + uint256 spot; + (,rate, spot,, dust) = vat.ilks(ilk); + require(spot > 0 && mul(ink, spot) < mul(art, rate), "Dog/not-unsafe"); + + // Get the minimum value between: + // 1) Remaining space in the general Hole + // 2) Remaining space in the collateral hole + require(Hole > Dirt && milk.hole > milk.dirt, "Dog/liquidation-limit-hit"); + uint256 room = min(Hole - Dirt, milk.hole - milk.dirt); + + // uint256.max()/(RAD*WAD) = 115,792,089,237,316 + dart = min(art, mul(room, WAD) / rate / milk.chop); + + // Partial liquidation edge case logic + if (art > dart) { + if (mul(art - dart, rate) < dust) { + + // If the leftover Vault would be dusty, just liquidate it entirely. + // This will result in at least one of dirt_i > hole_i or Dirt > Hole becoming true. + // The amount of excess will be bounded above by ceiling(dust_i * chop_i / WAD). + // This deviation is assumed to be small compared to both hole_i and Hole, so that + // the extra amount of target DAI over the limits intended is not of economic concern. + dart = art; + } else { + + // In a partial liquidation, the resulting auction should also be non-dusty. + require(mul(dart, rate) >= dust, "Dog/dusty-auction-from-partial-liquidation"); + } + } + } + + uint256 dink = mul(ink, dart) / art; + + require(dink > 0, "Dog/null-auction"); + require(dart <= 2**255 && dink <= 2**255, "Dog/overflow"); + + vat.grab( + ilk, urn, milk.clip, address(vow), -int256(dink), -int256(dart) + ); + + uint256 due = mul(dart, rate); + vow.fess(due); + + { // Avoid stack too deep + // This calcuation will overflow if dart*rate exceeds ~10^14 + uint256 tab = mul(due, milk.chop) / WAD; + Dirt = add(Dirt, tab); + ilks[ilk].dirt = add(milk.dirt, tab); + + id = ClipperLike(milk.clip).kick({ + tab: tab, + lot: dink, + usr: urn, + kpr: kpr + }); + } + + emit Bark(ilk, urn, dink, dart, due, milk.clip, id); + } + + function digs(bytes32 ilk, uint256 rad) external auth { + Dirt = sub(Dirt, rad); + ilks[ilk].dirt = sub(ilks[ilk].dirt, rad); + emit Digs(ilk, rad); + } + + function cage() external auth { + live = 0; + emit Cage(); + } +} diff --git a/src/end.sol b/src/end.sol index d8f13774..295f70a0 100644 --- a/src/end.sol +++ b/src/end.sol @@ -18,9 +18,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pragma solidity >=0.5.12; - -import "./lib.sol"; +pragma solidity >=0.6.12; interface VatLike { function dai(address) external view returns (uint256); @@ -43,6 +41,7 @@ interface VatLike { function suck(address u, address v, uint256 rad) external; function cage() external; } + interface CatLike { function ilks(bytes32) external returns ( address flip, @@ -51,14 +50,27 @@ interface CatLike { ); function cage() external; } + +interface DogLike { + function ilks(bytes32) external returns ( + address clip, + uint256 chop, + uint256 hole, + uint256 dirt + ); + function cage() external; +} + interface PotLike { function cage() external; } + interface VowLike { function cage() external; } -interface Flippy { - function bids(uint id) external view returns ( + +interface FlipLike { + function bids(uint256 id) external view returns ( uint256 bid, // [rad] uint256 lot, // [wad] address guy, @@ -68,14 +80,26 @@ interface Flippy { address gal, uint256 tab // [rad] ); - function yank(uint id) external; + function yank(uint256 id) external; +} + +interface ClipLike { + function sales(uint256 id) external view returns ( + uint256 pos, + uint256 tab, + uint256 lot, + address usr, + uint96 tic, + uint256 top + ); + function yank(uint256 id) external; } interface PipLike { function read() external view returns (bytes32); } -interface Spotty { +interface SpotLike { function par() external view returns (uint256); function ilks(bytes32) external view returns ( PipLike pip, @@ -118,11 +142,13 @@ interface Spotty { We determine (b) by processing ongoing dai generating processes, i.e. auctions. We need to ensure that auctions will not generate any - further dai income. In the two-way auction model this occurs when + further dai income. + + In the two-way auction model (Flipper) this occurs when all auctions are in the reverse (`dent`) phase. There are two ways of ensuring this: - 4. i) `wait`: set the cooldown period to be at least as long as the + 4a. i) `wait`: set the cooldown period to be at least as long as the longest auction duration, which needs to be determined by the cage administrator. @@ -137,13 +163,26 @@ interface Spotty { `skip(ilk, id)`: - cancel individual flip auctions in the `tend` (forward) phase - - retrieves collateral and returns dai to bidder + - retrieves collateral and debt (including penalty) to owner's CDP + - returns dai to last bidder - `dent` (reverse) phase auctions can continue normally - Option (i), `wait`, is sufficient for processing the system - settlement but option (ii), `skip`, will speed it up. Both options - are available in this implementation, with `skip` being enabled on a - per-auction basis. + Option (i), `wait`, is sufficient (if all auctions were bidded at least + once) for processing the system settlement but option (ii), `skip`, + will speed it up. Both options are available in this implementation, + with `skip` being enabled on a per-auction basis. + + In the case of the Dutch Auctions model (Clipper) they keep recovering + debt during the whole lifetime and there isn't a max duration time + guaranteed for the auction to end. + So the way to ensure the protocol will not receive extra dai income is: + + 4b. i) `snip`: cancel all ongoing auctions and seize the collateral. + + `snip(ilk, id)`: + - cancel individual running clip auctions + - retrieves remaining collateral and debt (including penalty) + to owner's CDP When a CDP has been processed and has no debt remaining, the remaining collateral can be removed. @@ -184,11 +223,11 @@ interface Spotty { - the number of gems is limited by how big your bag is */ -contract End is LibNote { +contract End { // --- Auth --- - mapping (address => uint) public wards; - function rely(address guy) external note auth { wards[guy] = 1; } - function deny(address guy) external note auth { wards[guy] = 0; } + 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, "End/not-authorized"); _; @@ -197,9 +236,10 @@ contract End is LibNote { // --- Data --- VatLike public vat; // CDP Engine CatLike public cat; + DogLike public dog; VowLike public vow; // Debt Engine PotLike public pot; - Spotty public spot; + SpotLike public spot; uint256 public live; // Active Flag uint256 public when; // Time of cage [unix epoch time] @@ -214,140 +254,190 @@ contract End is LibNote { mapping (address => uint256) public bag; // [wad] mapping (bytes32 => mapping (address => uint256)) public out; // [wad] + // --- 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); + + event Cage(); + event Cage(bytes32 indexed ilk); + event Snip(bytes32 indexed ilk, uint256 indexed id, address indexed usr, uint256 tab, uint256 lot, uint256 art); + event Skip(bytes32 indexed ilk, uint256 indexed id, address indexed usr, uint256 tab, uint256 lot, uint256 art); + event Skim(bytes32 indexed ilk, address indexed urn, uint256 wad, uint256 art); + event Free(bytes32 indexed ilk, address indexed usr, uint256 ink); + event Thaw(); + event Flow(bytes32 indexed ilk); + event Pack(address indexed usr, uint256 wad); + event Cash(bytes32 indexed ilk, address indexed usr, uint256 wad); + // --- Init --- constructor() public { wards[msg.sender] = 1; live = 1; + emit Rely(msg.sender); } // --- Math --- - function add(uint x, uint y) internal pure returns (uint z) { + uint256 constant WAD = 10 ** 18; + uint256 constant RAY = 10 ** 27; + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x + y; require(z >= x); } - function sub(uint x, uint y) internal pure returns (uint z) { + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { require((z = x - y) <= x); } - function mul(uint x, uint y) internal pure returns (uint z) { + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { require(y == 0 || (z = x * y) / y == x); } - function min(uint x, uint y) internal pure returns (uint z) { + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { return x <= y ? x : y; } - uint constant WAD = 10 ** 18; - uint constant RAY = 10 ** 27; - function rmul(uint x, uint y) internal pure returns (uint z) { + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { z = mul(x, y) / RAY; } - function rdiv(uint x, uint y) internal pure returns (uint z) { + function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { z = mul(x, RAY) / y; } - function wdiv(uint x, uint y) internal pure returns (uint z) { + function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { z = mul(x, WAD) / y; } // --- Administration --- - function file(bytes32 what, address data) external note auth { + function file(bytes32 what, address data) external auth { require(live == 1, "End/not-live"); if (what == "vat") vat = VatLike(data); - else if (what == "cat") cat = CatLike(data); - else if (what == "vow") vow = VowLike(data); - else if (what == "pot") pot = PotLike(data); - else if (what == "spot") spot = Spotty(data); + else if (what == "cat") cat = CatLike(data); + else if (what == "dog") dog = DogLike(data); + else if (what == "vow") vow = VowLike(data); + else if (what == "pot") pot = PotLike(data); + else if (what == "spot") spot = SpotLike(data); else revert("End/file-unrecognized-param"); + emit File(what, data); } - function file(bytes32 what, uint256 data) external note auth { + function file(bytes32 what, uint256 data) external auth { require(live == 1, "End/not-live"); if (what == "wait") wait = data; else revert("End/file-unrecognized-param"); + emit File(what, data); } // --- Settlement --- - function cage() external note auth { + function cage() external auth { require(live == 1, "End/not-live"); live = 0; - when = now; + when = block.timestamp; vat.cage(); cat.cage(); + dog.cage(); vow.cage(); spot.cage(); pot.cage(); + emit Cage(); } - function cage(bytes32 ilk) external note { + function cage(bytes32 ilk) external { require(live == 0, "End/still-live"); require(tag[ilk] == 0, "End/tag-ilk-already-defined"); (Art[ilk],,,,) = vat.ilks(ilk); (PipLike pip,) = spot.ilks(ilk); // par is a ray, pip returns a wad - tag[ilk] = wdiv(spot.par(), uint(pip.read())); + tag[ilk] = wdiv(spot.par(), uint256(pip.read())); + emit Cage(ilk); + } + + function snip(bytes32 ilk, uint256 id) external { + require(tag[ilk] != 0, "End/tag-ilk-not-defined"); + + (address _clip,,,) = dog.ilks(ilk); + ClipLike clip = ClipLike(_clip); + (, uint256 rate,,,) = vat.ilks(ilk); + (, uint256 tab, uint256 lot, address usr,,) = clip.sales(id); + + vat.suck(address(vow), address(vow), tab); + clip.yank(id); + + uint256 art = tab / rate; + Art[ilk] = add(Art[ilk], art); + require(int256(lot) >= 0 && int256(art) >= 0, "End/overflow"); + vat.grab(ilk, usr, address(this), address(vow), int256(lot), int256(art)); + emit Snip(ilk, id, usr, tab, lot, art); } - function skip(bytes32 ilk, uint256 id) external note { + function skip(bytes32 ilk, uint256 id) external { require(tag[ilk] != 0, "End/tag-ilk-not-defined"); - (address flipV,,) = cat.ilks(ilk); - Flippy flip = Flippy(flipV); - (, uint rate,,,) = vat.ilks(ilk); - (uint bid, uint lot,,,, address usr,, uint tab) = flip.bids(id); + (address _flip,,) = cat.ilks(ilk); + FlipLike flip = FlipLike(_flip); + (, uint256 rate,,,) = vat.ilks(ilk); + (uint256 bid, uint256 lot,,,, address usr,, uint256 tab) = flip.bids(id); vat.suck(address(vow), address(vow), tab); vat.suck(address(vow), address(this), bid); vat.hope(address(flip)); flip.yank(id); - uint art = tab / rate; + uint256 art = tab / rate; Art[ilk] = add(Art[ilk], art); - require(int(lot) >= 0 && int(art) >= 0, "End/overflow"); - vat.grab(ilk, usr, address(this), address(vow), int(lot), int(art)); + require(int256(lot) >= 0 && int256(art) >= 0, "End/overflow"); + vat.grab(ilk, usr, address(this), address(vow), int256(lot), int256(art)); + emit Skip(ilk, id, usr, tab, lot, art); } - function skim(bytes32 ilk, address urn) external note { + function skim(bytes32 ilk, address urn) external { require(tag[ilk] != 0, "End/tag-ilk-not-defined"); - (, uint rate,,,) = vat.ilks(ilk); - (uint ink, uint art) = vat.urns(ilk, urn); + (, uint256 rate,,,) = vat.ilks(ilk); + (uint256 ink, uint256 art) = vat.urns(ilk, urn); - uint owe = rmul(rmul(art, rate), tag[ilk]); - uint wad = min(ink, owe); + uint256 owe = rmul(rmul(art, rate), tag[ilk]); + uint256 wad = min(ink, owe); gap[ilk] = add(gap[ilk], sub(owe, wad)); require(wad <= 2**255 && art <= 2**255, "End/overflow"); - vat.grab(ilk, urn, address(this), address(vow), -int(wad), -int(art)); + vat.grab(ilk, urn, address(this), address(vow), -int256(wad), -int256(art)); + emit Skim(ilk, urn, wad, art); } - function free(bytes32 ilk) external note { + function free(bytes32 ilk) external { require(live == 0, "End/still-live"); - (uint ink, uint art) = vat.urns(ilk, msg.sender); + (uint256 ink, uint256 art) = vat.urns(ilk, msg.sender); require(art == 0, "End/art-not-zero"); require(ink <= 2**255, "End/overflow"); - vat.grab(ilk, msg.sender, msg.sender, address(vow), -int(ink), 0); + vat.grab(ilk, msg.sender, msg.sender, address(vow), -int256(ink), 0); + emit Free(ilk, msg.sender, ink); } - function thaw() external note { + function thaw() external { require(live == 0, "End/still-live"); require(debt == 0, "End/debt-not-zero"); require(vat.dai(address(vow)) == 0, "End/surplus-not-zero"); - require(now >= add(when, wait), "End/wait-not-finished"); + require(block.timestamp >= add(when, wait), "End/wait-not-finished"); debt = vat.debt(); + emit Thaw(); } - function flow(bytes32 ilk) external note { + function flow(bytes32 ilk) external { require(debt != 0, "End/debt-zero"); require(fix[ilk] == 0, "End/fix-ilk-already-defined"); - (, uint rate,,,) = vat.ilks(ilk); + (, uint256 rate,,,) = vat.ilks(ilk); uint256 wad = rmul(rmul(Art[ilk], rate), tag[ilk]); fix[ilk] = rdiv(mul(sub(wad, gap[ilk]), RAY), debt); + emit Flow(ilk); } - function pack(uint256 wad) external note { + function pack(uint256 wad) external { require(debt != 0, "End/debt-zero"); vat.move(msg.sender, address(vow), mul(wad, RAY)); bag[msg.sender] = add(bag[msg.sender], wad); + emit Pack(msg.sender, wad); } - function cash(bytes32 ilk, uint wad) external note { + function cash(bytes32 ilk, uint256 wad) external { require(fix[ilk] != 0, "End/fix-ilk-not-defined"); vat.flux(ilk, address(this), msg.sender, rmul(wad, fix[ilk])); out[ilk][msg.sender] = add(out[ilk][msg.sender], wad); require(out[ilk][msg.sender] <= bag[msg.sender], "End/insufficient-bag-balance"); + emit Cash(ilk, msg.sender, wad); } } diff --git a/src/flap.sol b/src/flap.sol index eb5ab42e..4c010e70 100644 --- a/src/flap.sol +++ b/src/flap.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface VatLike { function move(address,address,uint) external; @@ -39,11 +41,11 @@ interface GemLike { - `end` max auction duration */ -contract Flapper is LibNote { +contract Flapper { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Flapper/not-authorized"); _; @@ -94,7 +96,7 @@ contract Flapper is LibNote { } // --- Admin --- - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { if (what == "beg") beg = data; else if (what == "ttl") ttl = uint48(data); else if (what == "tau") tau = uint48(data); @@ -116,12 +118,12 @@ contract Flapper is LibNote { emit Kick(id, lot, bid); } - function tick(uint id) external note { + function tick(uint id) external { require(bids[id].end < now, "Flapper/not-finished"); require(bids[id].tic == 0, "Flapper/bid-already-placed"); bids[id].end = add(uint48(now), tau); } - function tend(uint id, uint lot, uint bid) external note { + function tend(uint id, uint lot, uint bid) external { require(live == 1, "Flapper/not-live"); require(bids[id].guy != address(0), "Flapper/guy-not-set"); require(bids[id].tic > now || bids[id].tic == 0, "Flapper/already-finished-tic"); @@ -140,7 +142,7 @@ contract Flapper is LibNote { bids[id].bid = bid; bids[id].tic = add(uint48(now), ttl); } - function deal(uint id) external note { + function deal(uint id) external { require(live == 1, "Flapper/not-live"); require(bids[id].tic != 0 && (bids[id].tic < now || bids[id].end < now), "Flapper/not-finished"); vat.move(address(this), bids[id].guy, bids[id].lot); @@ -148,11 +150,11 @@ contract Flapper is LibNote { delete bids[id]; } - function cage(uint rad) external note auth { + function cage(uint rad) external auth { live = 0; vat.move(address(this), msg.sender, rad); } - function yank(uint id) external note { + function yank(uint id) external { require(live == 0, "Flapper/still-live"); require(bids[id].guy != address(0), "Flapper/guy-not-set"); gem.move(address(this), bids[id].guy, bids[id].bid); diff --git a/src/flip.sol b/src/flip.sol index 193ba888..5b40dcab 100644 --- a/src/flip.sol +++ b/src/flip.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface VatLike { function move(address,address,uint256) external; @@ -44,11 +46,11 @@ interface CatLike { - `end` max auction duration */ -contract Flipper is LibNote { +contract Flipper { // --- Auth --- mapping (address => uint256) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Flipper/not-authorized"); _; @@ -105,13 +107,13 @@ contract Flipper is LibNote { } // --- Admin --- - function file(bytes32 what, uint256 data) external note auth { + function file(bytes32 what, uint256 data) external auth { if (what == "beg") beg = data; else if (what == "ttl") ttl = uint48(data); else if (what == "tau") tau = uint48(data); else revert("Flipper/file-unrecognized-param"); } - function file(bytes32 what, address data) external note auth { + function file(bytes32 what, address data) external auth { if (what == "cat") cat = CatLike(data); else revert("Flipper/file-unrecognized-param"); } @@ -135,12 +137,12 @@ contract Flipper is LibNote { emit Kick(id, lot, bid, tab, usr, gal); } - function tick(uint256 id) external note { + function tick(uint256 id) external { require(bids[id].end < now, "Flipper/not-finished"); require(bids[id].tic == 0, "Flipper/bid-already-placed"); bids[id].end = add(uint48(now), tau); } - function tend(uint256 id, uint256 lot, uint256 bid) external note { + function tend(uint256 id, uint256 lot, uint256 bid) external { require(bids[id].guy != address(0), "Flipper/guy-not-set"); require(bids[id].tic > now || bids[id].tic == 0, "Flipper/already-finished-tic"); require(bids[id].end > now, "Flipper/already-finished-end"); @@ -159,7 +161,7 @@ contract Flipper is LibNote { bids[id].bid = bid; bids[id].tic = add(uint48(now), ttl); } - function dent(uint256 id, uint256 lot, uint256 bid) external note { + function dent(uint256 id, uint256 lot, uint256 bid) external { require(bids[id].guy != address(0), "Flipper/guy-not-set"); require(bids[id].tic > now || bids[id].tic == 0, "Flipper/already-finished-tic"); require(bids[id].end > now, "Flipper/already-finished-end"); @@ -178,14 +180,14 @@ contract Flipper is LibNote { bids[id].lot = lot; bids[id].tic = add(uint48(now), ttl); } - function deal(uint256 id) external note { + function deal(uint256 id) external { require(bids[id].tic != 0 && (bids[id].tic < now || bids[id].end < now), "Flipper/not-finished"); cat.claw(bids[id].tab); vat.flux(ilk, address(this), bids[id].guy, bids[id].lot); delete bids[id]; } - function yank(uint256 id) external note auth { + function yank(uint256 id) external auth { require(bids[id].guy != address(0), "Flipper/guy-not-set"); require(bids[id].bid < bids[id].tab, "Flipper/already-dent-phase"); cat.claw(bids[id].tab); diff --git a/src/flop.sol b/src/flop.sol index 75ac383f..518f357d 100644 --- a/src/flop.sol +++ b/src/flop.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface VatLike { function move(address,address,uint) external; @@ -44,11 +46,11 @@ interface VowLike { - `end` max auction duration */ -contract Flopper is LibNote { +contract Flopper { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Flopper/not-authorized"); _; @@ -105,7 +107,7 @@ contract Flopper is LibNote { } // --- Admin --- - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { if (what == "beg") beg = data; else if (what == "pad") pad = data; else if (what == "ttl") ttl = uint48(data); @@ -126,13 +128,13 @@ contract Flopper is LibNote { emit Kick(id, lot, bid, gal); } - function tick(uint id) external note { + function tick(uint id) external { require(bids[id].end < now, "Flopper/not-finished"); require(bids[id].tic == 0, "Flopper/bid-already-placed"); bids[id].lot = mul(pad, bids[id].lot) / ONE; bids[id].end = add(uint48(now), tau); } - function dent(uint id, uint lot, uint bid) external note { + function dent(uint id, uint lot, uint bid) external { require(live == 1, "Flopper/not-live"); require(bids[id].guy != address(0), "Flopper/guy-not-set"); require(bids[id].tic > now || bids[id].tic == 0, "Flopper/already-finished-tic"); @@ -157,7 +159,7 @@ contract Flopper is LibNote { bids[id].lot = lot; bids[id].tic = add(uint48(now), ttl); } - function deal(uint id) external note { + function deal(uint id) external { require(live == 1, "Flopper/not-live"); require(bids[id].tic != 0 && (bids[id].tic < now || bids[id].end < now), "Flopper/not-finished"); gem.mint(bids[id].guy, bids[id].lot); @@ -165,11 +167,11 @@ contract Flopper is LibNote { } // --- Shutdown --- - function cage() external note auth { + function cage() external auth { live = 0; vow = msg.sender; } - function yank(uint id) external note { + function yank(uint id) external { require(live == 0, "Flopper/still-live"); require(bids[id].guy != address(0), "Flopper/guy-not-set"); vat.suck(vow, bids[id].guy, bids[id].bid); diff --git a/src/join.sol b/src/join.sol index 3a3b2cd7..a06d415b 100644 --- a/src/join.sol +++ b/src/join.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface GemLike { function decimals() external view returns (uint); @@ -61,11 +63,11 @@ interface VatLike { */ -contract GemJoin is LibNote { +contract GemJoin { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "GemJoin/not-authorized"); _; @@ -85,27 +87,27 @@ contract GemJoin is LibNote { gem = GemLike(gem_); dec = gem.decimals(); } - function cage() external note auth { + function cage() external auth { live = 0; } - function join(address usr, uint wad) external note { + function join(address usr, uint wad) external { require(live == 1, "GemJoin/not-live"); require(int(wad) >= 0, "GemJoin/overflow"); vat.slip(ilk, usr, int(wad)); require(gem.transferFrom(msg.sender, address(this), wad), "GemJoin/failed-transfer"); } - function exit(address usr, uint wad) external note { + function exit(address usr, uint wad) external { require(wad <= 2 ** 255, "GemJoin/overflow"); vat.slip(ilk, msg.sender, -int(wad)); require(gem.transfer(usr, wad), "GemJoin/failed-transfer"); } } -contract DaiJoin is LibNote { +contract DaiJoin { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "DaiJoin/not-authorized"); _; @@ -121,18 +123,18 @@ contract DaiJoin is LibNote { vat = VatLike(vat_); dai = DSTokenLike(dai_); } - function cage() external note auth { + function cage() external auth { live = 0; } uint constant ONE = 10 ** 27; function mul(uint x, uint y) internal pure returns (uint z) { require(y == 0 || (z = x * y) / y == x); } - function join(address usr, uint wad) external note { + function join(address usr, uint wad) external { vat.move(address(this), usr, mul(ONE, wad)); dai.burn(msg.sender, wad); } - function exit(address usr, uint wad) external note { + function exit(address usr, uint wad) external { require(live == 1, "DaiJoin/not-live"); vat.move(msg.sender, address(this), mul(ONE, wad)); dai.mint(usr, wad); diff --git a/src/jug.sol b/src/jug.sol index 68c5be2a..c72aa2f6 100644 --- a/src/jug.sol +++ b/src/jug.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface VatLike { function ilks(bytes32) external returns ( @@ -29,11 +31,11 @@ interface VatLike { function fold(bytes32,address,int) external; } -contract Jug is LibNote { +contract Jug { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Jug/not-authorized"); _; @@ -96,28 +98,28 @@ contract Jug is LibNote { } // --- Administration --- - function init(bytes32 ilk) external note auth { + function init(bytes32 ilk) external auth { Ilk storage i = ilks[ilk]; require(i.duty == 0, "Jug/ilk-already-init"); i.duty = ONE; i.rho = now; } - function file(bytes32 ilk, bytes32 what, uint data) external note auth { + function file(bytes32 ilk, bytes32 what, uint data) external auth { require(now == ilks[ilk].rho, "Jug/rho-not-updated"); if (what == "duty") ilks[ilk].duty = data; else revert("Jug/file-unrecognized-param"); } - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { if (what == "base") base = data; else revert("Jug/file-unrecognized-param"); } - function file(bytes32 what, address data) external note auth { + function file(bytes32 what, address data) external auth { if (what == "vow") vow = data; else revert("Jug/file-unrecognized-param"); } // --- Stability Fee Collection --- - function drip(bytes32 ilk) external note returns (uint rate) { + function drip(bytes32 ilk) external returns (uint rate) { require(now >= ilks[ilk].rho, "Jug/invalid-now"); (, uint prev) = vat.ilks(ilk); rate = rmul(rpow(add(base, ilks[ilk].duty), now - ilks[ilk].rho, ONE), prev); diff --git a/src/lib.sol b/src/lib.sol deleted file mode 100644 index 73ad7550..00000000 --- a/src/lib.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity >=0.5.12; - -contract LibNote { - event LogNote( - bytes4 indexed sig, - address indexed usr, - bytes32 indexed arg1, - bytes32 indexed arg2, - bytes data - ) anonymous; - - modifier note { - _; - assembly { - // log an 'anonymous' event with a constant 6 words of calldata - // and four indexed topics: selector, caller, arg1 and arg2 - let mark := msize() // end of memory ensures zero - mstore(0x40, add(mark, 288)) // update free memory pointer - mstore(mark, 0x20) // bytes type data offset - mstore(add(mark, 0x20), 224) // bytes size (padded) - calldatacopy(add(mark, 0x40), 0, 224) // bytes payload - log4(mark, 288, // calldata - shl(224, shr(224, calldataload(0))), // msg.sig - caller(), // msg.sender - calldataload(4), // arg1 - calldataload(36) // arg2 - ) - } - } -} diff --git a/src/pot.sol b/src/pot.sol index 61d60ac6..13ddce04 100644 --- a/src/pot.sol +++ b/src/pot.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). /* "Savings Dai" is obtained when Dai is deposited into @@ -45,11 +47,11 @@ interface VatLike { function suck(address,address,uint256) external; } -contract Pot is LibNote { +contract Pot { // --- Auth --- mapping (address => uint) public wards; - function rely(address guy) external note auth { wards[guy] = 1; } - function deny(address guy) external note auth { wards[guy] = 0; } + function rely(address guy) external auth { wards[guy] = 1; } + function deny(address guy) external auth { wards[guy] = 0; } modifier auth { require(wards[msg.sender] == 1, "Pot/not-authorized"); _; @@ -121,25 +123,25 @@ contract Pot is LibNote { } // --- Administration --- - function file(bytes32 what, uint256 data) external note auth { + function file(bytes32 what, uint256 data) external auth { require(live == 1, "Pot/not-live"); require(now == rho, "Pot/rho-not-updated"); if (what == "dsr") dsr = data; else revert("Pot/file-unrecognized-param"); } - function file(bytes32 what, address addr) external note auth { + function file(bytes32 what, address addr) external auth { if (what == "vow") vow = addr; else revert("Pot/file-unrecognized-param"); } - function cage() external note auth { + function cage() external auth { live = 0; dsr = ONE; } // --- Savings Rate Accumulation --- - function drip() external note returns (uint tmp) { + function drip() external returns (uint tmp) { require(now >= rho, "Pot/invalid-now"); tmp = rmul(rpow(dsr, now - rho, ONE), chi); uint chi_ = sub(tmp, chi); @@ -149,14 +151,14 @@ contract Pot is LibNote { } // --- Savings Dai Management --- - function join(uint wad) external note { + function join(uint wad) external { require(now == rho, "Pot/rho-not-updated"); pie[msg.sender] = add(pie[msg.sender], wad); Pie = add(Pie, wad); vat.move(msg.sender, address(this), mul(chi, wad)); } - function exit(uint wad) external note { + function exit(uint wad) external { pie[msg.sender] = sub(pie[msg.sender], wad); Pie = sub(Pie, wad); vat.move(address(this), msg.sender, mul(chi, wad)); diff --git a/src/spot.sol b/src/spot.sol index ffa30c25..404fbaed 100644 --- a/src/spot.sol +++ b/src/spot.sol @@ -17,7 +17,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface VatLike { function file(bytes32, bytes32, uint) external; @@ -27,11 +29,11 @@ interface PipLike { function peek() external returns (bytes32, bool); } -contract Spotter is LibNote { +contract Spotter { // --- Auth --- mapping (address => uint) public wards; - function rely(address guy) external note auth { wards[guy] = 1; } - function deny(address guy) external note auth { wards[guy] = 0; } + function rely(address guy) external auth { wards[guy] = 1; } + function deny(address guy) external auth { wards[guy] = 0; } modifier auth { require(wards[msg.sender] == 1, "Spotter/not-authorized"); _; @@ -76,17 +78,17 @@ contract Spotter is LibNote { } // --- Administration --- - function file(bytes32 ilk, bytes32 what, address pip_) external note auth { + function file(bytes32 ilk, bytes32 what, address pip_) external auth { require(live == 1, "Spotter/not-live"); if (what == "pip") ilks[ilk].pip = PipLike(pip_); else revert("Spotter/file-unrecognized-param"); } - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { require(live == 1, "Spotter/not-live"); if (what == "par") par = data; else revert("Spotter/file-unrecognized-param"); } - function file(bytes32 ilk, bytes32 what, uint data) external note auth { + function file(bytes32 ilk, bytes32 what, uint data) external auth { require(live == 1, "Spotter/not-live"); if (what == "mat") ilks[ilk].mat = data; else revert("Spotter/file-unrecognized-param"); @@ -100,7 +102,7 @@ contract Spotter is LibNote { emit Poke(ilk, val, spot); } - function cage() external note auth { + function cage() external auth { live = 0; } } diff --git a/src/test/abaci.t.sol b/src/test/abaci.t.sol new file mode 100644 index 00000000..d8dd8eb7 --- /dev/null +++ b/src/test/abaci.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity >=0.6.12; + +import "ds-test/test.sol"; + +import "../abaci.sol"; + +interface Hevm { + function warp(uint256) external; + function store(address,bytes32,bytes32) external; +} + +contract ClipperTest is DSTest { + Hevm hevm; + + uint256 RAY = 10 ** 27; + + // CHEAT_CODE = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + bytes20 constant CHEAT_CODE = + bytes20(uint160(uint256(keccak256('hevm cheat code')))); + + uint256 constant startTime = 604411200; // Used to avoid issues with `now` + + function setUp() public { + hevm = Hevm(address(CHEAT_CODE)); + hevm.warp(startTime); + } + + function assertEqWithinTolerance( + uint256 x, + uint256 y, + uint256 tolerance) internal { + uint256 diff; + if (x >= y) { + diff = x - y; + } else { + diff = y - x; + } + assertTrue(diff <= tolerance); + } + + function checkExpDecrease( + StairstepExponentialDecrease calc, + uint256 cut, + uint256 step, + uint256 top, + uint256 tic, + uint256 percentDecrease, + uint256 testTime, + uint256 tolerance + ) + public + { + uint256 price; + uint256 lastPrice; + uint256 testPrice; + + hevm.warp(startTime); + calc.file(bytes32("step"), step); + calc.file(bytes32("cut"), cut); + price = calc.price(top, now - tic); + assertEq(price, top); + + for(uint256 i = 1; i < testTime; i += 1) { + hevm.warp(startTime + i); + lastPrice = price; + price = calc.price(top, now - tic); + // Stairstep calculation + if (i % step == 0) { testPrice = lastPrice * percentDecrease / RAY; } + else { testPrice = lastPrice; } + assertEqWithinTolerance(testPrice, price, tolerance); + } + } + + function test_stairstep_exp_decrease() public { + StairstepExponentialDecrease calc = new StairstepExponentialDecrease(); + uint256 tic = now; // Start of auction + uint256 percentDecrease; + uint256 step; + uint256 testTime = 10 minutes; + + + /*** Extreme high collateral price ($50m) ***/ + + uint256 tolerance = 100000000; // Tolerance scales with price + uint256 top = 50000000 * RAY; + + // 1.1234567890% decrease every 1 second + // TODO: Check if there's a cleaner way to do this. I was getting rational_const errors. + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 1; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 2.1234567890% decrease every 1 second + percentDecrease = RAY - 2.1234567890E27 / 100; + step = 1; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 1.1234567890% decrease every 5 seconds + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 5; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 2.1234567890% decrease every 5 seconds + percentDecrease = RAY - 2.1234567890E27 / 100; + step = 5; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 1.1234567890% decrease every 5 minutes + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 5 minutes; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + + /*** Extreme low collateral price ($0.0000001) ***/ + + tolerance = 1; // Lowest tolerance is 1e-27 + top = 1 * RAY / 10000000; + + // 1.1234567890% decrease every 1 second + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 1; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 2.1234567890% decrease every 1 second + percentDecrease = RAY - 2.1234567890E27 / 100; + step = 1; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 1.1234567890% decrease every 5 seconds + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 5; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 2.1234567890% decrease every 5 seconds + percentDecrease = RAY - 2.1234567890E27 / 100; + step = 5; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + + // 1.1234567890% decrease every 5 minutes + percentDecrease = RAY - 1.1234567890E27 / 100; + step = 5 minutes; + checkExpDecrease(calc, percentDecrease, step, top, tic, percentDecrease, testTime, tolerance); + } + + function test_continuous_exp_decrease() public { + ExponentialDecrease calc = new ExponentialDecrease(); + uint256 tHalf = 900; + uint256 cut = 0.999230132966E27; // ~15 half life, cut ~= e^(ln(1/2)/900) + calc.file("cut", cut); + + uint256 top = 4000 * RAY; + uint256 expectedPrice = top; + uint256 tolerance = RAY / 1000; // 0.001, i.e 0.1% + for (uint256 i = 0; i < 5; i++) { // will cover initial value + four half-lives + assertEqWithinTolerance(calc.price(top, i*tHalf), expectedPrice, tolerance); + // each loop iteration advances one half-life, so expectedPrice decreases by a factor of 2 + expectedPrice /= 2; + } + } + + function test_linear_decrease() public { + hevm.warp(startTime); + LinearDecrease calc = new LinearDecrease(); + calc.file(bytes32("tau"), 3600); + + uint256 top = 1000 * RAY; + uint256 tic = now; // Start of auction + uint256 price = calc.price(top, now - tic); + assertEq(price, top); + + hevm.warp(startTime + 360); // 6min in, 1/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100) * RAY); + + hevm.warp(startTime + 360 * 2); // 12min in, 2/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 2) * RAY); + + hevm.warp(startTime + 360 * 3); // 18min in, 3/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 3) * RAY); + + hevm.warp(startTime + 360 * 4); // 24min in, 4/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 4) * RAY); + + hevm.warp(startTime + 360 * 5); // 30min in, 5/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 5) * RAY); + + hevm.warp(startTime + 360 * 6); // 36min in, 6/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 6) * RAY); + + hevm.warp(startTime + 360 * 7); // 42min in, 7/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 7) * RAY); + + hevm.warp(startTime + 360 * 8); // 48min in, 8/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 8) * RAY); + + hevm.warp(startTime + 360 * 9); // 54min in, 9/10 done + price = calc.price(top, now - tic); + assertEq(price, (1000 - 100 * 9) * RAY); + + hevm.warp(startTime + 360 * 10); // 60min in, 10/10 done + price = calc.price(top, now - tic); + assertEq(price, 0); + } +} diff --git a/src/test/clip.t.sol b/src/test/clip.t.sol new file mode 100644 index 00000000..db31b4ec --- /dev/null +++ b/src/test/clip.t.sol @@ -0,0 +1,1718 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity >=0.6.12; + +import "ds-test/test.sol"; +import "ds-token/token.sol"; +import "ds-value/value.sol"; + +import {Vat} from "../vat.sol"; +import {Spotter} from "../spot.sol"; +import {Vow} from "../vow.sol"; +import {GemJoin, DaiJoin} from "../join.sol"; + +import {Clipper} from "../clip.sol"; +import "../abaci.sol"; +import "../dog.sol"; + +interface Hevm { + function warp(uint256) external; + function store(address,bytes32,bytes32) external; +} + +contract Exchange { + + DSToken gold; + DSToken dai; + uint256 goldPrice; + + constructor(DSToken gold_, DSToken dai_, uint256 goldPrice_) public { + gold = gold_; + dai = dai_; + goldPrice = goldPrice_; + } + + function sellGold(uint256 goldAmt) external { + gold.transferFrom(msg.sender, address(this), goldAmt); + uint256 daiAmt = goldAmt * goldPrice / 1E18; + dai.transfer(msg.sender, daiAmt); + } +} + +contract Trader { + + Clipper clip; + Vat vat; + DSToken gold; + GemJoin goldJoin; + DSToken dai; + DaiJoin daiJoin; + Exchange exchange; + + constructor( + Clipper clip_, + Vat vat_, + DSToken gold_, + GemJoin goldJoin_, + DSToken dai_, + DaiJoin daiJoin_, + Exchange exchange_ + ) public { + clip = clip_; + vat = vat_; + gold = gold_; + goldJoin = goldJoin_; + dai = dai_; + daiJoin = daiJoin_; + exchange = exchange_; + } + + function take( + uint256 id, + uint256 amt, + uint256 max, + address who, + bytes calldata data + ) + external + { + clip.take({ + id: id, + amt: amt, + max: max, + who: who, + data: data + }); + } + + function clipperCall(address sender, uint256 owe, uint256 slice, bytes calldata data) + external { + data; + goldJoin.exit(address(this), slice); + gold.approve(address(exchange)); + exchange.sellGold(slice); + dai.approve(address(daiJoin)); + vat.hope(address(clip)); + daiJoin.join(sender, owe / 1E27); + } +} + +contract Guy { + Clipper clip; + + constructor(Clipper clip_) public { + clip = clip_; + } + + function hope(address usr) public { + Vat(address(clip.vat())).hope(usr); + } + + function take( + uint256 id, + uint256 amt, + uint256 max, + address who, + bytes calldata data + ) + external + { + clip.take({ + id: id, + amt: amt, + max: max, + who: who, + data: data + }); + } + + function bark(Dog dog, bytes32 ilk, address urn, address usr) external { + dog.bark(ilk, urn, usr); + } +} + +contract BadGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall(address sender, uint256 owe, uint256 slice, bytes calldata data) + external { + sender; owe; slice; data; + clip.take({ // attempt reentrancy + id: 1, + amt: 25 ether, + max: 5 ether * 10E27, + who: address(this), + data: "" + }); + } +} + +contract RedoGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall( + address sender, uint256 owe, uint256 slice, bytes calldata data + ) external { + owe; slice; data; + clip.redo(1, sender); + } +} + +contract KickGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall( + address sender, uint256 owe, uint256 slice, bytes calldata data + ) external { + sender; owe; slice; data; + clip.kick(1, 1, address(0), address(0)); + } +} + +contract FileUintGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall( + address sender, uint256 owe, uint256 slice, bytes calldata data + ) external { + sender; owe; slice; data; + clip.file("stopped", 1); + } +} + +contract FileAddrGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall( + address sender, uint256 owe, uint256 slice, bytes calldata data + ) external { + sender; owe; slice; data; + clip.file("vow", address(123)); + } +} + +contract YankGuy is Guy { + + constructor(Clipper clip_) Guy(clip_) public {} + + function clipperCall( + address sender, uint256 owe, uint256 slice, bytes calldata data + ) external { + sender; owe; slice; data; + clip.yank(1); + } +} + +contract PublicClip is Clipper { + + constructor(address vat, address spot, address dog, bytes32 ilk) public Clipper(vat, spot, dog, ilk) {} + + function add() public returns (uint256 id) { + id = ++kicks; + active.push(id); + sales[id].pos = active.length - 1; + } + + function remove(uint256 id) public { + _remove(id); + } +} + +contract ClipperTest is DSTest { + Hevm hevm; + + Vat vat; + Dog dog; + Spotter spot; + Vow vow; + DSValue pip; + DSToken gold; + GemJoin goldJoin; + DSToken dai; + DaiJoin daiJoin; + + Clipper clip; + + address me; + Exchange exchange; + + address ali; + address bob; + address che; + + uint256 WAD = 10 ** 18; + uint256 RAY = 10 ** 27; + uint256 RAD = 10 ** 45; + + // CHEAT_CODE = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + bytes20 constant CHEAT_CODE = + bytes20(uint160(uint256(keccak256('hevm cheat code')))); + + bytes32 constant ilk = "gold"; + uint256 constant goldPrice = 5 ether; + + uint256 constant startTime = 604411200; // Used to avoid issues with `now` + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function _ink(bytes32 ilk_, address urn_) internal view returns (uint256) { + (uint256 ink_,) = vat.urns(ilk_, urn_); + return ink_; + } + function _art(bytes32 ilk_, address urn_) internal view returns (uint256) { + (,uint256 art_) = vat.urns(ilk_, urn_); + return art_; + } + + modifier takeSetup { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + StairstepExponentialDecrease calc = new StairstepExponentialDecrease(); + calc.file("cut", RAY - ray(0.01 ether)); // 1% decrease + calc.file("step", 1); // Decrease every 1 second + + clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer + clip.file("calc", address(calc)); // File price contract + clip.file("cusp", ray(0.3 ether)); // 70% drop before reset + clip.file("tail", 3600); // 1 hour before reset + + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + assertEq(clip.kicks(), 0); + dog.bark(ilk, me, address(this)); + assertEq(clip.kicks(), 1); + + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 0); + assertEq(art, 0); + + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(5 ether)); // $4 plus 25% + + assertEq(vat.gem(ilk, ali), 0); + assertEq(vat.dai(ali), rad(1000 ether)); + assertEq(vat.gem(ilk, bob), 0); + assertEq(vat.dai(bob), rad(1000 ether)); + + _; + } + + function ray(uint256 wad) internal pure returns (uint256) { + return wad * 10 ** 9; + } + function rad(uint256 wad) internal pure returns (uint256) { + return wad * 10 ** 27; + } + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + function setUp() public { + hevm = Hevm(address(CHEAT_CODE)); + hevm.warp(startTime); + + me = address(this); + + vat = new Vat(); + + spot = new Spotter(address(vat)); + vat.rely(address(spot)); + + vow = new Vow(address(vat), address(0), address(0)); + gold = new DSToken("GLD"); + goldJoin = new GemJoin(address(vat), ilk, address(gold)); + vat.rely(address(goldJoin)); + dai = new DSToken("DAI"); + daiJoin = new DaiJoin(address(vat), address(dai)); + vat.suck(address(0), address(daiJoin), rad(1000 ether)); + exchange = new Exchange(gold, dai, goldPrice * 11 / 10); + + dai.mint(1000 ether); + dai.transfer(address(exchange), 1000 ether); + dai.setOwner(address(daiJoin)); + gold.mint(1000 ether); + gold.transfer(address(goldJoin), 1000 ether); + + dog = new Dog(address(vat)); + dog.file("vow", address(vow)); + vat.rely(address(dog)); + vow.rely(address(dog)); + + vat.init(ilk); + + vat.slip(ilk, me, 1000 ether); + + pip = new DSValue(); + pip.poke(bytes32(goldPrice)); // Spot = $2.5 + + spot.file(ilk, "pip", address(pip)); + spot.file(ilk, "mat", ray(2 ether)); // 200% liquidation ratio for easier test calcs + spot.poke(ilk); + + vat.file(ilk, "dust", rad(20 ether)); // $20 dust + vat.file(ilk, "line", rad(10000 ether)); + vat.file("Line", rad(10000 ether)); + + dog.file(ilk, "chop", 1.1 ether); // 10% chop + dog.file(ilk, "hole", rad(1000 ether)); + dog.file("Hole", rad(1000 ether)); + + // dust and chop filed previously so clip.chost will be set correctly + clip = new Clipper(address(vat), address(spot), address(dog), ilk); + clip.upchost(); + clip.rely(address(dog)); + + dog.file(ilk, "clip", address(clip)); + dog.rely(address(clip)); + vat.rely(address(clip)); + + assertEq(vat.gem(ilk, me), 1000 ether); + assertEq(vat.dai(me), 0); + vat.frob(ilk, me, me, me, 40 ether, 100 ether); + assertEq(vat.gem(ilk, me), 960 ether); + assertEq(vat.dai(me), rad(100 ether)); + + pip.poke(bytes32(uint256(4 ether))); // Spot = $2 + spot.poke(ilk); // Now unsafe + + ali = address(new Guy(clip)); + bob = address(new Guy(clip)); + che = address(new Trader(clip, vat, gold, goldJoin, dai, daiJoin, exchange)); + + vat.hope(address(clip)); + Guy(ali).hope(address(clip)); + Guy(bob).hope(address(clip)); + + vat.suck(address(0), address(this), rad(1000 ether)); + vat.suck(address(0), address(ali), rad(1000 ether)); + vat.suck(address(0), address(bob), rad(1000 ether)); + } + + function test_change_dog() public { + assertTrue(address(clip.dog()) != address(123)); + clip.file("dog", address(123)); + assertEq(address(clip.dog()), address(123)); + } + + function test_get_chop() public { + uint256 chop = dog.chop(ilk); + (, uint256 chop2,,) = dog.ilks(ilk); + assertEq(chop, chop2); + } + + function test_kick() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI + clip.file("chip", 0); // No linear increase + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + assertEq(vat.dai(ali), rad(1000 ether)); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + Guy(ali).bark(dog, ilk, me, address(ali)); + + assertEq(clip.kicks(), 1); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(4 ether)); + assertEq(vat.gem(ilk, me), 960 ether); + assertEq(vat.dai(ali), rad(1100 ether)); // Paid "tip" amount of DAI for calling bark() + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 0 ether); + assertEq(art, 0 ether); + + pip.poke(bytes32(goldPrice)); // Spot = $2.5 + spot.poke(ilk); // Now safe + + hevm.warp(startTime + 100); + vat.frob(ilk, me, me, me, 40 ether, 100 ether); + + pip.poke(bytes32(uint256(4 ether))); // Spot = $2 + spot.poke(ilk); // Now unsafe + + (pos, tab, lot, usr, tic, top) = clip.sales(2); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 920 ether); + + clip.file(bytes32("buf"), ray(1.25 ether)); // 25% Initial price buffer + + clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI + clip.file("chip", 0.02 ether); // Linear increase of 2% of tab + + assertEq(vat.dai(bob), rad(1000 ether)); + + Guy(bob).bark(dog, ilk, me, address(bob)); + + assertEq(clip.kicks(), 2); + (pos, tab, lot, usr, tic, top) = clip.sales(2); + assertEq(pos, 1); + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(5 ether)); + assertEq(vat.gem(ilk, me), 920 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 0 ether); + assertEq(art, 0 ether); + + assertEq(vat.dai(bob), rad(1000 ether) + rad(100 ether) + tab * 0.02 ether / WAD); // Paid (tip + due * chip) amount of DAI for calling bark() + } + + function testFail_kick_zero_price() public { + pip.poke(bytes32(0)); + dog.bark(ilk, me, address(this)); + } + + function testFail_redo_zero_price() public { + auctionResetSetup(1 hours); + + pip.poke(bytes32(0)); + + hevm.warp(startTime + 1801 seconds); + (bool needsRedo,,,) = clip.getStatus(1); + assertTrue(needsRedo); + clip.redo(1, address(this)); + } + + function try_kick(uint256 tab, uint256 lot, address usr, address kpr) internal returns (bool ok) { + string memory sig = "kick(uint256,uint256,address,address)"; + (ok,) = address(clip).call(abi.encodeWithSignature(sig, tab, lot, usr, kpr)); + } + + function test_kick_basic() public { + assertTrue(try_kick(1 ether, 2 ether, address(1), address(this))); + } + + function test_kick_zero_tab() public { + assertTrue(!try_kick(0, 2 ether, address(1), address(this))); + } + + function test_kick_zero_lot() public { + assertTrue(!try_kick(1 ether, 0, address(1), address(this))); + } + + function test_kick_zero_usr() public { + assertTrue(!try_kick(1 ether, 2 ether, address(0), address(this))); + } + + function try_bark(bytes32 ilk_, address urn_) internal returns (bool ok) { + string memory sig = "bark(bytes32,address,address)"; + (ok,) = address(dog).call(abi.encodeWithSignature(sig, ilk_, urn_, address(this))); + } + + function test_bark_not_leaving_dust() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + dog.file(ilk, "hole", rad(80 ether)); // Makes room = 80 WAD + dog.file(ilk, "chop", 1 ether); // 0% chop (for precise calculations) + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + assertTrue(try_bark(ilk, me)); // art - dart = 100 - 80 = dust (= 20) + + assertEq(clip.kicks(), 1); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(80 ether)); // No chop + assertEq(lot, 32 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(4 ether)); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 8 ether); + assertEq(art, 20 ether); + } + + function test_bark_not_leaving_dust_over_hole() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + dog.file(ilk, "hole", rad(80 ether) + ray(1 ether)); // Makes room = 80 WAD + 1 wei + dog.file(ilk, "chop", 1 ether); // 0% chop (for precise calculations) + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + assertTrue(try_bark(ilk, me)); // art - dart = 100 - (80 + 1 wei) < dust (= 20) then the whole debt is taken + + assertEq(clip.kicks(), 1); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(100 ether)); // No chop + assertEq(lot, 40 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(4 ether)); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 0 ether); + assertEq(art, 0 ether); + } + + function test_bark_not_leaving_dust_rate() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + vat.fold(ilk, address(vow), int256(ray(0.02 ether))); + (, uint256 rate,,,) = vat.ilks(ilk); + assertEq(rate, ray(1.02 ether)); + + dog.file(ilk, "hole", 100 * RAD); // Makes room = 100 RAD + dog.file(ilk, "chop", 1 ether); // 0% chop for precise calculations + vat.file(ilk, "dust", 20 * RAD); // 20 DAI minimum Vault debt + clip.upchost(); + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); // Full debt is 102 DAI since rate = 1.02 * RAY + + // (art - dart) * rate ~= 2 RAD < dust = 20 RAD + // => remnant would be dusty, so a full liquidation occurs. + assertTrue(try_bark(ilk, me)); + + assertEq(clip.kicks(), 1); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, mul(100 ether, rate)); // No chop + assertEq(lot, 40 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(4 ether)); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 0); + assertEq(art, 0); + } + + function test_bark_only_leaving_dust_over_hole_rate() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + vat.fold(ilk, address(vow), int256(ray(0.02 ether))); + (, uint256 rate,,,) = vat.ilks(ilk); + assertEq(rate, ray(1.02 ether)); + + dog.file(ilk, "hole", 816 * RAD / 10); // Makes room = 81.6 RAD => dart = 80 + dog.file(ilk, "chop", 1 ether); // 0% chop for precise calculations + vat.file(ilk, "dust", 204 * RAD / 10); // 20.4 DAI dust + clip.upchost(); + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + // (art - dart) * rate = 20.4 RAD == dust + // => marginal threshold at which partial liquidation is acceptable + assertTrue(try_bark(ilk, me)); + + assertEq(clip.kicks(), 1); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 816 * RAD / 10); // Equal to ilk.hole + assertEq(lot, 32 ether); + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(4 ether)); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 8 ether); + assertEq(art, 20 ether); + (,,,, uint256 dust) = vat.ilks(ilk); + assertEq(art * rate, dust); + } + + function test_Hole_hole() public { + assertEq(dog.Dirt(), 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + + dog.bark(ilk, me, address(this)); + + (, uint256 tab,,,,) = clip.sales(1); + + assertEq(dog.Dirt(), tab); + (,,, dirt) = dog.ilks(ilk); + assertEq(dirt, tab); + + bytes32 ilk2 = "silver"; + Clipper clip2 = new Clipper(address(vat), address(spot), address(dog), ilk2); + clip2.upchost(); + clip2.rely(address(dog)); + + dog.file(ilk2, "clip", address(clip2)); + dog.file(ilk2, "chop", 1.1 ether); + dog.file(ilk2, "hole", rad(1000 ether)); + dog.rely(address(clip2)); + + vat.init(ilk2); + vat.rely(address(clip2)); + vat.file(ilk2, "line", rad(100 ether)); + + vat.slip(ilk2, me, 40 ether); + + DSValue pip2 = new DSValue(); + pip2.poke(bytes32(goldPrice)); // Spot = $2.5 + + spot.file(ilk2, "pip", address(pip2)); + spot.file(ilk2, "mat", ray(2 ether)); + spot.poke(ilk2); + vat.frob(ilk2, me, me, me, 40 ether, 100 ether); + pip2.poke(bytes32(uint256(4 ether))); // Spot = $2 + spot.poke(ilk2); + + dog.bark(ilk2, me, address(this)); + + (, uint256 tab2,,,,) = clip2.sales(1); + + assertEq(dog.Dirt(), tab + tab2); + (,,, dirt) = dog.ilks(ilk); + (,,, uint256 dirt2) = dog.ilks(ilk2); + assertEq(dirt, tab); + assertEq(dirt2, tab2); + } + + function test_partial_liquidation_Hole_limit() public { + dog.file("Hole", rad(75 ether)); + + assertEq(_ink(ilk, me), 40 ether); + assertEq(_art(ilk, me), 100 ether); + + assertEq(dog.Dirt(), 0); + (,uint256 chop,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + + dog.bark(ilk, me, address(this)); + + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + + (, uint256 rate,,,) = vat.ilks(ilk); + + assertEq(lot, 40 ether * (tab * WAD / rate / chop) / 100 ether); + assertEq(tab, rad(75 ether) - ray(0.2 ether)); // 0.2 RAY rounding error + + assertEq(_ink(ilk, me), 40 ether - lot); + assertEq(_art(ilk, me), 100 ether - tab * WAD / rate / chop); + + assertEq(dog.Dirt(), tab); + (,,, dirt) = dog.ilks(ilk); + assertEq(dirt, tab); + } + + function test_partial_liquidation_hole_limit() public { + dog.file(ilk, "hole", rad(75 ether)); + + assertEq(_ink(ilk, me), 40 ether); + assertEq(_art(ilk, me), 100 ether); + + assertEq(dog.Dirt(), 0); + (,uint256 chop,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + + dog.bark(ilk, me, address(this)); + + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + + (, uint256 rate,,,) = vat.ilks(ilk); + + assertEq(lot, 40 ether * (tab * WAD / rate / chop) / 100 ether); + assertEq(tab, rad(75 ether) - ray(0.2 ether)); // 0.2 RAY rounding error + + assertEq(_ink(ilk, me), 40 ether - lot); + assertEq(_art(ilk, me), 100 ether - tab * WAD / rate / chop); + + assertEq(dog.Dirt(), tab); + (,,, dirt) = dog.ilks(ilk); + assertEq(dirt, tab); + } + + function try_take(uint256 id, uint256 amt, uint256 max, address who, bytes memory data) internal returns (bool ok) { + string memory sig = "take(uint256,uint256,uint256,address,bytes)"; + (ok,) = address(clip).call(abi.encodeWithSignature(sig, id, amt, max, who, data)); + } + + function test_take_zero_usr() public takeSetup { + // Auction id 2 is unpopulated. + (,,, address usr,,) = clip.sales(2); + assertEq(usr, address(0)); + assertTrue(!try_take(2, 25 ether, ray(5 ether), address(ali), "")); + } + + function test_take_over_tab() public takeSetup { + // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD) + // Readjusts slice to be tab/top = 25 + Guy(ali).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + + assertEq(vat.gem(ilk, ali), 22 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(890 ether)); // Didn't pay more than tab (110) + assertEq(vat.gem(ilk, me), 978 ether); // 960 + (40 - 22) returned to usr + + // Assert auction ends + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + + assertEq(dog.Dirt(), 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + } + + function test_take_at_tab() public takeSetup { + // Bid so owe (= 22 * 5 = 110 RAD) == tab (= 110 RAD) + Guy(ali).take({ + id: 1, + amt: 22 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + + assertEq(vat.gem(ilk, ali), 22 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(890 ether)); // Paid full tab (110) + assertEq(vat.gem(ilk, me), 978 ether); // 960 + (40 - 22) returned to usr + + // Assert auction ends + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + + assertEq(dog.Dirt(), 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + } + + function test_take_under_tab() public takeSetup { + // Bid so owe (= 11 * 5 = 55 RAD) < tab (= 110 RAD) + Guy(ali).take({ + id: 1, + amt: 11 ether, // Half of tab at $110 + max: ray(5 ether), + who: address(ali), + data: "" + }); + + assertEq(vat.gem(ilk, ali), 11 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(945 ether)); // Paid half tab (55) + assertEq(vat.gem(ilk, me), 960 ether); // Collateral not returned (yet) + + // Assert auction DOES NOT end + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(55 ether)); // 110 - 5 * 11 + assertEq(lot, 29 ether); // 40 - 11 + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(5 ether)); + + assertEq(dog.Dirt(), tab); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, tab); + } + + function test_take_full_lot_partial_tab() public takeSetup { + hevm.warp(now + 69); // approx 50% price decline + // Bid to purchase entire lot less than tab (~2.5 * 40 ~= 100 < 110) + Guy(ali).take({ + id: 1, + amt: 40 ether, // purchase all collateral + max: ray(2.5 ether), + who: address(ali), + data: "" + }); + + assertEq(vat.gem(ilk, ali), 40 ether); // Took entire lot + assertTrue(sub(vat.dai(ali), rad(900 ether)) < rad(0.1 ether)); // Paid about 100 ether + assertEq(vat.gem(ilk, me), 960 ether); // Collateral not returned + + // Assert auction ends + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + + // All dirt should be cleared, since the auction has ended, even though < 100% of tab was collected + assertEq(dog.Dirt(), 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + } + + function testFail_take_bid_too_low() public takeSetup { + // Bid so max (= 4) < price (= top = 5) (fails with "Clipper/too-expensive") + Guy(ali).take({ + id: 1, + amt: 22 ether, + max: ray(4 ether), + who: address(ali), + data: "" + }); + } + + function test_take_bid_recalculates_due_to_chost_check() public takeSetup { + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + + (, uint256 price,uint256 _lot, uint256 _tab) = clip.getStatus(1); + assertEq(_lot, lot); + assertEq(_tab, tab); + assertEq(price, ray(5 ether)); + + // Bid for an amount that would leave less than chost remaining tab--bid will be decreased + // to leave tab == chost post-execution. + Guy(ali).take({ + id: 1, + amt: 18 * WAD, // Costs 90 DAI at current price; 110 - 90 == 20 < 22 == chost + max: ray(5 ether), + who: address(ali), + data: "" + }); + + (, tab, lot,,,) = clip.sales(1); + assertEq(tab, clip.chost()); + assertEq(lot, 40 ether - (110 * RAD - clip.chost()) / price); + } + + function test_take_bid_avoids_recalculate_due_no_more_lot() public takeSetup { + hevm.warp(now + 60); // Reducing the price + + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + + (, uint256 price,,) = clip.getStatus(1); + assertEq(price, 2735783211953807380973706855); // 2.73 RAY + + // Bid so owe (= (22 - 1wei) * 5 = 110 RAD - 1) < tab (= 110 RAD) + // 1 < 20 RAD => owe = 110 RAD - 20 RAD + Guy(ali).take({ + id: 1, + amt: 40 ether, + max: ray(2.8 ether), + who: address(ali), + data: "" + }); + + // 40 * 2.73 = 109.42... + // It means a very low amount of tab (< dust) would remain but doesn't matter + // as the auction is finished because there isn't more lot + (, tab, lot,,,) = clip.sales(1); + assertEq(tab, 0); + assertEq(lot, 0); + } + + function test_take_bid_fails_no_partial_allowed() public takeSetup { + (, uint256 price,,) = clip.getStatus(1); + assertEq(price, ray(5 ether)); + + clip.take({ + id: 1, + amt: 17.6 ether, + max: ray(5 ether), + who: address(this), + data: "" + }); + + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + assertEq(tab, rad(22 ether)); + assertEq(lot, 22.4 ether); + assertTrue(!(tab > clip.chost())); + + assertTrue(!try_take({ + id: 1, + amt: 1 ether, // partial purchase attempt when !(tab > chost) + max: ray(5 ether), + who: address(this), + data: "" + })); + + clip.take({ + id: 1, + amt: tab / price, // This time take the whole tab + max: ray(5 ether), + who: address(this), + data: "" + }); + } + + function test_take_multiple_bids_different_prices() public takeSetup { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + + // Bid so owe (= 10 * 5 = 50 RAD) < tab (= 110 RAD) + Guy(ali).take({ + id: 1, + amt: 10 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + + assertEq(vat.gem(ilk, ali), 10 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(950 ether)); // Paid some tab (50) + assertEq(vat.gem(ilk, me), 960 ether); // Collateral not returned (yet) + + // Assert auction DOES NOT end + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(60 ether)); // 110 - 5 * 10 + assertEq(lot, 30 ether); // 40 - 10 + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(5 ether)); + + hevm.warp(now + 30); + + (, uint256 _price, uint256 _lot,) = clip.getStatus(1); + Guy(bob).take({ + id: 1, + amt: _lot, // Buy the rest of the lot + max: ray(_price), // 5 * 0.99 ** 30 = 3.698501866941401 RAY => max > price + who: address(bob), + data: "" + }); + + // Assert auction is over + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0 * WAD); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + + uint256 expectedGem = (RAY * 60 ether) / _price; // tab / price + assertEq(vat.gem(ilk, bob), expectedGem); // Didn't take whole lot + assertEq(vat.dai(bob), rad(940 ether)); // Paid rest of tab (60) + + uint256 lotReturn = 30 ether - expectedGem; // lot - loaf.tab / max = 15 + assertEq(vat.gem(ilk, me), 960 ether + lotReturn); // Collateral returned (10 WAD) + } + + function auctionResetSetup(uint256 tau) internal { + LinearDecrease calc = new LinearDecrease(); + calc.file(bytes32("tau"), tau); // tau hours till zero is reached (used to test tail) + + vat.file(ilk, "dust", rad(20 ether)); // $20 dust + + clip.file("buf", ray(1.25 ether)); // 25% Initial price buffer + clip.file("calc", address(calc)); // File price contract + clip.file("cusp", ray(0.5 ether)); // 50% drop before reset + clip.file("tail", 3600); // 1 hour before reset + + assertEq(clip.kicks(), 0); + dog.bark(ilk, me, address(this)); + assertEq(clip.kicks(), 1); + } + + function try_redo(uint256 id, address kpr) internal returns (bool ok) { + string memory sig = "redo(uint256,address)"; + (ok,) = address(clip).call(abi.encodeWithSignature(sig, id, kpr)); + } + + function test_auction_reset_tail() public { + auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail) + + pip.poke(bytes32(uint256(3 ether))); // Spot = $1.50 (update price before reset is called) + + (,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1); + assertEq(uint256(ticBefore), startTime); + assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke) + + hevm.warp(startTime + 3600 seconds); + (bool needsRedo,,,) = clip.getStatus(1); + assertTrue(!needsRedo); + assertTrue(!try_redo(1, address(this))); + hevm.warp(startTime + 3601 seconds); + (needsRedo,,,) = clip.getStatus(1); + assertTrue(needsRedo); + assertTrue(try_redo(1, address(this))); + + (,,,, uint96 ticAfter, uint256 topAfter) = clip.sales(1); + assertEq(uint256(ticAfter), startTime + 3601 seconds); // (now) + assertEq(topAfter, ray(3.75 ether)); // $3 spot + 25% buffer = $5 (used most recent OSM price) + } + + function test_auction_reset_cusp() public { + auctionResetSetup(1 hours); // 1 hour till zero is reached (used to test cusp) + + pip.poke(bytes32(uint256(3 ether))); // Spot = $1.50 (update price before reset is called) + + (,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1); + assertEq(uint256(ticBefore), startTime); + assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke) + + hevm.warp(startTime + 1800 seconds); + (bool needsRedo,,,) = clip.getStatus(1); + assertTrue(!needsRedo); + assertTrue(!try_redo(1, address(this))); + hevm.warp(startTime + 1801 seconds); + (needsRedo,,,) = clip.getStatus(1); + assertTrue(needsRedo); + assertTrue(try_redo(1, address(this))); + + (,,,, uint96 ticAfter, uint256 topAfter) = clip.sales(1); + assertEq(uint256(ticAfter), startTime + 1801 seconds); // (now) + assertEq(topAfter, ray(3.75 ether)); // $3 spot + 25% buffer = $3.75 (used most recent OSM price) + } + + function test_auction_reset_tail_twice() public { + auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail) + + hevm.warp(startTime + 3601 seconds); + clip.redo(1, address(this)); + + assertTrue(!try_redo(1, address(this))); + } + + function test_auction_reset_cusp_twice() public { + auctionResetSetup(1 hours); // 1 hour till zero is reached (used to test cusp) + + hevm.warp(startTime + 1801 seconds); // Price goes below 50% "cusp" after 30min01sec + clip.redo(1, address(this)); + + assertTrue(!try_redo(1, address(this))); + } + + function test_redo_zero_usr() public { + // Can't reset a non-existent auction. + assertTrue(!try_redo(1, address(this))); + } + + function test_setBreaker() public { + clip.file("stopped", 1); + assertEq(clip.stopped(), 1); + clip.file("stopped", 2); + assertEq(clip.stopped(), 2); + clip.file("stopped", 3); + assertEq(clip.stopped(), 3); + clip.file("stopped", 0); + assertEq(clip.stopped(), 0); + } + + function test_stopped_kick() public { + uint256 pos; + uint256 tab; + uint256 lot; + address usr; + uint96 tic; + uint256 top; + uint256 ink; + uint256 art; + + assertEq(clip.kicks(), 0); + (pos, tab, lot, usr, tic, top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + (ink, art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + // Any level of stoppage prevents kicking. + clip.file("stopped", 1); + assertTrue(!try_bark(ilk, me)); + + clip.file("stopped", 2); + assertTrue(!try_bark(ilk, me)); + + clip.file("stopped", 3); + assertTrue(!try_bark(ilk, me)); + + clip.file("stopped", 0); + assertTrue(try_bark(ilk, me)); + } + + // At a stopped == 1 we are ok to take + function test_stopped_1_take() public takeSetup { + clip.file("stopped", 1); + // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD) + // Readjusts slice to be tab/top = 25 + Guy(ali).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + } + + function test_stopped_2_take() public takeSetup { + clip.file("stopped", 2); + // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD) + // Readjusts slice to be tab/top = 25 + Guy(ali).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + } + + function testFail_stopped_3_take() public takeSetup { + clip.file("stopped", 3); + // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD) + // Readjusts slice to be tab/top = 25 + Guy(ali).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + } + + function test_stopped_1_auction_reset_tail() public { + auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail) + + clip.file("stopped", 1); + + pip.poke(bytes32(uint256(3 ether))); // Spot = $1.50 (update price before reset is called) + + (,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1); + assertEq(uint256(ticBefore), startTime); + assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke) + + hevm.warp(startTime + 3600 seconds); + assertTrue(!try_redo(1, address(this))); + hevm.warp(startTime + 3601 seconds); + assertTrue(try_redo(1, address(this))); + + (,,,, uint96 ticAfter, uint256 topAfter) = clip.sales(1); + assertEq(uint256(ticAfter), startTime + 3601 seconds); // (now) + assertEq(topAfter, ray(3.75 ether)); // $3 spot + 25% buffer = $5 (used most recent OSM price) + } + + function test_stopped_2_auction_reset_tail() public { + auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail) + + clip.file("stopped", 2); + + pip.poke(bytes32(uint256(3 ether))); // Spot = $1.50 (update price before reset is called) + + (,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1); + assertEq(uint256(ticBefore), startTime); + assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke) + + hevm.warp(startTime + 3601 seconds); + (bool needsRedo,,,) = clip.getStatus(1); + assertTrue(needsRedo); // Redo possible if circuit breaker not set + assertTrue(!try_redo(1, address(this))); // Redo fails because of circuit breaker + } + + function test_stopped_3_auction_reset_tail() public { + auctionResetSetup(10 hours); // 10 hours till zero is reached (used to test tail) + + clip.file("stopped", 3); + + pip.poke(bytes32(uint256(3 ether))); // Spot = $1.50 (update price before reset is called) + + (,,,, uint96 ticBefore, uint256 topBefore) = clip.sales(1); + assertEq(uint256(ticBefore), startTime); + assertEq(topBefore, ray(5 ether)); // $4 spot + 25% buffer = $5 (wasn't affected by poke) + + hevm.warp(startTime + 3601 seconds); + (bool needsRedo,,,) = clip.getStatus(1); + assertTrue(needsRedo); // Redo possible if circuit breaker not set + assertTrue(!try_redo(1, address(this))); // Redo fails because of circuit breaker + } + + function test_redo_incentive() public takeSetup { + clip.file("tip", rad(100 ether)); // Flat fee of 100 DAI + clip.file("chip", 0); // No linear increase + + (, uint256 tab, uint256 lot,,,) = clip.sales(1); + + assertEq(tab, rad(110 ether)); + assertEq(lot, 40 ether); + + hevm.warp(now + 300); + clip.redo(1, address(123)); + assertEq(vat.dai(address(123)), clip.tip()); + + clip.file("chip", 0.02 ether); // Reward 2% of tab + hevm.warp(now + 300); + clip.redo(1, address(234)); + assertEq(vat.dai(address(234)), clip.tip() + clip.chip() * tab / WAD); + + clip.file("tip", 0); // No more flat fee + hevm.warp(now + 300); + clip.redo(1, address(345)); + assertEq(vat.dai(address(345)), clip.chip() * tab / WAD); + + vat.file(ilk, "dust", rad(100 ether) + 1); // ensure wmul(dust, chop) > 110 DAI (tab) + clip.upchost(); + assertEq(clip.chost(), 110 * RAD + 1); + + hevm.warp(now + 300); + clip.redo(1, address(456)); + assertEq(vat.dai(address(456)), 0); + + // Set dust so that wmul(dust, chop) is well below tab to check the dusty lot case. + vat.file(ilk, "dust", rad(20 ether)); // $20 dust + clip.upchost(); + assertEq(clip.chost(), 22 * RAD); + + hevm.warp(now + 100); // Reducing the price + + (, uint256 price,,) = clip.getStatus(1); + assertEq(price, 1830161706366147524653080130); // 1.83 RAY + + clip.take({ + id: 1, + amt: 38 ether, + max: ray(5 ether), + who: address(this), + data: "" + }); + + (, tab, lot,,,) = clip.sales(1); + + assertEq(tab, rad(110 ether) - 38 ether * price); // > 22 DAI chost + // When auction is reset the current price of lot + // is calculated from oracle price ($4) to see if dusty + assertEq(lot, 2 ether); // (2 * $4) < $20 quivalent (dusty collateral) + + hevm.warp(now + 300); + clip.redo(1, address(567)); + assertEq(vat.dai(address(567)), 0); + } + + function test_incentive_max_values() public { + clip.file("chip", 2 ** 64 - 1); + clip.file("tip", 2 ** 192 - 1); + + assertEq(uint256(clip.chip()), uint256(18.446744073709551615 * 10 ** 18)); + assertEq(uint256(clip.tip()), uint256(6277101735386.680763835789423207666416102355444464034512895 * 10 ** 45)); + + clip.file("chip", 2 ** 64); + clip.file("tip", 2 ** 192); + + assertEq(uint256(clip.chip()), 0); + assertEq(uint256(clip.tip()), 0); + } + + function test_Clipper_yank() public takeSetup { + uint256 preGemBalance = vat.gem(ilk, address(this)); + (,, uint256 origLot,,,) = clip.sales(1); + + uint startGas = gasleft(); + clip.yank(1); + uint endGas = gasleft(); + emit log_named_uint("yank gas", startGas - endGas); + + // Assert that the auction was deleted. + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + + // Assert that callback to clear dirt was successful. + assertEq(dog.Dirt(), 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(dirt, 0); + + // Assert transfer of gem. + assertEq(vat.gem(ilk, address(this)), preGemBalance + origLot); + } + + function test_remove_id() public { + PublicClip pclip = new PublicClip(address(vat), address(spot), address(dog), "gold"); + uint256 pos; + + pclip.add(); + pclip.add(); + uint256 id = pclip.add(); + pclip.add(); + pclip.add(); + + // [1,2,3,4,5] + assertEq(pclip.count(), 5); // 5 elements added + assertEq(pclip.active(0), 1); + assertEq(pclip.active(1), 2); + assertEq(pclip.active(2), 3); + assertEq(pclip.active(3), 4); + assertEq(pclip.active(4), 5); + + pclip.remove(id); + + // [1,2,5,4] + assertEq(pclip.count(), 4); + assertEq(pclip.active(0), 1); + assertEq(pclip.active(1), 2); + assertEq(pclip.active(2), 5); // Swapped last for middle + (pos,,,,,) = pclip.sales(5); + assertEq(pos, 2); + assertEq(pclip.active(3), 4); + + pclip.remove(4); + + // [1,2,5] + assertEq(pclip.count(), 3); + + (pos,,,,,) = pclip.sales(1); + assertEq(pos, 0); // Sale 1 in slot 0 + assertEq(pclip.active(0), 1); + + (pos,,,,,) = pclip.sales(2); + assertEq(pos, 1); // Sale 2 in slot 1 + assertEq(pclip.active(1), 2); + + (pos,,,,,) = pclip.sales(5); + assertEq(pos, 2); // Sale 5 in slot 2 + assertEq(pclip.active(2), 5); // Final element removed + + (pos,,,,,) = pclip.sales(4); + assertEq(pos, 0); // Sale 4 was deleted. Returns 0 + } + + function testFail_id_out_of_range() public { + PublicClip pclip = new PublicClip(address(vat), address(spot), address(dog), "gold"); + + pclip.add(); + pclip.add(); + + pclip.active(9); // Fail because id is out of range + } + + function testFail_not_enough_dai() public takeSetup { + Guy(che).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(che), + data: "" + }); + } + + function test_flashsale() public takeSetup { + assertEq(vat.dai(che), 0); + assertEq(dai.balanceOf(che), 0); + Guy(che).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(che), + data: "hey" + }); + assertEq(vat.dai(che), 0); + assertTrue(dai.balanceOf(che) > 0); // Che turned a profit + } + + function testFail_reentrancy_take() public takeSetup { + BadGuy usr = new BadGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_reentrancy_redo() public takeSetup { + RedoGuy usr = new RedoGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_reentrancy_kick() public takeSetup { + KickGuy usr = new KickGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + clip.rely(address(usr)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_reentrancy_file_uint() public takeSetup { + FileUintGuy usr = new FileUintGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + clip.rely(address(usr)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_reentrancy_file_addr() public takeSetup { + FileAddrGuy usr = new FileAddrGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + clip.rely(address(usr)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_reentrancy_yank() public takeSetup { + YankGuy usr = new YankGuy(clip); + usr.hope(address(clip)); + vat.suck(address(0), address(usr), rad(1000 ether)); + clip.rely(address(usr)); + + usr.take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(usr), + data: "hey" + }); + } + + function testFail_take_impersonation() public takeSetup { // should fail, but works + Guy usr = new Guy(clip); + usr.take({ + id: 1, + amt: 99999999999999 ether, + max: ray(99999999999999 ether), + who: address(ali), + data: "" + }); + } + + function test_gas_bark_kick() public { + // Assertions to make sure setup is as expected. + assertEq(clip.kicks(), 0); + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + assertEq(vat.gem(ilk, me), 960 ether); + assertEq(vat.dai(ali), rad(1000 ether)); + (uint256 ink, uint256 art) = vat.urns(ilk, me); + assertEq(ink, 40 ether); + assertEq(art, 100 ether); + + uint256 preGas = gasleft(); + Guy(ali).bark(dog, ilk, me, address(ali)); + uint256 diffGas = preGas - gasleft(); + log_named_uint("bark with kick gas", diffGas); + } + + function test_gas_partial_take() public takeSetup { + uint256 preGas = gasleft(); + // Bid so owe (= 11 * 5 = 55 RAD) < tab (= 110 RAD) + Guy(ali).take({ + id: 1, + amt: 11 ether, // Half of tab at $110 + max: ray(5 ether), + who: address(ali), + data: "" + }); + uint256 diffGas = preGas - gasleft(); + log_named_uint("partial take gas", diffGas); + + assertEq(vat.gem(ilk, ali), 11 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(945 ether)); // Paid half tab (55) + assertEq(vat.gem(ilk, me), 960 ether); // Collateral not returned (yet) + + // Assert auction DOES NOT end + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, rad(55 ether)); // 110 - 5 * 11 + assertEq(lot, 29 ether); // 40 - 11 + assertEq(usr, me); + assertEq(uint256(tic), now); + assertEq(top, ray(5 ether)); + } + + function test_gas_full_take() public takeSetup { + uint256 preGas = gasleft(); + // Bid so owe (= 25 * 5 = 125 RAD) > tab (= 110 RAD) + // Readjusts slice to be tab/top = 25 + Guy(ali).take({ + id: 1, + amt: 25 ether, + max: ray(5 ether), + who: address(ali), + data: "" + }); + uint256 diffGas = preGas - gasleft(); + log_named_uint("full take gas", diffGas); + + assertEq(vat.gem(ilk, ali), 22 ether); // Didn't take whole lot + assertEq(vat.dai(ali), rad(890 ether)); // Didn't pay more than tab (110) + assertEq(vat.gem(ilk, me), 978 ether); // 960 + (40 - 22) returned to usr + + // Assert auction ends + (uint256 pos, uint256 tab, uint256 lot, address usr, uint256 tic, uint256 top) = clip.sales(1); + assertEq(pos, 0); + assertEq(tab, 0); + assertEq(lot, 0); + assertEq(usr, address(0)); + assertEq(uint256(tic), 0); + assertEq(top, 0); + } +} diff --git a/src/test/dog.t.sol b/src/test/dog.t.sol new file mode 100644 index 00000000..a93269e4 --- /dev/null +++ b/src/test/dog.t.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity >=0.6.12; + +import { DSTest } from "ds-test/test.sol"; +import { Vat } from "../vat.sol"; +import { Dog } from "../dog.sol"; + +contract VowMock { + function fess (uint256 due) public {} +} + +contract ClipperMock { + bytes32 public ilk; + function setIlk(bytes32 wat) external { + ilk = wat; + } + function kick(uint256, uint256, address, address) + external pure returns (uint256 id) { + id = 42; + } +} + +contract DogTest is DSTest { + + bytes32 constant ilk = "gold"; + address constant usr = address(1337); + uint256 constant THOUSAND = 1E3; + uint256 constant WAD = 1E18; + uint256 constant RAY = 1E27; + uint256 constant RAD = 1E45; + Vat vat; + VowMock vow; + ClipperMock clip; + Dog dog; + + function setUp() public { + vat = new Vat(); + vat.init(ilk); + vat.file(ilk, "spot", THOUSAND * RAY); + vat.file(ilk, "dust", 100 * RAD); + vow = new VowMock(); + clip = new ClipperMock(); + clip.setIlk(ilk); + dog = new Dog(address(vat)); + vat.rely(address(dog)); + dog.file(ilk, "chop", 11 * WAD / 10); + dog.file("vow", address(vow)); + dog.file(ilk, "clip", address(clip)); + dog.file("Hole", 10 * THOUSAND * RAD); + dog.file(ilk, "hole", 10 * THOUSAND * RAD); + } + + function test_file_chop() public { + dog.file(ilk, "chop", WAD); + dog.file(ilk, "chop", WAD * 113 / 100); + } + + function testFail_file_chop_lt_WAD() public { + dog.file(ilk, "chop", WAD - 1); + } + + function testFail_file_chop_eq_zero() public { + dog.file(ilk, "chop", 0); + } + + function testFail_file_clip_wrong_ilk() public { + dog.file("mismatched_ilk", "clip", address(clip)); + } + + function setUrn(uint256 ink, uint256 art) internal { + vat.slip(ilk, usr, int256(ink)); + (, uint256 rate,,,) = vat.ilks(ilk); + vat.suck(address(vow), address(vow), art * rate); + vat.grab(ilk, usr, usr, address(vow), int256(ink), int256(art)); + (uint256 actualInk, uint256 actualArt) = vat.urns(ilk, usr); + assertEq(ink, actualInk); + assertEq(art, actualArt); + } + + function isDusty() internal view returns (bool dusty) { + (, uint256 rate,,, uint256 dust) = vat.ilks(ilk); + (, uint256 art) = vat.urns(ilk, usr); + uint256 due = art * rate; + dusty = due > 0 && due < dust; + } + + function test_bark_basic() public { + setUrn(WAD, 2 * THOUSAND * WAD); + dog.bark(ilk, usr, address(this)); + (uint256 ink, uint256 art) = vat.urns(ilk, usr); + assertEq(ink, 0); + assertEq(art, 0); + } + + function testFail_bark_not_unsafe() public { + setUrn(WAD, 500 * WAD); + dog.bark(ilk, usr, address(this)); + } + + // dog.bark will liquidate vaults even if they are dusty + function test_bark_dusty_vault() public { + uint256 dust = 200; + vat.file(ilk, "dust", dust * RAD); + setUrn(1, (dust / 2) * WAD); + assertTrue(isDusty()); + dog.bark(ilk, usr, address(this)); + } + + function test_bark_partial_liquidation_dirt_exceeds_hole_to_avoid_dusty_remnant() public { + uint256 dust = 200; + vat.file(ilk, "dust", dust * RAD); + uint256 hole = 5 * THOUSAND; + dog.file(ilk, "hole", hole * RAD); + (, uint256 chop,,) = dog.ilks(ilk); + uint256 artStart = hole * WAD * WAD / chop + dust * WAD - 1; + setUrn(WAD, artStart); + dog.bark(ilk, usr, address(this)); + assertTrue(!isDusty()); + (, uint256 art) = vat.urns(ilk, usr); + + // The full vault has been liquidated so as not to leave a dusty remnant, + // at the expense of slightly exceeding hole. + assertEq(art, 0); + (,,, uint256 dirt) = dog.ilks(ilk); + assertTrue(dirt > hole * RAD); + assertEq(dirt, artStart * RAY * chop / WAD); + } + + function test_bark_partial_liquidation_dirt_does_not_exceed_hole_if_remnant_is_nondusty() public { + uint256 dust = 200; + vat.file(ilk, "dust", dust * RAD); + uint256 hole = 5 * THOUSAND; + dog.file(ilk, "hole", hole * RAD); + (, uint256 chop,,) = dog.ilks(ilk); + setUrn(WAD, hole * WAD * WAD / chop + dust * WAD); + dog.bark(ilk, usr, address(this)); + assertTrue(!isDusty()); + (, uint256 art) = vat.urns(ilk, usr); + + // The vault remnant respects the dust limit, so we don't exceed hole to liquidate it. + assertEq(art, dust * WAD); + (,,, uint256 dirt) = dog.ilks(ilk); + assertTrue(dirt <= hole * RAD); + assertEq(dirt, hole * RAD * WAD / RAY / chop * RAY * chop / WAD); + } + + function test_bark_partial_liquidation_Dirt_exceeds_Hole_to_avoid_dusty_remnant() public { + uint256 dust = 200; + vat.file(ilk, "dust", dust * RAD); + uint256 Hole = 5 * THOUSAND; + dog.file("Hole", Hole * RAD); + (, uint256 chop,,) = dog.ilks(ilk); + uint256 artStart = Hole * WAD * WAD / chop + dust * WAD - 1; + setUrn(WAD, artStart); + dog.bark(ilk, usr, address(this)); + assertTrue(!isDusty()); + + // The full vault has been liquidated so as not to leave a dusty remnant, + // at the expense of slightly exceeding hole. + (, uint256 art) = vat.urns(ilk, usr); + assertEq(art, 0); + assertTrue(dog.Dirt() > Hole * RAD); + assertEq(dog.Dirt(), artStart * RAY * chop / WAD); + } + + function test_bark_partial_liquidation_Dirt_does_not_exceed_Hole_if_remnant_is_nondusty() public { + uint256 dust = 200; + vat.file(ilk, "dust", dust * RAD); + uint256 Hole = 5 * THOUSAND; + dog.file("Hole", Hole * RAD); + (, uint256 chop,,) = dog.ilks(ilk); + setUrn(WAD, Hole * WAD * WAD / chop + dust * WAD); + dog.bark(ilk, usr, address(this)); + assertTrue(!isDusty()); + + // The full vault has been liquidated so as not to leave a dusty remnant, + // at the expense of slightly exceeding hole. + (, uint256 art) = vat.urns(ilk, usr); + assertEq(art, dust * WAD); + assertTrue(dog.Dirt() <= Hole * RAD); + assertEq(dog.Dirt(), Hole * RAD * WAD / RAY / chop * RAY * chop / WAD); + } + + // A previous version reverted if room was dusty, even if the Vault being liquidated + // was also dusty and would fit in the remaining hole/Hole room. + function test_bark_dusty_vault_dusty_room() public { + // Use a chop that will give nice round numbers + uint256 CHOP = 110 * WAD / 100; // 10% + dog.file(ilk, "chop", CHOP); + + // set both hole_i and Hole to the same value for this test + uint256 ROOM = 200; + uint256 HOLE = 33 * THOUSAND + ROOM; + dog.file( "Hole", HOLE * RAD); + dog.file(ilk, "hole", HOLE * RAD); + + // Test using a non-zero rate to ensure the code is handling stability fees correctly. + vat.fold(ilk, address(vow), (5 * int256(RAY)) / 10); + (, uint256 rate,,,) = vat.ilks(ilk); + assertEq(rate, (15 * RAY) / 10); + + // First, make both holes nearly full. + setUrn(WAD, (HOLE - ROOM) * RAD / rate * WAD / CHOP); + dog.bark(ilk, usr, address(this)); + assertEq(HOLE * RAD - dog.Dirt(), ROOM * RAD); + (,,, uint256 dirt) = dog.ilks(ilk); + assertEq(HOLE * RAD - dirt, ROOM * RAD); + + // Create a small vault + uint256 DUST_1 = 30; + vat.file(ilk, "dust", DUST_1 * RAD); + setUrn(WAD / 10**4, DUST_1 * RAD / rate); + + // Dust limit goes up! + uint256 DUST_2 = 1500; + vat.file(ilk, "dust", DUST_2 * RAD); + + // The testing vault is now dusty + assertTrue(isDusty()); + + // In fact, there is only room to create dusty auctions at this point. + assertTrue(dog.Hole() - dog.Dirt() < DUST_2 * RAD * CHOP / WAD); + uint256 hole; + (,, hole, dirt) = dog.ilks(ilk); + assertTrue(hole - dirt < DUST_2 * RAD * CHOP / WAD); + + // But...our Vault is small enough to fit in ROOM + assertTrue(DUST_1 * RAD * CHOP / WAD < ROOM * RAD); + + // bark should still succeed + dog.bark(ilk, usr, address(this)); + } + + function try_bark(bytes32 ilk_, address usr_, address kpr_) internal returns (bool ok) { + string memory sig = "bark(bytes32,address,address)"; + (ok,) = address(dog).call(abi.encodeWithSignature(sig, ilk_, usr_, kpr_)); + } + + function test_bark_do_not_create_dusty_auction_hole() public { + uint256 dust = 300; + vat.file(ilk, "dust", dust * RAD); + uint256 hole = 3 * THOUSAND; + dog.file(ilk, "hole", hole * RAD); + + // Test using a non-zero rate to ensure the code is handling stability fees correctly. + vat.fold(ilk, address(vow), (5 * int256(RAY)) / 10); + (, uint256 rate,,,) = vat.ilks(ilk); + assertEq(rate, (15 * RAY) / 10); + + (, uint256 chop,,) = dog.ilks(ilk); + setUrn(WAD, (hole - dust / 2) * RAD / rate * WAD / chop); + dog.bark(ilk, usr, address(this)); + + // Make sure any partial liquidation would be dusty (assuming non-dusty remnant) + (,,, uint256 dirt) = dog.ilks(ilk); + uint256 room = hole * RAD - dirt; + uint256 dart = room * WAD / rate / chop; + assertTrue(dart * rate < dust * RAD); + + // This will need to be partially liquidated + setUrn(WAD, hole * WAD * WAD / chop); + assertTrue(!try_bark(ilk, usr, address(this))); // should revert, as the auction would be dusty + } + + function test_bark_do_not_create_dusty_auction_Hole() public { + uint256 dust = 300; + vat.file(ilk, "dust", dust * RAD); + uint256 Hole = 3 * THOUSAND; + dog.file("Hole", Hole * RAD); + + // Test using a non-zero rate to ensure the code is handling stability fees correctly. + vat.fold(ilk, address(vow), (5 * int256(RAY)) / 10); + (, uint256 rate,,,) = vat.ilks(ilk); + assertEq(rate, (15 * RAY) / 10); + + (, uint256 chop,,) = dog.ilks(ilk); + setUrn(WAD, (Hole - dust / 2) * RAD / rate * WAD / chop); + dog.bark(ilk, usr, address(this)); + + // Make sure any partial liquidation would be dusty (assuming non-dusty remnant) + uint256 room = Hole * RAD - dog.Dirt(); + uint256 dart = room * WAD / rate / chop; + assertTrue(dart * rate < dust * RAD); + + // This will need to be partially liquidated + setUrn(WAD, Hole * WAD * WAD / chop); + assertTrue(!try_bark(ilk, usr, address(this))); // should revert, as the auction would be dusty + } +} diff --git a/src/test/end.t.sol b/src/test/end.t.sol index 66db5190..6a192887 100644 --- a/src/test/end.t.sol +++ b/src/test/end.t.sol @@ -18,7 +18,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pragma solidity >=0.5.12; +pragma solidity >=0.6.12; import "ds-test/test.sol"; import "ds-token/token.sol"; @@ -26,9 +26,11 @@ import "ds-value/value.sol"; import {Vat} from '../vat.sol'; import {Cat} from '../cat.sol'; +import {Dog} from '../dog.sol'; import {Vow} from '../vow.sol'; import {Pot} from '../pot.sol'; import {Flipper} from '../flip.sol'; +import {Clipper} from '../clip.sol'; import {Flapper} from '../flap.sol'; import {Flopper} from '../flop.sol'; import {GemJoin} from '../join.sol'; @@ -81,6 +83,7 @@ contract EndTest is DSTest { Vow vow; Pot pot; Cat cat; + Dog dog; Spotter spot; @@ -89,6 +92,7 @@ contract EndTest is DSTest { DSToken gem; GemJoin gemA; Flipper flip; + Clipper clip; } mapping (bytes32 => Ilk) ilks; @@ -148,17 +152,16 @@ contract EndTest is DSTest { DSValue pip = new DSValue(); spot.file(name, "pip", address(pip)); - spot.file(name, "mat", ray(1.5 ether)); - // initial collateral price of 5 - pip.poke(bytes32(5 * WAD)); + spot.file(name, "mat", ray(2 ether)); + // initial collateral price of 6 + pip.poke(bytes32(6 * WAD)); + spot.poke(name); vat.init(name); - GemJoin gemA = new GemJoin(address(vat), name, address(coin)); - - // 1 coin = 6 dai and liquidation ratio is 200% - vat.file(name, "spot", ray(3 ether)); vat.file(name, "line", rad(1000 ether)); + GemJoin gemA = new GemJoin(address(vat), name, address(coin)); + coin.approve(address(gemA)); coin.approve(address(vat)); @@ -174,10 +177,22 @@ contract EndTest is DSTest { cat.file(name, "dunk", rad(25000 ether)); cat.file("box", rad((10 ether) * MLN)); + Clipper clip = new Clipper(address(vat), address(spot), address(dog), name); + vat.rely(address(clip)); + vat.hope(address(clip)); + clip.rely(address(end)); + clip.rely(address(dog)); + dog.rely(address(clip)); + dog.file(name, "clip", address(clip)); + dog.file(name, "chop", 1.1 ether); + dog.file(name, "hole", rad(25000 ether)); + dog.file("Hole", rad((25000 ether))); + ilks[name].pip = pip; ilks[name].gem = coin; ilks[name].gemA = gemA; ilks[name].flip = flip; + ilks[name].clip = clip; return ilks[name]; } @@ -204,6 +219,11 @@ contract EndTest is DSTest { vat.rely(address(cat)); vow.rely(address(cat)); + dog = new Dog(address(vat)); + dog.file("vow", address(vow)); + vat.rely(address(dog)); + vow.rely(address(dog)); + spot = new Spotter(address(vat)); vat.file("Line", rad(1000 ether)); vat.rely(address(spot)); @@ -211,6 +231,7 @@ contract EndTest is DSTest { end = new End(); end.file("vat", address(vat)); end.file("cat", address(cat)); + end.file("dog", address(dog)); end.file("vow", address(vow)); end.file("pot", address(pot)); end.file("spot", address(spot)); @@ -220,6 +241,7 @@ contract EndTest is DSTest { spot.rely(address(end)); pot.rely(address(end)); cat.rely(address(end)); + dog.rely(address(end)); flap.rely(address(vow)); flop.rely(address(vow)); } @@ -483,6 +505,100 @@ contract EndTest is DSTest { assertEq(balanceOf("gold", address(gold.gemA)), 0); } + // -- Scenario where there is one collateralised CDP + // -- undergoing auction at the time of cage + function test_cage_snip() public { + Ilk memory gold = init_collateral("gold"); + + Usr ali = new Usr(vat, end); + + vat.fold("gold", address(vow), int256(ray(0.25 ether))); + + // Make a CDP: + address urn1 = address(ali); + gold.gemA.join(urn1, 10 ether); + ali.frob("gold", urn1, urn1, urn1, 10 ether, 15 ether); + (uint ink1, uint art1) = vat.urns("gold", urn1); // CDP before liquidation + (, uint rate,,,) = vat.ilks("gold"); + + assertEq(vat.gem("gold", urn1), 0); + assertEq(rate, ray(1.25 ether)); + assertEq(ink1, 10 ether); + assertEq(art1, 15 ether); + + vat.file("gold", "spot", ray(1 ether)); // Now unsafe + + uint256 id = dog.bark("gold", urn1, address(this)); + + uint256 tab1; + uint256 lot1; + { + uint256 pos1; + address usr1; + uint96 tic1; + uint256 top1; + (pos1, tab1, lot1, usr1, tic1, top1) = gold.clip.sales(id); + assertEq(pos1, 0); + assertEq(tab1, art1 * rate * 1.1 ether / WAD); // tab uses chop + assertEq(lot1, ink1); + assertEq(usr1, address(ali)); + assertEq(uint256(tic1), now); + assertEq(uint256(top1), ray(6 ether)); + } + + assertEq(dog.Dirt(), tab1); + + { + (uint ink2, uint art2) = vat.urns("gold", urn1); // CDP after liquidation + assertEq(ink2, 0); + assertEq(art2, 0); + } + + // Collateral price is $5 + gold.pip.poke(bytes32(5 * WAD)); + spot.poke("gold"); + end.cage(); + end.cage("gold"); + assertEq(end.tag("gold"), ray(0.2 ether)); // par / price = collateral per DAI + + assertEq(vat.gem("gold", address(gold.clip)), lot1); // From grab in dog.bark() + assertEq(vat.sin(address(vow)), art1 * rate); // From grab in dog.bark() + assertEq(vat.vice(), art1 * rate); // From grab in dog.bark() + assertEq(vat.debt(), art1 * rate); // From frob + assertEq(vat.dai(address(vow)), 0); // vat.suck() hasn't been called + + end.snip("gold", id); + + { + uint256 pos2; + uint256 tab2; + uint256 lot2; + address usr2; + uint96 tic2; + uint256 top2; + (pos2, tab2, lot2, usr2, tic2, top2) = gold.clip.sales(id); + assertEq(pos2, 0); + assertEq(tab2, 0); + assertEq(lot2, 0); + assertEq(usr2, address(0)); + assertEq(uint256(tic2), 0); + assertEq(uint256(top2), 0); + } + + assertEq(dog.Dirt(), 0); // From clip.yank() + assertEq(vat.gem("gold", address(gold.clip)), 0); // From clip.yank() + assertEq(vat.gem("gold", address(end)), 0); // From grab in end.snip() + assertEq(vat.sin(address(vow)), art1 * rate); // From grab in dog.bark() + assertEq(vat.vice(), art1 * rate); // From grab in dog.bark() + assertEq(vat.debt(), tab1 + art1 * rate); // From frob and suck + assertEq(vat.dai(address(vow)), tab1); // From vat.suck() + assertEq(end.Art("gold") * rate, tab1); // Incrementing total Art in End + + (uint ink3, uint art3) = vat.urns("gold", urn1); // CDP after snip + assertEq(ink3, 10 ether); // All collateral returned to CDP + assertEq(art3, tab1 / rate); // Tab amount of normalized debt transferred back into CDP + } + // -- Scenario where there is one over-collateralised CDP // -- and there is a deficit in the Vow function test_cage_collateralised_deficit() public { diff --git a/src/vat.sol b/src/vat.sol index b3f9cbd2..6518009c 100644 --- a/src/vat.sol +++ b/src/vat.sol @@ -19,19 +19,23 @@ pragma solidity >=0.5.12; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). + contract Vat { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { require(live == 1, "Vat/not-live"); wards[usr] = 1; } - function deny(address usr) external note auth { require(live == 1, "Vat/not-live"); wards[usr] = 0; } + function rely(address usr) external auth { require(live == 1, "Vat/not-live"); wards[usr] = 1; } + function deny(address usr) external auth { require(live == 1, "Vat/not-live"); wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Vat/not-authorized"); _; } mapping(address => mapping (address => uint)) public can; - function hope(address usr) external note { can[msg.sender][usr] = 1; } - function nope(address usr) external note { can[msg.sender][usr] = 0; } + function hope(address usr) external { can[msg.sender][usr] = 1; } + function nope(address usr) external { can[msg.sender][usr] = 0; } function wish(address bit, address usr) internal view returns (bool) { return either(bit == usr, can[bit][usr] == 1); } @@ -60,34 +64,6 @@ contract Vat { uint256 public Line; // Total Debt Ceiling [rad] uint256 public live; // Active Flag - // --- Logs --- - event LogNote( - bytes4 indexed sig, - bytes32 indexed arg1, - bytes32 indexed arg2, - bytes32 indexed arg3, - bytes data - ) anonymous; - - modifier note { - _; - assembly { - // log an 'anonymous' event with a constant 6 words of calldata - // and four indexed topics: the selector and the first three args - let mark := msize() // end of memory ensures zero - mstore(0x40, add(mark, 288)) // update free memory pointer - mstore(mark, 0x20) // bytes type data offset - mstore(add(mark, 0x20), 224) // bytes size (padded) - calldatacopy(add(mark, 0x40), 0, 224) // bytes payload - log4(mark, 288, // calldata - shl(224, shr(224, calldataload(0))), // msg.sig - calldataload(4), // arg1 - calldataload(36), // arg2 - calldataload(68) // arg3 - ) - } - } - // --- Init --- constructor() public { wards[msg.sender] = 1; @@ -121,36 +97,36 @@ contract Vat { } // --- Administration --- - function init(bytes32 ilk) external note auth { + function init(bytes32 ilk) external auth { require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); ilks[ilk].rate = 10 ** 27; } - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { require(live == 1, "Vat/not-live"); if (what == "Line") Line = data; else revert("Vat/file-unrecognized-param"); } - function file(bytes32 ilk, bytes32 what, uint data) external note auth { + function file(bytes32 ilk, bytes32 what, uint data) external auth { require(live == 1, "Vat/not-live"); if (what == "spot") ilks[ilk].spot = data; else if (what == "line") ilks[ilk].line = data; else if (what == "dust") ilks[ilk].dust = data; else revert("Vat/file-unrecognized-param"); } - function cage() external note auth { + function cage() external auth { live = 0; } // --- Fungibility --- - function slip(bytes32 ilk, address usr, int256 wad) external note auth { + function slip(bytes32 ilk, address usr, int256 wad) external auth { gem[ilk][usr] = add(gem[ilk][usr], wad); } - function flux(bytes32 ilk, address src, address dst, uint256 wad) external note { + function flux(bytes32 ilk, address src, address dst, uint256 wad) external { require(wish(src, msg.sender), "Vat/not-allowed"); gem[ilk][src] = sub(gem[ilk][src], wad); gem[ilk][dst] = add(gem[ilk][dst], wad); } - function move(address src, address dst, uint256 rad) external note { + function move(address src, address dst, uint256 rad) external { require(wish(src, msg.sender), "Vat/not-allowed"); dai[src] = sub(dai[src], rad); dai[dst] = add(dai[dst], rad); @@ -164,7 +140,7 @@ contract Vat { } // --- CDP Manipulation --- - function frob(bytes32 i, address u, address v, address w, int dink, int dart) external note { + function frob(bytes32 i, address u, address v, address w, int dink, int dart) external { // system is live require(live == 1, "Vat/not-live"); @@ -203,7 +179,7 @@ contract Vat { ilks[i] = ilk; } // --- CDP Fungibility --- - function fork(bytes32 ilk, address src, address dst, int dink, int dart) external note { + function fork(bytes32 ilk, address src, address dst, int dink, int dart) external { Urn storage u = urns[ilk][src]; Urn storage v = urns[ilk][dst]; Ilk storage i = ilks[ilk]; @@ -228,7 +204,7 @@ contract Vat { require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); } // --- CDP Confiscation --- - function grab(bytes32 i, address u, address v, address w, int dink, int dart) external note auth { + function grab(bytes32 i, address u, address v, address w, int dink, int dart) external auth { Urn storage urn = urns[i][u]; Ilk storage ilk = ilks[i]; @@ -244,14 +220,14 @@ contract Vat { } // --- Settlement --- - function heal(uint rad) external note { + function heal(uint rad) external { address u = msg.sender; sin[u] = sub(sin[u], rad); dai[u] = sub(dai[u], rad); vice = sub(vice, rad); debt = sub(debt, rad); } - function suck(address u, address v, uint rad) external note auth { + function suck(address u, address v, uint rad) external auth { sin[u] = add(sin[u], rad); dai[v] = add(dai[v], rad); vice = add(vice, rad); @@ -259,7 +235,7 @@ contract Vat { } // --- Rates --- - function fold(bytes32 i, address u, int rate) external note auth { + function fold(bytes32 i, address u, int rate) external auth { require(live == 1, "Vat/not-live"); Ilk storage ilk = ilks[i]; ilk.rate = add(ilk.rate, rate); diff --git a/src/vow.sol b/src/vow.sol index 932a651e..d34a502c 100644 --- a/src/vow.sol +++ b/src/vow.sol @@ -19,7 +19,9 @@ pragma solidity >=0.5.12; -import "./lib.sol"; +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). interface FlopLike { function kick(address gal, uint lot, uint bid) external returns (uint); @@ -41,11 +43,11 @@ interface VatLike { function nope(address) external; } -contract Vow is LibNote { +contract Vow { // --- Auth --- mapping (address => uint) public wards; - function rely(address usr) external note auth { require(live == 1, "Vow/not-live"); wards[usr] = 1; } - function deny(address usr) external note auth { wards[usr] = 0; } + function rely(address usr) external auth { require(live == 1, "Vow/not-live"); wards[usr] = 1; } + function deny(address usr) external auth { wards[usr] = 0; } modifier auth { require(wards[msg.sender] == 1, "Vow/not-authorized"); _; @@ -91,7 +93,7 @@ contract Vow is LibNote { } // --- Administration --- - function file(bytes32 what, uint data) external note auth { + function file(bytes32 what, uint data) external auth { if (what == "wait") wait = data; else if (what == "bump") bump = data; else if (what == "sump") sump = data; @@ -100,7 +102,7 @@ contract Vow is LibNote { else revert("Vow/file-unrecognized-param"); } - function file(bytes32 what, address data) external note auth { + function file(bytes32 what, address data) external auth { if (what == "flapper") { vat.nope(address(flapper)); flapper = FlapLike(data); @@ -111,24 +113,24 @@ contract Vow is LibNote { } // Push to debt-queue - function fess(uint tab) external note auth { + function fess(uint tab) external auth { sin[now] = add(sin[now], tab); Sin = add(Sin, tab); } // Pop from debt-queue - function flog(uint era) external note { + function flog(uint era) external { require(add(era, wait) <= now, "Vow/wait-not-finished"); Sin = sub(Sin, sin[era]); sin[era] = 0; } // Debt settlement - function heal(uint rad) external note { + function heal(uint rad) external { require(rad <= vat.dai(address(this)), "Vow/insufficient-surplus"); require(rad <= sub(sub(vat.sin(address(this)), Sin), Ash), "Vow/insufficient-debt"); vat.heal(rad); } - function kiss(uint rad) external note { + function kiss(uint rad) external { require(rad <= Ash, "Vow/not-enough-ash"); require(rad <= vat.dai(address(this)), "Vow/insufficient-surplus"); Ash = sub(Ash, rad); @@ -136,20 +138,20 @@ contract Vow is LibNote { } // Debt auction - function flop() external note returns (uint id) { + function flop() external returns (uint id) { require(sump <= sub(sub(vat.sin(address(this)), Sin), Ash), "Vow/insufficient-debt"); require(vat.dai(address(this)) == 0, "Vow/surplus-not-zero"); Ash = add(Ash, sump); id = flopper.kick(address(this), dump, sump); } // Surplus auction - function flap() external note returns (uint id) { + function flap() external returns (uint id) { require(vat.dai(address(this)) >= add(add(vat.sin(address(this)), bump), hump), "Vow/insufficient-surplus"); require(sub(sub(vat.sin(address(this)), Sin), Ash) == 0, "Vow/debt-not-zero"); id = flapper.kick(bump, 0); } - function cage() external note auth { + function cage() external auth { require(live == 1, "Vow/not-live"); live = 0; Sin = 0;