diff --git a/src/dog.sol b/src/dog.sol index bd2da748..d55cfa0e 100644 --- a/src/dog.sol +++ b/src/dog.sol @@ -66,6 +66,11 @@ contract Dog { 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] + uint256 trid; // Amt of dirt dug by auctions. [rad] + // Packed in to a single slot to minimze gas costs. + uint224 clod; // Amount of dirt to remove per time quantum [rad] + uint32 qntm; // Time between removal of clods [seconds] + uint256 last; // Timstamp of most recent dirt reduction [Unix epoch] } VatLike immutable public vat; // CDP Engine @@ -112,6 +117,10 @@ contract Dog { function min(uint256 x, uint256 y) internal pure returns (uint256 z) { z = x <= y ? x : y; } + function min(uint256 x, uint256 y, uint256 z) internal pure returns (uint256 w) { + w = x <= y ? x : y; + if (z < w) w = z; + } function add(uint256 x, uint256 y) internal pure returns (uint256 z) { require((z = x + y) >= x); } @@ -195,15 +204,36 @@ contract Dog { uint256 rate; uint256 dust; { - uint256 spot; - (,rate, spot,, dust) = vat.ilks(ilk); - require(spot > 0 && mul(ink, spot) < mul(art, rate), "Dog/not-unsafe"); + { + uint256 spot; + (,rate, spot,, dust) = vat.ilks(ilk); + require(spot > 0 && mul(ink, spot) < mul(art, rate), "Dog/not-unsafe"); + } + + uint256 _Dirt = Dirt; + { + // Risk of overflow should be extremely small, even if milk.last = 0; + // can anyway be made less likely to overflow with a little more work. + uint256 pile = mul(sub(block.timestamp, milk.last) / milk.qntm, milk.clod); + pile = min(milk.dirt, milk.trid, pile); + if (pile > 0) { + milk.trid = sub(milk.trid, pile); + milk.dirt = sub(milk.dirt, pile); // In principle safe if trid <= dirt can be formally verified. + _Dirt = sub(_Dirt, pile); // In principle safe is dirt <= Dirt can be formally verified. + } + } // 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); + require(Hole > _Dirt && milk.hole > milk.dirt, "Dog/liquidation-limit-hit"); + uint256 room = min(Hole - _Dirt, milk.hole - milk.dirt); + + // Commit trid, dirt, Dirt, and last updates to storage. + ilks[ilk].trid = milk.trid; + ilks[ilk].dirt = milk.dirt; + Dirt = _Dirt; + ilks[ilk].last = block.timestamp; // uint256.max()/(RAD*WAD) = 115,792,089,237,316 dart = min(art, mul(room, WAD) / rate / milk.chop); @@ -256,8 +286,7 @@ contract Dog { } function digs(bytes32 ilk, uint256 rad) external auth { - Dirt = sub(Dirt, rad); - ilks[ilk].dirt = sub(ilks[ilk].dirt, rad); + ilks[ilk].trid = add(ilks[ilk].trid, rad); emit Digs(ilk, rad); } diff --git a/src/test/clip.t.sol b/src/test/clip.t.sol index 170f5e93..620c1811 100644 --- a/src/test/clip.t.sol +++ b/src/test/clip.t.sol @@ -415,7 +415,7 @@ contract ClipperTest is DSTest { function test_get_chop() public { uint256 chop = dog.chop(ilk); - (, uint256 chop2,,) = dog.ilks(ilk); + (, uint256 chop2,,,,,,) = dog.ilks(ilk); assertEq(chop, chop2); } @@ -729,7 +729,7 @@ contract ClipperTest is DSTest { function test_Hole_hole() public { assertEq(dog.Dirt(), 0); - (,,, uint256 dirt) = dog.ilks(ilk); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); dog.bark(ilk, me, address(this)); @@ -737,7 +737,7 @@ contract ClipperTest is DSTest { (, uint256 tab,,,,) = clip.sales(1); assertEq(dog.Dirt(), tab); - (,,, dirt) = dog.ilks(ilk); + (,,, dirt,,,,) = dog.ilks(ilk); assertEq(dirt, tab); bytes32 ilk2 = "silver"; @@ -770,8 +770,8 @@ contract ClipperTest is DSTest { (, uint256 tab2,,,,) = clip2.sales(1); assertEq(dog.Dirt(), tab + tab2); - (,,, dirt) = dog.ilks(ilk); - (,,, uint256 dirt2) = dog.ilks(ilk2); + (,,, dirt,,,,) = dog.ilks(ilk); + (,,, uint256 dirt2,,,,) = dog.ilks(ilk2); assertEq(dirt, tab); assertEq(dirt2, tab2); } @@ -783,7 +783,7 @@ contract ClipperTest is DSTest { assertEq(_art(ilk, me), 100 ether); assertEq(dog.Dirt(), 0); - (,uint256 chop,, uint256 dirt) = dog.ilks(ilk); + (,uint256 chop,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); dog.bark(ilk, me, address(this)); @@ -799,7 +799,7 @@ contract ClipperTest is DSTest { assertEq(_art(ilk, me), 100 ether - tab * WAD / rate / chop); assertEq(dog.Dirt(), tab); - (,,, dirt) = dog.ilks(ilk); + (,,, dirt,,,,) = dog.ilks(ilk); assertEq(dirt, tab); } @@ -810,7 +810,7 @@ contract ClipperTest is DSTest { assertEq(_art(ilk, me), 100 ether); assertEq(dog.Dirt(), 0); - (,uint256 chop,, uint256 dirt) = dog.ilks(ilk); + (,uint256 chop,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); dog.bark(ilk, me, address(this)); @@ -826,7 +826,7 @@ contract ClipperTest is DSTest { assertEq(_art(ilk, me), 100 ether - tab * WAD / rate / chop); assertEq(dog.Dirt(), tab); - (,,, dirt) = dog.ilks(ilk); + (,,, dirt,,,,) = dog.ilks(ilk); assertEq(dirt, tab); } @@ -867,7 +867,7 @@ contract ClipperTest is DSTest { assertEq(top, 0); assertEq(dog.Dirt(), 0); - (,,, uint256 dirt) = dog.ilks(ilk); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); } @@ -895,7 +895,7 @@ contract ClipperTest is DSTest { assertEq(top, 0); assertEq(dog.Dirt(), 0); - (,,, uint256 dirt) = dog.ilks(ilk); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); } @@ -923,7 +923,7 @@ contract ClipperTest is DSTest { assertEq(top, ray(5 ether)); assertEq(dog.Dirt(), tab); - (,,, uint256 dirt) = dog.ilks(ilk); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, tab); } @@ -953,7 +953,7 @@ contract ClipperTest is DSTest { // 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); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); } @@ -1391,7 +1391,7 @@ contract ClipperTest is DSTest { // Assert that callback to clear dirt was successful. assertEq(dog.Dirt(), 0); - (,,, uint256 dirt) = dog.ilks(ilk); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(dirt, 0); // Assert transfer of gem. diff --git a/src/test/dog.t.sol b/src/test/dog.t.sol index a93269e4..4a6b2241 100644 --- a/src/test/dog.t.sol +++ b/src/test/dog.t.sol @@ -112,7 +112,7 @@ contract DogTest is DSTest { vat.file(ilk, "dust", dust * RAD); uint256 hole = 5 * THOUSAND; dog.file(ilk, "hole", hole * RAD); - (, uint256 chop,,) = dog.ilks(ilk); + (, uint256 chop,,,,,,) = dog.ilks(ilk); uint256 artStart = hole * WAD * WAD / chop + dust * WAD - 1; setUrn(WAD, artStart); dog.bark(ilk, usr, address(this)); @@ -122,7 +122,7 @@ contract DogTest is DSTest { // 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); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertTrue(dirt > hole * RAD); assertEq(dirt, artStart * RAY * chop / WAD); } @@ -132,7 +132,7 @@ contract DogTest is DSTest { vat.file(ilk, "dust", dust * RAD); uint256 hole = 5 * THOUSAND; dog.file(ilk, "hole", hole * RAD); - (, uint256 chop,,) = dog.ilks(ilk); + (, uint256 chop,,,,,,) = dog.ilks(ilk); setUrn(WAD, hole * WAD * WAD / chop + dust * WAD); dog.bark(ilk, usr, address(this)); assertTrue(!isDusty()); @@ -140,7 +140,7 @@ contract DogTest is DSTest { // 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); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertTrue(dirt <= hole * RAD); assertEq(dirt, hole * RAD * WAD / RAY / chop * RAY * chop / WAD); } @@ -150,7 +150,7 @@ contract DogTest is DSTest { vat.file(ilk, "dust", dust * RAD); uint256 Hole = 5 * THOUSAND; dog.file("Hole", Hole * RAD); - (, uint256 chop,,) = dog.ilks(ilk); + (, uint256 chop,,,,,,) = dog.ilks(ilk); uint256 artStart = Hole * WAD * WAD / chop + dust * WAD - 1; setUrn(WAD, artStart); dog.bark(ilk, usr, address(this)); @@ -169,7 +169,7 @@ contract DogTest is DSTest { vat.file(ilk, "dust", dust * RAD); uint256 Hole = 5 * THOUSAND; dog.file("Hole", Hole * RAD); - (, uint256 chop,,) = dog.ilks(ilk); + (, uint256 chop,,,,,,) = dog.ilks(ilk); setUrn(WAD, Hole * WAD * WAD / chop + dust * WAD); dog.bark(ilk, usr, address(this)); assertTrue(!isDusty()); @@ -204,7 +204,7 @@ contract DogTest is DSTest { 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); + (,,, uint256 dirt,,,,) = dog.ilks(ilk); assertEq(HOLE * RAD - dirt, ROOM * RAD); // Create a small vault @@ -222,7 +222,7 @@ contract DogTest is DSTest { // 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); + (,, hole, dirt,,,,) = dog.ilks(ilk); assertTrue(hole - dirt < DUST_2 * RAD * CHOP / WAD); // But...our Vault is small enough to fit in ROOM @@ -248,12 +248,12 @@ contract DogTest is DSTest { (, uint256 rate,,,) = vat.ilks(ilk); assertEq(rate, (15 * RAY) / 10); - (, uint256 chop,,) = dog.ilks(ilk); + (, 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 dirt,,,,) = dog.ilks(ilk); uint256 room = hole * RAD - dirt; uint256 dart = room * WAD / rate / chop; assertTrue(dart * rate < dust * RAD); @@ -274,7 +274,7 @@ contract DogTest is DSTest { (, uint256 rate,,,) = vat.ilks(ilk); assertEq(rate, (15 * RAY) / 10); - (, uint256 chop,,) = dog.ilks(ilk); + (, uint256 chop,,,,,,) = dog.ilks(ilk); setUrn(WAD, (Hole - dust / 2) * RAD / rate * WAD / chop); dog.bark(ilk, usr, address(this));