Skip to content

Commit

Permalink
Merge pull request #162 from yieldprotocol/feat/nft-oracles
Browse files Browse the repository at this point in the history
feat: nft ready oracles
  • Loading branch information
alcueca authored Apr 30, 2021
2 parents d2a9298 + b9e3d5e commit 6642d1a
Show file tree
Hide file tree
Showing 25 changed files with 455 additions and 448 deletions.
55 changes: 33 additions & 22 deletions contracts/Cauldron.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ contract Cauldron is AccessControl() {
using CastU256I256 for uint256;
using CastI128U128 for int128;

event AuctionIntervalSet(uint32 indexed auctionInterval);
event AssetAdded(bytes6 indexed assetId, address indexed asset);
event SeriesAdded(bytes6 indexed seriesId, bytes6 indexed baseId, address indexed fyToken);
event IlkAdded(bytes6 indexed seriesId, bytes6 indexed ilkId);
Expand All @@ -45,7 +46,7 @@ contract Cauldron is AccessControl() {
event VaultPoured(bytes12 indexed vaultId, bytes6 indexed seriesId, bytes6 indexed ilkId, int128 ink, int128 art);
event VaultStirred(bytes12 indexed from, bytes12 indexed to, uint128 ink, uint128 art);
event VaultRolled(bytes12 indexed vaultId, bytes6 indexed seriesId, uint128 art);
event VaultTimestamped(bytes12 indexed vaultId, uint256 indexed timestamp);
event VaultLocked(bytes12 indexed vaultId, uint256 indexed timestamp);

event SeriesMatured(bytes6 indexed seriesId, uint256 rateAtMaturity);

Expand All @@ -60,11 +61,12 @@ contract Cauldron is AccessControl() {
// ==== Protocol data ====
mapping (bytes6 => mapping(bytes6 => DataTypes.Debt)) public debt; // [baseId][ilkId] Max and sum of debt per underlying and collateral.
mapping (bytes6 => uint256) public ratesAtMaturity; // Borrowing rate at maturity for a mature series
uint32 public auctionInterval;// Time that vaults in liquidation are protected from being grabbed by a different engine.

// ==== User data ====
mapping (bytes12 => DataTypes.Vault) public vaults; // An user can own one or more Vaults, each one with a bytes12 identifier
mapping (bytes12 => DataTypes.Balances) public balances; // Both debt and assets
mapping (bytes12 => uint32) public timestamps; // If grater than zero, time that a vault was timestamped. Used for liquidation.
mapping (bytes12 => uint32) public auctions; // If grater than zero, time that a vault was timestamped. Used for liquidation.

// ==== Administration ====

Expand All @@ -84,8 +86,8 @@ contract Cauldron is AccessControl() {
external
auth
{
require (assets[baseId] != address(0), "Asset not found");
require (assets[ilkId] != address(0), "Asset not found");
require (assets[baseId] != address(0), "Base not found");
require (assets[ilkId] != address(0), "Ilk not found");
debt[baseId][ilkId].max = max;
emit MaxDebtSet(baseId, ilkId, max);
}
Expand All @@ -95,19 +97,28 @@ contract Cauldron is AccessControl() {
external
auth
{
require (assets[baseId] != address(0), "Asset not found");
require (assets[baseId] != address(0), "Base not found");
// TODO: The oracle should record the asset it refers to, and we should match it against assets[baseId]
rateOracles[baseId] = oracle;
emit RateOracleAdded(baseId, address(oracle));
}

/// @dev Set the interval for which vaults being auctioned can't be grabbed by another liquidation engine
function setAuctionInterval(uint32 auctionInterval_)
external
auth
{
auctionInterval = auctionInterval_;
emit AuctionIntervalSet(auctionInterval_);
}

/// @dev Set a spot oracle and its collateralization ratio. Can be reset.
function setSpotOracle(bytes6 baseId, bytes6 ilkId, IOracle oracle, uint32 ratio)
external
auth
{
require (assets[baseId] != address(0), "Asset not found");
require (assets[ilkId] != address(0), "Asset not found");
require (assets[baseId] != address(0), "Base not found");
require (assets[ilkId] != address(0), "Ilk not found");
// TODO: The oracle should record the assets it refers to, and we should match it against assets[baseId] and assets[ilkId]
spotOracles[baseId][ilkId] = DataTypes.SpotOracle({
oracle: oracle,
Expand All @@ -122,10 +133,10 @@ contract Cauldron is AccessControl() {
auth
{
require (seriesId != bytes6(0), "Series id is zero");
address asset = assets[baseId];
require (asset != address(0), "Asset not found");
address base = assets[baseId];
require (base != address(0), "Base not found");
require (fyToken != IFYToken(address(0)), "Series need a fyToken");
require (fyToken.asset() == asset, "Mismatched series and base");
require (fyToken.underlying() == base, "Mismatched series and base");
require (rateOracles[baseId] != IOracle(address(0)), "Rate oracle not found");
require (series[seriesId].fyToken == IFYToken(address(0)), "Id already used");
series[seriesId] = DataTypes.Series({
Expand Down Expand Up @@ -184,7 +195,7 @@ contract Cauldron is AccessControl() {
{
DataTypes.Balances memory balances_ = balances[vaultId];
require (balances_.art == 0 && balances_.ink == 0, "Only empty vaults");
delete timestamps[vaultId];
delete auctions[vaultId];
delete vaults[vaultId];
emit VaultDestroyed(vaultId);
}
Expand Down Expand Up @@ -329,22 +340,22 @@ contract Cauldron is AccessControl() {
return balances_;
}

/// @dev Give a non-timestamped vault to the caller, and timestamp it.
/// @dev Give a non-timestamped vault to another user, and timestamp it.
/// To be used for liquidation engines.
function grab(bytes12 vaultId)
function grab(bytes12 vaultId, address receiver)
external
auth
{
uint32 now_ = uint32(block.timestamp);
require (timestamps[vaultId] + 24*60*60 <= now_, "Timestamped"); // Grabbing a vault protects it for a day from being grabbed by another liquidator. All grabbed vaults will be suddenly released on the 7th of February 2106, at 06:28:16 GMT. I can live with that.
require (auctions[vaultId] + auctionInterval <= now_, "Vault under auction"); // Grabbing a vault protects it for a day from being grabbed by another liquidator. All grabbed vaults will be suddenly released on the 7th of February 2106, at 06:28:16 GMT. I can live with that.

(DataTypes.Vault memory vault_, DataTypes.Series memory series_, DataTypes.Balances memory balances_) = vaultData(vaultId, true);
require(_level(vault_, balances_, series_) < 0, "Not undercollateralized");

timestamps[vaultId] = now_;
_give(vaultId, msg.sender);
auctions[vaultId] = now_;
_give(vaultId, receiver);

emit VaultTimestamped(vaultId, now_);
emit VaultLocked(vaultId, now_);
}

/// @dev Reduce debt and collateral from a vault, ignoring collateralization checks.
Expand Down Expand Up @@ -409,7 +420,7 @@ contract Cauldron is AccessControl() {
internal
{
IOracle rateOracle = rateOracles[series_.baseId];
(uint256 rateAtMaturity,) = rateOracle.get();
(uint256 rateAtMaturity,) = rateOracle.get(series_.baseId, bytes32("rate"), 1e18);
ratesAtMaturity[seriesId] = rateAtMaturity;
emit SeriesMatured(seriesId, rateAtMaturity);
}
Expand All @@ -436,7 +447,7 @@ contract Cauldron is AccessControl() {
_mature(seriesId, series_);
} else {
IOracle rateOracle = rateOracles[series_.baseId];
(uint256 rate,) = rateOracle.get();
(uint256 rate,) = rateOracle.get(series_.baseId, bytes32("rate"), 1e18);
accrual_ = rate.wdiv(rateAtMaturity);
}
accrual_ = accrual_ >= 1e18 ? accrual_ : 1e18; // The accrual can't be below 1 (with 18 decimals)
Expand All @@ -452,14 +463,14 @@ contract Cauldron is AccessControl() {
returns (int256)
{
DataTypes.SpotOracle memory spotOracle_ = spotOracles[series_.baseId][vault_.ilkId];
(uint256 spot,) = spotOracle_.oracle.get();
uint256 ratio = uint256(spotOracle_.ratio) * 1e12; // Normalized to 18 decimals
(uint256 inkValue,) = spotOracle_.oracle.get(series_.baseId, vault_.ilkId, balances_.ink); // ink * spot

if (uint32(block.timestamp) >= series_.maturity) {
uint256 accrual_ = _accrual(vault_.seriesId, series_);
return uint256(balances_.ink).wmul(spot).i256() - uint256(balances_.art).wmul(accrual_).wmul(ratio).i256();
return inkValue.i256() - uint256(balances_.art).wmul(accrual_).wmul(ratio).i256();
}

return uint256(balances_.ink).wmul(spot).i256() - uint256(balances_.art).wmul(ratio).i256();
return inkValue.i256() - uint256(balances_.art).wmul(ratio).i256();
}
}
15 changes: 10 additions & 5 deletions contracts/FYToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,26 @@ contract FYToken is IFYToken, IERC3156FlashLender, AccessControl(), ERC20Permit
event Redeemed(address indexed from, address indexed to, uint256 amount, uint256 redeemed);
event OracleSet(address indexed oracle);

bytes32 constant CHI = "chi";

uint256 constant internal MAX_TIME_TO_MATURITY = 126144000; // seconds in four years
bytes32 constant internal FLASH_LOAN_RETURN = keccak256("ERC3156FlashBorrower.onFlashLoan");

IOracle public oracle; // Oracle for the savings rate.
IJoin public immutable join; // Source of redemption funds.
address public immutable override asset;
address public immutable override underlying;
bytes6 public immutable underlyingId; // Needed to access the oracle
uint256 public immutable override maturity;
uint256 public chiAtMaturity = type(uint256).max; // Spot price (exchange rate) between the base and an interest accruing token at maturity

constructor(
bytes6 underlyingId_,
IOracle oracle_, // Underlying vs its interest-bearing version
IJoin join_,
uint256 maturity_,
string memory name,
string memory symbol
) ERC20Permit(name, symbol, IERC20Metadata(address(IJoin(join_).asset())).decimals()) { // The join asset is this fyToken's base, from which we inherit the decimals
) ERC20Permit(name, symbol, IERC20Metadata(address(IJoin(join_).asset())).decimals()) { // The join asset is this fyToken's underlying, from which we inherit the decimals
uint256 now_ = block.timestamp;
require(
maturity_ > now_ &&
Expand All @@ -48,9 +52,10 @@ contract FYToken is IFYToken, IERC3156FlashLender, AccessControl(), ERC20Permit
"Invalid maturity"
);

underlyingId = underlyingId_;
join = join_;
maturity = maturity_;
asset = address(IJoin(join_).asset());
underlying = address(IJoin(join_).asset());
setOracle(oracle_);
}

Expand Down Expand Up @@ -94,7 +99,7 @@ contract FYToken is IFYToken, IERC3156FlashLender, AccessControl(), ERC20Permit
private
returns (uint256 _chiAtMaturity)
{
(_chiAtMaturity,) = oracle.get();
(_chiAtMaturity,) = oracle.get(underlyingId, CHI, 1e18);
chiAtMaturity = _chiAtMaturity;
emit SeriesMatured(_chiAtMaturity);
}
Expand All @@ -117,7 +122,7 @@ contract FYToken is IFYToken, IERC3156FlashLender, AccessControl(), ERC20Permit
if (chiAtMaturity == type(uint256).max) { // After maturity, but chi not yet recorded. Let's record it, and accrual is then 1.
_mature();
} else {
(uint256 chi,) = oracle.get();
(uint256 chi,) = oracle.get(underlyingId, CHI, 1e18);
accrual_ = chi.wdiv(chiAtMaturity);
}
accrual_ = accrual_ >= 1e18 ? accrual_ : 1e18; // The accrual can't be below 1 (with 18 decimals)
Expand Down
2 changes: 1 addition & 1 deletion contracts/Ladle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ contract Ladle is AccessControl() {
{
IFYToken fyToken = getSeries(seriesId).fyToken;
require (fyToken == pool.fyToken(), "Mismatched pool fyToken and series");
require (fyToken.asset() == address(pool.baseToken()), "Mismatched pool base and series");
require (fyToken.underlying() == address(pool.baseToken()), "Mismatched pool base and series");
pools[seriesId] = pool;
emit PoolAdded(seriesId, address(pool));
}
Expand Down
44 changes: 31 additions & 13 deletions contracts/Witch.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "@yield-protocol/utils-v2/contracts/access/AccessControl.sol";
import "@yield-protocol/vault-interfaces/ILadle.sol";
import "@yield-protocol/vault-interfaces/ICauldron.sol";
import "@yield-protocol/vault-interfaces/DataTypes.sol";
Expand All @@ -9,15 +11,19 @@ import "./math/WDivUp.sol";
import "./math/CastU256U128.sol";


contract Witch {
contract Witch is AccessControl() {
using WMul for uint256;
using WDiv for uint256;
using WDivUp for uint256;
using CastU256U128 for uint256;

event AuctionTimeSet(uint128 indexed auctionTime);
event InitialProportionSet(uint128 indexed initialProportion);
event Bought(address indexed buyer, bytes12 indexed vaultId, uint256 ink, uint256 art);

uint256 constant public AUCTION_TIME = 4 * 60 * 60; // Time that auctions take to go to minimal price and stay there.
uint128 public auctionTime = 4 * 60 * 60; // Time that auctions take to go to minimal price and stay there.
uint128 public initialProportion = 5e17; // Proportion of collateral that is sold at auction start.

ICauldron immutable public cauldron;
ILadle immutable public ladle;

Expand All @@ -26,31 +32,43 @@ contract Witch {
ladle = ladle_;
}

/// @dev Set the auction time to calculate liquidation prices
function setAuctionTime(uint128 auctionTime_) public auth {
auctionTime = auctionTime_;
emit AuctionTimeSet(auctionTime_);
}

/// @dev Set the proportion of the collateral that will be sold at auction start
function setInitialProportion(uint128 initialProportion_) public auth {
require (initialProportion_ <= 1e18, "Only at or under 100%");
initialProportion = initialProportion_;
emit InitialProportionSet(initialProportion_);
}

/// @dev Put an undercollateralized vault up for liquidation.
function grab(bytes12 vaultId) public {
cauldron.grab(vaultId);
cauldron.grab(vaultId, address(this));
}

/// @dev Buy an amount of collateral off a vault in liquidation, paying at most `max` underlying.
function buy(bytes12 vaultId, uint128 art, uint128 min) public {
DataTypes.Balances memory balances_ = cauldron.balances(vaultId);

require (balances_.art > 0, "Nothing to buy"); // Cheapest way of failing gracefully if given a non existing vault
uint256 elapsed = uint32(block.timestamp) - cauldron.timestamps(vaultId); // Auctions will malfunction on the 7th of February 2106, at 06:28:16 GMT, we should replace this contract before then.
uint256 elapsed = uint32(block.timestamp) - cauldron.auctions(vaultId); // Auctions will malfunction on the 7th of February 2106, at 06:28:16 GMT, we should replace this contract before then.
uint256 price;
{
// Price of a collateral unit, in underlying, at the present moment, for a given vault
//
// ink 1 min(auction, elapsed)
// price = 1 / (------- * (--- + -----------------------))
// art 2 2 * auction
// solhint-disable-next-line var-name-mixedcase
// ink min(auction, elapsed)
// price = 1 / (------- * (p + (1 - p) * -----------------------))
// art auction
(uint256 auctionTime_, uint256 initialProportion_) = (auctionTime, initialProportion);
uint256 term1 = uint256(balances_.ink).wdiv(balances_.art);
uint256 term2 = 1e18 / 2;
uint256 dividend3 = AUCTION_TIME < elapsed ? AUCTION_TIME : elapsed;
uint256 divisor3 = AUCTION_TIME * 2;
uint256 term3 = dividend3.wdiv(divisor3);
price = uint256(1e18).wdiv(term1.wmul(term2 + term3));
uint256 dividend2 = auctionTime_ < elapsed ? auctionTime_ : elapsed;
uint256 divisor2 = auctionTime_;
uint256 term2 = initialProportion_ + (1e18 - initialProportion_).wmul(dividend2.wdiv(divisor2));
price = uint256(1e18).wdiv(term1.wmul(term2));
}
uint256 ink = uint256(art).wdivup(price); // Calculate collateral to sell. Using divdrup stops rounding from leaving 1 stray wei in vaults.
require (ink >= min, "Not enough bought");
Expand Down
9 changes: 9 additions & 0 deletions contracts/math/CastBytes32Bytes6.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;


library CastBytes32Bytes6 {
function b6(bytes32 x) internal pure returns (bytes6 y){
require (bytes32(y = bytes6(x)) == x, "Cast overflow");
}
}
14 changes: 7 additions & 7 deletions contracts/mocks/OracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "@yield-protocol/vault-interfaces/IOracle.sol";
/// @dev An oracle that allows to set the spot price to anyone. It also allows to record spot values and return the accrual between a recorded and current spots.
contract OracleMock is IOracle {

address public immutable override source;
address public immutable source;

uint256 public spot;
uint256 public updated;
Expand All @@ -15,15 +15,15 @@ contract OracleMock is IOracle {
source = address(this);
}

/// @dev Return the spot price with 18 decimals.
function peek() external view virtual override returns (uint256, uint256) {
return (spot, updated);
/// @dev Return the value of the amount at the spot price.
function peek(bytes32, bytes32, uint256 amount) external view virtual override returns (uint256, uint256) {
return (spot * amount / 1e18, updated);
}

/// @dev Return the spot price with 18 decimals.
function get() external virtual override returns (uint256, uint256) {
/// @dev Return the value of the amount at the spot price.
function get(bytes32, bytes32, uint256 amount) external virtual override returns (uint256, uint256) {
updated = block.timestamp;
return (spot, updated = block.timestamp);
return (spot * amount / 1e18, updated = block.timestamp);
}

/// @dev Set the spot price with 18 decimals. Overriding contracts with different formats must convert from 18 decimals.
Expand Down
Loading

0 comments on commit 6642d1a

Please sign in to comment.