From b059b3935de838105db7c77373ffa35310cb440b Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 20 May 2021 01:25:00 -0300 Subject: [PATCH 1/6] Add Clapper --- src/clap.sol | 280 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 src/clap.sol diff --git a/src/clap.sol b/src/clap.sol new file mode 100644 index 00000000..de095b56 --- /dev/null +++ b/src/clap.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// clap.sol -- Surplus auction + +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +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). + +interface VatLike { + function suck(address,address,uint256) external; +} + +interface GemLike { + function burn(address,uint256) external; +} + +interface PipLike { + function peek() external returns (bytes32, bool); +} + +interface SpotterLike { + function par() external returns (uint256); +} + +interface ClapperCallee { + function clapperCall(address, uint256, uint256, bytes calldata) external; +} + +interface AbacusLike { + function price(uint256, uint256) external view returns (uint256); +} + +/* + This thing lets you sell some dai in return for gems. + + - `lot` dai in return for bid + - `bid` gems paid + - `ttl` single bid lifetime + - `beg` minimum bid increase + - `end` max auction duration +*/ + +contract Clapper { + // --- 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, "Clapper/not-authorized"); + _; + } + + // --- Data --- + struct Sale { + uint256 lot; // dai to sell [rad] + uint256 dip; // Starting price [ray] + uint48 tic; // Auction start time [timestamp] + } + + mapping (uint256 => Sale) public sales; + + VatLike public immutable vat; // CDP Engine + GemLike public immutable gem; + address public vow; + address public spotter; + address public pip; + AbacusLike public calc; // Current price calculator + + uint256 public buf; // Multiplicative factor to decrease starting price [ray] + uint256 public tail; // Time elapsed before auction reset [seconds] + uint256 public cusp; // Percentage increment before auction reset [ray] + + uint256 public kicks; + uint256 public live; // Active Flag + + 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 lot, + uint256 dip + ); + event Take( + uint256 indexed id, + uint256 min, + uint256 price, + uint256 lot, + uint256 slice + ); + event Redo( + uint256 indexed id, + uint256 lot, + uint256 dip + ); + + // event Yank(uint256 id); + + // --- Init --- + constructor(address vat_, address gem_) public { + wards[msg.sender] = 1; + vat = VatLike(vat_); + gem = GemLike(gem_); + live = 1; + } + + modifier lock { + require(locked == 0, "Clapper/system-locked"); + locked = 1; + _; + locked = 0; + } + + modifier isStopped(uint256 level) { + require(stopped < level, "Clapper/stopped-incorrect"); + _; + } + + // --- Math --- + uint256 constant BLN = 10 ** 9; + uint256 constant WAD = 10 ** 18; + uint256 constant RAY = 10 ** 27; + + 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 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; + } + + // --- Admin --- + function file(bytes32 what, uint256 data) external auth { + if (what == "buf") buf = data; + else if (what == "tail") tail = data; // Time elapsed before auction reset + else if (what == "cusp") cusp = data; // Percentage increment before auction reset + else revert("Clapper/file-unrecognized-param"); + emit File(what, data); + } + + function file(bytes32 what, address data) external auth lock { + if (what == "spotter") spotter = data; + else if (what == "pip") pip = data; + else if (what == "vow") vow = data; + else if (what == "calc") calc = AbacusLike(data); + else revert("Clapper/file-unrecognized-param"); + emit File(what, data); + } + + // --- Auction --- + function kick( + uint256 lot, + uint256 // For compatibility with actual vow + ) external auth lock isStopped(1) returns (uint256 id) { + require(live == 1, "Clapper/not-live"); + require(kicks < uint256(-1), "Clapper/overflow"); + id = ++kicks; + + sales[id].lot = lot; + sales[id].tic = uint48(block.timestamp); + uint256 dip = rmul(getFeedPrice(), buf); + require(dip > 0, "Clapper/zero-dip-price"); + sales[id].dip = dip; + + emit Kick(id, lot, dip); + } + + function redo( + uint256 id // id of the auction to reset + ) external lock isStopped(2) { + uint256 lot = sales[id].lot; + require(lot > 0, "Clapper/not-running-auction"); + + (bool done,) = status(sales[id].tic, sales[id].dip); + require(done, "Clapper/cannot-reset"); + + sales[id].tic = uint48(block.timestamp); + + uint256 dip = rmul(getFeedPrice(), buf); + require(dip > 0, "Clapper/zero-dip-price"); + sales[id].dip = dip; + + emit Redo(id, lot, dip); + } + + function take( + uint256 id, + uint256 lot, + uint256 min, + address who, + bytes calldata data + ) external lock isStopped(3) { + require(live == 1, "Clapper/not-live"); + require(sales[id].lot > 0, "Clapper/not-running-auction"); + + uint48 tic = sales[id].tic; + (bool done, uint256 price) = status(tic, sales[id].dip); + require(!done, "Clapper/needs-reset"); + + require(lot <= sales[id].lot, "Clapper/lot-not-matching"); + require(min < price, "Clapper/bid-not-higher"); + + sales[id].lot -= lot; + + // TODO: Add dust check for remaining lot + + vat.suck(vow, who, lot); + + uint256 slice = lot / price; + + if (data.length > 0 && who != address(vat) && who != address(this)) { + ClapperCallee(who).clapperCall(msg.sender, lot, slice, data); + } + + gem.burn(msg.sender, slice); + + emit Take(id, min, price, lot, slice); + } + + function cage( + uint256 // For compatibility with actual vow + ) external auth { + live = 0; + } + + function getFeedPrice() internal returns (uint256 feedPrice) { + (bytes32 val, bool has) = PipLike(pip).peek(); + require(has, "Clapper/invalid-price"); + feedPrice = rdiv(mul(uint256(val), BLN), SpotterLike(spotter).par()); + } + + // 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) { + bool done; + (done, price) = status(sales[id].tic, sales[id].dip); + lot = sales[id].lot; + needsRedo = lot > 0 && done; + } + + // Internally returns boolean for if an auction needs a redo + function status(uint96 tic, uint256 dip) internal view returns (bool done, uint256 price) { + price = calc.price(dip, sub(block.timestamp, tic)); + done = (sub(block.timestamp, tic) > tail || rdiv(price, dip) > cusp); + } +} From 0f378f6a4993e8ce01d397c2466ca6c0fd995b6a Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 20 May 2021 10:27:23 -0300 Subject: [PATCH 2/6] Use move and not suck --- src/clap.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/clap.sol b/src/clap.sol index de095b56..1527a445 100644 --- a/src/clap.sol +++ b/src/clap.sol @@ -24,7 +24,7 @@ pragma solidity >=0.5.12; // New deployments of this contract will need to include custom events (TO DO). interface VatLike { - function suck(address,address,uint256) external; + function move(address,address,uint256) external; } interface GemLike { @@ -197,6 +197,8 @@ contract Clapper { require(dip > 0, "Clapper/zero-dip-price"); sales[id].dip = dip; + vat.move(msg.sender, address(this), lot); + emit Kick(id, lot, dip); } @@ -239,7 +241,7 @@ contract Clapper { // TODO: Add dust check for remaining lot - vat.suck(vow, who, lot); + vat.move(address(this), who, lot); uint256 slice = lot / price; @@ -253,9 +255,10 @@ contract Clapper { } function cage( - uint256 // For compatibility with actual vow + uint256 rad ) external auth { live = 0; + vat.move(address(this), msg.sender, rad); } function getFeedPrice() internal returns (uint256 feedPrice) { From bd860ac32fac5e202453f566843859afe14755de Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 20 May 2021 10:37:39 -0300 Subject: [PATCH 3/6] Add dust --- src/clap.sol | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/clap.sol b/src/clap.sol index 1527a445..3267e105 100644 --- a/src/clap.sol +++ b/src/clap.sol @@ -86,6 +86,7 @@ contract Clapper { uint256 public buf; // Multiplicative factor to decrease starting price [ray] uint256 public tail; // Time elapsed before auction reset [seconds] uint256 public cusp; // Percentage increment before auction reset [ray] + uint256 public dust; // Min amount of DAI in lot to be left on a partial purchase [rad] uint256 public kicks; uint256 public live; // Active Flag @@ -166,9 +167,10 @@ contract Clapper { // --- Admin --- function file(bytes32 what, uint256 data) external auth { - if (what == "buf") buf = data; - else if (what == "tail") tail = data; // Time elapsed before auction reset - else if (what == "cusp") cusp = data; // Percentage increment before auction reset + if (what == "buf") buf = data; + else if (what == "tail") tail = data; + else if (what == "cusp") cusp = data; + else if (what == "dust") dust = data; else revert("Clapper/file-unrecognized-param"); emit File(what, data); } @@ -236,11 +238,10 @@ contract Clapper { require(lot <= sales[id].lot, "Clapper/lot-not-matching"); require(min < price, "Clapper/bid-not-higher"); + uint256 rLot = sales[id].lot - lot; + require(rLot >= dust, "Clapper/dusty-lot-left"); - sales[id].lot -= lot; - - // TODO: Add dust check for remaining lot - + sales[id].lot = rLot; vat.move(address(this), who, lot); uint256 slice = lot / price; @@ -256,7 +257,7 @@ contract Clapper { function cage( uint256 rad - ) external auth { + ) external auth lock { live = 0; vat.move(address(this), msg.sender, rad); } From 648a1dcedc795da9209c59c9007dda5544087db6 Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 20 May 2021 10:54:40 -0300 Subject: [PATCH 4/6] Add comment --- src/clap.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/clap.sol b/src/clap.sol index 3267e105..dd61081a 100644 --- a/src/clap.sol +++ b/src/clap.sol @@ -204,6 +204,12 @@ contract Clapper { emit Kick(id, lot, dip); } + // We might want to remove this function and the values tail and cusp. + // Probably also the functions status and getStatus would be a bit useless. + // The reasoning behind this is in this case the system is not in a hurry to + // auction the lot, so it can wait until the price catches the market one. + // It would only require a yank function that allows governance to reset a + // specific auction just in case it is necessary. function redo( uint256 id // id of the auction to reset ) external lock isStopped(2) { From f29a4a0bbb1ac11017dd8455c9c5c0b9f64e4cf1 Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Thu, 20 May 2021 10:58:12 -0300 Subject: [PATCH 5/6] Add yank function (commented for now) --- src/clap.sol | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/clap.sol b/src/clap.sol index dd61081a..8c36c780 100644 --- a/src/clap.sol +++ b/src/clap.sol @@ -261,19 +261,18 @@ contract Clapper { emit Take(id, min, price, lot, slice); } - function cage( - uint256 rad - ) external auth lock { - live = 0; - vat.move(address(this), msg.sender, rad); - } - function getFeedPrice() internal returns (uint256 feedPrice) { (bytes32 val, bool has) = PipLike(pip).peek(); require(has, "Clapper/invalid-price"); feedPrice = rdiv(mul(uint256(val), BLN), SpotterLike(spotter).par()); } + // Internally returns boolean for if an auction needs a redo + function status(uint96 tic, uint256 dip) internal view returns (bool done, uint256 price) { + price = calc.price(dip, sub(block.timestamp, tic)); + done = (sub(block.timestamp, tic) > tail || rdiv(price, dip) > cusp); + } + // 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) { bool done; @@ -282,9 +281,21 @@ contract Clapper { needsRedo = lot > 0 && done; } - // Internally returns boolean for if an auction needs a redo - function status(uint96 tic, uint256 dip) internal view returns (bool done, uint256 price) { - price = calc.price(dip, sub(block.timestamp, tic)); - done = (sub(block.timestamp, tic) > tail || rdiv(price, dip) > cusp); + function cage( + uint256 rad + ) external auth lock { + live = 0; + vat.move(address(this), msg.sender, rad); } + + // Only worth it if the redo function is removed + // function yank( + // uint id + // ) external auth lock { + // require(live == 1, "Clapper/already-caged"); + // uint256 lot = sales[id].lot; + // require(lot > 0, "Clapper/not-running-auction"); + // vat.move(address(this), msg.sender, lot); + // delete sales[id]; + // } } From cfdeec2b46559b4d52bda005aaee32434114819f Mon Sep 17 00:00:00 2001 From: Gonzalo Balabasquer Date: Wed, 26 May 2021 15:27:30 -0300 Subject: [PATCH 6/6] Some changes + tests --- src/abaci.sol | 87 +++++++++++++++++++ src/clap.sol | 18 ++-- src/test/clap.t.sol | 207 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+), 10 deletions(-) create mode 100644 src/test/clap.t.sol diff --git a/src/abaci.sol b/src/abaci.sol index 8aa7fb96..e38e209f 100644 --- a/src/abaci.sol +++ b/src/abaci.sol @@ -258,3 +258,90 @@ contract ExponentialDecrease is Abacus { return rmul(top, rpow(cut, dur, RAY)); } } + +contract StairstepExponentialIncrease 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, "StairstepExponentialIncrease/not-authorized"); + _; + } + + // --- Data --- + uint256 public step; // Length of time between price drops [seconds] + uint256 public gain; // 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: `gain` 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 == "gain") require((gain = data) >= RAY, "StairstepExponentialIncrease/gain-lt-RAY"); + else if (what == "step") step = data; + else revert("StairstepExponentialIncrease/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) + } + } + } + } + } + } + + // dip: initial price + // dur: seconds since the auction has started + // step: seconds between a price increment + // gain: gain encodes the percentage to increase per step. + // The values is set as (1 + (% value / 100)) * RAY + // So, for a 1% increment per step, gain would be (1 + 0.01) * RAY + // + // returns: dip * (gain ^ dur) + // + // + function price(uint256 dip, uint256 dur) override external view returns (uint256) { + return rmul(dip, rpow(gain, dur / step, RAY)); + } +} diff --git a/src/clap.sol b/src/clap.sol index 8c36c780..0ce6366a 100644 --- a/src/clap.sol +++ b/src/clap.sol @@ -69,16 +69,15 @@ contract Clapper { // --- Data --- struct Sale { - uint256 lot; // dai to sell [rad] - uint256 dip; // Starting price [ray] - uint48 tic; // Auction start time [timestamp] + uint256 lot; // dai to sell [rad] + uint256 dip; // Starting price [ray] + uint256 tic; // Auction start time [timestamp] } mapping (uint256 => Sale) public sales; VatLike public immutable vat; // CDP Engine GemLike public immutable gem; - address public vow; address public spotter; address public pip; AbacusLike public calc; // Current price calculator @@ -178,7 +177,6 @@ contract Clapper { function file(bytes32 what, address data) external auth lock { if (what == "spotter") spotter = data; else if (what == "pip") pip = data; - else if (what == "vow") vow = data; else if (what == "calc") calc = AbacusLike(data); else revert("Clapper/file-unrecognized-param"); emit File(what, data); @@ -194,7 +192,7 @@ contract Clapper { id = ++kicks; sales[id].lot = lot; - sales[id].tic = uint48(block.timestamp); + sales[id].tic = block.timestamp; uint256 dip = rmul(getFeedPrice(), buf); require(dip > 0, "Clapper/zero-dip-price"); sales[id].dip = dip; @@ -219,7 +217,7 @@ contract Clapper { (bool done,) = status(sales[id].tic, sales[id].dip); require(done, "Clapper/cannot-reset"); - sales[id].tic = uint48(block.timestamp); + sales[id].tic = block.timestamp; uint256 dip = rmul(getFeedPrice(), buf); require(dip > 0, "Clapper/zero-dip-price"); @@ -238,12 +236,12 @@ contract Clapper { require(live == 1, "Clapper/not-live"); require(sales[id].lot > 0, "Clapper/not-running-auction"); - uint48 tic = sales[id].tic; + uint256 tic = sales[id].tic; (bool done, uint256 price) = status(tic, sales[id].dip); require(!done, "Clapper/needs-reset"); require(lot <= sales[id].lot, "Clapper/lot-not-matching"); - require(min < price, "Clapper/bid-not-higher"); + require(min <= price, "Clapper/bid-not-higher"); uint256 rLot = sales[id].lot - lot; require(rLot >= dust, "Clapper/dusty-lot-left"); @@ -268,7 +266,7 @@ contract Clapper { } // Internally returns boolean for if an auction needs a redo - function status(uint96 tic, uint256 dip) internal view returns (bool done, uint256 price) { + function status(uint256 tic, uint256 dip) internal view returns (bool done, uint256 price) { price = calc.price(dip, sub(block.timestamp, tic)); done = (sub(block.timestamp, tic) > tail || rdiv(price, dip) > cusp); } diff --git a/src/test/clap.t.sol b/src/test/clap.t.sol new file mode 100644 index 00000000..c8941c95 --- /dev/null +++ b/src/test/clap.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity >=0.5.12; + +import "ds-test/test.sol"; +import {DSValue} from "ds-value/value.sol"; +import {DSToken} from "ds-token/token.sol"; +import {Clapper} from "../clap.sol"; +import {Vat} from "../vat.sol"; +import {Spotter} from "../spot.sol"; +import {StairstepExponentialIncrease} from "../abaci.sol"; +import {Spotter} from "../spot.sol"; + + +interface Hevm { + function warp(uint256) external; +} + +contract Guy { + Clapper clap; + constructor(Clapper clap_) public { + clap = clap_; + Vat(address(clap.vat())).hope(address(clap)); + DSToken(address(clap.gem())).approve(address(clap)); + } + function take(uint256 id, uint256 lot, uint256 min, address who, bytes calldata data) public { + clap.take(id, lot, min, who, data); + } + function try_take(uint256 id, uint256 lot, uint256 min, address who, bytes calldata data) + public returns (bool ok) + { + string memory sig = "take(uint256,uint256,uint256,address,bytes)"; + (ok,) = address(clap).call(abi.encodeWithSignature(sig, id, lot, min, who, data)); + } +} + +contract ClapperTest is DSTest { + Hevm hevm; + + Clapper clap; + Vat vat; + Spotter spotter; + DSValue pip; + DSToken gem; + + address ali; + address bob; + + uint256 constant mkrPrice = 5000 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 setUp() public { + hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + hevm.warp(604411200); + + vat = new Vat(); + spotter = new Spotter(address(vat)); + vat.rely(address(spotter)); + gem = new DSToken(''); + + pip = new DSValue(); + pip.poke(bytes32(mkrPrice)); + + clap = new Clapper(address(vat), address(gem)); + + StairstepExponentialIncrease calc = new StairstepExponentialIncrease(); + calc.file("gain", ray(1.01 ether)); // 1% increment + calc.file("step", 90); // Increment every 90 second + + clap.file("spotter", address(spotter)); + clap.file("pip", address(pip)); + clap.file("calc", address(calc)); + + clap.file("buf", ray(0.80 ether)); // 80% of current price as Initial + clap.file("cusp", ray(1.3 ether)); // 30% increment before reset + clap.file("tail", 1 hours); // 1 hour before reset + + ali = address(new Guy(clap)); + bob = address(new Guy(clap)); + + vat.hope(address(clap)); + gem.approve(address(clap)); + + vat.suck(address(this), address(this), rad(100_000 ether)); + + gem.mint(1000 ether); + gem.setOwner(address(clap)); + + gem.push(ali, 200 ether); + gem.push(bob, 200 ether); + } + + function test_kick() public { + assertEq(vat.dai(address(this)), rad(100_000 ether)); + assertEq(vat.dai(address(clap)), 0 ether); + uint256 id = clap.kick(rad(10_000 ether), 0); + assertEq(vat.dai(address(this)), rad(90_000 ether)); + assertEq(vat.dai(address(clap)), rad(10_000 ether)); + (uint256 lot, uint256 dip, uint256 tic) = clap.sales(id); + assertEq(lot, rad(10_000 ether)); + assertEq(dip, ray(4_000 ether)); // 5,000 (mkrPrice) * 0.8 (buf) + assertEq(tic, block.timestamp); + } + + function test_take() public { + uint256 id = clap.kick(rad(10_000 ether), 0); + + Guy(ali).take({ + id: id, + lot: rad(6_000 ether), + min: ray(4_000 ether), // starting price + who: address(ali), + data: "" + }); + + // bid taken from bidder + assertEq(gem.balanceOf(ali), 200 ether - 6_000 ether / 4_000); + assertEq(vat.dai(address(ali)), rad(6_000 ether)); + + hevm.warp(block.timestamp + 90); + + (uint256 lot, uint256 dip, uint256 tic) = clap.sales(id); + assertEq(lot, 4_000 * 10**45); // 10_000 - 6_000 + + (bool needsRedo, uint256 price, uint256 lot2) = clap.getStatus(id); + assertEq(lot2, lot); + assertTrue(!needsRedo); + assertEq(price, 4_040 * 10**27); // 4,000 * 1%^(90/90) + + Guy(bob).take({ + id: id, + lot: rad(4_000 ether), + min: price, + who: address(bob), + data: "" + }); + assertEq(gem.balanceOf(bob), 200 ether - 4_000 ether * 10 ** 27 / price); + assertEq(vat.dai(address(bob)), rad(4_000 ether)); + + (lot, dip, tic) = clap.sales(id); + assertEq(lot, 0); + } + + function test_redo_tail() public { + clap.file("cusp", ray(2 ether)); // High limit so it triggers due tail and not cusp + uint256 id = clap.kick(rad(10_000 ether), 0); + + (bool needsRedo, uint256 price,) = clap.getStatus(id); + assertTrue(!needsRedo); + assertEq(price, ray(mkrPrice * clap.buf() / 10**27)); + + hevm.warp(block.timestamp + clap.tail()); + (needsRedo,,) = clap.getStatus(id); + assertTrue(!needsRedo); + + hevm.warp(block.timestamp + 1); + (needsRedo, price,) = clap.getStatus(id); + assertTrue(needsRedo); + (, uint256 dip,) = clap.sales(id); + assertTrue(price * 10**27 / dip <= clap.cusp()); // Not failing due price/cusp + + pip.poke(bytes32(uint256(5_200 ether))); // 200 higher at the redo moment + + clap.redo(id); + (needsRedo, price,) = clap.getStatus(id); + assertTrue(!needsRedo); + assertEq(price, ray(5_200 ether * clap.buf() / 10**27)); + } + + function test_redo_price() public { + uint256 id = clap.kick(rad(10_000 ether), 0); + + (bool needsRedo, uint256 price,) = clap.getStatus(id); + assertTrue(!needsRedo); + assertEq(price, ray(mkrPrice * clap.buf() / 10**27)); + + hevm.warp(block.timestamp + 30 minutes); + (needsRedo,,) = clap.getStatus(id); + assertTrue(!needsRedo); + + hevm.warp(block.timestamp + 20 minutes); + (needsRedo, price,) = clap.getStatus(id); + assertTrue(needsRedo); + (, uint256 dip,) = clap.sales(id); + assertTrue(price * 10**27 / dip > clap.cusp()); // Fails due price/cusp (time passed lower than tail) + + pip.poke(bytes32(uint256(5_200 ether))); // 200 higher at the redo moment + + clap.redo(id); + (needsRedo, price,) = clap.getStatus(id); + assertTrue(!needsRedo); + assertEq(price, ray(5_200 ether * clap.buf() / 10**27)); + } + + function testFail_redo() public { + uint256 id = clap.kick(rad(10_000 ether), 0); + hevm.warp(block.timestamp + 30 minutes); + clap.redo(id); + } +}