diff --git a/contracts/interfaces/IBondSDA.sol b/contracts/interfaces/IBondSDA.sol index 653636f52..0abf45c44 100644 --- a/contracts/interfaces/IBondSDA.sol +++ b/contracts/interfaces/IBondSDA.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.15; -import {IERC20} from "./IERC20.sol"; +import {IERC20} from "./IERC20.v2.sol"; interface IBondSDA { /// @notice Creates a new bond market /// @param params_ Configuration data needed for market creation /// @return id ID of new bond market function createMarket(bytes calldata params_) external returns (uint256); + + /// @notice Disable existing bond market + /// @notice Must be market owner + /// @param id_ ID of market to close + function closeMarket(uint256 id_) external; } diff --git a/contracts/interfaces/IBondTeller.sol b/contracts/interfaces/IBondTeller.sol index f6f15aacd..9c748b6b4 100644 --- a/contracts/interfaces/IBondTeller.sol +++ b/contracts/interfaces/IBondTeller.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.15; -import {IERC20} from "./IERC20.sol"; +import {IERC20} from "./IERC20.v2.sol"; interface IBondTeller { /// @notice Instantiates a new fixed expiry bond token diff --git a/contracts/interfaces/IERC20.v2.sol b/contracts/interfaces/IERC20.v2.sol new file mode 100644 index 000000000..953687d1c --- /dev/null +++ b/contracts/interfaces/IERC20.v2.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.15; + +interface IERC20 { + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address recipient, uint256 amount) external returns (bool); + + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/contracts/interfaces/IEasyAuction.sol b/contracts/interfaces/IEasyAuction.sol index d857e786b..cec1285ba 100644 --- a/contracts/interfaces/IEasyAuction.sol +++ b/contracts/interfaces/IEasyAuction.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.15; -import {IERC20} from "./IERC20.sol"; +import {IERC20} from "./IERC20.v2.sol"; interface IEasyAuction { /// @notice Initiates an auction through Gnosis Auctions @@ -29,4 +29,8 @@ interface IEasyAuction { address accessManager, bytes calldata accessManagerData ) external returns (uint256); + + /// @notice Settles an auction and identifies the clearing price + /// @param auctionId The ID of the auction to settle + function settleAuction(uint256 auctionId) external returns (bytes32 clearingOrder); } diff --git a/contracts/peripheral/OhmBondManager.sol b/contracts/peripheral/OhmBondManager.sol index e8d6f6284..154234db2 100644 --- a/contracts/peripheral/OhmBondManager.sol +++ b/contracts/peripheral/OhmBondManager.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.15; import {IBondSDA} from "../interfaces/IBondSDA.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IEasyAuction} from "../interfaces/IEasyAuction.sol"; -import {IERC20} from "../interfaces/IERC20.sol"; +import {IERC20} from "../interfaces/IERC20.v2.sol"; import {ITreasury} from "../interfaces/ITreasury.sol"; import {IOlympusAuthority} from "../interfaces/IOlympusAuthority.sol"; import {OlympusAccessControlled} from "../types/OlympusAccessControlled.sol"; @@ -61,9 +61,9 @@ contract OhmBondManager is OlympusAccessControlled { // ========= MARKET CREATION ========= // function createBondProtocolMarket(uint256 capacity_, uint256 bondTerm_) external onlyPolicy returns (uint256) { - _topUpOhm(capacity_); + treasury.mint(address(this), capacity_); - /// Encodes the information needed for creating a bond market on Bond Protocol + // Encodes the information needed for creating a bond market on Bond Protocol bytes memory createMarketParams = abi.encode( ohm, // payoutToken ohm, // quoteToken @@ -79,23 +79,27 @@ contract OhmBondManager is OlympusAccessControlled { int8(0) // scaleAdjustment ); - ohm.approve(address(fixedExpiryTeller), capacity_); + ohm.increaseAllowance(address(fixedExpiryTeller), capacity_); uint256 marketId = fixedExpiryAuctioneer.createMarket(createMarketParams); return marketId; } + function closeBondProtocolMarket(uint256 id_) external onlyPolicy { + fixedExpiryAuctioneer.closeMarket(id_); + } + function createGnosisAuction(uint96 capacity_, uint256 bondTerm_) external onlyPolicy returns (uint256) { - _topUpOhm(capacity_); + treasury.mint(address(this), capacity_); uint48 expiry = uint48(block.timestamp + bondTerm_); - /// Create bond token - ohm.approve(address(fixedExpiryTeller), capacity_); + // Create bond token + ohm.increaseAllowance(address(fixedExpiryTeller), capacity_); fixedExpiryTeller.deploy(ohm, expiry); (IERC20 bondToken, ) = fixedExpiryTeller.create(ohm, expiry, capacity_); - /// Launch Gnosis Auction + // Launch Gnosis Auction bondToken.approve(address(gnosisEasyAuction), capacity_); uint256 auctionId = gnosisEasyAuction.initiateAuction( bondToken, // auctioningToken @@ -114,6 +118,10 @@ contract OhmBondManager is OlympusAccessControlled { return auctionId; } + function settleGnosisAuction(uint256 id_) external onlyPolicy { + gnosisEasyAuction.settleAuction(id_); + } + // ========= PARAMETER ADJUSTMENT ========= // function setBondProtocolParameters( uint256 initialPrice_, @@ -147,18 +155,12 @@ contract OhmBondManager is OlympusAccessControlled { }); } - // ========= INTERNAL FUNCTIONS ========= // - function _topUpOhm(uint256 amountToDeploy_) internal { - uint256 ohmBalance = ohm.balanceOf(address(this)); - - if (amountToDeploy_ > ohmBalance) { - uint256 amountToMint = amountToDeploy_ - ohmBalance; - treasury.mint(address(this), amountToMint); - } + // ========= EMERGENCY FUNCTIONS ========= // + function setEmergencyApproval(address token_, address spender_, uint256 amount_) external onlyPolicy { + IERC20(token_).approve(spender_, amount_); } - // ========= EMERGENCY FUNCTIONS ========= // - function emergencyWithdraw(uint256 amount) external onlyPolicy { - ohm.transfer(address(treasury), amount); + function emergencyWithdraw(uint256 amount_) external onlyPolicy { + ohm.transfer(address(treasury), amount_); } } diff --git a/test/bonds/OhmBondManager.test.ts b/test/bonds/OhmBondManager.test.ts index 465d0cde5..995a372a8 100644 --- a/test/bonds/OhmBondManager.test.ts +++ b/test/bonds/OhmBondManager.test.ts @@ -9,11 +9,11 @@ import { OlympusTreasury, } from "../../types"; import { addEth, addressZero, bne, getCoin, impersonate, pinBlock } from "../utils/scripts"; +import { advanceTime } from "../utils/Utilities"; import { olympus } from "../utils/olympus"; import { coins } from "../utils/coins"; import { BigNumber } from "ethers"; import { easyAuctionAbi, feAuctioneerAbi, feTellerAbi, bondAggregatorAbi } from "../utils/abi"; -import { defaultAbiCoder } from "ethers/lib/utils"; // Network const url: string = config.networks.hardhat.forking!.url; @@ -252,6 +252,37 @@ describe.only("OhmBondManager", () => { }); }); + describe("closeBondProtocolMarket", () => { + beforeEach(async () => { + await ohmBondManager.connect(policy).setBondProtocolParameters( + "1000000000000000000000000000000000000", // 1e36 + "500000000000000000000000000000000000", // 5e35 + 100_000, + 604800, + 21600 + ); + await ohmBondManager + .connect(policy) + .createBondProtocolMarket("10000000000000", 1210000); + }); + + it("can only be called by policy", async () => { + const marketId = (await bondAggregator.marketCounter()).sub("1"); + await expect(ohmBondManager.connect(policy).closeBondProtocolMarket(marketId)).to.not.be.reverted; + + await expect(ohmBondManager.connect(other).closeBondProtocolMarket(marketId)).to.be.reverted; + + await expect(ohmBondManager.connect(guardian).closeBondProtocolMarket(marketId)).to.be.reverted; + }); + + it("should correctly close the market", async () => { + const marketId = (await bondAggregator.marketCounter()).sub("1"); + await ohmBondManager.connect(policy).closeBondProtocolMarket(marketId); + + expect((await feAuctioneer.isLive(marketId))).to.be.false; + }); + }); + describe("createGnosisAuction", () => { beforeEach(async () => { await ohmBondManager @@ -290,6 +321,77 @@ describe.only("OhmBondManager", () => { }); }); + describe("settleGnosisAuction", () => { + beforeEach(async () => { + await ohmBondManager + .connect(policy) + .setGnosisAuctionParameters(518400, 604800, 2, "10000000", "1000000000000"); + + await ohmBondManager.connect(policy).createGnosisAuction("10000000000000", 1210000); + }); + + it("can only be called by policy", async () => { + await advanceTime(1210005); + + const auctionId = await easyAuction.auctionCounter(); + await expect(ohmBondManager.connect(policy).settleGnosisAuction(auctionId)).to.not.be.reverted; + + await expect(ohmBondManager.connect(other).settleGnosisAuction(auctionId)).to.be.reverted; + + await expect(ohmBondManager.connect(guardian).settleGnosisAuction(auctionId)).to.be.reverted; + }); + + it("should settle the auction", async () => { + await advanceTime(1210005); + + const auctionId = await easyAuction.auctionCounter(); + await expect(ohmBondManager.connect(policy).settleGnosisAuction(auctionId)).to.not.be.reverted; + + const auctionData = await easyAuction.auctionData(auctionId); + expect(auctionData.minimumBiddingAmountPerOrder).to.equal("0"); + }); + }); + + describe("series of market creations", () => { + beforeEach(async () => { + await ohmBondManager.connect(policy).setBondProtocolParameters( + "1000000000000000000000000000000000000", // 1e36 + "500000000000000000000000000000000000", // 5e35 + 100_000, + 604800, + 21600 + ); + + await ohmBondManager + .connect(policy) + .setGnosisAuctionParameters(518400, 604800, 2, "10000000", "1000000000000"); + }); + + it("should not steal OHM when launching subsequent markets", async () => { + await ohmBondManager.connect(policy).createBondProtocolMarket(10000000000000, 1210000); + await ohmBondManager.connect(policy).createGnosisAuction(10000000000000, 1210000); + + expect((await ohm.allowance(ohmBondManager.address, feTeller.address))).to.equal(10000000000000); + expect((await ohm.balanceOf(ohmBondManager.address))).to.equal(10000000000000); + }); + }); + + describe("setEmergencyApproval", () => { + it("can only be called by policy", async () => { + await expect(ohmBondManager.connect(policy).setEmergencyApproval(ohm.address, feTeller.address, "10000000000000")).to.not.be.reverted; + + await expect(ohmBondManager.connect(other).setEmergencyApproval(ohm.address, feTeller.address, "10000000000000")).to.be.reverted; + + await expect(ohmBondManager.connect(guardian).setEmergencyApproval(ohm.address, feTeller.address, "10000000000000")).to.be.reverted; + }); + + it("should set approval on the passed token", async () => { + expect((await ohm.allowance(ohmBondManager.address, feTeller.address))).to.equal("0"); + await ohmBondManager.connect(policy).setEmergencyApproval(ohm.address, feTeller.address, "10000000000000"); + expect((await ohm.allowance(ohmBondManager.address, feTeller.address))).to.equal("10000000000000"); + }); + }); + describe("emergencyWithdraw", () => { beforeEach(async () => { ohm.connect(guardian).transfer(ohmBondManager.address, "1000000000000");