diff --git a/.env.example b/.env.example deleted file mode 100644 index be5c611..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -MAINNET_RPC_URL= -ETHERSCAN_API_KEY= -PRIVATE_KEY= \ No newline at end of file diff --git a/script/AuctionMainnetDeployer.sol b/script/AuctionMainnetDeployer.s.sol similarity index 80% rename from script/AuctionMainnetDeployer.sol rename to script/AuctionMainnetDeployer.s.sol index ceef80f..509619e 100644 --- a/script/AuctionMainnetDeployer.sol +++ b/script/AuctionMainnetDeployer.s.sol @@ -16,26 +16,26 @@ contract AuctionMainnetDeployerScript is Script { address gov = 0x926dF14a23BE491164dCF93f4c468A50ef659D5B; address fedChair = 0x8F97cCA30Dbe80e7a8B462F1dD1a51C32accDfC8; - address dola = 0x865377367054516e17014CcdED1e7d814EDC9ce4; + address asset = 0x865377367054516e17014CcdED1e7d814EDC9ce4; address dbr = 0xAD038Eb671c44b853887A7E32528FaB35dC5D710; // 5:1 ratio, implying a 20c DBR starting price - uint dolaReserve = 500_000 * 1e18; - uint dbrReserve = dolaReserve * 5; - + uint assetReserve = 500_000 * 1e18; + uint dbrReserve = assetReserve * 5; + Auction auction = new Auction( gov, fedChair, dbr, - dola, + asset, handler, - dolaReserve, + assetReserve, dbrReserve ); new Helper( address(auction), - address(dola) + address(asset) ); } } diff --git a/script/HandlerMainnetDeployer.s.sol b/script/DolaSaleHandlerMainnetDeployer.s.sol similarity index 51% rename from script/HandlerMainnetDeployer.s.sol rename to script/DolaSaleHandlerMainnetDeployer.s.sol index 4d9d81b..1a4cf2c 100644 --- a/script/HandlerMainnetDeployer.s.sol +++ b/script/DolaSaleHandlerMainnetDeployer.s.sol @@ -2,33 +2,27 @@ pragma solidity 0.8.21; import {Script, console2} from "forge-std/Script.sol"; -import {SaleHandler} from "../src/SaleHandler.sol"; +import {DolaSaleHandler} from "../src/DolaSaleHandler.sol"; +import {Auction} from "../src/Auction.sol"; import {Helper} from "../src/Helper.sol"; -contract HandlerMainnetDeployerScript is Script { +contract DolaSaleHandlerMainnetDeployerScript is Script { function setUp() public {} function run() public { vm.startBroadcast(); - address gov = 0x926dF14a23BE491164dCF93f4c468A50ef659D5B; - address beneficiary = 0x9D5Df30F475CEA915b1ed4C0CCa59255C897b61B; address anDola = 0x7Fcb7DAC61eE35b3D4a51117A7c58D53f0a8a670; - address dola = 0x865377367054516e17014CcdED1e7d814EDC9ce4; + address asset = 0x865377367054516e17014CcdED1e7d814EDC9ce4; address borrower1 = 0xf508c58ce37ce40a40997C715075172691F92e2D; address borrower2 = 0xeA0c959BBb7476DDD6cD4204bDee82b790AA1562; - - // 30% minimum goes to repayments, 70% goes to beneficiary - uint minRepayBps = 3000; - new SaleHandler( - gov, - beneficiary, - minRepayBps, - dola, + DolaSaleHandler handler = new DolaSaleHandler( + asset, anDola, borrower1, borrower2 ); + } } diff --git a/sh/HandlerMainnetDeployer.sh b/sh/HandlerMainnetDeployer.sh deleted file mode 100644 index e69de29..0000000 diff --git a/src/Auction.sol b/src/Auction.sol index c65c7ba..cb7a999 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -21,36 +21,37 @@ contract Auction { address public gov; address public operator; IDBR public immutable dbr; - IERC20 public immutable dola; + IERC20 public immutable asset; ISaleHandler public saleHandler; - uint public dolaReserve; + uint public assetReserve; uint public dbrReserve; uint public dbrRatePerYear; - uint public maxDbrRatePerYear; + uint public minDbrRatePerYear; + uint public maxDbrRatePerYear = type(uint).max; uint public lastUpdate; constructor ( address _gov, address _operator, address _dbr, - address _dola, + address _asset, address handler, - uint _dolaReserve, + uint _assetReserve, uint _dbrReserve ) { - require(_dolaReserve > 0, "Dola reserve must be positive"); + require(_assetReserve > 0, "Asset reserve must be positive"); require(_dbrReserve > 0, "DBR reserve must be positive"); gov = _gov; operator = _operator; dbr = IDBR(_dbr); - dola = IERC20(_dola); + asset = IERC20(_asset); saleHandler = ISaleHandler(handler); - dolaReserve = _dolaReserve; + assetReserve = _assetReserve; dbrReserve = _dbrReserve; } modifier updateReserves { - (dolaReserve, dbrReserve) = getCurrentReserves(); + (assetReserve, dbrReserve) = getCurrentReserves(); lastUpdate = block.timestamp; _; } @@ -65,15 +66,15 @@ contract Auction { _; } - function getCurrentReserves() public view returns (uint _dolaReserve, uint _dbrReserve) { + function getCurrentReserves() public view returns (uint _assetReserve, uint _dbrReserve) { uint timeElapsed = block.timestamp - lastUpdate; if(timeElapsed > 0) { - uint K = dolaReserve * dbrReserve; + uint K = assetReserve * dbrReserve; uint DbrsIn = timeElapsed * dbrRatePerYear / 365 days; _dbrReserve = dbrReserve + DbrsIn; - _dolaReserve = K / _dbrReserve; + _assetReserve = K / _dbrReserve; } else { - _dolaReserve = dolaReserve; + _assetReserve = assetReserve; _dbrReserve = dbrReserve; } } @@ -83,6 +84,7 @@ contract Auction { function setSaleHandler(address _saleHandler) external onlyGov { saleHandler = ISaleHandler(_saleHandler); } function setMaxDbrRatePerYear(uint _maxRate) external onlyGov updateReserves { + require(_maxRate >= minDbrRatePerYear, "Max below min"); maxDbrRatePerYear = _maxRate; emit MaxRateUpdate(_maxRate); if(dbrRatePerYear > _maxRate) { @@ -91,59 +93,83 @@ contract Auction { } } + function setMinDbrRatePerYear(uint _minRate) external onlyGov updateReserves { + require(_minRate <= maxDbrRatePerYear, "Min above max"); + minDbrRatePerYear = _minRate; + emit MinRateUpdate(_minRate); + if(dbrRatePerYear < _minRate) { + dbrRatePerYear = _minRate; + emit RateUpdate(_minRate); + } + } + function setDbrRatePerYear(uint _rate) external onlyGovOrOperator updateReserves { require(_rate <= maxDbrRatePerYear, "Rate exceeds max"); + require(_rate >= minDbrRatePerYear, "Rate below min"); dbrRatePerYear = _rate; emit RateUpdate(_rate); } - function setDolaReserve(uint _dolaReserve) external onlyGov updateReserves { - require(_dolaReserve > 0, "Dola reserve must be positive"); - uint K = dolaReserve * dbrReserve; - dolaReserve = _dolaReserve; - dbrReserve = K / _dolaReserve; + // changes K to preserve the ratio (price) + function setAssetReserve(uint _assetReserve) external onlyGov updateReserves { + require(_assetReserve > 0, "Asset reserve must be positive"); + uint newDbrReserve = _assetReserve * dbrReserve / assetReserve; + require(newDbrReserve > 0, "Resulting DBR reserve must be positive"); + assetReserve = _assetReserve; + dbrReserve = newDbrReserve; } + // changes K to preserve the ratio (price) function setDbrReserve(uint _dbrReserve) external onlyGov updateReserves { require(_dbrReserve > 0, "DBR reserve must be positive"); - uint K = dolaReserve * dbrReserve; + uint newAssetReserve = _dbrReserve * assetReserve / dbrReserve; + require(newAssetReserve > 0, "Resulting asset reserve must be positive"); dbrReserve = _dbrReserve; - dolaReserve = K / _dbrReserve; + assetReserve = newAssetReserve; } - function overrideReserves(uint _dbrReserve, uint _dolaReserve) external onlyGov { - require(_dolaReserve > 0, "Dola reserve must be positive"); + function overrideReserves(uint _dbrReserve, uint _assetReserve) external onlyGov { + require(_assetReserve > 0, "Asset reserve must be positive"); require(_dbrReserve > 0, "DBR reserve must be positive"); - dolaReserve = _dolaReserve; + assetReserve = _assetReserve; dbrReserve = _dbrReserve; lastUpdate = block.timestamp; } - function buyDBR(uint exactDolaIn, uint exactDbrOut, address to) external updateReserves { - uint K = dolaReserve * dbrReserve; - dolaReserve += exactDolaIn; + function buyDBR(uint exactAssetIn, uint exactDbrOut, address to) external updateReserves { + uint K = assetReserve * dbrReserve; + assetReserve += exactAssetIn; dbrReserve -= exactDbrOut; - require(dolaReserve * dbrReserve >= K, "Invariant"); - dola.transferFrom(msg.sender, address(this), exactDolaIn); + require(assetReserve * dbrReserve >= K, "Invariant"); + asset.transferFrom(msg.sender, address(this), exactAssetIn); dbr.mint(to, exactDbrOut); - emit Buy(msg.sender, to, exactDolaIn, exactDbrOut); + emit Buy(msg.sender, to, exactAssetIn, exactDbrOut); } function sendToSaleHandler() public { require(address(saleHandler) != address(0), "No sale handler"); - uint bal = dola.balanceOf(address(this)); - require(bal > 0, "No DOLA to send"); + uint bal = asset.balanceOf(address(this)); + require(bal > 0, "No asset to send"); uint capacity = saleHandler.getCapacity(); uint amount = bal > capacity ? capacity : bal; - dola.transfer(address(saleHandler), amount); + asset.transfer(address(saleHandler), amount); saleHandler.onReceive(); } + // only if no sale handler is set + function sendToGov() public { + require(address(saleHandler) == address(0), "Sale handler set"); + uint bal = asset.balanceOf(address(this)); + require(bal > 0, "No asset to send"); + asset.transfer(gov, bal); + } + function sweep(address token, address destination, uint amount) external onlyGov { IERC20(token).transfer(destination, amount); } - event Buy(address indexed caller, address indexed to, uint dolaIn, uint dbrOut); + event Buy(address indexed caller, address indexed to, uint assetIn, uint dbrOut); event RateUpdate(uint newRate); event MaxRateUpdate(uint newMaxRate); + event MinRateUpdate(uint newMinRate); } diff --git a/src/DolaSaleHandler.sol b/src/DolaSaleHandler.sol new file mode 100644 index 0000000..2b5524f --- /dev/null +++ b/src/DolaSaleHandler.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +interface IERC20 { + function approve(address,uint) external returns (bool); + function balanceOf(address) external view returns (uint); +} + +interface IAnDola { + function repayBorrowBehalf(address borrower, uint repayAmount) external returns (uint); + function borrowBalanceStored(address account) external view returns (uint); // stored is good enough for our use case +} + +contract DolaSaleHandler { + + IERC20 public immutable asset; + IAnDola public immutable anDola; + address public immutable borrower1; + address public immutable borrower2; + + constructor( + address _asset, + address _anDola, + address _borrower1, + address _borrower2 + ) { + asset = IERC20(_asset); + anDola = IAnDola(_anDola); + borrower1 = _borrower1; + borrower2 = _borrower2; + asset.approve(_anDola,type(uint).max); + } + + function onReceive() external { + uint bal = asset.balanceOf(address(this)); + uint debt1 = getDebtOf(borrower1); + uint debt2 = getDebtOf(borrower2); + if(debt1 > debt2) { + uint errCode = anDola.repayBorrowBehalf(borrower1, bal); + if(errCode > 0) require(anDola.repayBorrowBehalf(borrower2, bal) == 0, "Failed to repay"); + } else { + uint errCode = anDola.repayBorrowBehalf(borrower2, bal); + if(errCode > 0) require(anDola.repayBorrowBehalf(borrower1, bal) == 0, "Failed to repay"); + } + } + + function getDebtOf(address borrower) internal view returns (uint) { + return anDola.borrowBalanceStored(borrower); + } + + function getCapacity() external view returns (uint) { + return getDebtOf(borrower1) + getDebtOf(borrower2) - asset.balanceOf(address(this)); + } + +} \ No newline at end of file diff --git a/src/Helper.sol b/src/Helper.sol index f80e114..e56323b 100644 --- a/src/Helper.sol +++ b/src/Helper.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.21; interface IAuction { - function getCurrentReserves() external view returns (uint dolaReserve, uint dbrReserve); - function buyDBR(uint exactDolaIn, uint exactDbrOut, address to) external; + function getCurrentReserves() external view returns (uint assetReserve, uint dbrReserve); + function buyDBR(uint exactAssetIn, uint exactDbrOut, address to) external; } interface IERC20 { @@ -14,45 +14,45 @@ interface IERC20 { contract Helper { IAuction public immutable auction; - IERC20 public immutable dola; + IERC20 public immutable asset; constructor( address _auction, - address _dola + address _asset ) { auction = IAuction(_auction); - dola = IERC20(_dola); - dola.approve(_auction, type(uint).max); + asset = IERC20(_asset); + asset.approve(_auction, type(uint).max); } - function getDbrOut(uint dolaIn) public view returns (uint dbrOut) { - require(dolaIn > 0, "dolaIn must be positive"); - (uint dolaReserve, uint dbrReserve) = auction.getCurrentReserves(); - uint numerator = dolaIn * dbrReserve; - uint denominator = dolaReserve + dolaIn; + function getDbrOut(uint assetIn) public view returns (uint dbrOut) { + require(assetIn > 0, "assetIn must be positive"); + (uint assetReserve, uint dbrReserve) = auction.getCurrentReserves(); + uint numerator = assetIn * dbrReserve; + uint denominator = assetReserve + assetIn; dbrOut = numerator / denominator; } - function getDolaIn(uint dbrOut) public view returns (uint dolaIn) { + function getAssetIn(uint dbrOut) public view returns (uint assetIn) { require(dbrOut > 0, "dbrOut must be positive"); - (uint dolaReserve, uint dbrReserve) = auction.getCurrentReserves(); - uint numerator = dbrOut * dolaReserve; + (uint assetReserve, uint dbrReserve) = auction.getCurrentReserves(); + uint numerator = dbrOut * assetReserve; uint denominator = dbrReserve - dbrOut; - dolaIn = (numerator / denominator) + 1; + assetIn = (numerator / denominator) + 1; } - function swapExactDolaForDbr(uint dolaIn, uint dbrOutMin) external returns (uint dbrOut) { - dbrOut = getDbrOut(dolaIn); + function swapExactAssetForDbr(uint assetIn, uint dbrOutMin) external returns (uint dbrOut) { + dbrOut = getDbrOut(assetIn); require(dbrOut >= dbrOutMin, "dbrOut must be greater than dbrOutMin"); - dola.transferFrom(msg.sender, address(this), dolaIn); - auction.buyDBR(dolaIn, dbrOut, msg.sender); + asset.transferFrom(msg.sender, address(this), assetIn); + auction.buyDBR(assetIn, dbrOut, msg.sender); } - function swapDolaForExactDbr(uint dbrOut, uint dolaInMax) external returns (uint dolaIn) { - dolaIn = getDolaIn(dbrOut); - require(dolaIn <= dolaInMax, "dolaIn must be less than dolaInMax"); - dola.transferFrom(msg.sender, address(this), dolaIn); - auction.buyDBR(dolaIn, dbrOut, msg.sender); + function swapAssetForExactDbr(uint dbrOut, uint assetInMax) external returns (uint assetIn) { + assetIn = getAssetIn(dbrOut); + require(assetIn <= assetInMax, "assetIn must be less than assetInMax"); + asset.transferFrom(msg.sender, address(this), assetIn); + auction.buyDBR(assetIn, dbrOut, msg.sender); } } \ No newline at end of file diff --git a/src/SaleHandler.sol b/src/SaleHandler.sol deleted file mode 100644 index 78fed5a..0000000 --- a/src/SaleHandler.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.21; - -interface IERC20 { - function approve(address,uint) external returns (bool); - function balanceOf(address) external view returns (uint); - function transfer(address,uint) external returns (bool); -} - -interface IAnDola { - function repayBorrowBehalf(address borrower, uint repayAmount) external returns (uint); - function borrowBalanceStored(address account) external view returns (uint); // stored is good enough for our use case -} - -contract SaleHandler { - - address public owner; - address public beneficiary; - IERC20 public immutable dola; - IAnDola public immutable anDola; - address public immutable borrower1; - address public immutable borrower2; - uint public minRepayBps; - uint public repayBps = 10000; // initializes to 100% - - modifier onlyOwner() { - require(msg.sender == owner, "Only owner can call this function"); - _; - } - - constructor( - address _owner, - address _beneficiary, - uint _minRepayBps, - address _dola, - address _anDola, - address _borrower1, - address _borrower2 - ) { - require(_minRepayBps <= 10000, "Minimum repay bps must be less than or equal to 10000"); - owner = _owner; - beneficiary = _beneficiary; - minRepayBps = _minRepayBps; - dola = IERC20(_dola); - anDola = IAnDola(_anDola); - borrower1 = _borrower1; - borrower2 = _borrower2; - dola.approve(_anDola,type(uint).max); - } - - function onReceive() external { - require(repayBps == 10000 || beneficiary != address(0), "Either all goes to repayment or beneficiary must be set"); - uint bal = dola.balanceOf(address(this)); - uint repayment = bal * repayBps / 10000; - uint debt1 = getDebtOf(borrower1); - uint debt2 = getDebtOf(borrower2); - if(debt1 > debt2) { - uint errCode = anDola.repayBorrowBehalf(borrower1, repayment); - if(errCode > 0) anDola.repayBorrowBehalf(borrower2, repayment); - } else { - uint errCode = anDola.repayBorrowBehalf(borrower2, repayment); - if(errCode > 0) anDola.repayBorrowBehalf(borrower1, repayment); - } - - // recalculating remaining in case of failure to repay above - bal = dola.balanceOf(address(this)); - if(bal > 0 && beneficiary != address(0)) dola.transfer(beneficiary, bal); - } - - function getDebtOf(address borrower) internal view returns (uint) { - return anDola.borrowBalanceStored(borrower); - } - - function getCapacity() external view returns (uint) { - // excess goes to beneficiary - return type(uint).max; - } - - function setOwner(address _owner) external onlyOwner { owner = _owner; } - - function setBeneficiary(address _beneficiary) external onlyOwner { beneficiary = _beneficiary; } - - function setRepayBps(uint _repayBps) external { - require(msg.sender == beneficiary || msg.sender == owner, "Only beneficiary or owner can call this function"); - require(_repayBps <= 10000, "Repay bps must be less than or equal to 10000"); - require(_repayBps >= minRepayBps, "Repay bps must be greater than or equal to minRepayBps"); - repayBps = _repayBps; - } - - function setMinRepayBps(uint _minRepayBps) external onlyOwner { - require(_minRepayBps <= 10000, "Minimum repay bps must be less than or equal to 10000"); - minRepayBps = _minRepayBps; - if(repayBps < _minRepayBps) repayBps = _minRepayBps; - } - -} \ No newline at end of file diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 8ba2f3c..21b6c9d 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -23,17 +23,17 @@ contract AuctionTest is Test { address gov = address(1); address operator = address(2); ERC20 dbr; - ERC20 dola; + ERC20 asset; Auction auction; function setUp() public { dbr = new ERC20(); - dola = new ERC20(); + asset = new ERC20(); auction = new Auction( gov, operator, address(dbr), - address(dola), + address(asset), address(0), 1e18, 1e18 @@ -44,25 +44,28 @@ contract AuctionTest is Test { assertEq(auction.gov(), gov); assertEq(auction.operator(), operator); assertEq(address(auction.dbr()), address(dbr)); - assertEq(address(auction.dola()), address(dola)); - assertEq(auction.dolaReserve(), 1e18); + assertEq(address(auction.asset()), address(asset)); + assertEq(auction.assetReserve(), 1e18); assertEq(auction.dbrReserve(), 1e18); + assertEq(auction.dbrRatePerYear(), 0); + assertEq(auction.minDbrRatePerYear(), 0); + assertEq(auction.maxDbrRatePerYear(), type(uint).max); } function test_getCurrentReserves() public { - (uint _dolaReserve, uint _dbrReserve) = auction.getCurrentReserves(); - assertEq(_dolaReserve, 1e18); + (uint _assetReserve, uint _dbrReserve) = auction.getCurrentReserves(); + assertEq(_assetReserve, 1e18); assertEq(_dbrReserve, 1e18); vm.prank(gov); auction.setMaxDbrRatePerYear(1e18); vm.prank(gov); auction.setDbrRatePerYear(1e18); - (_dolaReserve, _dbrReserve) = auction.getCurrentReserves(); - assertEq(_dolaReserve, 1e18); + (_assetReserve, _dbrReserve) = auction.getCurrentReserves(); + assertEq(_assetReserve, 1e18); assertEq(_dbrReserve, 1e18); vm.warp(block.timestamp + 365 days); - (_dolaReserve, _dbrReserve) = auction.getCurrentReserves(); - assertApproxEqAbs(_dolaReserve, 0.5e18, 1e7); + (_assetReserve, _dbrReserve) = auction.getCurrentReserves(); + assertApproxEqAbs(_assetReserve, 0.5e18, 1e7); assertApproxEqAbs(_dbrReserve, 2 * 1e18, 1e8); } @@ -94,39 +97,155 @@ contract AuctionTest is Test { vm.expectRevert("onlyGov"); auction.setMaxDbrRatePerYear(1e18); vm.startPrank(gov); + + // Test setting max rate auction.setMaxDbrRatePerYear(1e18); assertEq(auction.maxDbrRatePerYear(), 1e18); + + // Test that current rate can be set to max auction.setDbrRatePerYear(1e18); assertEq(auction.dbrRatePerYear(), 1e18); - auction.setMaxDbrRatePerYear(0); - assertEq(auction.maxDbrRatePerYear(), 0); - assertEq(auction.dbrRatePerYear(), 0); + + // Test that setting max below current rate adjusts current rate + auction.setMaxDbrRatePerYear(0.5e18); + assertEq(auction.maxDbrRatePerYear(), 0.5e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + + // Test that setting max below min rate fails + auction.setMinDbrRatePerYear(0.3e18); + vm.expectRevert("Max below min"); + auction.setMaxDbrRatePerYear(0.2e18); + } + + function test_setMinDbrRatePerYear() public { + vm.expectRevert("onlyGov"); + auction.setMinDbrRatePerYear(1e18); + vm.startPrank(gov); + + // Test setting min rate + auction.setMinDbrRatePerYear(0.1e18); + assertEq(auction.minDbrRatePerYear(), 0.1e18); + + // Test that setting min above current rate adjusts current rate + auction.setMinDbrRatePerYear(0.5e18); + assertEq(auction.minDbrRatePerYear(), 0.5e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + + // Test that setting min above max rate fails + auction.setMaxDbrRatePerYear(1e18); + vm.expectRevert("Min above max"); + auction.setMinDbrRatePerYear(2e18); + + // Test that setting min rate works when below max + auction.setMinDbrRatePerYear(0.3e18); + assertEq(auction.minDbrRatePerYear(), 0.3e18); } function test_setDbrRatePerYear() public { vm.expectRevert("onlyGov"); auction.setDbrRatePerYear(1e18); vm.startPrank(gov); - vm.expectRevert("Rate exceeds max"); - auction.setDbrRatePerYear(1e18); - assertEq(auction.dbrRatePerYear(), 0); + + // Set up min and max bounds + auction.setMinDbrRatePerYear(0.1e18); auction.setMaxDbrRatePerYear(1e18); - auction.setDbrRatePerYear(1e18); - assertEq(auction.dbrRatePerYear(), 1e18); + + // Test setting rate within bounds + auction.setDbrRatePerYear(0.5e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + + // Test setting rate above max fails + vm.expectRevert("Rate exceeds max"); + auction.setDbrRatePerYear(2e18); + + // Test setting rate below min fails + vm.expectRevert("Rate below min"); + auction.setDbrRatePerYear(0.05e18); + + // Test operator can also set rate vm.startPrank(operator); + auction.setDbrRatePerYear(0.8e18); + assertEq(auction.dbrRatePerYear(), 0.8e18); + + // Test operator cannot set rate above max + vm.expectRevert("Rate exceeds max"); + auction.setDbrRatePerYear(2e18); + + // Test operator cannot set rate below min + vm.expectRevert("Rate below min"); + auction.setDbrRatePerYear(0.05e18); + } + + function test_dbrRateBoundaryConditions() public { + vm.startPrank(gov); + + // Test setting min and max to same value + auction.setMinDbrRatePerYear(0.5e18); + auction.setMaxDbrRatePerYear(0.5e18); + assertEq(auction.minDbrRatePerYear(), 0.5e18); + assertEq(auction.maxDbrRatePerYear(), 0.5e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + + // Test that rate can only be set to the exact min/max value + auction.setDbrRatePerYear(0.5e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + + // Test that any other value fails + vm.expectRevert("Rate exceeds max"); + auction.setDbrRatePerYear(0.51e18); + vm.expectRevert("Rate below min"); + auction.setDbrRatePerYear(0.49e18); + + // Test that setting max to 0 fails when min is above 0 + vm.expectRevert("Max below min"); + auction.setMaxDbrRatePerYear(0); + + // Test that min can be set to 0 first + auction.setMinDbrRatePerYear(0); + assertEq(auction.minDbrRatePerYear(), 0); + + // Now test that max can be set to 0 + auction.setMaxDbrRatePerYear(0); + assertEq(auction.maxDbrRatePerYear(), 0); + assertEq(auction.dbrRatePerYear(), 0); + + // Test that rate can be set to 0 auction.setDbrRatePerYear(0); assertEq(auction.dbrRatePerYear(), 0); } - function test_setDolaReserve() public { + function test_dbrRateEvents() public { + vm.startPrank(gov); + + // Test that events are emitted (simplified test) + auction.setMinDbrRatePerYear(0.1e18); + auction.setMaxDbrRatePerYear(1e18); + auction.setDbrRatePerYear(0.5e18); + + // Verify the state changes + assertEq(auction.minDbrRatePerYear(), 0.1e18); + assertEq(auction.maxDbrRatePerYear(), 1e18); + assertEq(auction.dbrRatePerYear(), 0.5e18); + } + + function test_setAssetReserve() public { vm.expectRevert("onlyGov"); - auction.setDolaReserve(1e19); + auction.setAssetReserve(1e19); vm.startPrank(gov); - vm.expectRevert("Dola reserve must be positive"); - auction.setDolaReserve(0); - auction.setDolaReserve(1e19); - assertEq(auction.dolaReserve(), 1e19); - assertEq(auction.dbrReserve(), 1e17); + vm.expectRevert("Asset reserve must be positive"); + auction.setAssetReserve(0); + uint oldAssetReserve = auction.assetReserve(); + uint oldDbrReserve = auction.dbrReserve(); + uint ratioMantissaBefore = oldDbrReserve * 1e18 / oldAssetReserve; + uint newAssetReserve = 1e19; + uint expectedDbrReserve = newAssetReserve * oldDbrReserve / oldAssetReserve; + auction.setAssetReserve(newAssetReserve); + uint updatedAssetReserve = auction.assetReserve(); + uint updatedDbrReserve = auction.dbrReserve(); + assertEq(updatedAssetReserve, newAssetReserve); + assertEq(updatedDbrReserve, expectedDbrReserve); + uint ratioMantissaAfter = updatedDbrReserve * 1e18 / updatedAssetReserve; + assertEq(ratioMantissaAfter, ratioMantissaBefore); } function test_setDbrReserve() public { @@ -135,16 +254,25 @@ contract AuctionTest is Test { vm.startPrank(gov); vm.expectRevert("DBR reserve must be positive"); auction.setDbrReserve(0); - auction.setDbrReserve(1e19); - assertEq(auction.dolaReserve(), 1e17); - assertEq(auction.dbrReserve(), 1e19); + uint oldAssetReserve = auction.assetReserve(); + uint oldDbrReserve = auction.dbrReserve(); + uint ratioMantissaBefore = oldDbrReserve * 1e18 / oldAssetReserve; + uint newDbrReserve = 1e19; + uint expectedAssetReserve = newDbrReserve * oldAssetReserve / oldDbrReserve; + auction.setDbrReserve(newDbrReserve); + uint updatedDbrReserve = auction.dbrReserve(); + uint updatedAssetReserve = auction.assetReserve(); + assertEq(updatedDbrReserve, newDbrReserve); + assertEq(updatedAssetReserve, expectedAssetReserve); + uint ratioMantissaAfter = updatedDbrReserve * 1e18 / updatedAssetReserve; + assertEq(ratioMantissaAfter, ratioMantissaBefore); } function test_overrideReserves() public { vm.expectRevert("onlyGov"); auction.overrideReserves(0,0); vm.startPrank(gov); - vm.expectRevert("Dola reserve must be positive"); + vm.expectRevert("Asset reserve must be positive"); auction.overrideReserves(1,0); vm.expectRevert("DBR reserve must be positive"); auction.overrideReserves(0,1); @@ -152,55 +280,55 @@ contract AuctionTest is Test { vm.warp(newTimestamp); auction.overrideReserves(2,3); assertEq(auction.dbrReserve(), 2); - assertEq(auction.dolaReserve(), 3); + assertEq(auction.assetReserve(), 3); assertEq(auction.lastUpdate(), newTimestamp); } function test_sweep() public { vm.expectRevert("onlyGov"); auction.sweep(address(1), address(1), 1); - dola.mint(address(auction), 1); - assertEq(dola.balanceOf(address(auction)), 1); + asset.mint(address(auction), 1); + assertEq(asset.balanceOf(address(auction)), 1); vm.prank(gov); - auction.sweep(address(dola), address(this), 1); - assertEq(dola.balanceOf(address(auction)), 0); - assertEq(dola.balanceOf(address(this)), 1); + auction.sweep(address(asset), address(this), 1); + assertEq(asset.balanceOf(address(auction)), 0); + assertEq(asset.balanceOf(address(this)), 1); } - function test_sendToSaleHandler(uint dolaIn) public { - dolaIn = bound(dolaIn, 1, type(uint).max - 1e18); // max dola in = (max uint - dola reserve) + function test_sendToSaleHandler(uint assetIn) public { + assetIn = bound(assetIn, 1, type(uint).max - 1e18); // max asset in = (max uint - asset reserve) MockSaleHandler handler = new MockSaleHandler(); vm.expectRevert("No sale handler"); auction.sendToSaleHandler(); vm.prank(gov); auction.setSaleHandler(address(handler)); - dola.mint(address(auction), dolaIn); - handler.setCapacity(dolaIn - 1); + asset.mint(address(auction), assetIn); + handler.setCapacity(assetIn - 1); auction.sendToSaleHandler(); - assertEq(dola.balanceOf(address(auction)), 1); - assertEq(dola.balanceOf(address(handler)), dolaIn - 1); + assertEq(asset.balanceOf(address(auction)), 1); + assertEq(asset.balanceOf(address(handler)), assetIn - 1); assertEq(handler.received(), true); } - function test_buyDBR(uint exactDolaIn, uint exactDbrOut) public { + function test_buyDBR(uint exactAssetIn, uint exactDbrOut) public { exactDbrOut = bound(exactDbrOut, 1, auction.dbrReserve()); - exactDolaIn = bound(exactDolaIn, 0, (type(uint).max - auction.dolaReserve()) / auction.dbrReserve() - exactDbrOut); - dola.mint(address(this), exactDolaIn); - dola.approve(address(auction), exactDolaIn); - uint K = auction.dolaReserve() * auction.dbrReserve(); + exactAssetIn = bound(exactAssetIn, 0, (type(uint).max - auction.assetReserve()) / auction.dbrReserve() - exactDbrOut); + asset.mint(address(this), exactAssetIn); + asset.approve(address(auction), exactAssetIn); + uint K = auction.assetReserve() * auction.dbrReserve(); uint newDbrReserve = auction.dbrReserve() - exactDbrOut; - uint newDolaReserve = auction.dolaReserve() + exactDolaIn; - uint newK = newDolaReserve * newDbrReserve; + uint newAssetReserve = auction.assetReserve() + exactAssetIn; + uint newK = newAssetReserve * newDbrReserve; if(newK < K) { vm.expectRevert("Invariant"); - auction.buyDBR(exactDolaIn, exactDbrOut, address(1)); + auction.buyDBR(exactAssetIn, exactDbrOut, address(1)); } else { - auction.buyDBR(exactDolaIn, exactDbrOut, address(1)); - assertEq(dola.balanceOf(address(this)), 0); - assertEq(dola.balanceOf(address(auction)), exactDolaIn); + auction.buyDBR(exactAssetIn, exactDbrOut, address(1)); + assertEq(asset.balanceOf(address(this)), 0); + assertEq(asset.balanceOf(address(auction)), exactAssetIn); assertEq(dbr.balanceOf(address(1)), exactDbrOut); assertEq(auction.dbrReserve(), newDbrReserve); - assertEq(auction.dolaReserve(), newDolaReserve); + assertEq(auction.assetReserve(), newAssetReserve); } } diff --git a/test/DolaSaleHandler.t.sol b/test/DolaSaleHandler.t.sol new file mode 100644 index 0000000..3ca75d6 --- /dev/null +++ b/test/DolaSaleHandler.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import {DolaSaleHandler} from "../src/DolaSaleHandler.sol"; +import {ERC20} from "./mocks/ERC20.sol"; + +contract MockAnDola { + + mapping(address => uint) public borrowBalanceStored; + address public borrower; + uint public repayAmount; + + function repayBorrowBehalf(address _borrower, uint _repayAmount) external returns (uint) { + require(_repayAmount <= borrowBalanceStored[_borrower], "Insufficient debt"); + borrower = _borrower; + repayAmount = _repayAmount; + return 0; + } + + function setBorrowBalance(address _borrower, uint value) external { + borrowBalanceStored[_borrower] = value; + } + + +} + +contract SaleHandlerTest is Test { + + ERC20 asset; + MockAnDola anDola; + DolaSaleHandler handler; + + function setUp() public { + asset = new ERC20(); + anDola = new MockAnDola(); + handler = new DolaSaleHandler( + address(asset), + address(anDola), + address(1), + address(2) + ); + } + + function test_constructor() public { + assertEq(address(handler.asset()), address(asset)); + assertEq(address(handler.anDola()), address(anDola)); + assertEq(handler.borrower1(), address(1)); + assertEq(handler.borrower2(), address(2)); + assertEq(asset.allowance(address(handler), address(anDola)), type(uint).max); + } + + function test_onReceiveBorrower1(uint amount) public { + amount = bound(amount, 1, type(uint).max / 2); + asset.mint(address(handler), amount); + anDola.setBorrowBalance(address(1), amount); + handler.onReceive(); + assertEq(anDola.borrower(), address(1)); + assertEq(anDola.repayAmount(), amount); + } + + function test_onReceiveBorrower2(uint amount) public { + amount = bound(amount, 1, type(uint).max / 2); + asset.mint(address(handler), amount); + anDola.setBorrowBalance(address(2), amount); + handler.onReceive(); + assertEq(anDola.borrower(), address(2)); + assertEq(anDola.repayAmount(), amount); + } + + function test_getCapacity(uint debt1, uint debt2) public { + debt1 = bound(debt1, 1, type(uint).max / 2); + debt2 = bound(debt2, 1, type(uint).max / 2); + anDola.setBorrowBalance(address(1), debt1); + anDola.setBorrowBalance(address(2), debt2); + assertEq(handler.getCapacity(), debt1 + debt2); + } + +} diff --git a/test/Helper.t.sol b/test/Helper.t.sol index f995fb8..cea4841 100644 --- a/test/Helper.t.sol +++ b/test/Helper.t.sol @@ -9,72 +9,72 @@ import {ERC20} from "./mocks/ERC20.sol"; contract HelperTest is Test { Auction auction; - ERC20 dola; + ERC20 asset; ERC20 dbr; Helper helper; function setUp() public { - dola = new ERC20(); + asset = new ERC20(); dbr = new ERC20(); auction = new Auction( address(1), address(2), address(dbr), - address(dola), + address(asset), address(0), 1e18, 1e18 ); - helper = new Helper(address(auction), address(dola)); + helper = new Helper(address(auction), address(asset)); } function test_constructor() public { assertEq(address(helper.auction()), address(auction)); - assertEq(address(helper.dola()), address(dola)); + assertEq(address(helper.asset()), address(asset)); } - function test_getDbrOut(uint dolaIn) public { - dolaIn = bound(dolaIn, 1, (type(uint).max - auction.dolaReserve()) / auction.dbrReserve()); - uint newDbrReserve = auction.dbrReserve() - helper.getDbrOut(dolaIn); - uint newDolaReserve = auction.dolaReserve() + dolaIn; - assertGe(newDbrReserve * newDolaReserve, auction.dbrReserve() * auction.dolaReserve()); + function test_getDbrOut(uint assetIn) public { + assetIn = bound(assetIn, 1, (type(uint).max - auction.assetReserve()) / auction.dbrReserve()); + uint newDbrReserve = auction.dbrReserve() - helper.getDbrOut(assetIn); + uint newAssetReserve = auction.assetReserve() + assetIn; + assertGe(newDbrReserve * newAssetReserve, auction.dbrReserve() * auction.assetReserve()); } - function test_getDolaIn(uint dbrOut) public { + function test_getAssetIn(uint dbrOut) public { dbrOut = bound(dbrOut, 1, auction.dbrReserve() - 1); uint newDbrReserve = auction.dbrReserve() - dbrOut; - uint newDolaReserve = auction.dolaReserve() + helper.getDolaIn(dbrOut); - assertGe(newDbrReserve * newDolaReserve, auction.dbrReserve() * auction.dolaReserve()); + uint newAssetReserve = auction.assetReserve() + helper.getAssetIn(dbrOut); + assertGe(newDbrReserve * newAssetReserve, auction.dbrReserve() * auction.assetReserve()); } - function test_swapExactDolaForDbr(uint dolaIn) public { - dolaIn = bound(dolaIn, 1, (type(uint).max - auction.dolaReserve()) / auction.dbrReserve()); - uint dbrOut = helper.getDbrOut(dolaIn); + function test_swapExactAssetForDbr(uint assetIn) public { + assetIn = bound(assetIn, 1, (type(uint).max - auction.assetReserve()) / auction.dbrReserve()); + uint dbrOut = helper.getDbrOut(assetIn); uint newDbrReserve = auction.dbrReserve() - dbrOut; - uint newDolaReserve = auction.dolaReserve() + dolaIn; - dola.mint(address(this), dolaIn); - dola.approve(address(helper), dolaIn); - helper.swapExactDolaForDbr(dolaIn, dbrOut); + uint newAssetReserve = auction.assetReserve() + assetIn; + asset.mint(address(this), assetIn); + asset.approve(address(helper), assetIn); + helper.swapExactAssetForDbr(assetIn, dbrOut); assertEq(auction.dbrReserve(), newDbrReserve); - assertEq(auction.dolaReserve(), newDolaReserve); + assertEq(auction.assetReserve(), newAssetReserve); assertEq(dbr.balanceOf(address(this)), dbrOut); - assertEq(dola.balanceOf(address(this)), 0); - assertEq(dola.balanceOf(address(auction)), dolaIn); + assertEq(asset.balanceOf(address(this)), 0); + assertEq(asset.balanceOf(address(auction)), assetIn); } - function test_swapDolaForExactDbr(uint dbrOut) public { + function test_swapAssetForExactDbr(uint dbrOut) public { dbrOut = bound(dbrOut, 1, auction.dbrReserve() - 1); - uint dolaIn = helper.getDolaIn(dbrOut); + uint assetIn = helper.getAssetIn(dbrOut); uint newDbrReserve = auction.dbrReserve() - dbrOut; - uint newDolaReserve = auction.dolaReserve() + dolaIn; - dola.mint(address(this), dolaIn); - dola.approve(address(helper), dolaIn); - helper.swapDolaForExactDbr(dbrOut, dolaIn); + uint newAssetReserve = auction.assetReserve() + assetIn; + asset.mint(address(this), assetIn); + asset.approve(address(helper), assetIn); + helper.swapAssetForExactDbr(dbrOut, assetIn); assertEq(auction.dbrReserve(), newDbrReserve); - assertEq(auction.dolaReserve(), newDolaReserve); + assertEq(auction.assetReserve(), newAssetReserve); assertEq(dbr.balanceOf(address(this)), dbrOut); - assertEq(dola.balanceOf(address(this)), 0); - assertEq(dola.balanceOf(address(auction)), dolaIn); + assertEq(asset.balanceOf(address(this)), 0); + assertEq(asset.balanceOf(address(auction)), assetIn); } } \ No newline at end of file diff --git a/test/SaleHandler.t.sol b/test/SaleHandler.t.sol deleted file mode 100644 index 3861c9c..0000000 --- a/test/SaleHandler.t.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.21; - -import {Test, console2} from "forge-std/Test.sol"; -import {SaleHandler} from "../src/SaleHandler.sol"; -import {ERC20} from "./mocks/ERC20.sol"; - -contract MockAnDola { - - ERC20 public dola; - mapping(address => uint) public borrowBalanceStored; - address public borrower; - uint public repayAmount; - - constructor(ERC20 _dola) { - dola = _dola; - } - - function repayBorrowBehalf(address _borrower, uint _repayAmount) external returns (uint) { - if (_repayAmount > borrowBalanceStored[_borrower]) return 1; - borrower = _borrower; - repayAmount = _repayAmount; - dola.transferFrom(msg.sender, address(this), _repayAmount); - return 0; - } - - function setBorrowBalance(address _borrower, uint value) external { - borrowBalanceStored[_borrower] = value; - } - - -} - -contract SaleHandlerTest is Test { - - ERC20 dola; - MockAnDola anDola; - SaleHandler handler; - - address immutable OWNER = address(this); - address constant BENEFICIARY = address(1); - address constant BORROWER1 = address(2); - address constant BORROWER2 = address(3); - address constant NON_OWNER = address(4); - address constant NEW_OWNER = address(5); - address constant NEW_BENEFICIARY = address(6); - uint256 constant MIN_REPAY_BPS = 0; - - function setUp() public { - dola = new ERC20(); - anDola = new MockAnDola(dola); - handler = new SaleHandler( - OWNER, - BENEFICIARY, - MIN_REPAY_BPS, - address(dola), - address(anDola), - BORROWER1, - BORROWER2 - ); - } - - function test_constructor() public { - assertEq(address(handler.dola()), address(dola)); - assertEq(address(handler.anDola()), address(anDola)); - assertEq(handler.borrower1(), BORROWER1); - assertEq(handler.borrower2(), BORROWER2); - assertEq(handler.minRepayBps(), MIN_REPAY_BPS); - assertEq(handler.repayBps(), 10000); - assertEq(handler.owner(), OWNER); - assertEq(handler.beneficiary(), BENEFICIARY); - assertEq(dola.allowance(address(handler), address(anDola)), type(uint).max); - } - - function test_onReceiveBorrower1(uint amount) public { - amount = bound(amount, 1, type(uint128).max); - dola.mint(address(handler), amount); - anDola.setBorrowBalance(BORROWER1, amount); - handler.onReceive(); - assertEq(anDola.borrower(), BORROWER1); - assertEq(anDola.repayAmount(), amount); - } - - function test_onReceiveBorrower2(uint amount) public { - amount = bound(amount, 1, type(uint128).max); - dola.mint(address(handler), amount); - anDola.setBorrowBalance(BORROWER2, amount); - handler.onReceive(); - assertEq(anDola.borrower(), BORROWER2); - assertEq(anDola.repayAmount(), amount); - } - - function test_onReceiveWithBeneficiary(uint amount) public { - amount = bound(amount, 1, type(uint128).max); - handler.setRepayBps(5000); - - dola.mint(address(handler), amount); - - uint repayAmount = amount / 2; - anDola.setBorrowBalance(BORROWER1, repayAmount); - - handler.onReceive(); - - uint beneficiaryAmount = amount - repayAmount; - assertEq(dola.balanceOf(BENEFICIARY), beneficiaryAmount); - } - - function test_onReceiveExcessToBeneficiary(uint amount) public { - amount = bound(amount, 2, type(uint128).max); - uint debtAmount = amount - 1; // Create a smaller debt than the received amount - - dola.mint(address(handler), amount); - anDola.setBorrowBalance(BORROWER1, debtAmount); - - handler.onReceive(); - - assertEq(anDola.repayAmount(), 0); - assertEq(dola.balanceOf(BENEFICIARY), amount); - } - - function test_getCapacity() public { - assertEq(handler.getCapacity(), type(uint).max); - } - - function test_setOwner() public { - handler.setOwner(NEW_OWNER); - assertEq(handler.owner(), NEW_OWNER); - } - - function test_setBeneficiary() public { - handler.setBeneficiary(NEW_BENEFICIARY); - assertEq(handler.beneficiary(), NEW_BENEFICIARY); - } - - function test_setRepayBps() public { - handler.setRepayBps(5000); - assertEq(handler.repayBps(), 5000); - } - - function test_fail_setRepayBpsAbove10000() public { - vm.expectRevert("Repay bps must be less than or equal to 10000"); - handler.setRepayBps(10001); - } - - function test_fail_onReceiveWithoutBeneficiary() public { - handler.setRepayBps(5000); - handler.setBeneficiary(address(0)); - dola.mint(address(handler), 100); - vm.expectRevert("Either all goes to repayment or beneficiary must be set"); - handler.onReceive(); - } - - function test_fail_onlyOwnerCanSetOwner() public { - vm.prank(NON_OWNER); - vm.expectRevert("Only owner can call this function"); - handler.setOwner(NEW_OWNER); - } - - function test_fail_onlyOwnerCanSetBeneficiary() public { - vm.prank(NON_OWNER); - vm.expectRevert("Only owner can call this function"); - handler.setBeneficiary(NEW_BENEFICIARY); - } - - function test_fail_onlyOwnerOrBeneficiaryCanSetRepayBps() public { - vm.prank(NON_OWNER); - vm.expectRevert("Only beneficiary or owner can call this function"); - handler.setRepayBps(5000); - } - - function test_fail_setRepayBpsBelowMinRepayBps() public { - vm.prank(OWNER); - handler.setMinRepayBps(5000); - vm.expectRevert("Repay bps must be greater than or equal to minRepayBps"); - handler.setRepayBps(4999); - } - - function test_setMinRepayBps() public { - vm.prank(OWNER); - handler.setMinRepayBps(5000); - assertEq(handler.minRepayBps(), 5000); - } - - function test_setMinRepayBpsBelowRepayBps() public { - vm.prank(OWNER); - handler.setRepayBps(0); - assertEq(handler.repayBps(), 0); - vm.prank(OWNER); - handler.setMinRepayBps(1000); - assertEq(handler.repayBps(), 1000); - } -}