Skip to content

Commit 6aee5a6

Browse files
authored
Spot pricer for automanager (#220)
* added cdr pricing strategy for spot * updated tasks and tests * added upper bound check for usdcoin price * fixed name
1 parent 1381036 commit 6aee5a6

File tree

13 files changed

+380
-42
lines changed

13 files changed

+380
-42
lines changed

.github/workflows/nightly.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
node-version: [20.x]
15-
os: [ubuntu-latest]
15+
os: [macos-latest]
1616

1717
steps:
1818
- name: Setup Repo

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: true
1515
matrix:
1616
node-version: [20.x]
17-
os: [ubuntu-latest]
17+
os: [macos-latest]
1818

1919
steps:
2020
- name: Setup Repo

spot-vaults/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ This repository is a collection of vault strategies leveraging the SPOT system.
55
The official mainnet addresses are:
66

77
- Bill Broker (SPOT-USDC): [0xA088Aef966CAD7fE0B38e28c2E07590127Ab4ccB](https://etherscan.io/address/0xA088Aef966CAD7fE0B38e28c2E07590127Ab4ccB)
8-
- SpotAppraiser: [0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c](https://etherscan.io/address/0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c)
98
- WethWamplManager: [0x6785fa26191eb531c54fd093931f395c4b01b583](https://etherscan.io/address/0x6785fa26191eb531c54fd093931f395c4b01b583)
109
- UsdcSpotManager: [0x780eB92040bf24cd9BF993505390e88E8ED59935](https://etherscan.io/address/0x780eB92040bf24cd9BF993505390e88E8ED59935)
10+
- SpotAppraiser: [0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c](https://etherscan.io/address/0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c) Used by the Bill Broker
11+
- SpotCDRPricer: [0x10B03340d27BC5470aa46Da007cD5BDE89201739](https://etherscan.io/address/0x10B03340d27BC5470aa46Da007cD5BDE89201739) Used by the Charm auto manager
1112

1213
The official testnet addresses are:
1314

spot-vaults/contracts/BillBroker.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC
1313
import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
1414

1515
import { IPerpetualTranche } from "@ampleforthorg/spot-contracts/contracts/_interfaces/IPerpetualTranche.sol";
16-
import { IBillBrokerPricingStrategy } from "./_interfaces/IBillBrokerPricingStrategy.sol";
16+
import { ISpotPricingStrategy } from "./_interfaces/ISpotPricingStrategy.sol";
1717
import { ReserveState, BillBrokerFees, Line, Range } from "./_interfaces/BillBrokerTypes.sol";
1818
import { UnacceptableSwap, UnreliablePrice, UnexpectedDecimals, InvalidPerc, InvalidARBound, SlippageTooHigh, UnauthorizedCall, UnexpectedARDelta } from "./_interfaces/BillBrokerErrors.sol";
1919

@@ -100,7 +100,7 @@ contract BillBroker is
100100
address public keeper;
101101

102102
/// @notice The pricing strategy.
103-
IBillBrokerPricingStrategy public pricingStrategy;
103+
ISpotPricingStrategy public pricingStrategy;
104104

105105
/// @notice All of the system fees.
106106
BillBrokerFees public fees;
@@ -141,7 +141,7 @@ contract BillBroker is
141141
string memory symbol,
142142
IERC20Upgradeable usd_,
143143
IPerpetualTranche perp_,
144-
IBillBrokerPricingStrategy pricingStrategy_
144+
ISpotPricingStrategy pricingStrategy_
145145
) public initializer {
146146
// initialize dependencies
147147
__ERC20_init(name, symbol);
@@ -188,7 +188,7 @@ contract BillBroker is
188188
/// @notice Updates the reference to the pricing strategy.
189189
/// @param pricingStrategy_ The address of the new pricing strategy.
190190
function updatePricingStrategy(
191-
IBillBrokerPricingStrategy pricingStrategy_
191+
ISpotPricingStrategy pricingStrategy_
192192
) public onlyOwner {
193193
if (pricingStrategy_.decimals() != DECIMALS) {
194194
revert UnexpectedDecimals();

spot-vaults/contracts/UsdcSpotManager.sol

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol";
77
import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
88
import { PositionKey } from "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol";
99

10-
import { IBillBrokerPricingStrategy } from "./_interfaces/IBillBrokerPricingStrategy.sol";
10+
import { ISpotPricingStrategy } from "./_interfaces/ISpotPricingStrategy.sol";
1111
import { IAlphaProVault } from "./_interfaces/external/IAlphaProVault.sol";
1212
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
1313

@@ -41,7 +41,7 @@ contract UsdcSpotManager {
4141
address public immutable SPOT;
4242

4343
/// @notice Pricing strategy to price the SPOT token.
44-
IBillBrokerPricingStrategy public spotAppraiser;
44+
ISpotPricingStrategy public pricingStrategy;
4545

4646
/// @notice The contract owner.
4747
address public owner;
@@ -66,18 +66,18 @@ contract UsdcSpotManager {
6666

6767
/// @notice Constructor initializes the contract with provided addresses.
6868
/// @param vault_ Address of the AlphaProVault contract.
69-
/// @param spotAppraiser_ Address of the spot appraiser.
70-
constructor(IAlphaProVault vault_, IBillBrokerPricingStrategy spotAppraiser_) {
69+
/// @param pricingStrategy_ Address of the spot appraiser.
70+
constructor(IAlphaProVault vault_, ISpotPricingStrategy pricingStrategy_) {
7171
owner = msg.sender;
7272

7373
VAULT = vault_;
7474
POOL = vault_.pool();
7575
USDC = vault_.token0();
7676
SPOT = vault_.token1();
7777

78-
spotAppraiser = spotAppraiser_;
78+
pricingStrategy = pricingStrategy_;
7979
// solhint-disable-next-line custom-errors
80-
require(spotAppraiser.decimals() == DECIMALS, "Invalid decimals");
80+
require(pricingStrategy.decimals() == DECIMALS, "Invalid decimals");
8181
}
8282

8383
//--------------------------------------------------------------------------
@@ -88,11 +88,11 @@ contract UsdcSpotManager {
8888
owner = owner_;
8989
}
9090

91-
/// @notice Updates the Spot Appraiser reference.
92-
function setSpotAppraiser(
93-
IBillBrokerPricingStrategy spotAppraiser_
91+
/// @notice Updates the Spot pricing strategy reference.
92+
function updatePricingStrategy(
93+
ISpotPricingStrategy pricingStrategy_
9494
) external onlyOwner {
95-
spotAppraiser = spotAppraiser_;
95+
pricingStrategy = pricingStrategy_;
9696
}
9797

9898
/// @notice Updates the vault's liquidity range parameters.
@@ -157,8 +157,9 @@ contract UsdcSpotManager {
157157
/// @return The computed deviation factor.
158158
function computeDeviationFactor() public returns (uint256, bool) {
159159
uint256 spotMarketPrice = getSpotUSDPrice();
160-
(uint256 spotTargetPrice, bool spotTargetPriceValid) = spotAppraiser.perpPrice();
161-
(, bool usdcPriceValid) = spotAppraiser.usdPrice();
160+
(uint256 spotTargetPrice, bool spotTargetPriceValid) = pricingStrategy
161+
.perpPrice();
162+
(, bool usdcPriceValid) = pricingStrategy.usdPrice();
162163
bool deviationValid = (spotTargetPriceValid && usdcPriceValid);
163164
uint256 deviation = spotTargetPrice > 0
164165
? FullMath.mulDiv(spotMarketPrice, ONE, spotTargetPrice)

spot-vaults/contracts/_interfaces/IBillBrokerPricingStrategy.sol renamed to spot-vaults/contracts/_interfaces/ISpotPricingStrategy.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// SPDX-License-Identifier: BUSL-1.1
22

33
/**
4-
* @title IBillBrokerPricingStrategy
4+
* @title ISpotPricingStrategy
55
*
66
* @notice Pricing strategy adapter for a BillBroker vault
77
* which accepts Perp and USDC tokens.
88
*
99
*/
1010
// solhint-disable-next-line compiler-version
11-
interface IBillBrokerPricingStrategy {
11+
interface ISpotPricingStrategy {
1212
/// @return Number of decimals representing the prices returned.
1313
function decimals() external pure returns (uint8);
1414

spot-vaults/contracts/_strategies/SpotAppraiser.sol

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IBondController } from "@ampleforthorg/spot-contracts/contracts/_interf
1010
import { IPerpetualTranche } from "@ampleforthorg/spot-contracts/contracts/_interfaces/IPerpetualTranche.sol";
1111
import { IChainlinkOracle } from "../_interfaces/external/IChainlinkOracle.sol";
1212
import { IAmpleforthOracle } from "../_interfaces/external/IAmpleforthOracle.sol";
13-
import { IBillBrokerPricingStrategy } from "../_interfaces/IBillBrokerPricingStrategy.sol";
13+
import { ISpotPricingStrategy } from "../_interfaces/ISpotPricingStrategy.sol";
1414
import { InvalidSeniorCDRBound } from "../_interfaces/BillBrokerErrors.sol";
1515

1616
/**
@@ -36,7 +36,7 @@ import { InvalidSeniorCDRBound } from "../_interfaces/BillBrokerErrors.sol";
3636
* And the MULTIPLIER is directly queried from the SPOT contract.
3737
*
3838
*/
39-
contract SpotAppraiser is Ownable, IBillBrokerPricingStrategy {
39+
contract SpotAppraiser is Ownable, ISpotPricingStrategy {
4040
//-------------------------------------------------------------------------
4141
// Libraries
4242
using Math for uint256;
@@ -50,6 +50,7 @@ contract SpotAppraiser is Ownable, IBillBrokerPricingStrategy {
5050
uint256 private constant SPOT_DR_ONE = (10 ** SPOT_DR_DECIMALS);
5151
uint256 public constant CL_ORACLE_DECIMALS = 8;
5252
uint256 public constant CL_ORACLE_STALENESS_THRESHOLD_SEC = 3600 * 48; // 2 days
53+
uint256 public constant USD_UPPER_BOUND = (101 * ONE) / 100; // 1.01$
5354
uint256 public constant USD_LOWER_BOUND = (99 * ONE) / 100; // 0.99$
5455
uint256 public constant AMPL_DUST_AMT = 25000 * (10 ** 9); // 25000 AMPL
5556

@@ -130,10 +131,10 @@ contract SpotAppraiser is Ownable, IBillBrokerPricingStrategy {
130131
/// @return v True if the price is valid and can be used by downstream consumers.
131132
function usdPrice() external view override returns (uint256, bool) {
132133
(uint256 p, bool v) = _getCLOracleData(USD_ORACLE, USD_ORACLE_DECIMALS);
133-
// If the market price of the USD coin fallen too much below 1$,
134+
// If the market price of the USD coin deviated too much from 1$,
134135
// it's an indication of some systemic issue with the USD token
135136
// and thus its price should be considered unreliable.
136-
return (ONE, (v && p > USD_LOWER_BOUND));
137+
return (ONE, (v && p < USD_UPPER_BOUND && p > USD_LOWER_BOUND));
137138
}
138139

139140
/// @return p The price of the spot token in dollar coins.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
5+
6+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import { IPerpetualTranche } from "@ampleforthorg/spot-contracts/contracts/_interfaces/IPerpetualTranche.sol";
8+
import { IChainlinkOracle } from "../_interfaces/external/IChainlinkOracle.sol";
9+
import { IAmpleforthOracle } from "../_interfaces/external/IAmpleforthOracle.sol";
10+
import { ISpotPricingStrategy } from "../_interfaces/ISpotPricingStrategy.sol";
11+
12+
/**
13+
* @title SpotCDRPricer
14+
*
15+
* @notice Pricing strategy adapter for SPOT.
16+
*
17+
* SPOT is a perpetual claim on AMPL senior tranches.
18+
* We price spot based on the redeemable value of it's collateral at maturity.
19+
* NOTE: SPOT's internal `getTVL` prices the collateral this way.
20+
*
21+
* SPOT_PRICE = (spot.getTVL() / spot.totalSupply()) * AMPL_TARGET
22+
*
23+
* We get the AMPL target price from Ampleforth's CPI oracle,
24+
* which is also used by the protocol to adjust AMPL supply through rebasing.
25+
*
26+
*/
27+
contract SpotCDRPricer is ISpotPricingStrategy {
28+
//-------------------------------------------------------------------------
29+
// Libraries
30+
using Math for uint256;
31+
32+
//-------------------------------------------------------------------------
33+
// Constants & Immutables
34+
35+
uint256 private constant DECIMALS = 18;
36+
uint256 private constant ONE = (10 ** DECIMALS);
37+
uint256 public constant CL_ORACLE_DECIMALS = 8;
38+
uint256 public constant CL_ORACLE_STALENESS_THRESHOLD_SEC = 3600 * 48; // 2 days
39+
uint256 public constant USD_UPPER_BOUND = (101 * ONE) / 100; // 1.01$
40+
uint256 public constant USD_LOWER_BOUND = (99 * ONE) / 100; // 0.99$
41+
42+
/// @notice Address of the SPOT (perpetual tranche) ERC-20 token contract.
43+
IPerpetualTranche public immutable SPOT;
44+
45+
/// @notice Address of the AMPL ERC-20 token contract.
46+
IERC20 public immutable AMPL;
47+
48+
/// @notice Address of the USD token market price oracle.
49+
IChainlinkOracle public immutable USD_ORACLE;
50+
51+
/// @notice Number of decimals representing the prices returned by the chainlink oracle.
52+
uint256 public immutable USD_ORACLE_DECIMALS;
53+
54+
/// @notice Address of the Ampleforth CPI oracle. (provides the inflation-adjusted target price for AMPL).
55+
IAmpleforthOracle public immutable AMPL_CPI_ORACLE;
56+
57+
/// @notice Number of decimals representing the prices returned by the ampleforth oracle.
58+
uint256 public immutable AMPL_CPI_ORACLE_DECIMALS;
59+
60+
//-----------------------------------------------------------------------------
61+
// Constructor
62+
63+
/// @notice Contract constructor.
64+
/// @param spot Address of the SPOT token.
65+
/// @param usdOracle Address of the USD token market price oracle token.
66+
/// @param cpiOracle Address of the Ampleforth CPI oracle.
67+
constructor(
68+
IPerpetualTranche spot,
69+
IChainlinkOracle usdOracle,
70+
IAmpleforthOracle cpiOracle
71+
) {
72+
SPOT = spot;
73+
AMPL = IERC20(address(spot.underlying()));
74+
75+
USD_ORACLE = usdOracle;
76+
USD_ORACLE_DECIMALS = usdOracle.decimals();
77+
78+
AMPL_CPI_ORACLE = cpiOracle;
79+
AMPL_CPI_ORACLE_DECIMALS = cpiOracle.DECIMALS();
80+
}
81+
82+
//--------------------------------------------------------------------------
83+
// External methods
84+
85+
/// @return p The price of the usd token in dollars.
86+
/// @return v True if the price is valid and can be used by downstream consumers.
87+
function usdPrice() external view override returns (uint256, bool) {
88+
(uint256 p, bool v) = _getCLOracleData(USD_ORACLE, USD_ORACLE_DECIMALS);
89+
// If the market price of the USD coin deviated too much from 1$,
90+
// it's an indication of some systemic issue with the USD token
91+
// and thus its price should be considered unreliable.
92+
return (ONE, (v && p < USD_UPPER_BOUND && p > USD_LOWER_BOUND));
93+
}
94+
95+
/// @return p The price of the spot token in dollar coins.
96+
/// @return v True if the price is valid and can be used by downstream consumers.
97+
function perpPrice() external override returns (uint256, bool) {
98+
// NOTE: Since {DECIMALS} == {AMPL_CPI_ORACLE_DECIMALS} == 18
99+
// we don't adjust the returned values.
100+
(uint256 targetPrice, bool targetPriceValid) = AMPL_CPI_ORACLE.getData();
101+
uint256 p = targetPrice.mulDiv(SPOT.getTVL(), SPOT.totalSupply());
102+
return (p, targetPriceValid);
103+
}
104+
105+
/// @return Number of decimals representing a price of 1.0 USD.
106+
function decimals() external pure override returns (uint8) {
107+
return uint8(DECIMALS);
108+
}
109+
110+
//-----------------------------------------------------------------------------
111+
// Private methods
112+
113+
/// @dev Fetches most recent report from the given chain link oracle contract.
114+
/// The data is considered invalid if the latest report is stale.
115+
function _getCLOracleData(
116+
IChainlinkOracle oracle,
117+
uint256 oracleDecimals
118+
) private view returns (uint256, bool) {
119+
(, int256 p, , uint256 updatedAt, ) = oracle.latestRoundData();
120+
uint256 price = uint256(p).mulDiv(ONE, 10 ** oracleDecimals);
121+
return (
122+
price,
123+
(block.timestamp - updatedAt) <= CL_ORACLE_STALENESS_THRESHOLD_SEC
124+
);
125+
}
126+
}

spot-vaults/tasks/deploy.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ task("deploy:SpotAppraiser")
3636
await sleep(30);
3737
await hre.run("verify:contract", {
3838
address: spotAppraiser.target,
39+
constructorArguments: [perp, usdOracle, cpiOracle],
40+
});
41+
} else {
42+
console.log("Skipping verification");
43+
}
44+
});
45+
46+
task("deploy:SpotCDRPricer")
47+
.addParam("perp", "the address of the perp token", undefined, types.string, false)
48+
.addParam("usdOracle", "the address of the usd oracle", undefined, types.string, false)
49+
.addParam("cpiOracle", "the address of the usd oracle", undefined, types.string, false)
50+
.addParam("verify", "flag to set false for local deployments", true, types.boolean)
51+
.setAction(async function (args: TaskArguments, hre) {
52+
const deployer = (await hre.ethers.getSigners())[0];
53+
console.log("Signer", await deployer.getAddress());
54+
55+
const { perp, usdOracle, cpiOracle } = args;
56+
57+
const SpotCDRPricer = await hre.ethers.getContractFactory("SpotCDRPricer");
58+
const spotCDRPricer = await SpotCDRPricer.deploy(perp, usdOracle, cpiOracle);
59+
console.log("spotCDRPricer", spotCDRPricer.target);
60+
61+
if (args.verify) {
62+
await sleep(30);
63+
await hre.run("verify:contract", {
64+
address: spotCDRPricer.target,
65+
constructorArguments: [perp, usdOracle, cpiOracle],
3966
});
4067
} else {
4168
console.log("Skipping verification");

spot-vaults/tasks/info.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,21 @@ task("info:BillBroker")
3131
const unitUsd = await billBroker.usdUnitAmt();
3232
const unitPerp = await billBroker.perpUnitAmt();
3333

34-
const spotAppraiser = await hre.ethers.getContractAt(
35-
"SpotAppraiser",
34+
const pricingStrategy = await hre.ethers.getContractAt(
35+
"pricingStrategy",
3636
await billBroker.pricingStrategy.staticCall(),
3737
);
38-
const appraiserDecimals = await spotAppraiser.decimals();
38+
const pricingStrategyDecimals = await pricingStrategy.decimals();
3939
console.log("---------------------------------------------------------------");
40-
console.log("SpotAppraiser:", spotAppraiser.target);
41-
console.log("owner:", await spotAppraiser.owner());
42-
const usdPriceCall = await spotAppraiser.usdPrice.staticCall();
43-
console.log("usdPrice:", pp(usdPriceCall[0], appraiserDecimals));
40+
console.log("pricingStrategy:", pricingStrategy.target);
41+
console.log("owner:", await pricingStrategy.owner());
42+
const usdPriceCall = await pricingStrategy.usdPrice.staticCall();
43+
console.log("usdPrice:", pp(usdPriceCall[0], pricingStrategyDecimals));
4444
console.log("usdPriceValid:", usdPriceCall[1]);
45-
const perpPriceCall = await spotAppraiser.perpPrice.staticCall();
46-
console.log("perpPrice:", pp(perpPriceCall[0], appraiserDecimals));
45+
const perpPriceCall = await pricingStrategy.perpPrice.staticCall();
46+
console.log("perpPrice:", pp(perpPriceCall[0], pricingStrategyDecimals));
4747
console.log("perpPriceValid:", perpPriceCall[1]);
48-
console.log("isSpotHealthy:", await spotAppraiser.isSPOTHealthy.staticCall());
48+
console.log("isSpotHealthy:", await pricingStrategy.isSPOTHealthy.staticCall());
4949
console.log("---------------------------------------------------------------");
5050
console.log("BillBroker:", billBroker.target);
5151
console.log("owner:", await billBroker.owner());
@@ -197,7 +197,6 @@ task("info:UsdcSpotManager")
197197
console.log("---------------------------------------------------------------");
198198
console.log("UsdcSpotManager:", manager.target);
199199
console.log("owner:", await manager.owner());
200-
console.log("spotAppraiser:", await manager.spotAppraiser());
201200

202201
console.log("---------------------------------------------------------------");
203202
const spotPrice = await manager.getSpotUSDPrice();

0 commit comments

Comments
 (0)