diff --git a/.gitignore b/.gitignore index 9cc7e09..29c710c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ out/ cache/ .env .DS_Store +**/artifacts/** diff --git a/.gitmodules b/.gitmodules index f579e40..0b530b0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,14 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/solmate"] - path = lib/solmate - url = https://github.com/transmissions11/solmate -[submodule "lib/clones-with-immutable-args"] - path = lib/clones-with-immutable-args - url = https://github.com/wighawag/clones-with-immutable-args +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable + branch=v4.9.6 +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch=v4.9.6 +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/foundry.toml b/foundry.toml index e11553c..c9b64f3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,9 +2,10 @@ verbosity = 0 optimizer = true # enable or disable the solc optimizer optimizer_runs = 100000 # the number of optimizer runs -solc_version = '0.8.15' +solc_version = '0.8.20' remappings = [ - 'solmate/=lib/solmate/src/', - 'clones/=lib/clones-with-immutable-args/src/', + 'ds-test/=lib/forge-std/lib/ds-test/src/', 'forge-std/=lib/forge-std/src/', + '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', + '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/' ] \ No newline at end of file diff --git a/lib/clones-with-immutable-args b/lib/clones-with-immutable-args deleted file mode 160000 index 96f7855..0000000 --- a/lib/clones-with-immutable-args +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 96f785571b534764094e268f7e608c393e88f7b6 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dbb6104 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..723f8ca --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..332bd33 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 332bd3306242e09520df2685b2edb99ebd7f5d37 diff --git a/lib/solmate b/lib/solmate deleted file mode 160000 index bff24e8..0000000 --- a/lib/solmate +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bff24e835192470ed38bf15dbed6084c2d723ace diff --git a/src/BondAggregator.sol b/src/BondAggregator.sol index 5d71c3e..4315515 100644 --- a/src/BondAggregator.sol +++ b/src/BondAggregator.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; - -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; +pragma solidity 0.8.20; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Auth} from "./lib/Auth.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; import {IBondAuctioneer} from "./interfaces/IBondAuctioneer.sol"; @@ -56,7 +56,7 @@ contract BondAggregator is IBondAggregator, Auth { // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. uint48 private constant MAX_FIXED_TERM = 52 weeks * 50; - constructor(address guardian_, Authority authority_) Auth(guardian_, authority_) {} + constructor(address guardian_, IAuthority authority_) Auth(guardian_, authority_) {} /// @inheritdoc IBondAggregator function registerAuctioneer(IBondAuctioneer auctioneer_) external requiresAuth { diff --git a/src/BondFixedExpiryFPA.sol b/src/BondFixedExpiryFPA.sol index decdd19..5a6dec1 100644 --- a/src/BondFixedExpiryFPA.sol +++ b/src/BondFixedExpiryFPA.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {BondBaseFPA, IBondAggregator, Authority} from "./bases/BondBaseFPA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseFPA} from "./bases/BondBaseFPA.sol"; /// @title Bond Fixed-Expiry Fixed Price Auctioneer /// @notice Bond Fixed-Expiry Fixed Price Auctioneer Contract @@ -33,7 +34,7 @@ contract BondFixedExpiryFPA is BondBaseFPA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseFPA(teller_, aggregator_, guardian_, authority_) {} /// @inheritdoc BondBaseFPA diff --git a/src/BondFixedExpiryOFDA.sol b/src/BondFixedExpiryOFDA.sol index 50a1f2c..0ba2f8f 100644 --- a/src/BondFixedExpiryOFDA.sol +++ b/src/BondFixedExpiryOFDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {BondBaseOFDA, IBondAggregator, Authority} from "./bases/BondBaseOFDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseOFDA} from "./bases/BondBaseOFDA.sol"; /// @title Bond Fixed-Expiry Fixed Discount Auctioneer /// @notice Bond Fixed-Expiry Fixed Discount Auctioneer Contract @@ -33,7 +35,7 @@ contract BondFixedExpiryOFDA is BondBaseOFDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseOFDA(teller_, aggregator_, guardian_, authority_) {} /// @inheritdoc BondBaseOFDA diff --git a/src/BondFixedExpiryOSDA.sol b/src/BondFixedExpiryOSDA.sol index 3a9ef56..8897276 100644 --- a/src/BondFixedExpiryOSDA.sol +++ b/src/BondFixedExpiryOSDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.15; -import {BondBaseOSDA, IBondAggregator, Authority} from "./bases/BondBaseOSDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseOSDA} from "./bases/BondBaseOSDA.sol"; /// @title Bond Fixed-Expiry Oracle-based Sequential Dutch Auctioneer /// @notice Bond Fixed-Expiry Oracle-based Sequential Dutch Auctioneer Contract @@ -25,7 +27,7 @@ contract BondFixedExpiryOSDA is BondBaseOSDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseOSDA(teller_, aggregator_, guardian_, authority_) {} /// @inheritdoc BondBaseOSDA diff --git a/src/BondFixedExpirySDA.sol b/src/BondFixedExpirySDA.sol index 70298dd..9502bd7 100644 --- a/src/BondFixedExpirySDA.sol +++ b/src/BondFixedExpirySDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {BondBaseSDA, IBondAggregator, Authority} from "./bases/BondBaseSDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseSDA} from "./bases/BondBaseSDA.sol"; /// @title Bond Fixed-Expiry Sequential Dutch Auctioneer v1.1 /// @notice Bond Fixed-Expiry Sequential Dutch Auctioneer Contract @@ -25,7 +27,7 @@ contract BondFixedExpirySDA is BondBaseSDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseSDA(teller_, aggregator_, guardian_, authority_) {} /// @inheritdoc BondBaseSDA diff --git a/src/BondFixedExpiryTeller.sol b/src/BondFixedExpiryTeller.sol index ee2ecab..317914f 100644 --- a/src/BondFixedExpiryTeller.sol +++ b/src/BondFixedExpiryTeller.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; - -import {BondBaseTeller, IBondAggregator, Authority} from "./bases/BondBaseTeller.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; +import {BondBaseTeller} from "./bases/BondBaseTeller.sol"; import {FullMath} from "./lib/FullMath.sol"; -import "./bases/BondTeller1155.sol"; +import {BondTeller1155Upgradeable} from "./bases/BondTeller1155Upgradeable.sol"; /// @title Bond Fixed Expiry Teller /// @notice Bond Fixed Expiry Teller Contract @@ -24,17 +25,42 @@ import "./bases/BondTeller1155.sol"; /// duplicate tokens with the same name/symbol. /// /// @author Oighty, Zeus, Potted Meat, indigo -contract BondFixedExpiryTeller is BondTeller1155 { - using TransferHelper for ERC20; +contract BondFixedExpiryTeller is BondTeller1155Upgradeable { + using SafeERC20 for ERC20; /* ========== CONSTRUCTOR ========== */ - constructor( + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( address protocol_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) BondTeller1155(protocol_, aggregator_, guardian_, authority_) {} + IAuthority authority_ + ) public initializer { + __BondFixedExpiryTeller_init( + protocol_, + aggregator_, + guardian_, + authority_ + ); + } + function __BondFixedExpiryTeller_init( + address protocol_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) public onlyInitializing { + __BondTeller1155_init( + protocol_, + aggregator_, + guardian_, + authority_ + ); + } /* ========== PURCHASE ========== */ /// @notice Handle payout to recipient @@ -77,7 +103,7 @@ contract BondFixedExpiryTeller is BondTeller1155 { } else { // If no expiry, then transfer payout directly to user if (address(payoutToken_) == address(0)) { - bool sent = payable(recipient_).send(payout_); + (bool sent,) = payable(address(recipient_)).call{value: payout_}(""); require(sent, "Failed to send native tokens"); } else { payoutToken_.safeTransfer(recipient_, payout_); diff --git a/src/BondFixedTermFPA.sol b/src/BondFixedTermFPA.sol index e7ba8ba..d883172 100644 --- a/src/BondFixedTermFPA.sol +++ b/src/BondFixedTermFPA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {BondBaseFPA, IBondAggregator, Authority} from "./bases/BondBaseFPA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseFPA} from "./bases/BondBaseFPA.sol"; /// @title Bond Fixed-Term Fixed Price Auctioneer /// @notice Bond Fixed-Term Fixed Price Auctioneer Contract @@ -32,8 +34,8 @@ contract BondFixedTermFPA is BondBaseFPA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) BondBaseFPA(teller_, aggregator_, guardian_, authority_) {} + IAuthority authority_ + ) BondBaseFPA(teller_, aggregator_,guardian_, authority_) {} /* ========== MARKET FUNCTIONS ========== */ /// @inheritdoc BondBaseFPA diff --git a/src/BondFixedTermOFDA.sol b/src/BondFixedTermOFDA.sol index b8df8ee..cfeac36 100644 --- a/src/BondFixedTermOFDA.sol +++ b/src/BondFixedTermOFDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {BondBaseOFDA, IBondAggregator, Authority} from "./bases/BondBaseOFDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseOFDA} from "./bases/BondBaseOFDA.sol"; /// @title Bond Fixed-Term Fixed Discount Auctioneer /// @notice Bond Fixed-Term Fixed Discount Auctioneer Contract @@ -33,7 +35,7 @@ contract BondFixedTermOFDA is BondBaseOFDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseOFDA(teller_, aggregator_, guardian_, authority_) {} /* ========== MARKET FUNCTIONS ========== */ diff --git a/src/BondFixedTermOSDA.sol b/src/BondFixedTermOSDA.sol index 386e232..313c092 100644 --- a/src/BondFixedTermOSDA.sol +++ b/src/BondFixedTermOSDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.15; +pragma solidity 0.8.20; -import {BondBaseOSDA, IBondAggregator, Authority} from "./bases/BondBaseOSDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseOSDA} from "./bases/BondBaseOSDA.sol"; /// @title Bond Fixed-Term Oracle-based Sequential Dutch Auctioneer /// @notice Bond Fixed-Term Oracle-based Sequential Dutch Auctioneer Contract @@ -25,7 +27,7 @@ contract BondFixedTermOSDA is BondBaseOSDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseOSDA(teller_, aggregator_, guardian_, authority_) {} /* ========== MARKET FUNCTIONS ========== */ diff --git a/src/BondFixedTermSDA.sol b/src/BondFixedTermSDA.sol index cae0acf..4425b50 100644 --- a/src/BondFixedTermSDA.sol +++ b/src/BondFixedTermSDA.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {BondBaseSDA, IBondAggregator, Authority} from "./bases/BondBaseSDA.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; +import {BondBaseSDA} from "./bases/BondBaseSDA.sol"; /// @title Bond Fixed-Term Sequential Dutch Auctioneer /// @notice Bond Fixed-Term Sequential Dutch Auctioneer Contract @@ -25,7 +27,7 @@ contract BondFixedTermSDA is BondBaseSDA { IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseSDA(teller_, aggregator_, guardian_, authority_) {} /* ========== MARKET FUNCTIONS ========== */ diff --git a/src/BondFixedTermTeller.sol b/src/BondFixedTermTeller.sol index 7da6225..5914385 100644 --- a/src/BondFixedTermTeller.sol +++ b/src/BondFixedTermTeller.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; - -import {BondBaseTeller, IBondAggregator, Authority} from "./bases/BondBaseTeller.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondTeller1155} from "./interfaces/IBondTeller1155.sol"; - -import {TransferHelper} from "./lib/TransferHelper.sol"; +import {BondBaseTeller} from "./bases/BondBaseTeller.sol"; import {FullMath} from "./lib/FullMath.sol"; -import {ERC1155} from "./lib/ERC1155.sol"; -import "./bases/BondTeller1155.sol"; +import {BondTeller1155Upgradeable} from "./bases/BondTeller1155Upgradeable.sol"; /// @title Bond Fixed Term Teller /// @notice Bond Fixed Term Teller Contract @@ -27,16 +26,42 @@ import "./bases/BondTeller1155.sol"; /// (rounded to the minute) as ERC1155 tokens. /// /// @author Oighty, Zeus, Potted Meat, indigo -contract BondFixedTermTeller is BondTeller1155 { - using TransferHelper for ERC20; +contract BondFixedTermTeller is BondTeller1155Upgradeable { + using SafeERC20 for ERC20; /* ========== CONSTRUCTOR ========== */ - constructor( + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address protocol_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) public initializer { + __BondFixedTermTeller_init( + protocol_, + aggregator_, + guardian_, + authority_ + ); + } + + function __BondFixedTermTeller_init( address protocol_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) BondTeller1155(protocol_, aggregator_, guardian_, authority_) {} + IAuthority authority_ + ) internal onlyInitializing { + __BondTeller1155_init( + protocol_, + aggregator_, + guardian_, + authority_ + ); + } /* ========== PURCHASE ========== */ @@ -81,7 +106,7 @@ contract BondFixedTermTeller is BondTeller1155 { } else { // If no expiry, then transfer payout directly to user if (address(payoutToken_) == address(0)) { - bool sent = payable(recipient_).send(payout_); + (bool sent,) = payable(address(recipient_)).call{value: payout_}(""); require(sent, "Failed to send native tokens"); } else { payoutToken_.safeTransfer(recipient_, payout_); diff --git a/src/BondOracle.sol b/src/BondOracle.sol new file mode 100644 index 0000000..4ec0948 --- /dev/null +++ b/src/BondOracle.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Auth} from "./lib/Auth.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; +import {IBondOracle} from "./interfaces/IBondOracle.sol"; + +contract BondOracle is IBondOracle, Auth { + struct MarketTokens {ERC20 quoteToken; ERC20 payoutToken;} + + // @note This is a constant value that represents the time window in seconds that a price is considered valid + uint256 public constant VALIDITY_WINDOW = 300; // 5 minutes + // ------------ Error for old price ------------ + error Oracle_OldPrice(address token, uint lastUpdatedAt, uint currentWindow); + // ------------ Data entry struct ------------ + struct DataWithTimestamp { + uint price; + uint256 updatedAt; + } + // --------------------------------------------- + + + mapping(uint256 => MarketTokens) public marketsTokens; + mapping(address => DataWithTimestamp) internal prices; // price of token in USD + uint8 public constant oracleDecimals = 18; + + constructor( + address guardian_, + IAuthority authority_ + ) Auth(guardian_, authority_) {} + + /// @notice Modifier to restrict old price calculations + modifier onlyValidPrice(address tokenAddress) { + uint updatedAt = prices[tokenAddress].updatedAt; + uint window = block.timestamp - VALIDITY_WINDOW; + if(updatedAt < window) + revert Oracle_OldPrice(tokenAddress, updatedAt, window); + _; + } + + /// @notice Set price (in USD) for smallest unit of token + function setPrice(address token, uint price) external requiresAuth { + prices[token] = DataWithTimestamp(price, block.timestamp); + } + + /// @notice Register a new bond market on the oracle + function registerMarket(uint256 id_, ERC20 quoteToken_, ERC20 payoutToken_) external requiresAuth { + // must ba auctioneer + marketsTokens[id_] = MarketTokens(quoteToken_, payoutToken_); + } + + /// @notice Returns token price in USD + function usdPrice(address tokenAddress) external view onlyValidPrice(tokenAddress) returns (uint256) { + return prices[tokenAddress].price; + } + + /// @notice Returns the price as a ratio of quote tokens to base tokens for the provided market id scaled by 10^decimals + function currentPrice(uint256 id_) external view returns (uint256) { + MarketTokens memory tokens = marketsTokens[id_]; + return this.currentPrice(tokens.quoteToken, tokens.payoutToken); + } + + /// @notice Returns the price as a ratio of quote tokens to base tokens for the provided token pair scaled by 10^decimals + function currentPrice( + ERC20 quoteToken_, + ERC20 payoutToken_ + ) + external + view + onlyValidPrice(address(quoteToken_)) + onlyValidPrice(address(payoutToken_)) + returns (uint256) + { + uint quotePrice = prices[address(quoteToken_)].price; + uint payoutPrice = prices[address(payoutToken_)].price; + uint quoteDecimals = quoteToken_.decimals(); + uint payoutDecimals = payoutToken_.decimals(); + + return (quotePrice * 10 ** (payoutDecimals + oracleDecimals)) / + (payoutPrice * 10 ** quoteDecimals); + } + + /// @notice Returns the number of configured decimals of the price value for the provided market id + function decimals(uint256 id_) external view returns (uint8) { + return oracleDecimals; + } + + /// @notice Returns the number of configured decimals of the price value for the provided token pair + function decimals(ERC20 quoteToken_, ERC20 payoutToken_) + external + view + returns (uint8) + { + return oracleDecimals; + } + +} \ No newline at end of file diff --git a/src/ERC20BondToken.sol b/src/ERC20BondToken.sol deleted file mode 100644 index 8526b42..0000000 --- a/src/ERC20BondToken.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; - -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {CloneERC20} from "./lib/CloneERC20.sol"; - -/// @title ERC20 Bond Token -/// @notice ERC20 Bond Token Contract -/// @dev Bond Protocol is a permissionless system to create Olympus-style bond markets -/// for any token pair. The markets do not require maintenance and will manage -/// bond prices based on activity. Bond issuers create BondMarkets that pay out -/// a Payout Token in exchange for deposited Quote Tokens. Users can purchase -/// future-dated Payout Tokens with Quote Tokens at the current market price and -/// receive Bond Tokens to represent their position while their bond vests. -/// Once the Bond Tokens vest, they can redeem it for the Quote Tokens. -/// -/// @dev The ERC20 Bond Token contract is issued by a Fixed Expiry Teller to -/// represent bond positions until they vest. Bond tokens can be redeemed for -// the underlying token 1:1 at or after expiry. -/// -/// @dev This contract uses Clones (https://github.com/wighawag/clones-with-immutable-args) -/// to save gas on deployment and is based on VestedERC20 (https://github.com/ZeframLou/vested-erc20) -/// -/// @author Oighty, Zeus, Potted Meat, indigo -contract ERC20BondToken is CloneERC20 { - /* ========== ERRORS ========== */ - error BondToken_OnlyTeller(); - - /* ========== IMMUTABLE PARAMETERS ========== */ - - /// @notice The token to be redeemed when the bond vests - /// @return _underlying The address of the underlying token - function underlying() external pure returns (ERC20 _underlying) { - return ERC20(_getArgAddress(0x41)); - } - - /// @notice Timestamp at which the BondToken can be redeemed for the underlying - /// @return _expiry The vest start timestamp - function expiry() external pure returns (uint48 _expiry) { - return uint48(_getArgUint256(0x55)); - } - - /// @notice Address of the Teller that created the token - function teller() public pure returns (address _teller) { - return _getArgAddress(0x75); - } - - /* ========== MINT/BURN ========== */ - - function mint(address to, uint256 amount) external { - if (msg.sender != teller()) revert BondToken_OnlyTeller(); - _mint(to, amount); - } - - function burn(address from, uint256 amount) external { - if (msg.sender != teller()) revert BondToken_OnlyTeller(); - _burn(from, amount); - } -} diff --git a/src/LimitOrders.sol b/src/LimitOrders.sol index 1752cdb..203cf7a 100644 --- a/src/LimitOrders.sol +++ b/src/LimitOrders.sol @@ -1,16 +1,17 @@ /// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; -import {TransferHelper} from "./lib/TransferHelper.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Auth} from "./lib/Auth.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; import {FullMath} from "./lib/FullMath.sol"; import {IBondAggregator} from "./interfaces/IBondAggregator.sol"; import {IBondAuctioneer} from "./interfaces/IBondAuctioneer.sol"; import {IBondTeller} from "./interfaces/IBondTeller.sol"; contract LimitOrders is Auth { - using TransferHelper for ERC20; + using SafeERC20 for ERC20; using FullMath for uint256; /* ========== EVENTS ========== */ @@ -64,7 +65,7 @@ contract LimitOrders is Auth { /* ========== CONSTRUCTOR ========== */ - constructor(IBondAggregator aggregator_, Authority authority_) Auth(address(0), authority_) { + constructor(IBondAggregator aggregator_, IAuthority authority_) Auth(address(0), authority_) { aggregator = aggregator_; chainId = block.chainid; domainSeparator = computeDomainSeparator(); @@ -141,7 +142,7 @@ contract LimitOrders is Auth { // Approve teller to spend token IBondTeller teller = auctioneer.getTeller(); - quoteToken.safeApprove(address(teller), amount); + quoteToken.forceApprove(address(teller), amount); // Execute purchase teller.purchase(order_.recipient, order_.referrer, order_.marketId, amount, minAmountOut); diff --git a/src/RolesAuthority.sol b/src/RolesAuthority.sol index 7e6555a..1c1c9ba 100644 --- a/src/RolesAuthority.sol +++ b/src/RolesAuthority.sol @@ -1,25 +1,32 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.0; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; +import {Auth} from "./lib/Auth.sol"; +import {IAuthority} from "./interfaces/IAuthority.sol"; /// @notice Role based Authority that supports up to 256 roles. -/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) /// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) -contract RolesAuthority is Auth, Authority { +contract RolesAuthority is Auth, IAuthority { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ - /* ========== EVENTS ========== */ event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); event PublicCapabilityUpdated(address indexed target, bytes4 indexed functionSig, bool enabled); event RoleCapabilityUpdated(uint8 indexed role, address indexed target, bytes4 indexed functionSig, bool enabled); - /* ========== CONSTRUCTOR ========== */ + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ - constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} + constructor(address _owner, IAuthority _authority) Auth(_owner, _authority) {} - /* ========== ROLE/USER STORAGE ========== */ + /*////////////////////////////////////////////////////////////// + ROLE/USER STORAGE + //////////////////////////////////////////////////////////////*/ mapping(address => bytes32) public getUserRoles; @@ -39,7 +46,9 @@ contract RolesAuthority is Auth, Authority { return (uint256(getRolesWithCapability[target][functionSig]) >> role) & 1 != 0; } - /* ========== AUTHORIZATION LOGIC ========== */ + /*////////////////////////////////////////////////////////////// + AUTHORIZATION LOGIC + //////////////////////////////////////////////////////////////*/ function canCall( address user, @@ -51,7 +60,9 @@ contract RolesAuthority is Auth, Authority { bytes32(0) != getUserRoles[user] & getRolesWithCapability[target][functionSig]; } - /* ========== ROLE CAPABILITY CONFIGURATION LOGIC ========== */ + /*////////////////////////////////////////////////////////////// + ROLE CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ function setPublicCapability( address target, @@ -78,7 +89,9 @@ contract RolesAuthority is Auth, Authority { emit RoleCapabilityUpdated(role, target, functionSig, enabled); } - /* ========== USER ROLE ASSIGNMENT LOGIC ========== */ + /*////////////////////////////////////////////////////////////// + USER ROLE ASSIGNMENT LOGIC + //////////////////////////////////////////////////////////////*/ function setUserRole( address user, @@ -93,4 +106,4 @@ contract RolesAuthority is Auth, Authority { emit UserRoleUpdated(user, role, enabled); } -} \ No newline at end of file +} diff --git a/src/bases/BondBaseAuctioneer.sol b/src/bases/BondBaseAuctioneer.sol new file mode 100644 index 0000000..e1b4064 --- /dev/null +++ b/src/bases/BondBaseAuctioneer.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Auth} from "../lib/Auth.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {IBondTeller} from "../interfaces/IBondTeller.sol"; + + +/// @title Bond Auctioneer Base v1.1 +/// @notice Bond Auctioneer Base Contract +/// @dev Bond Protocol is a system to create Olympus-style bond markets +/// for any token pair. The markets do not require maintenance and will manage +/// bond prices based on activity. Bond issuers create BondMarkets that pay out +/// a Payout Token in exchange for deposited Quote Tokens. Users can purchase +/// future-dated Payout Tokens with Quote Tokens at the current market price and +/// receive Bond Tokens to represent their position while their bond vests. +/// Once the Bond Tokens vest, they can redeem it for the Quote Tokens. +/// +/// @dev The Auctioneer contract allows users to create and manage bond markets. +/// All bond pricing logic and market data is stored in the Auctioneer. +/// A Auctioneer is dependent on a Teller to serve external users and +/// an Aggregator to register new markets. +/// +/// @author Oighty, Zeus, Potted Meat, indigo +abstract contract BondBaseAuctioneer is IBondAuctioneer, Auth, Pausable, ReentrancyGuard { + + // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. + uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50; + uint48 internal constant ONE_HUNDRED_PERCENT = 1e5; // one percent equals 1000. + + /// @notice Whether or not the auctioneer allows new markets to be created + /// @dev Changing to false will sunset the auctioneer after all active markets end + bool public allowNewMarkets; + + // BondAggregator contract with utility functions + IBondAggregator internal immutable _aggregator; + + // BondTeller contract that handles interactions with users and issues tokens + IBondTeller internal immutable _teller; + + constructor( + IBondTeller teller_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) Auth(guardian_, authority_) { + _aggregator = aggregator_; + _teller = teller_; + + allowNewMarkets = true; + } + + /* ========== ERRORS ========== */ + + error Auctioneer_Unreachable(); + + error Auctioneer_OnlyMarketOwner(); + error Auctioneer_MarketNotActive(); + error Auctioneer_MaxPayoutExceeded(); + error Auctioneer_AmountLessThanMinimum(); + error Auctioneer_NotEnoughCapacity(); + error Auctioneer_InvalidCallback(); + error Auctioneer_BadExpiry(); + error Auctioneer_InvalidParams(); + error Auctioneer_NotAuthorized(); + error Auctioneer_NewMarketsNotAllowed(); + error Auctioneer_UnsupportedToken(); + + + /** + * @dev Modifier that checks that an account is market owner. Reverts + * with Auctioneer_OnlyMarketOwner error. + */ + modifier onlyMarketOwner(uint256 id_) { + if (msg.sender != this.ownerOf(id_)) revert Auctioneer_OnlyMarketOwner(); + _; + } + + modifier onlyTeller() { + if ( + address(msg.sender) != address(_teller) || + address(msg.sender) == address(0) || + address(msg.sender).code.length == 0 + ) revert Auctioneer_NotAuthorized(); + _; + } + + /// @inheritdoc IBondAuctioneer + function getTeller() external view override returns (IBondTeller) { + return _teller; + } + + /// @inheritdoc IBondAuctioneer + function getAggregator() external view override returns (IBondAggregator) { + return _aggregator; + } + + function pause() external requiresAuth { + _pause(); + } + + function unpause() external requiresAuth { + _unpause(); + } + + function _setAllowNewMarkets(bool status_) internal requiresAuth { + allowNewMarkets = status_; + } + + /// @inheritdoc IBondAuctioneer + function setAllowNewMarkets(bool status_) external virtual override { + _setAllowNewMarkets(status_); + } +} \ No newline at end of file diff --git a/src/bases/BondBaseFPA.sol b/src/bases/BondBaseFPA.sol index bf0b3b1..fd62ca5 100644 --- a/src/bases/BondBaseFPA.sol +++ b/src/bases/BondBaseFPA.sol @@ -1,15 +1,16 @@ /// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; - -import {IBondFPA, IBondAuctioneer} from "../interfaces/IBondFPA.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; - -import {TransferHelper} from "../lib/TransferHelper.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; import {FullMath} from "../lib/FullMath.sol"; +import {IBondFPA} from "../interfaces/IBondFPA.sol"; +import {BondBaseAuctioneer} from "./BondBaseAuctioneer.sol"; + /// @title Bond Fixed Price Auctioneer /// @notice Bond Fixed Price Auctioneer Base Contract @@ -30,24 +31,10 @@ import {FullMath} from "../lib/FullMath.sol"; /// See IBondFPA.sol for price format details. /// /// @author Oighty -abstract contract BondBaseFPA is IBondFPA, Auth { - using TransferHelper for ERC20; +abstract contract BondBaseFPA is IBondFPA, BondBaseAuctioneer { + using SafeERC20 for ERC20; using FullMath for uint256; - /* ========== ERRORS ========== */ - - error Auctioneer_OnlyMarketOwner(); - error Auctioneer_MarketNotActive(); - error Auctioneer_MaxPayoutExceeded(); - error Auctioneer_AmountLessThanMinimum(); - error Auctioneer_NotEnoughCapacity(); - error Auctioneer_InvalidCallback(); - error Auctioneer_BadExpiry(); - error Auctioneer_InvalidParams(); - error Auctioneer_NotAuthorized(); - error Auctioneer_NewMarketsNotAllowed(); - error Auctioneer_UnsupportedToken(); - /* ========== EVENTS ========== */ event MarketCreated( @@ -59,6 +46,7 @@ abstract contract BondBaseFPA is IBondFPA, Auth { ); event MarketClosed(uint256 indexed id); + /* ========== STATE VARIABLES ========== */ /// @notice Information pertaining to bond markets @@ -70,10 +58,6 @@ abstract contract BondBaseFPA is IBondFPA, Auth { /// @notice New address to designate as market owner. They must accept ownership to transfer permissions. mapping(uint256 => address) public newOwners; - /// @notice Whether or not the auctioneer allows new markets to be created - /// @dev Changing to false will sunset the auctioneer after all active markets end - bool public allowNewMarkets; - // Minimum time parameter values. Can be updated by admin. /// @notice Minimum deposit interval for a market uint48 public minDepositInterval; @@ -81,29 +65,14 @@ abstract contract BondBaseFPA is IBondFPA, Auth { /// @notice Minimum market duration in seconds uint48 public minMarketDuration; - // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. - uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50; - uint48 internal constant ONE_HUNDRED_PERCENT = 1e5; // one percent equals 1000. - - // BondAggregator contract with utility functions - IBondAggregator internal immutable _aggregator; - - // BondTeller contract that handles interactions with users and issues tokens - IBondTeller internal immutable _teller; - constructor( IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) Auth(guardian_, authority_) { - _aggregator = aggregator_; - _teller = teller_; - + IAuthority authority_ + ) BondBaseAuctioneer(teller_, aggregator_, guardian_, authority_) { minDepositInterval = 1 minutes; minMarketDuration = 10 minutes; - - allowNewMarkets = true; } /* ========== MARKET FUNCTIONS ========== */ @@ -112,7 +81,7 @@ abstract contract BondBaseFPA is IBondFPA, Auth { function createMarket(bytes calldata params_) external payable virtual returns (uint256); /// @notice core market creation logic, see IBondFPA.MarketParams documentation - function _createMarket(MarketParams memory params_) internal returns (uint256) { + function _createMarket(MarketParams memory params_) internal whenNotPaused returns (uint256) { { // Check that the auctioneer is allowing new markets to be created if (!allowNewMarkets) revert Auctioneer_NewMarketsNotAllowed(); @@ -157,7 +126,7 @@ abstract contract BondBaseFPA is IBondFPA, Auth { // Ensure capacity is equal to the value sent if (params_.capacity != msg.value) revert Auctioneer_InvalidParams(); // Send tokens to teller as it operates over purchase - bool sent = payable(address(_teller)).send(msg.value); + (bool sent,) = payable(address(_teller)).call{value: msg.value}(""); require(sent, "Failed to send tokens to teller"); } else { // Check balance before and after to ensure full amount received, revert if not @@ -203,13 +172,12 @@ abstract contract BondBaseFPA is IBondFPA, Auth { } /// @inheritdoc IBondAuctioneer - function pushOwnership(uint256 id_, address newOwner_) external override { - if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner(); + function pushOwnership(uint256 id_, address newOwner_) external override onlyMarketOwner(id_) whenNotPaused { newOwners[id_] = newOwner_; } /// @inheritdoc IBondAuctioneer - function pullOwnership(uint256 id_) external override { + function pullOwnership(uint256 id_) external override whenNotPaused { if (msg.sender != newOwners[id_]) revert Auctioneer_NotAuthorized(); markets[id_].owner = newOwners[id_]; } @@ -235,20 +203,13 @@ abstract contract BondBaseFPA is IBondFPA, Auth { } // Unused, but required by interface - function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override {} + function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override onlyMarketOwner(id_) {} // Unused, but required by interface - function setDefaults(uint32[6] memory defaults_) external override {} + function setDefaults(uint32[6] memory defaults_) external override requiresAuth {} /// @inheritdoc IBondAuctioneer - function setAllowNewMarkets(bool status_) external override requiresAuth { - // Restricted to authorized addresses - allowNewMarkets = status_; - } - - /// @inheritdoc IBondAuctioneer - function closeMarket(uint256 id_) external override { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); + function closeMarket(uint256 id_) external override onlyTeller whenNotPaused { // If market closed early, set conclusion to current timestamp if (terms[id_].conclusion > uint48(block.timestamp)) { @@ -267,9 +228,8 @@ abstract contract BondBaseFPA is IBondFPA, Auth { uint256 id_, uint256 amount_, uint256 minAmountOut_ - ) external override returns (uint256 payout) { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); - + ) external override onlyTeller whenNotPaused returns (uint256 payout) { + BondMarket storage market = markets[id_]; // Check if market is live, if not revert @@ -319,7 +279,7 @@ abstract contract BondBaseFPA is IBondFPA, Auth { } /// @inheritdoc IBondAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { + function marketPrice(uint256 id_) public view override(IBondAuctioneer, IBondFPA) returns (uint256) { return markets[id_].price; } @@ -393,17 +353,7 @@ abstract contract BondBaseFPA is IBondFPA, Auth { function ownerOf(uint256 id_) external view override returns (address) { return markets[id_].owner; } - - /// @inheritdoc IBondAuctioneer - function getTeller() external view override returns (IBondTeller) { - return _teller; - } - - /// @inheritdoc IBondAuctioneer - function getAggregator() external view override returns (IBondAggregator) { - return _aggregator; - } - + /// @inheritdoc IBondAuctioneer function currentCapacity(uint256 id_) external view override returns (uint256) { return markets[id_].capacity; diff --git a/src/bases/BondBaseOFDA.sol b/src/bases/BondBaseOFDA.sol index db22823..67c8d50 100644 --- a/src/bases/BondBaseOFDA.sol +++ b/src/bases/BondBaseOFDA.sol @@ -1,16 +1,17 @@ /// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; - -import {IBondOFDA, IBondAuctioneer} from "../interfaces/IBondOFDA.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {FullMath} from "../lib/FullMath.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {IBondOFDA} from "../interfaces/IBondOFDA.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; import {IBondOracle} from "../interfaces/IBondOracle.sol"; +import {BondBaseOracleAuctioneer, BondBaseAuctioneer} from "./BondBaseOracleAuctioneer.sol"; -import {TransferHelper} from "../lib/TransferHelper.sol"; -import {FullMath} from "../lib/FullMath.sol"; /// @title Bond Oracle-based Fixed Discount Auctioneer /// @notice Bond Oracle-based Fixed Discount Auctioneer Base Contract @@ -31,25 +32,10 @@ import {FullMath} from "../lib/FullMath.sol"; /// the duration of a market. /// /// @author Oighty -abstract contract BondBaseOFDA is IBondOFDA, Auth { - using TransferHelper for ERC20; +abstract contract BondBaseOFDA is IBondOFDA, BondBaseOracleAuctioneer { + using SafeERC20 for ERC20; using FullMath for uint256; - /* ========== ERRORS ========== */ - - error Auctioneer_OnlyMarketOwner(); - error Auctioneer_MarketNotActive(); - error Auctioneer_MaxPayoutExceeded(); - error Auctioneer_AmountLessThanMinimum(); - error Auctioneer_NotEnoughCapacity(); - error Auctioneer_InvalidCallback(); - error Auctioneer_BadExpiry(); - error Auctioneer_InvalidParams(); - error Auctioneer_NotAuthorized(); - error Auctioneer_NewMarketsNotAllowed(); - error Auctioneer_OraclePriceZero(); - error Auctioneer_UnsupportedToken(); - /* ========== EVENTS ========== */ event MarketCreated(uint256 indexed id, address indexed payoutToken, address indexed quoteToken, uint48 vesting); @@ -66,10 +52,6 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { /// @notice New address to designate as market owner. They must accept ownership to transfer permissions. mapping(uint256 => address) public newOwners; - /// @notice Whether or not the auctioneer allows new markets to be created - /// @dev Changing to false will sunset the auctioneer after all active markets end - bool public allowNewMarkets; - // Minimum time parameter values. Can be updated by admin. /// @notice Minimum deposit interval for a market uint48 public minDepositInterval; @@ -80,29 +62,14 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { /// @notice Whether or not the market creator is authorized to use a callback address mapping(address => bool) public callbackAuthorized; - // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. - uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50; - uint48 internal constant ONE_HUNDRED_PERCENT = 1e5; // one percent equals 1000. - - // BondAggregator contract with utility functions - IBondAggregator internal immutable _aggregator; - - // BondTeller contract that handles interactions with users and issues tokens - IBondTeller internal immutable _teller; - constructor( IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) Auth(guardian_, authority_) { - _aggregator = aggregator_; - _teller = teller_; - + IAuthority authority_ + ) BondBaseOracleAuctioneer(teller_, aggregator_, guardian_, authority_) { minDepositInterval = 1 minutes; minMarketDuration = 10 minutes; - - allowNewMarkets = true; } /* ========== MARKET FUNCTIONS ========== */ @@ -111,7 +78,7 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { function createMarket(bytes calldata params_) external payable virtual returns (uint256); /// @notice core market creation logic, see IBondOFDA.MarketParams documentation - function _createMarket(MarketParams memory params_) internal returns (uint256) { + function _createMarket(MarketParams memory params_) internal whenNotPaused returns (uint256) { // Upfront permission and timing checks { // Check that the auctioneer is allowing new markets to be created @@ -168,7 +135,7 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { // Ensure capacity is equal to the value sent if (params_.capacity != msg.value) revert Auctioneer_InvalidParams(); // Send tokens to teller as it operates over purchase - bool sent = payable(address(_teller)).send(msg.value); + (bool sent,) = payable(address(_teller)).call{value: msg.value}(""); require(sent, "Failed to send tokens to teller"); } else { // Check balance before and after to ensure full amount received, revert if not @@ -194,87 +161,15 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { return marketId; } - function _validateOracle( - uint256 id_, - IBondOracle oracle_, - ERC20 quoteToken_, - ERC20 payoutToken_, - uint48 fixedDiscount_ - ) internal returns (uint256, uint256, uint256) { - // Default value for native token - uint8 payoutTokenDecimals = 18; - uint8 quoteTokenDecimals = 18; - - // Ensure token decimals are in bounds - // If token is native no need to check decimals - if (address(payoutToken_) != address(0)) { - payoutTokenDecimals = payoutToken_.decimals(); - if (payoutTokenDecimals < 6 || payoutTokenDecimals > 18) revert Auctioneer_InvalidParams(); - } - if (address(quoteToken_) != address(0)) { - quoteTokenDecimals = quoteToken_.decimals(); - if (quoteTokenDecimals < 6 || quoteTokenDecimals > 18) revert Auctioneer_InvalidParams(); - } - - // Check that oracle is valid. It should: - // 1. Be a contract - if (address(oracle_) == address(0) || address(oracle_).code.length == 0) revert Auctioneer_InvalidParams(); - - // 2. Allow registering markets - oracle_.registerMarket(id_, quoteToken_, payoutToken_); - - // 3. Return a valid price for the quote token : payout token pair - uint256 currentPrice = oracle_.currentPrice(id_); - if (currentPrice == 0) revert Auctioneer_OraclePriceZero(); - - // 4. Return a valid decimal value for the quote token : payout token pair price - uint8 oracleDecimals = oracle_.decimals(id_); - if (oracleDecimals < 6 || oracleDecimals > 18) revert Auctioneer_InvalidParams(); - - // Calculate scaling values for market: - // 1. We need a value to convert between the oracle decimals to the bond market decimals - // 2. We need the bond scaling value to convert between quote and payout tokens using the market price - - // Get the price decimals for the current oracle price - // Oracle price is in quote tokens per payout token - // E.g. if quote token is $10 and payout token is $2000, - // then the oracle price is 200 quote tokens per payout token. - // If the oracle has 18 decimals, then it would return 200 * 10^18. - // In this case, the price decimals would be 2 since 200 = 2 * 10^2. - int8 priceDecimals = _getPriceDecimals( - currentPrice.mulDivUp(uint256(ONE_HUNDRED_PERCENT - fixedDiscount_), uint256(ONE_HUNDRED_PERCENT)), - oracleDecimals - ); - // Check price decimals in reasonable range - // These bounds are quite large and it is unlikely any combination of tokens - // will have a price difference larger than 10^24 in either direction. - // Check that oracle decimals are large enough to avoid precision loss from negative price decimals - if (int8(oracleDecimals) <= -priceDecimals || priceDecimals > 24) revert Auctioneer_InvalidParams(); - - // Calculate the oracle price conversion factor - // oraclePriceFactor = int8(oracleDecimals) + priceDecimals; - // bondPriceFactor = 36 - priceDecimals / 2 + priceDecimals; - // oracleConversion = 10^(bondPriceFactor - oraclePriceFactor); - uint256 oracleConversion = 10 ** uint8(36 - priceDecimals / 2 - int8(oracleDecimals)); - - // Unit to scale calculation for this market by to ensure reasonable values - // for price, debt, and control variable without under/overflows. - // - // scaleAdjustment should be equal to (payoutDecimals - quoteDecimals) - ((payoutPriceDecimals - quotePriceDecimals) / 2) - // scale = 10^(36 + scaleAdjustment); - uint256 scale = 10 ** uint8(36 + int8(payoutTokenDecimals) - int8(quoteTokenDecimals) - priceDecimals / 2); - - return (currentPrice * oracleConversion, oracleConversion, scale); - } /// @inheritdoc IBondAuctioneer - function pushOwnership(uint256 id_, address newOwner_) external override { + function pushOwnership(uint256 id_, address newOwner_) external override onlyMarketOwner(id_) whenNotPaused { if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner(); newOwners[id_] = newOwner_; } /// @inheritdoc IBondAuctioneer - function pullOwnership(uint256 id_) external override { + function pullOwnership(uint256 id_) external override whenNotPaused { if (msg.sender != newOwners[id_]) revert Auctioneer_NotAuthorized(); markets[id_].owner = newOwners[id_]; } @@ -300,20 +195,18 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { } // Unused, but required by interface - function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override {} + function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override onlyMarketOwner(id_) {} // Unused, but required by interface - function setDefaults(uint32[6] memory defaults_) external override {} + function setDefaults(uint32[6] memory defaults_) external override requiresAuth {} /// @inheritdoc IBondAuctioneer - function setAllowNewMarkets(bool status_) external override requiresAuth { - // Restricted to authorized addresses - allowNewMarkets = status_; + function setAllowNewMarkets(bool status_) external override(IBondAuctioneer, BondBaseAuctioneer) requiresAuth { + _setAllowNewMarkets(status_); } /// @inheritdoc IBondAuctioneer - function closeMarket(uint256 id_) external override { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); + function closeMarket(uint256 id_) external override onlyTeller whenNotPaused { // If market closed early, set conclusion to current timestamp if (terms[id_].conclusion > uint48(block.timestamp)) { @@ -332,9 +225,8 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { uint256 id_, uint256 amount_, uint256 minAmountOut_ - ) external override returns (uint256 payout) { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); - + ) external override onlyTeller whenNotPaused returns (uint256 payout) { + BondMarket storage market = markets[id_]; BondTerms memory term = terms[id_]; @@ -380,35 +272,23 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { market.sold += payout; } - /* ========== INTERNAL VIEW FUNCTIONS ========== */ - - /// @notice Helper function to calculate number of price decimals based on the value returned from the price feed. - /// @param price_ The price to calculate the number of decimals for - /// @return The number of decimals - function _getPriceDecimals(uint256 price_, uint8 feedDecimals_) internal pure returns (int8) { - int8 decimals; - while (price_ >= 10) { - price_ = price_ / 10; - decimals++; - } - - // Subtract the stated decimals from the calculated decimals to get the relative price decimals. - // Required to do it this way vs. normalizing at the beginning since price decimals can be negative. - return decimals - int8(feedDecimals_); - } - /* ========== EXTERNAL VIEW FUNCTIONS ========== */ /// @inheritdoc IBondAuctioneer function getMarketInfoForPurchase( uint256 id_ - ) external view returns (address owner, ERC20 payoutToken, ERC20 quoteToken, uint48 vesting, uint256 maxPayout_) { + ) + external + view + override + returns (address owner, ERC20 payoutToken, ERC20 quoteToken, uint48 vesting, uint256 maxPayout_) + { BondMarket memory market = markets[id_]; return (market.owner, market.payoutToken, market.quoteToken, terms[id_].vesting, maxPayout(id_)); } /// @inheritdoc IBondAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { + function marketPrice(uint256 id_) public view override (IBondAuctioneer, IBondOFDA) returns (uint256) { // Get the current price from the oracle BondTerms memory term = terms[id_]; uint256 oraclePrice = term.oracle.currentPrice(id_); @@ -498,16 +378,6 @@ abstract contract BondBaseOFDA is IBondOFDA, Auth { return markets[id_].owner; } - /// @inheritdoc IBondAuctioneer - function getTeller() external view override returns (IBondTeller) { - return _teller; - } - - /// @inheritdoc IBondAuctioneer - function getAggregator() external view override returns (IBondAggregator) { - return _aggregator; - } - /// @inheritdoc IBondAuctioneer function currentCapacity(uint256 id_) external view override returns (uint256) { return markets[id_].capacity; diff --git a/src/bases/BondBaseOSDA.sol b/src/bases/BondBaseOSDA.sol index 5a1b773..d7d8435 100644 --- a/src/bases/BondBaseOSDA.sol +++ b/src/bases/BondBaseOSDA.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {ReentrancyGuard} from "solmate/src/utils/ReentrancyGuard.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; - -import {IBondOSDA, IBondAuctioneer} from "../interfaces/IBondOSDA.sol"; -import {IBondOracle} from "../interfaces/IBondOracle.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {FullMath} from "../lib/FullMath.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {IBondOSDA} from "../interfaces/IBondOSDA.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IBondOracle} from "../interfaces/IBondOracle.sol"; +import {BondBaseOracleAuctioneer, BondBaseAuctioneer} from "./BondBaseOracleAuctioneer.sol"; -import {TransferHelper} from "../lib/TransferHelper.sol"; -import {FullMath} from "../lib/FullMath.sol"; /// @title Bond Oracle-based Sequential Dutch Auctioneer (OSDA) /// @notice Bond Oracle-based Sequential Dutch Auctioneer Base Contract @@ -32,29 +32,21 @@ import {FullMath} from "../lib/FullMath.sol"; /// an Aggregator to register new markets. /// /// @author Oighty -abstract contract BondBaseOSDA is IBondOSDA, Auth { - using TransferHelper for ERC20; +abstract contract BondBaseOSDA is IBondOSDA, BondBaseOracleAuctioneer { + using SafeERC20 for ERC20; using FullMath for uint256; - /* ========== ERRORS ========== */ - - error Auctioneer_OnlyMarketOwner(); error Auctioneer_InitialPriceLessThanMin(); - error Auctioneer_MarketNotActive(); - error Auctioneer_MaxPayoutExceeded(); - error Auctioneer_AmountLessThanMinimum(); - error Auctioneer_NotEnoughCapacity(); - error Auctioneer_InvalidCallback(); - error Auctioneer_BadExpiry(); - error Auctioneer_InvalidParams(); - error Auctioneer_NotAuthorized(); - error Auctioneer_NewMarketsNotAllowed(); - error Auctioneer_OraclePriceZero(); - error Auctioneer_UnsupportedToken(); + /* ========== EVENTS ========== */ - event MarketCreated(uint256 indexed id, address indexed payoutToken, address indexed quoteToken, uint48 vesting); + event MarketCreated( + uint256 indexed id, + address indexed payoutToken, + address indexed quoteToken, + uint48 vesting + ); event MarketClosed(uint256 indexed id); event Tuned(uint256 indexed id, uint256 oldControlVariable, uint256 newControlVariable); @@ -69,13 +61,6 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { /// @notice New address to designate as market owner. They must accept ownership to transfer permissions. mapping(uint256 => address) public newOwners; - /// @notice Whether or not the market creator is authorized to use a callback address - mapping(address => bool) public callbackAuthorized; - - /// @notice Whether or not the auctioneer allows new markets to be created - /// @dev Changing to false will sunset the auctioneer after all active markets end - bool public allowNewMarkets; - // Minimum time parameter values. Can be updated by admin. /// @notice Minimum deposit interval for a market uint48 public minDepositInterval; @@ -83,29 +68,17 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { /// @notice Minimum duration for a market uint48 public minMarketDuration; - // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. - uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50; - uint48 internal constant ONE_HUNDRED_PERCENT = 100e3; // one percent equals 1000. - - // BondAggregator contract with utility functions - IBondAggregator internal immutable _aggregator; - - // BondTeller contract that handles interactions with users and issues tokens - IBondTeller internal immutable _teller; + /// @notice Whether or not the market creator is authorized to use a callback address + mapping(address => bool) public callbackAuthorized; constructor( IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) Auth(guardian_, authority_) { - _aggregator = aggregator_; - _teller = teller_; - + IAuthority authority_ + ) BondBaseOracleAuctioneer( teller_, aggregator_,guardian_, authority_){ minDepositInterval = 1 minutes; minMarketDuration = 10 minutes; - - allowNewMarkets = true; } /* ========== MARKET FUNCTIONS ========== */ @@ -114,7 +87,7 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { function createMarket(bytes calldata params_) external payable virtual returns (uint256); /// @notice core market creation logic, see IBondOSDA.MarketParams documentation - function _createMarket(MarketParams memory params_) internal returns (uint256) { + function _createMarket(MarketParams memory params_) internal whenNotPaused returns (uint256) { // Upfront permission and timing checks { // Check that the auctioneer is allowing new markets to be created @@ -171,7 +144,7 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { // Ensure capacity is equal to the value sent if (params_.capacity != msg.value) revert Auctioneer_InvalidParams(); // Send tokens to teller as it operates over purchase - bool sent = payable(address(_teller)).send(msg.value); + (bool sent,) = payable(address(_teller)).call{value: msg.value}(""); require(sent, "Failed to send tokens to teller"); } else { // Check balance before and after to ensure full amount received, revert if not @@ -203,89 +176,15 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { return marketId; } - function _validateOracle( - uint256 id_, - IBondOracle oracle_, - ERC20 quoteToken_, - ERC20 payoutToken_, - uint48 baseDiscount_ - ) internal returns (uint256, uint256, uint256) { - // Default value for native token - uint8 payoutTokenDecimals = 18; - uint8 quoteTokenDecimals = 18; - - // Ensure token decimals are in bounds - // If token is native no need to check decimals - if (address(payoutToken_) != address(0)) { - payoutTokenDecimals = payoutToken_.decimals(); - if (payoutTokenDecimals < 6 || payoutTokenDecimals > 18) revert Auctioneer_InvalidParams(); - } - if (address(quoteToken_) != address(0)) { - quoteTokenDecimals = quoteToken_.decimals(); - if (quoteTokenDecimals < 6 || quoteTokenDecimals > 18) revert Auctioneer_InvalidParams(); - } - - // Check that oracle is valid. It should: - // 1. Be a contract - if (address(oracle_) == address(0) || address(oracle_).code.length == 0) revert Auctioneer_InvalidParams(); - - // 2. Allow registering markets - oracle_.registerMarket(id_, quoteToken_, payoutToken_); - - // 3. Return a valid price for the quote token : payout token pair - uint256 currentPrice = oracle_.currentPrice(id_); - if (currentPrice == 0) revert Auctioneer_OraclePriceZero(); - - // 4. Return a valid decimal value for the quote token : payout token pair price - uint8 oracleDecimals = oracle_.decimals(id_); - if (oracleDecimals < 6 || oracleDecimals > 18) revert Auctioneer_InvalidParams(); - - // Calculate scaling values for market: - // 1. We need a value to convert between the oracle decimals to the bond market decimals - // 2. We need the bond scaling value to convert between quote and payout tokens using the market price - - // Get the price decimals for the current oracle price - // Oracle price is in quote tokens per payout token - // E.g. if quote token is $10 and payout token is $2000, - // then the oracle price is 200 quote tokens per payout token. - // If the oracle has 18 decimals, then it would return 200 * 10^18. - // In this case, the price decimals would be 2 since 200 = 2 * 10^2. - // We apply the base discount to the oracle price before calculating - // since this will be the initial equilibrium price of the market. - int8 priceDecimals = _getPriceDecimals( - currentPrice.mulDivUp(uint256(ONE_HUNDRED_PERCENT - baseDiscount_), uint256(ONE_HUNDRED_PERCENT)), - oracleDecimals - ); - // Check price decimals in reasonable range - // These bounds are quite large and it is unlikely any combination of tokens - // will have a price difference larger than 10^24 in either direction. - // Check that oracle decimals are large enough to avoid precision loss from negative price decimals - if (int8(oracleDecimals) <= -priceDecimals || priceDecimals > 24) revert Auctioneer_InvalidParams(); - - // Calculate the oracle price conversion factor - // oraclePriceFactor = int8(oracleDecimals) + priceDecimals; - // bondPriceFactor = 36 - priceDecimals / 2 + priceDecimals; - // oracleConversion = 10^(bondPriceFactor - oraclePriceFactor); - uint256 oracleConversion = 10 ** uint8(36 - priceDecimals / 2 - int8(oracleDecimals)); - - // Unit to scale calculation for this market by to ensure reasonable values - // for price, debt, and control variable without under/overflows. - // - // scaleAdjustment should be equal to (payoutDecimals - quoteDecimals) - ((payoutPriceDecimals - quotePriceDecimals) / 2) - // scale = 10^(36 + scaleAdjustment); - uint256 scale = 10 ** uint8(36 + int8(payoutTokenDecimals) - int8(quoteTokenDecimals) - priceDecimals / 2); - - return (currentPrice * oracleConversion, oracleConversion, scale); - } /// @inheritdoc IBondAuctioneer - function pushOwnership(uint256 id_, address newOwner_) external override { + function pushOwnership(uint256 id_, address newOwner_) external override onlyMarketOwner(id_) whenNotPaused { if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner(); newOwners[id_] = newOwner_; } /// @inheritdoc IBondAuctioneer - function pullOwnership(uint256 id_) external override { + function pullOwnership(uint256 id_) external override whenNotPaused { if (msg.sender != newOwners[id_]) revert Auctioneer_NotAuthorized(); markets[id_].owner = newOwners[id_]; } @@ -311,21 +210,19 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { } // Unused, but required by interface - function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override {} + function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override onlyMarketOwner(id_) {} // Unused, but required by interface - function setDefaults(uint32[6] memory defaults_) external override {} + function setDefaults(uint32[6] memory defaults_) external override requiresAuth {} /// @inheritdoc IBondAuctioneer - function setAllowNewMarkets(bool status_) external override requiresAuth { - /// Restricted to authorized addresses, initially restricted to guardian - allowNewMarkets = status_; + function setAllowNewMarkets(bool status_) external override(IBondAuctioneer, BondBaseAuctioneer) requiresAuth { + _setAllowNewMarkets(status_); } /// @inheritdoc IBondAuctioneer - function closeMarket(uint256 id_) external override { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); - + function closeMarket(uint256 id_) external override onlyTeller whenNotPaused { + // If market closed early, set conclusion to current timestamp if (terms[id_].conclusion > uint48(block.timestamp)) { terms[id_].conclusion = uint48(block.timestamp); @@ -343,9 +240,8 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { uint256 id_, uint256 amount_, uint256 minAmountOut_ - ) external override returns (uint256 payout) { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); - + ) external override onlyTeller whenNotPaused returns (uint256 payout) { + BondMarket storage market = markets[id_]; BondTerms memory term = terms[id_]; @@ -455,23 +351,6 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { return price.mulDivUp(adjustment, ONE_HUNDRED_PERCENT); } - /* ========== INTERNAL VIEW FUNCTIONS ========== */ - - /// @notice Helper function to calculate number of price decimals based on the value returned from the price feed. - /// @param price_ The price to calculate the number of decimals for - /// @return The number of decimals - function _getPriceDecimals(uint256 price_, uint8 feedDecimals_) internal pure returns (int8) { - int8 decimals; - while (price_ >= 10) { - price_ = price_ / 10; - decimals++; - } - - // Subtract the stated decimals from the calculated decimals to get the relative price decimals. - // Required to do it this way vs. normalizing at the beginning since price decimals can be negative. - return decimals - int8(feedDecimals_); - } - /* ========== EXTERNAL VIEW FUNCTIONS ========== */ /// @inheritdoc IBondAuctioneer @@ -488,7 +367,7 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { } /// @inheritdoc IBondOSDA - function marketPrice(uint256 id_) public view override returns (uint256) { + function marketPrice(uint256 id_) public view override(IBondAuctioneer, IBondOSDA) returns (uint256) { uint256 price = _currentMarketPrice(id_); return (price > terms[id_].minPrice) ? price : terms[id_].minPrice; @@ -566,16 +445,6 @@ abstract contract BondBaseOSDA is IBondOSDA, Auth { return markets[id_].owner; } - /// @inheritdoc IBondAuctioneer - function getTeller() external view override returns (IBondTeller) { - return _teller; - } - - /// @inheritdoc IBondAuctioneer - function getAggregator() external view override returns (IBondAggregator) { - return _aggregator; - } - /// @inheritdoc IBondAuctioneer function currentCapacity(uint256 id_) external view override returns (uint256) { return markets[id_].capacity; diff --git a/src/bases/BondBaseOracleAuctioneer.sol b/src/bases/BondBaseOracleAuctioneer.sol new file mode 100644 index 0000000..cf4e21f --- /dev/null +++ b/src/bases/BondBaseOracleAuctioneer.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IBondOracle} from "../interfaces/IBondOracle.sol"; +import {IBondTeller} from "../interfaces/IBondTeller.sol"; +import {FullMath} from "../lib/FullMath.sol"; + +import {BondBaseAuctioneer} from "./BondBaseAuctioneer.sol"; +abstract contract BondBaseOracleAuctioneer is BondBaseAuctioneer { + using FullMath for uint256; + + error Auctioneer_OraclePriceZero(); + + /* ========== INTERNAL VIEW FUNCTIONS ========== */ + + /// @notice Helper function to calculate number of price decimals based on the value returned from the price feed. + /// @param price_ The price to calculate the number of decimals for + /// @return The number of decimals + function _getPriceDecimals(uint256 price_, uint8 feedDecimals_) internal pure returns (int8) { + int8 decimals; + while (price_ >= 10) { + price_ = price_ / 10; + decimals++; + } + + // Subtract the stated decimals from the calculated decimals to get the relative price decimals. + // Required to do it this way vs. normalizing at the beginning since price decimals can be negative. + return decimals - int8(feedDecimals_); + } + + constructor( + IBondTeller teller_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) BondBaseAuctioneer(teller_, aggregator_, guardian_, authority_) + {} + + /* ========== INTERNAL FUNCTIONS ========== */ + + function _validateOracle( + uint256 id_, + IBondOracle oracle_, + ERC20 quoteToken_, + ERC20 payoutToken_, + uint48 fixedDiscount_ + ) internal returns (uint256, uint256, uint256) { + // Default value for native token + uint8 payoutTokenDecimals = 18; + uint8 quoteTokenDecimals = 18; + + // Ensure token decimals are in bounds + // If token is native no need to check decimals + if (address(payoutToken_) != address(0)) { + payoutTokenDecimals = payoutToken_.decimals(); + if (payoutTokenDecimals < 6 || payoutTokenDecimals > 18) revert Auctioneer_InvalidParams(); + } + if (address(quoteToken_) != address(0)) { + quoteTokenDecimals = quoteToken_.decimals(); + if (quoteTokenDecimals < 6 || quoteTokenDecimals > 18) revert Auctioneer_InvalidParams(); + } + + // Check that oracle is valid. It should: + // 1. Be a contract + if (address(oracle_) == address(0) || address(oracle_).code.length == 0) revert Auctioneer_InvalidParams(); + + // 2. Allow registering markets + oracle_.registerMarket(id_, quoteToken_, payoutToken_); + + // 3. Return a valid price for the quote token : payout token pair + uint256 currentPrice = oracle_.currentPrice(id_); + if (currentPrice == 0) revert Auctioneer_OraclePriceZero(); + + // 4. Return a valid decimal value for the quote token : payout token pair price + uint8 oracleDecimals = oracle_.decimals(id_); + if (oracleDecimals < 6 || oracleDecimals > 18) revert Auctioneer_InvalidParams(); + + // Calculate scaling values for market: + // 1. We need a value to convert between the oracle decimals to the bond market decimals + // 2. We need the bond scaling value to convert between quote and payout tokens using the market price + + // Get the price decimals for the current oracle price + // Oracle price is in quote tokens per payout token + // E.g. if quote token is $10 and payout token is $2000, + // then the oracle price is 200 quote tokens per payout token. + // If the oracle has 18 decimals, then it would return 200 * 10^18. + // In this case, the price decimals would be 2 since 200 = 2 * 10^2. + int8 priceDecimals = _getPriceDecimals( + currentPrice.mulDivUp(uint256(ONE_HUNDRED_PERCENT - fixedDiscount_), uint256(ONE_HUNDRED_PERCENT)), + oracleDecimals + ); + // Check price decimals in reasonable range + // These bounds are quite large and it is unlikely any combination of tokens + // will have a price difference larger than 10^24 in either direction. + // Check that oracle decimals are large enough to avoid precision loss from negative price decimals + if (int8(oracleDecimals) <= -priceDecimals || priceDecimals > 24) revert Auctioneer_InvalidParams(); + + // Calculate the oracle price conversion factor + // oraclePriceFactor = int8(oracleDecimals) + priceDecimals; + // bondPriceFactor = 36 - priceDecimals / 2 + priceDecimals; + // oracleConversion = 10^(bondPriceFactor - oraclePriceFactor); + uint256 oracleConversion = 10 ** uint8(36 - priceDecimals / 2 - int8(oracleDecimals)); + + // Unit to scale calculation for this market by to ensure reasonable values + // for price, debt, and control variable without under/overflows. + // + // scaleAdjustment should be equal to (payoutDecimals - quoteDecimals) - ((payoutPriceDecimals - quotePriceDecimals) / 2) + // scale = 10^(36 + scaleAdjustment); + // TODO: Check if this is correct calculation (priceDecimals / 2 instead of (payoutPriceDecimals - quotePriceDecimals) / 2) + uint256 scale = 10 ** uint8(36 + int8(payoutTokenDecimals) - int8(quoteTokenDecimals) - priceDecimals / 2); + + return (currentPrice * oracleConversion, oracleConversion, scale); + } + +} \ No newline at end of file diff --git a/src/bases/BondBaseSDA.sol b/src/bases/BondBaseSDA.sol index 2b811f2..9d98d0e 100644 --- a/src/bases/BondBaseSDA.sol +++ b/src/bases/BondBaseSDA.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {ReentrancyGuard} from "solmate/src/utils/ReentrancyGuard.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; - -import {IBondSDA, IBondAuctioneer} from "../interfaces/IBondSDA.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {FullMath} from "../lib/FullMath.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {IBondSDA} from "../interfaces/IBondSDA.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {BondBaseAuctioneer} from "./BondBaseAuctioneer.sol"; -import {TransferHelper} from "../lib/TransferHelper.sol"; -import {FullMath} from "../lib/FullMath.sol"; /// @title Bond Sequential Dutch Auctioneer (SDA) v1.1 /// @notice Bond Sequential Dutch Auctioneer Base Contract @@ -30,24 +30,13 @@ import {FullMath} from "../lib/FullMath.sol"; /// tokens or sell a target amount of payout tokens over the duration of a market. /// /// @author Oighty, Zeus, Potted Meat, indigo -abstract contract BondBaseSDA is IBondSDA, Auth { - using TransferHelper for ERC20; +abstract contract BondBaseSDA is IBondSDA, BondBaseAuctioneer { + using SafeERC20 for ERC20; using FullMath for uint256; /* ========== ERRORS ========== */ - error Auctioneer_OnlyMarketOwner(); error Auctioneer_InitialPriceLessThanMin(); - error Auctioneer_MarketNotActive(); - error Auctioneer_MaxPayoutExceeded(); - error Auctioneer_AmountLessThanMinimum(); - error Auctioneer_NotEnoughCapacity(); - error Auctioneer_InvalidCallback(); - error Auctioneer_BadExpiry(); - error Auctioneer_InvalidParams(); - error Auctioneer_NotAuthorized(); - error Auctioneer_NewMarketsNotAllowed(); - error Auctioneer_UnsupportedToken(); /* ========== EVENTS ========== */ @@ -87,10 +76,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { /// @notice New address to designate as market owner. They must accept ownership to transfer permissions. mapping(uint256 => address) public newOwners; - /// @notice Whether or not the auctioneer allows new markets to be created - /// @dev Changing to false will sunset the auctioneer after all active markets end - bool public allowNewMarkets; - + /// @notice Whether or not the market creator is authorized to use a callback address mapping(address => bool) public callbackAuthorized; @@ -103,33 +89,18 @@ abstract contract BondBaseSDA is IBondSDA, Auth { uint32 public minMarketDuration; uint32 public minDebtBuffer; - // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry. - uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50; - uint48 internal constant FEE_DECIMALS = 1e5; // one percent equals 1000. - - // BondAggregator contract with utility functions - IBondAggregator internal immutable _aggregator; - - // BondTeller contract that handles interactions with users and issues tokens - IBondTeller internal immutable _teller; - constructor( IBondTeller teller_, IBondAggregator aggregator_, address guardian_, - Authority authority_ - ) Auth(guardian_, authority_) { - _aggregator = aggregator_; - _teller = teller_; - + IAuthority authority_ + ) BondBaseAuctioneer( teller_, aggregator_,guardian_, authority_) { defaultTuneInterval = 3 minutes; defaultTuneAdjustment = 1 minutes; minDebtDecayInterval = 5 minutes; minDepositInterval = 1 minutes; minMarketDuration = 10 minutes; minDebtBuffer = 10000; // 10% - - allowNewMarkets = true; } /* ========== MARKET FUNCTIONS ========== */ @@ -138,7 +109,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { function createMarket(bytes calldata params_) external payable virtual returns (uint256); /// @notice core market creation logic, see IBondSDA.MarketParams documentation - function _createMarket(MarketParams memory params_) internal returns (uint256) { + function _createMarket(MarketParams memory params_) internal whenNotPaused returns (uint256) { { // Check that the auctioneer is allowing new markets to be created if (!allowNewMarkets) revert Auctioneer_NewMarketsNotAllowed(); @@ -186,7 +157,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { // Ensure capacity is equal to the value sent if (params_.capacity != msg.value) revert Auctioneer_InvalidParams(); // Send tokens to teller as it operates over purchase - bool sent = payable(address(_teller)).send(msg.value); + (bool sent,) = payable(address(_teller)).call{value: msg.value}(""); require(sent, "Failed to send tokens to teller"); } else { // Check balance before and after to ensure full amount received, revert if not @@ -269,8 +240,8 @@ abstract contract BondBaseSDA is IBondSDA, Auth { // See IBondSDA.MarketParams for more information on determining a reasonable debt buffer. uint256 maxDebt; { - uint256 minDebtBuffer_ = _maxPayout.mulDiv(FEE_DECIMALS, targetDebt) > minDebtBuffer - ? _maxPayout.mulDiv(FEE_DECIMALS, targetDebt) + uint256 minDebtBuffer_ = _maxPayout.mulDiv(ONE_HUNDRED_PERCENT, targetDebt) > minDebtBuffer + ? _maxPayout.mulDiv(ONE_HUNDRED_PERCENT, targetDebt) : minDebtBuffer; maxDebt = targetDebt + @@ -308,7 +279,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { } /// @inheritdoc IBondAuctioneer - function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override { + function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override onlyMarketOwner(id_) whenNotPaused { // Check that the market is live if (!isLive(id_)) revert Auctioneer_InvalidParams(); @@ -325,9 +296,11 @@ abstract contract BondBaseSDA is IBondSDA, Auth { // Check that debtDecayInterval >= minDebtDecayInterval if (intervals_[2] < minDebtDecayInterval) revert Auctioneer_InvalidParams(); - // Check that sender is market owner BondMarket memory market = markets[id_]; - if (msg.sender != market.owner) revert Auctioneer_OnlyMarketOwner(); + + // @note handled in `onlyMarketOwner` + // Check that sender is market owner + // if (msg.sender != market.owner) revert Auctioneer_OnlyMarketOwner(); // Update intervals meta.tuneInterval = intervals_[0]; @@ -343,13 +316,12 @@ abstract contract BondBaseSDA is IBondSDA, Auth { } /// @inheritdoc IBondAuctioneer - function pushOwnership(uint256 id_, address newOwner_) external override { - if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner(); + function pushOwnership(uint256 id_, address newOwner_) external override onlyMarketOwner(id_) whenNotPaused { newOwners[id_] = newOwner_; } /// @inheritdoc IBondAuctioneer - function pullOwnership(uint256 id_) external override { + function pullOwnership(uint256 id_) external override whenNotPaused { if (msg.sender != newOwners[id_]) revert Auctioneer_NotAuthorized(); markets[id_].owner = newOwners[id_]; } @@ -390,15 +362,12 @@ abstract contract BondBaseSDA is IBondSDA, Auth { } /// @inheritdoc IBondAuctioneer - function setAllowNewMarkets(bool status_) external override requiresAuth { - // Restricted to authorized addresses - allowNewMarkets = status_; + function setAllowNewMarkets(bool status_) external override(IBondAuctioneer, BondBaseAuctioneer) requiresAuth { + _setAllowNewMarkets(status_); } /// @inheritdoc IBondAuctioneer - function closeMarket(uint256 id_) external override { - if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); - + function closeMarket(uint256 id_) external override whenNotPaused onlyTeller { // If market closed early, set conclusion to current timestamp if (terms[id_].conclusion > uint48(block.timestamp)) { terms[id_].conclusion = uint48(block.timestamp); @@ -416,7 +385,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { uint256 id_, uint256 amount_, uint256 minAmountOut_ - ) external override returns (uint256 payout) { + ) external override whenNotPaused onlyTeller returns (uint256 payout) { if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized(); BondMarket storage market = markets[id_]; @@ -469,7 +438,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { /// @notice Stops markets bonding by setting conclusion to current timestamp /// @dev This allow to stop the market and allow to withdraw left funds back to market owner - function _closeMarketPartialy(uint256 id_) internal { + function _closeMarketPartialy(uint256 id_) internal whenNotPaused { terms[id_].conclusion = uint48(block.timestamp); emit MarketPartialyClosed(id_); @@ -485,7 +454,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { uint256 id_, uint256 amount_, uint48 time_ - ) internal returns (uint256 marketPrice_, uint256 payout_) { + ) internal whenNotPaused returns (uint256 marketPrice_, uint256 payout_) { BondMarket memory market = markets[id_]; // Debt is a time-decayed sum of tokens spent in a market @@ -562,7 +531,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { /// @param id_ ID of market /// @param time_ Timestamp (saves gas when passed in) /// @param price_ Current price of the market - function _tune(uint256 id_, uint48 time_, uint256 price_) internal { + function _tune(uint256 id_, uint48 time_, uint256 price_) internal whenNotPaused { BondMetadata memory meta = metadata[id_]; BondMarket memory market = markets[id_]; BondTerms memory term = terms[id_]; @@ -673,7 +642,7 @@ abstract contract BondBaseSDA is IBondSDA, Auth { } /// @inheritdoc IBondSDA - function marketPrice(uint256 id_) public view override returns (uint256) { + function marketPrice(uint256 id_) public view override (IBondAuctioneer, IBondSDA) returns (uint256) { uint256 price = currentControlVariable(id_).mulDivUp(currentDebt(id_), markets[id_].scale); return (price > markets[id_].minPrice) ? price : markets[id_].minPrice; @@ -794,16 +763,6 @@ abstract contract BondBaseSDA is IBondSDA, Auth { return markets[id_].owner; } - /// @inheritdoc IBondAuctioneer - function getTeller() external view override returns (IBondTeller) { - return _teller; - } - - /// @inheritdoc IBondAuctioneer - function getAggregator() external view override returns (IBondAggregator) { - return _aggregator; - } - /// @inheritdoc IBondAuctioneer function currentCapacity(uint256 id_) external view override returns (uint256) { return markets[id_].capacity; diff --git a/src/bases/BondBaseTeller.sol b/src/bases/BondBaseTeller.sol index 138a144..7cc582c 100644 --- a/src/bases/BondBaseTeller.sol +++ b/src/bases/BondBaseTeller.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; - -import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {ReentrancyGuard} from "solmate/src/utils/ReentrancyGuard.sol"; -import {Auth, Authority} from "solmate/src/auth/Auth.sol"; +pragma solidity 0.8.20; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; - -import {TransferHelper} from "../lib/TransferHelper.sol"; +import {Auth} from "../lib/Auth.sol"; import {FullMath} from "../lib/FullMath.sol"; /// @title Bond Teller @@ -31,7 +30,7 @@ import {FullMath} from "../lib/FullMath.sol"; /// /// @author Oighty, Zeus, Potted Meat, indigo abstract contract BondBaseTeller is IBondTeller, Auth, ReentrancyGuard { - using TransferHelper for ERC20; + using SafeERC20 for ERC20; using FullMath for uint256; /* ========== ERRORS ========== */ @@ -72,7 +71,7 @@ abstract contract BondBaseTeller is IBondTeller, Auth, ReentrancyGuard { address beneficiary_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) Auth(guardian_, authority_) { beneficiary = beneficiary_; _aggregator = aggregator_; @@ -122,7 +121,7 @@ abstract contract BondBaseTeller is IBondTeller, Auth, ReentrancyGuard { // Return remaining capacity to owner if (capacity != 0) { if (address(payoutToken) == address(0)) { - bool sent = payable(owner).send(capacity); + (bool sent,) = payable(address(owner)).call{value: capacity}(""); require(sent, "Failed to send native tokens"); } else { payoutToken.safeTransfer(owner, capacity); @@ -196,8 +195,7 @@ abstract contract BondBaseTeller is IBondTeller, Auth, ReentrancyGuard { // otherwise, transfer quoteToken from msg.sender to this contract if (address(quoteToken) == address(0)) { if (msg.value != amount_) revert Teller_InvalidParams(); - - bool sent = payable(owner).send(amountLessFee); + (bool sent,) = payable(address(owner)).call{value: amountLessFee}(""); require(sent, "Failed to send native tokens"); } else { // Have to transfer to teller first since fee is in quote token @@ -236,7 +234,7 @@ abstract contract BondBaseTeller is IBondTeller, Auth, ReentrancyGuard { /// @param amount_ Amount of token to be paid function _handleFeePayout(address recipient_, ERC20 token_, uint256 amount_) internal { if (address(token_) == address(0)) { - bool sent = payable(recipient_).send(amount_); + (bool sent,) = payable(address(recipient_)).call{value: amount_}(""); require(sent, "Failed to send native tokens"); } else { // Check balance before and after to ensure full amount received, revert if not diff --git a/src/bases/BondBaseTellerUpgradeable.sol b/src/bases/BondBaseTellerUpgradeable.sol new file mode 100644 index 0000000..4143810 --- /dev/null +++ b/src/bases/BondBaseTellerUpgradeable.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {AuthUpgradeable} from "../lib/AuthUpgradeable.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {IBondTeller} from "../interfaces/IBondTeller.sol"; +import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {FullMath} from "../lib/FullMath.sol"; + +/// @title Bond Teller +/// @notice Bond Teller Base Contract +/// @dev Bond Protocol is a permissionless system to create Olympus-style bond markets +/// for any token pair. The markets do not require maintenance and will manage +/// bond prices based on activity. Bond issuers create BondMarkets that pay out +/// a Payout Token in exchange for deposited Quote Tokens. Users can purchase +/// future-dated Payout Tokens with Quote Tokens at the current market price and +/// receive Bond Tokens to represent their position while their bond vests. +/// Once the Bond Tokens vest, they can redeem it for the Quote Tokens. +/// +/// @dev The Teller contract handles all interactions with end users and manages tokens +/// issued to represent bond positions. Users purchase bonds by depositing Quote Tokens +/// and receive a Bond Token (token type is implementation-specific) that represents +/// their payout and the designated expiry. Once a bond vests, Investors can redeem their +/// Bond Tokens for the underlying Payout Token. A Teller requires one or more Auctioneer +/// contracts to be deployed to provide markets for users to purchase bonds from. +/// +/// @author Oighty, Zeus, Potted Meat, indigo +abstract contract BondBaseTellerUpgradeable is + IBondTeller, + AuthUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable +{ + using SafeERC20 for ERC20; + using FullMath for uint256; + + /* ========== ERRORS ========== */ + + error Teller_InvalidCallback(); + error Teller_TokenNotMatured(uint48 maturesOn); + error Teller_NotAuthorized(); + error Teller_TokenDoesNotExist(ERC20 underlying, uint48 expiry); + error Teller_UnsupportedToken(); + error Teller_InvalidParams(); + + /* ========== EVENTS ========== */ + event Bonded(uint256 indexed id, address indexed referrer, uint256 amount, uint256 payout); + + /* ========== STATE VARIABLES ========== */ + + /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). + /// @dev There are some situations where the fees may round down to zero if quantity of baseToken + /// is < 1e5 wei (can happen with big price differences on small decimal tokens). This is purely + /// a theoretical edge case, as the bond amount would not be practical. + mapping(address => uint48) public referrerFees; + + /// @notice Fee paid to protocol in basis points (3 decimal places). + uint48 public protocolFee; + + /// @notice 'Create' function fee discount in basis points (3 decimal places). Amount standard fee is reduced by for partners who just want to use the 'create' function to issue bond tokens. + uint48 public createFeeDiscount; + + uint48 public constant FEE_DECIMALS = 1e5; // one percent equals 1000. + + // Address the protocol receives fees at + address public beneficiary; + + // BondAggregator contract with utility functions + IBondAggregator internal _aggregator; + + uint256[30] private __gap; + + /* ========== INITIALIZER ========== */ + + function __BondBaseTeller_init( + address beneficiary_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) internal onlyInitializing { + __ReentrancyGuard_init(); + // Initialize Auth + __AuthUpgradeable_init(guardian_, authority_); + beneficiary = beneficiary_; + _aggregator = aggregator_; + protocolFee = 0; + createFeeDiscount = 0; + } + + /* ================================ */ + + function pause() public requiresAuth { + _pause(); + } + + function unpause() public requiresAuth { + _unpause(); + } + + + /// @inheritdoc IBondTeller + function setBeneficiary(address beneficiary_) external override requiresAuth { + beneficiary = beneficiary_; + } + + /// @inheritdoc IBondTeller + function setReferrerFee(uint48 fee_) external override nonReentrant { + if (fee_ > 5e3) revert Teller_InvalidParams(); + referrerFees[msg.sender] = fee_; + } + + /// @inheritdoc IBondTeller + function setProtocolFee(uint48 fee_) external override requiresAuth { + if (fee_ > 5e3) revert Teller_InvalidParams(); + protocolFee = fee_; + } + + /// @inheritdoc IBondTeller + function setCreateFeeDiscount(uint48 discount_) external override requiresAuth { + if (discount_ > protocolFee) revert Teller_InvalidParams(); + createFeeDiscount = discount_; + } + + /// @inheritdoc IBondTeller + function closeMarket(uint256 id_) external override nonReentrant { + IBondAuctioneer auctioneer = _aggregator.getAuctioneer(id_); + address owner; + ERC20 payoutToken; + (owner, payoutToken, , , ) = auctioneer.getMarketInfoForPurchase(id_); + uint256 capacity = auctioneer.currentCapacity(id_); + uint48 conclusion = auctioneer.getConclusion(id_); + + // Only owner can close market before conclusion + if (conclusion > block.timestamp) { + if (msg.sender != owner) revert Teller_NotAuthorized(); + } + + // Return remaining capacity to owner + if (capacity != 0) { + if (address(payoutToken) == address(0)) { + (bool sent,) = payable(address(owner)).call{value: capacity}(""); + require(sent, "Failed to send native tokens"); + } else { + payoutToken.safeTransfer(owner, capacity); + } + } + + auctioneer.closeMarket(id_); + } + + /// @inheritdoc IBondTeller + function getFee(address referrer_) external view returns (uint48) { + return protocolFee + referrerFees[referrer_]; + } + + /* ========== USER FUNCTIONS ========== */ + + function _purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_ + ) internal returns (uint256, uint48) { + ERC20 payoutToken; + ERC20 quoteToken; + uint48 vesting; + uint256 payout; + + // Calculate fees for purchase + // 1. Calculate referrer fee + // 2. Calculate protocol fee as the total expected fee amount minus the referrer fee + // to avoid issues with rounding from separate fee calculations + uint256 toReferrer = amount_.mulDiv(referrerFees[referrer_], FEE_DECIMALS); + uint256 toProtocol = amount_.mulDiv(protocolFee + referrerFees[referrer_], FEE_DECIMALS) - toReferrer; + + { + IBondAuctioneer auctioneer = _aggregator.getAuctioneer(id_); + address owner; + (owner, payoutToken, quoteToken, vesting, ) = auctioneer.getMarketInfoForPurchase(id_); + + // Auctioneer handles bond pricing, capacity, and duration + uint256 amountLessFee = amount_ - toReferrer - toProtocol; + payout = auctioneer.purchaseBond(id_, amountLessFee, minAmountOut_); + } + + // Transfer quote tokens from sender and ensure enough payout tokens are available + _handleTransfers(id_, amount_, toReferrer + toProtocol); + + // Transfer fees to beneficiary and referrer + _handleFeePayout(referrer_, quoteToken, toReferrer); + _handleFeePayout(beneficiary, quoteToken, toProtocol); + + // Handle payout to user (either transfer tokens if instant swap or issue bond token) + uint48 expiry = _handlePayout(recipient_, payout, payoutToken, vesting); + + emit Bonded(id_, referrer_, amount_, payout); + + return (payout, expiry); + } + + /// @inheritdoc IBondTeller + function purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_ + ) external payable virtual nonReentrant returns (uint256, uint48) { + return _purchase(recipient_, referrer_, id_, amount_, minAmountOut_); + } + + /// @notice Handles transfer of funds from user + function _handleTransfers(uint256 id_, uint256 amount_, uint256 feePaid_) internal whenNotPaused { + // Get info from auctioneer + (address owner, , ERC20 quoteToken, , ) = _aggregator.getAuctioneer(id_).getMarketInfoForPurchase(id_); + + // Calculate amount net of fees + uint256 amountLessFee = amount_ - feePaid_; + + // if quoteToken is native token, ensure msg.value is equal to amount_ + // otherwise, transfer quoteToken from msg.sender to this contract + if (address(quoteToken) == address(0)) { + if (msg.value != amount_) revert Teller_InvalidParams(); + (bool sent,) = payable(address(owner)).call{value: amountLessFee}(""); + require(sent, "Failed to send native tokens"); + } else { + // Have to transfer to teller first since fee is in quote token + // Check balance before and after to ensure full amount received, revert if not + // Handles edge cases like fee-on-transfer tokens (which are not supported) + uint256 quoteBalance = quoteToken.balanceOf(address(this)); + quoteToken.safeTransferFrom(msg.sender, address(this), amount_); + if (quoteToken.balanceOf(address(this)) < quoteBalance + amount_) revert Teller_UnsupportedToken(); + + // Send quote token to callback (transferred in first to allow use during callback) + quoteToken.safeTransfer(owner, amountLessFee); + } + } + + receive() external payable {} // need to receive native token from auctioner + + /// @notice Handle payout to recipient + /// @dev Implementation-agnostic. Must be implemented in contracts that + /// extend this base since it is called by purchase. + /// @param recipient_ Address to receive payout + /// @param payout_ Amount of payoutToken to be paid + /// @param underlying_ Token to be paid out + /// @param vesting_ Time parameter for when the payout is available, could be a + /// timestamp or duration depending on the implementation + /// @return expiry Timestamp when the payout will vest + function _handlePayout( + address recipient_, + uint256 payout_, + ERC20 underlying_, + uint48 vesting_ + ) internal virtual returns (uint48 expiry); + + /// @notice Handle reward payout to recipient + /// @param recipient_ Address to receive payout + /// @param token_ Token to be paid out + /// @param amount_ Amount of token to be paid + function _handleFeePayout( + address recipient_, + ERC20 token_, + uint256 amount_ + ) internal { + if (address(token_) == address(0)) { + (bool sent,) = payable(address(recipient_)).call{value: amount_}(""); + require(sent, "Failed to send native tokens"); + } else { + // Check balance before and after to ensure full amount received, revert if not + // Handles edge cases like fee-on-transfer tokens (which are not supported) + uint256 tokenBalance = token_.balanceOf(address(recipient_)); + token_.safeTransfer(recipient_, amount_); + if (token_.balanceOf(address(recipient_)) < tokenBalance + amount_) revert Teller_UnsupportedToken(); + } + } + + /// @notice Derive name and symbol of token for market + /// @param underlying_ Underlying token to be paid out when the Bond Token vests + /// @param expiry_ Timestamp that the Bond Token vests at + /// @return name Bond token name, format is "Token YYYY-MM-DD" + /// @return symbol Bond token symbol, format is "TKN-YYYYMMDD" + function _getNameAndSymbol( + ERC20 underlying_, + uint256 expiry_ + ) internal view returns (string memory name, string memory symbol) { + // Convert a number of days into a human-readable date, courtesy of BokkyPooBah. + // Source: https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary/blob/master/contracts/BokkyPooBahsDateTimeLibrary.sol + + uint256 year; + uint256 month; + uint256 day; + { + int256 __days = int256(expiry_ / 1 days); + + int256 num1 = __days + 68569 + 2440588; // 2440588 = OFFSET19700101 + int256 num2 = (4 * num1) / 146097; + num1 = num1 - (146097 * num2 + 3) / 4; + int256 _year = (4000 * (num1 + 1)) / 1461001; + num1 = num1 - (1461 * _year) / 4 + 31; + int256 _month = (80 * num1) / 2447; + int256 _day = num1 - (2447 * _month) / 80; + num1 = _month / 11; + _month = _month + 2 - 12 * num1; + _year = 100 * (num2 - 49) + _year + num1; + + year = uint256(_year); + month = uint256(_month); + day = uint256(_day); + } + + string memory yearStr = _uint2str(year % 10000); + string memory monthStr = month < 10 ? string(abi.encodePacked("0", _uint2str(month))) : _uint2str(month); + string memory dayStr = day < 10 ? string(abi.encodePacked("0", _uint2str(day))) : _uint2str(day); + + string memory initialName = "amb"; + string memory initialSymbol = "AMB"; + if (address(underlying_) != address(0)) { + initialName = underlying_.name(); + initialSymbol = underlying_.symbol(); + } + + // Construct name/symbol strings. + name = string(abi.encodePacked(initialName, " ", yearStr, "-", monthStr, "-", dayStr)); + symbol = string(abi.encodePacked(initialSymbol, "-", yearStr, monthStr, dayStr)); + } + + // Some fancy math to convert a uint into a string, courtesy of Provable Things. + // Updated to work with solc 0.8.0. + // https://github.com/provable-things/ethereum-api/blob/master/provableAPI_0.6.sol + function _uint2str(uint256 _i) internal pure returns (string memory) { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } +} diff --git a/src/bases/BondTeller1155.sol b/src/bases/BondTeller1155.sol index c004c57..11f4423 100644 --- a/src/bases/BondTeller1155.sol +++ b/src/bases/BondTeller1155.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; - -import {BondBaseTeller, IBondAggregator, Authority} from "./BondBaseTeller.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {BondBaseTeller} from "./BondBaseTeller.sol"; +import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; import {IBondTeller1155} from "../interfaces/IBondTeller1155.sol"; - -import {TransferHelper} from "../lib/TransferHelper.sol"; import {FullMath} from "../lib/FullMath.sol"; import {ERC1155} from "../lib/ERC1155.sol"; @@ -27,7 +27,7 @@ import {ERC1155} from "../lib/ERC1155.sol"; /// /// @author Oighty, Zeus, Potted Meat, indigo abstract contract BondTeller1155 is BondBaseTeller, IBondTeller1155, ERC1155 { - using TransferHelper for ERC20; + using SafeERC20 for ERC20; using FullMath for uint256; /* ========== EVENTS ========== */ @@ -42,7 +42,7 @@ abstract contract BondTeller1155 is BondBaseTeller, IBondTeller1155, ERC1155 { address protocol_, IBondAggregator aggregator_, address guardian_, - Authority authority_ + IAuthority authority_ ) BondBaseTeller(protocol_, aggregator_, guardian_, authority_) {} /* ========== PURCHASE ========== */ @@ -123,7 +123,7 @@ abstract contract BondTeller1155 is BondBaseTeller, IBondTeller1155, ERC1155 { // If payout token is native, handle it differently if (address(meta.underlying) == address(0)) { - bool sent = payable(msg.sender).send(amount_); + (bool sent,) = payable(address(msg.sender)).call{value: amount_}(""); require(sent, "Failed to send native tokens"); } else { meta.underlying.safeTransfer(msg.sender, amount_); @@ -177,7 +177,7 @@ abstract contract BondTeller1155 is BondBaseTeller, IBondTeller1155, ERC1155 { } // Store token metadata - tokenMetadata[tokenId_] = TokenMetadata(true, underlying_, decimals, expiry, 0); + tokenMetadata[tokenId_] = TokenMetadata(true, tokenId_, underlying_, decimals, expiry, 0); emit ERC1155BondTokenCreated(tokenId_, underlying_, expiry); } diff --git a/src/bases/BondTeller1155Upgradeable.sol b/src/bases/BondTeller1155Upgradeable.sol new file mode 100644 index 0000000..17dc481 --- /dev/null +++ b/src/bases/BondTeller1155Upgradeable.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.20; + + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {FullMath} from "../lib/FullMath.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; +import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; +import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; +import {IBondTeller1155} from "../interfaces/IBondTeller1155.sol"; +import {TicketUpgradeable} from "../lib/TicketUpgradeable.sol"; +import {BondBaseTellerUpgradeable} from "./BondBaseTellerUpgradeable.sol"; + +/// @title Bond Fixed Term Teller +/// @notice Bond Fixed Term Teller Contract +/// @dev Bond Protocol is a permissionless system to create Olympus-style bond markets +/// for any token pair. The markets do not require maintenance and will manage +/// bond prices based on activity. Bond issuers create BondMarkets that pay out +/// a Payout Token in exchange for deposited Quote Tokens. Users can purchase +/// future-dated Payout Tokens with Quote Tokens at the current market price and +/// receive Bond Tokens to represent their position while their bond vests. +/// Once the Bond Tokens vest, they can redeem it for the Quote Tokens. +/// +/// @dev The Bond Fixed Term Teller is an implementation of the +/// Bond Base Teller contract specific to handling user bond transactions +/// and tokenizing bond markets where purchases vest in a fixed amount of time +/// (rounded to the minute) as ERC1155 tokens. +/// +/// @author Oighty, Zeus, Potted Meat, indigo +abstract contract BondTeller1155Upgradeable is + IBondTeller1155, + TicketUpgradeable, + BondBaseTellerUpgradeable, + UUPSUpgradeable +{ + using SafeERC20 for ERC20; + using FullMath for uint256; + + /* ========== EVENTS ========== */ + event ERC1155BondTokenCreated(uint256 tokenId, ERC20 indexed underlying, uint48 indexed expiry); + + /* ========== STATE VARIABLES ========== */ + + mapping(uint256 => TokenMetadata) public tokenMetadata; // metadata for bond tokens + mapping(uint256 => uint256) public tonkenIdToMarketId; // total supply of bond tokens + + function __BondTeller1155_init( + address protocol_, + IBondAggregator aggregator_, + address guardian_, + IAuthority authority_ + ) internal onlyInitializing { + __UUPSUpgradeable_init(); + __Ticket_init(); + __BondBaseTeller_init( + protocol_, + aggregator_, + guardian_, + authority_ + ); + + } + + + function _authorizeUpgrade(address newImplementation) + internal + override + requiresAuth + {} + + /* ========== PURCHASE ========== */ + + /// @notice Handle payout to recipient + /// @param recipient_ Address to receive payout + /// @param payout_ Amount of payoutToken to be paid + /// @param payoutToken_ Token to be paid out + /// @param vesting_ Amount of time to vest from current timestamp + /// @return expiry Timestamp when the payout will vest + function _handlePayout( + address recipient_, + uint256 payout_, + ERC20 payoutToken_, + uint48 vesting_ + ) internal virtual override returns (uint48 expiry); + + function purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_ + ) external payable virtual override nonReentrant returns (uint256, uint48) { + ERC20 payoutToken; + uint256 payout; + uint48 expiry; + { + IBondAuctioneer auctioneer = _aggregator.getAuctioneer(id_); + (, payoutToken,,,) = auctioneer.getMarketInfoForPurchase(id_); + } + (payout, expiry) = _purchase(recipient_, referrer_, id_, amount_, minAmountOut_); + uint tokenId = getTokenId(payoutToken, expiry); + tonkenIdToMarketId[tokenId] = id_; + return (payout, expiry); + } + + /* ========== DEPOSIT/MINT ========== */ + + /// @inheritdoc IBondTeller1155 + function create( + ERC20 underlying_, + uint48 expiry_, + uint256 amount_ + ) external override nonReentrant returns (uint256, uint256) { + // Expiry is rounded to the nearest minute at 0000 UTC (in seconds) since bond tokens + // are only unique to a minute, not a specific timestamp. + uint48 expiry = uint48(expiry_ / 1 minutes) * 1 minutes; + + // Revert if expiry is in the past + if (expiry < block.timestamp) revert Teller_InvalidParams(); + + uint256 tokenId = getTokenId(underlying_, expiry); + + // Revert if no token exists, must call deploy first + if (!tokenMetadata[tokenId].active) revert Teller_TokenDoesNotExist(underlying_, expiry); + + // Transfer in underlying + // Check that amount received is not less than amount expected + // Handles edge cases like fee-on-transfer tokens (which are not supported) + uint256 oldBalance = underlying_.balanceOf(address(this)); + underlying_.safeTransferFrom(msg.sender, address(this), amount_); + if (underlying_.balanceOf(address(this)) < oldBalance + amount_) revert Teller_UnsupportedToken(); + + // If fee is greater than the create discount, then calculate the fee and store it + // Otherwise, fee is zero. + if (protocolFee > createFeeDiscount) { + // Calculate fee amount + uint256 feeAmount = amount_.mulDiv(protocolFee - createFeeDiscount, FEE_DECIMALS); + _handleFeePayout(beneficiary, underlying_, feeAmount); + + // Mint new bond tokens + _mintToken(msg.sender, tokenId, amount_ - feeAmount); + + return (tokenId, amount_ - feeAmount); + } else { + // Mint new bond tokens + _mintToken(msg.sender, tokenId, amount_); + + return (tokenId, amount_); + } + } + + /* ========== REDEEM ========== */ + + function _redeem(uint256 tokenId_, uint256 amount_) internal whenNotPaused { + // Check that the tokenId is active + if (!tokenMetadata[tokenId_].active) revert Teller_InvalidParams(); + + // Cache token metadata + TokenMetadata memory meta = tokenMetadata[tokenId_]; + + // Check that the token has matured + if (block.timestamp < meta.expiry) revert Teller_TokenNotMatured(meta.expiry); + + // Burn bond token and transfer underlying to sender + _burnToken(msg.sender, tokenId_, amount_); + + // If payout token is native, handle it differently + if (address(meta.underlying) == address(0)) { + (bool sent,) = payable(address(msg.sender)).call{value: amount_}(""); + require(sent, "Failed to send native tokens"); + } else { + meta.underlying.safeTransfer(msg.sender, amount_); + } + } + + /// @inheritdoc IBondTeller1155 + function redeem(uint256 tokenId_, uint256 amount_) public override nonReentrant { + _redeem(tokenId_, amount_); + } + + /// @inheritdoc IBondTeller1155 + function batchRedeem(uint256[] calldata tokenIds_, uint256[] calldata amounts_) external override nonReentrant { + uint256 len = tokenIds_.length; + if (len != amounts_.length) revert Teller_InvalidParams(); + for (uint256 i; i < len; ++i) { + _redeem(tokenIds_[i], amounts_[i]); + } + } + + /* ========== TOKENIZATION ========== */ + + /// @inheritdoc IBondTeller1155 + function deploy(ERC20 underlying_, uint48 expiry_) external override nonReentrant whenNotPaused returns (uint256) { + uint256 tokenId = getTokenId(underlying_, expiry_); + // Only creates token if it does not exist + if (!tokenMetadata[tokenId].active) { + _deploy(tokenId, underlying_, expiry_); + } + return tokenId; + } + + /// @notice "Deploy" a new ERC1155 bond token and stores its ID + /// @dev ERC1155 tokens used for fixed term bonds + /// @param tokenId_ Calculated ID of new bond token (from getTokenId) + /// @param underlying_ Underlying token to be paid out when the bond token vests + /// @param expiry_ Timestamp that the token will vest at, will be rounded to the nearest minute + function _deploy(uint256 tokenId_, ERC20 underlying_, uint48 expiry_) internal whenNotPaused { + // Expiry is rounded to the nearest minute at 0000 UTC (in seconds) since bond tokens + // are only unique to a minute, not a specific timestamp. + uint48 expiry = uint48(expiry_ / 1 minutes) * 1 minutes; + + // Revert if expiry is in the past + if (uint256(expiry) < block.timestamp) revert Teller_InvalidParams(); + + // If token is native than decimals equal to 18, + // otherwise get decimals from token contrtact + uint8 decimals = 18; + if (address(underlying_) != address(0)) { + decimals = uint8(underlying_.decimals()); + } + + // Store token metadata + tokenMetadata[tokenId_] = TokenMetadata(true, tokenId_, underlying_, decimals, expiry, 0); + + emit ERC1155BondTokenCreated(tokenId_, underlying_, expiry); + } + + /// @notice Mint bond token and update supply + /// @param to_ Address to mint tokens to + /// @param tokenId_ ID of bond token to mint + /// @param amount_ Amount of bond tokens to mint + function _mintToken(address to_, uint256 tokenId_, uint256 amount_) internal whenNotPaused { + tokenMetadata[tokenId_].supply += amount_; + _mint(to_, tokenId_, amount_, bytes("")); + } + + /// @notice Burn bond token and update supply + /// @param from_ Address to burn tokens from + /// @param tokenId_ ID of bond token to burn + /// @param amount_ Amount of bond token to burn + function _burnToken(address from_, uint256 tokenId_, uint256 amount_) internal whenNotPaused { + tokenMetadata[tokenId_].supply -= amount_; + _burn(from_, tokenId_, amount_); + } + + /* ========== TOKEN NAMING ========== */ + + /// @inheritdoc IBondTeller1155 + function getTokenId(ERC20 underlying_, uint48 expiry_) public pure override returns (uint256) { + // Expiry is divided by 1 minute (in seconds) since bond tokens are only unique + // to a minute, not a specific timestamp. + uint256 tokenId = uint256(keccak256(abi.encodePacked(underlying_, expiry_ / uint48(1 minutes)))); + return tokenId; + } + + /// @inheritdoc IBondTeller1155 + function getTokenNameAndSymbol(uint256 tokenId_) external view override returns (string memory, string memory) { + TokenMetadata memory meta = tokenMetadata[tokenId_]; + (string memory name, string memory symbol) = _getNameAndSymbol(meta.underlying, meta.expiry); + return (name, symbol); + } +} diff --git a/src/interfaces/IAuthority.sol b/src/interfaces/IAuthority.sol new file mode 100644 index 0000000..7462257 --- /dev/null +++ b/src/interfaces/IAuthority.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; +/// @notice A generic interface for a contract which provides authorization data to an Auth instance. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +interface IAuthority { + function canCall( + address user, + address target, + bytes4 functionSig + ) external view returns (bool); +} \ No newline at end of file diff --git a/src/interfaces/IBondAggregator.sol b/src/interfaces/IBondAggregator.sol index 3c3e84d..0809516 100644 --- a/src/interfaces/IBondAggregator.sol +++ b/src/interfaces/IBondAggregator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; diff --git a/src/interfaces/IBondAuctioneer.sol b/src/interfaces/IBondAuctioneer.sol index dcd5432..72a2393 100644 --- a/src/interfaces/IBondAuctioneer.sol +++ b/src/interfaces/IBondAuctioneer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondTeller} from "../interfaces/IBondTeller.sol"; import {IBondAggregator} from "../interfaces/IBondAggregator.sol"; diff --git a/src/interfaces/IBondFPA.sol b/src/interfaces/IBondFPA.sol index 595307d..b894c1f 100644 --- a/src/interfaces/IBondFPA.sol +++ b/src/interfaces/IBondFPA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; interface IBondFPA is IBondAuctioneer { diff --git a/src/interfaces/IBondOFDA.sol b/src/interfaces/IBondOFDA.sol index 8d89515..52a528c 100644 --- a/src/interfaces/IBondOFDA.sol +++ b/src/interfaces/IBondOFDA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; import {IBondOracle} from "../interfaces/IBondOracle.sol"; diff --git a/src/interfaces/IBondOSDA.sol b/src/interfaces/IBondOSDA.sol index 2ef635a..88cde0d 100644 --- a/src/interfaces/IBondOSDA.sol +++ b/src/interfaces/IBondOSDA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; import {IBondOracle} from "../interfaces/IBondOracle.sol"; diff --git a/src/interfaces/IBondOracle.sol b/src/interfaces/IBondOracle.sol index cea6cfc..c1ac13a 100644 --- a/src/interfaces/IBondOracle.sol +++ b/src/interfaces/IBondOracle.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; interface IBondOracle { /// @notice Register a new bond market on the oracle diff --git a/src/interfaces/IBondSDA.sol b/src/interfaces/IBondSDA.sol index 918ac99..044310c 100644 --- a/src/interfaces/IBondSDA.sol +++ b/src/interfaces/IBondSDA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol"; interface IBondSDA is IBondAuctioneer { diff --git a/src/interfaces/IBondTeller.sol b/src/interfaces/IBondTeller.sol index 91a8fa8..1042402 100644 --- a/src/interfaces/IBondTeller.sol +++ b/src/interfaces/IBondTeller.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; interface IBondTeller { /// @notice Exchange quote tokens for a bond in a specified market diff --git a/src/interfaces/IBondTeller1155.sol b/src/interfaces/IBondTeller1155.sol index 69cc8b2..0a1fbd5 100644 --- a/src/interfaces/IBondTeller1155.sol +++ b/src/interfaces/IBondTeller1155.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; interface IBondTeller1155 { // Info for bond token struct TokenMetadata { bool active; + uint256 tokenId; ERC20 underlying; uint8 decimals; uint48 expiry; diff --git a/src/lib/Auth.sol b/src/lib/Auth.sol new file mode 100644 index 0000000..6d67045 --- /dev/null +++ b/src/lib/Auth.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {IAuthority} from "../interfaces/IAuthority.sol"; + +/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +abstract contract Auth { + event OwnerUpdated(address indexed user, address indexed newOwner); + + event AuthorityUpdated(address indexed user, IAuthority indexed newAuthority); + + address public owner; + + IAuthority public authority; + + constructor(address _owner, IAuthority _authority) { + owner = _owner; + authority = _authority; + + emit OwnerUpdated(msg.sender, _owner); + emit AuthorityUpdated(msg.sender, _authority); + } + + modifier requiresAuth() virtual { + require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); + + _; + } + + function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { + IAuthority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. + + // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be + // aware that this makes protected functions uncallable even to the owner if the authority is out of order. + return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner; + } + + function setAuthority(IAuthority newAuthority) public virtual { + // We check if the caller is the owner first because we want to ensure they can + // always swap out the authority even if it's reverting or using up a lot of gas. + require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig)); + + authority = newAuthority; + + emit AuthorityUpdated(msg.sender, newAuthority); + } + + function setOwner(address newOwner) public virtual requiresAuth { + owner = newOwner; + + emit OwnerUpdated(msg.sender, newOwner); + } +} diff --git a/src/lib/AuthUpgradeable.sol b/src/lib/AuthUpgradeable.sol new file mode 100644 index 0000000..51e69a9 --- /dev/null +++ b/src/lib/AuthUpgradeable.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IAuthority} from "../interfaces/IAuthority.sol"; + +/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. +/// @author inc4 (https://github.com/ambrosus/bond-contracts/blob/main/src/lib/AuthUpgradeable.sol) +/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) +abstract contract AuthUpgradeable is Initializable { + event OwnerUpdated(address indexed user, address indexed newOwner); + + event AuthorityUpdated(address indexed user, IAuthority indexed newAuthority); + + address public owner; + + IAuthority public authority; + + function __AuthUpgradeable_init(address _owner, IAuthority _authority) internal onlyInitializing { + owner = _owner; + authority = _authority; + + emit OwnerUpdated(msg.sender, _owner); + emit AuthorityUpdated(msg.sender, _authority); + } + + modifier requiresAuth() virtual { + require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); + _; + } + + function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { + IAuthority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. + + // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be + // aware that this makes protected functions uncallable even to the owner if the authority is out of order. + return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner; + } + + function setAuthority(IAuthority newAuthority) public virtual { + // We check if the caller is the owner first because we want to ensure they can + // always swap out the authority even if it's reverting or using up a lot of gas. + require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig)); + + authority = newAuthority; + + emit AuthorityUpdated(msg.sender, newAuthority); + } + + function setOwner(address newOwner) public virtual requiresAuth { + owner = newOwner; + + emit OwnerUpdated(msg.sender, newOwner); + } +} diff --git a/src/lib/CloneERC20.sol b/src/lib/CloneERC20.sol deleted file mode 100644 index 2244c18..0000000 --- a/src/lib/CloneERC20.sol +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.0; - -import {Clone} from "clones-with-immutable-args/Clone.sol"; - -/// @notice Modern and gas efficient ERC20 implementation. -/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) -/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) -/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. -abstract contract CloneERC20 is Clone { - /*/////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event Transfer(address indexed from, address indexed to, uint256 amount); - - event Approval(address indexed owner, address indexed spender, uint256 amount); - - /*/////////////////////////////////////////////////////////////// - ERC20 STORAGE - //////////////////////////////////////////////////////////////*/ - - uint256 public totalSupply; - - mapping(address => uint256) public balanceOf; - - mapping(address => mapping(address => uint256)) public allowance; - - /*/////////////////////////////////////////////////////////////// - METADATA - //////////////////////////////////////////////////////////////*/ - - function name() external pure returns (string memory) { - return string(abi.encodePacked(_getArgUint256(0))); - } - - function symbol() external pure returns (string memory) { - return string(abi.encodePacked(_getArgUint256(0x20))); - } - - function decimals() external pure returns (uint8) { - return _getArgUint8(0x40); - } - - /*/////////////////////////////////////////////////////////////// - ERC20 LOGIC - //////////////////////////////////////////////////////////////*/ - - function approve(address spender, uint256 amount) public virtual returns (bool) { - allowance[msg.sender][spender] = amount; - - emit Approval(msg.sender, spender, amount); - - return true; - } - - function increaseAllowance(address spender, uint256 amount) public virtual returns (bool) { - allowance[msg.sender][spender] += amount; - - emit Approval(msg.sender, spender, allowance[msg.sender][spender]); - - return true; - } - - function decreaseAllowance(address spender, uint256 amount) public virtual returns (bool) { - allowance[msg.sender][spender] -= amount; - - emit Approval(msg.sender, spender, allowance[msg.sender][spender]); - - return true; - } - - function transfer(address to, uint256 amount) public virtual returns (bool) { - balanceOf[msg.sender] -= amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } - - emit Transfer(msg.sender, to, amount); - - return true; - } - - function transferFrom( - address from, - address to, - uint256 amount - ) public virtual returns (bool) { - uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. - - if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; - - balanceOf[from] -= amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } - - emit Transfer(from, to, amount); - - return true; - } - - /*/////////////////////////////////////////////////////////////// - INTERNAL LOGIC - //////////////////////////////////////////////////////////////*/ - - function _mint(address to, uint256 amount) internal virtual { - totalSupply += amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } - - emit Transfer(address(0), to, amount); - } - - function _burn(address from, uint256 amount) internal virtual { - balanceOf[from] -= amount; - - // Cannot underflow because a user's balance - // will never be larger than the total supply. - unchecked { - totalSupply -= amount; - } - - emit Transfer(from, address(0), amount); - } - - function _getImmutableVariablesOffset() internal pure returns (uint256 offset) { - assembly { - offset := sub(calldatasize(), add(shr(240, calldataload(sub(calldatasize(), 2))), 2)) - } - } -} diff --git a/src/lib/TicketUpgradeable.sol b/src/lib/TicketUpgradeable.sol new file mode 100644 index 0000000..403914a --- /dev/null +++ b/src/lib/TicketUpgradeable.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^4.9.3 +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; + +abstract contract TicketUpgradeable is + ERC1155Upgradeable, + ERC1155PausableUpgradeable, + ERC1155BurnableUpgradeable, + ERC1155SupplyUpgradeable +{ + + function __Ticket_init() + internal onlyInitializing + { + __ERC1155_init(""); + __ERC1155Pausable_init(); + __ERC1155Burnable_init(); + __ERC1155Supply_init(); + } + + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) + internal + override ( + ERC1155Upgradeable, + ERC1155PausableUpgradeable, + ERC1155SupplyUpgradeable + ) + { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + } +} diff --git a/src/lib/TransferHelper.sol b/src/lib/TransferHelper.sol deleted file mode 100644 index 96bb0d6..0000000 --- a/src/lib/TransferHelper.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.0; - -import {ERC20} from "solmate/src/tokens/ERC20.sol"; - -/// @notice Safe ERC20 and ETH transfer library that safely handles missing return values. -/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/TransferHelper.sol) -/// @author Taken from Solmate. -library TransferHelper { - function safeTransferFrom( - ERC20 token, - address from, - address to, - uint256 amount - ) internal { - (bool success, bytes memory data) = address(token).call( - abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount) - ); - - require( - success && - (data.length == 0 || abi.decode(data, (bool))) && - address(token).code.length > 0, - "TRANSFER_FROM_FAILED" - ); - } - - function safeTransfer( - ERC20 token, - address to, - uint256 amount - ) internal { - (bool success, bytes memory data) = address(token).call( - abi.encodeWithSelector(ERC20.transfer.selector, to, amount) - ); - - require( - success && - (data.length == 0 || abi.decode(data, (bool))) && - address(token).code.length > 0, - "TRANSFER_FAILED" - ); - } - - function safeApprove( - ERC20 token, - address to, - uint256 amount - ) internal { - (bool success, bytes memory data) = address(token).call( - abi.encodeWithSelector(ERC20.approve.selector, to, amount) - ); - - require(success && (data.length == 0 || abi.decode(data, (bool))), "APPROVE_FAILED"); - } - - // function safeTransferETH(address to, uint256 amount) internal { - // (bool success, ) = to.call{value: amount}(new bytes(0)); - - // require(success, "ETH_TRANSFER_FAILED"); - // } -} diff --git a/src/testHelpers/Token.sol b/src/testHelpers/Token.sol index 2b6ac81..daac6a7 100644 --- a/src/testHelpers/Token.sol +++ b/src/testHelpers/Token.sol @@ -1,13 +1,20 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity 0.8.15; +pragma solidity 0.8.20; -import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract ERC20TestToken is ERC20 { + uint8 public immutable DECIMALS; constructor(string memory name_, string memory symbol_, uint8 decimals_ - ) ERC20(name_, symbol_, decimals_) {} + ) ERC20(name_, symbol_) { + DECIMALS = decimals_; + } + + function decimals() public view override returns (uint8) { + return DECIMALS; + } function mint(address to, uint256 amount) external { _mint(to, amount);