Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# OptionsToken

An options token representing the right to purchase the underlying token at an oracle-specified rate. It's similar to a call option but with a variable strike price that's always at a certain discount to the market price.
It also has no expiry date.
An options token representing the right to exercise any one of the whitelisted exercise contracts, allowing the user to receive different forms of discounted assets in return for the appropriate payment. The option does not expire. The options token receives user input and a specified exercise contract address, passing through to the exercise contract to execute the option. We fork https://github.com/timeless-fi/options-token, which is a simple implementation of an option for discounted tokens at an adjusted oracle rate. Here, we divorce the exercise functionality from the token contract, and allow an admin to whitelist and fund exercise contracts as the desired. We also implement more potential oracle types, and make several other minor changes.

We want to ensure there are no attacks on pricing in DiscountExercise, atomically or otherwise, in each oracle implementation. We want to ensure users will never pay more than maxPaymentAmount. When properly specified, this should ensure users experience no more deviation in price than they specify.

Given the nature of this token, it is fine for the admin to have some centralized permissions (admin can mint tokens, admin is the one who funds exercise contracts, etc). The team is responsible for refilling the exercise contracts. We limit the amount of funds we leave in an exercise contract at any given time to limit risk.

# Flow of an Option Token Exercise (Ex. Discount Exercise)

The user will always interact with the OptionsToken itself, and never with any exercise contract directly.

1. The user approves OptionsToken the amount of WETH they wish to spend
2. User calls exercise on the OptionsToken, specifying their desired exercise contract and encoding exercise parameters
3. OptionsToken validates the exercise contract, decodes the parameters for the exercise function on the exercise contract of choice, and calls said function. In the case of DiscountExercise, the params are maxPaymentAmount and deadline.
4. oTokens are burnt, WETH is sent to the treasury, and underlyingTokens, discounted by the multiplier, are sent to the user exercising
a. Can be priced using balancer, thena, univ3 twap oracles
b. Reverts above maxPaymentAmount or past deadline


## Installation

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ verbosity = 1
via_ir = true

[fmt]
line_length = 130
line_length = 150

# Extreme Fuzzing CI Profile :P
[profile.ci]
Expand Down
4 changes: 1 addition & 3 deletions src/OptionsToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ contract OptionsToken is IOptionsToken, ERC20Upgradeable, OwnableUpgradeable, UU
/// Events
/// -----------------------------------------------------------------------

event Exercise(
address indexed sender, address indexed recipient, uint256 amount, address data0, uint256 data1, uint256 data2
);
event Exercise(address indexed sender, address indexed recipient, uint256 amount, address data0, uint256 data1, uint256 data2);
event SetOracle(IOracle indexed newOracle);
event SetExerciseContract(address indexed _address, bool _isExercise);

Expand Down
5 changes: 1 addition & 4 deletions src/interfaces/IBalancerVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,7 @@ interface IVault {
*
* Emits a `Swap` event.
*/
function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline)
external
payable
returns (uint256);
function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline) external payable returns (uint256);

/**
* @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on
Expand Down
10 changes: 2 additions & 8 deletions src/interfaces/IThenaPair.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ interface IThenaPair {

function reserve1CumulativeLast() external view returns (uint256);

function currentCumulativePrices()
external
view
returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp);
function currentCumulativePrices() external view returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp);

function stable() external view returns (bool);

function observationLength() external view returns (uint256);

function observations(uint256)
external
view
returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative);
function observations(uint256) external view returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative);

function token0() external view returns (address);

Expand Down
15 changes: 2 additions & 13 deletions src/oracles/BalancerOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,7 @@ contract BalancerOracle is IOracle, Owned {
/// Constructor
/// -----------------------------------------------------------------------

constructor(
IBalancerTwapOracle balancerTwapOracle_,
address token,
address owner_,
uint56 secs_,
uint56 ago_,
uint128 minPrice_
) Owned(owner_) {
constructor(IBalancerTwapOracle balancerTwapOracle_, address token, address owner_, uint56 secs_, uint56 ago_, uint128 minPrice_) Owned(owner_) {
balancerTwapOracle = balancerTwapOracle_;

IVault vault = balancerTwapOracle.getVault();
Expand Down Expand Up @@ -118,11 +111,7 @@ contract BalancerOracle is IOracle, Owned {
// query Balancer oracle to get TWAP value
{
IBalancerTwapOracle.OracleAverageQuery[] memory queries = new IBalancerTwapOracle.OracleAverageQuery[](1);
queries[0] = IBalancerTwapOracle.OracleAverageQuery({
variable: IBalancerTwapOracle.Variable.PAIR_PRICE,
secs: secs_,
ago: ago_
});
queries[0] = IBalancerTwapOracle.OracleAverageQuery({variable: IBalancerTwapOracle.Variable.PAIR_PRICE, secs: secs_, ago: ago_});
price = balancerTwapOracle.getTimeWeightedAverage(queries)[0];
}

Expand Down
3 changes: 1 addition & 2 deletions src/oracles/ThenaOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ contract ThenaOracle is IOracle, Owned {
thenaPair.observations(observationLength - 1);
uint32 T = uint32(blockTimestampCurrent - blockTimestampLast);
if (T < secs_) {
(blockTimestampLast, reserve0CumulativeLast, reserve1CumulativeLast) =
thenaPair.observations(observationLength - 2);
(blockTimestampLast, reserve0CumulativeLast, reserve1CumulativeLast) = thenaPair.observations(observationLength - 2);
T = uint32(blockTimestampCurrent - blockTimestampLast);
}
uint112 reserve0 = safe112((reserve0CumulativeCurrent - reserve0CumulativeLast) / T);
Expand Down
12 changes: 3 additions & 9 deletions src/oracles/UniswapV3Oracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ contract UniswapV3Oracle is IOracle, Owned {
/// Constructor
/// -----------------------------------------------------------------------

constructor(IUniswapV3Pool uniswapPool_, address token, address owner_, uint32 secs_, uint32 ago_, uint128 minPrice_)
Owned(owner_)
{
constructor(IUniswapV3Pool uniswapPool_, address token, address owner_, uint32 secs_, uint32 ago_, uint128 minPrice_) Owned(owner_) {
uniswapPool = uniswapPool_;
isToken0 = token == uniswapPool_.token0();
secs = secs_;
Expand Down Expand Up @@ -113,14 +111,10 @@ contract UniswapV3Oracle is IOracle, Owned {
// Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself
if (sqrtRatioX96 <= type(uint128).max) {
uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
price = isToken0
? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192)
: FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192);
price = isToken0 ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192);
} else {
uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
price = isToken0
? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128)
: FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128);
price = isToken0 ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128);
}
}

Expand Down
13 changes: 4 additions & 9 deletions test/BalancerOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,8 @@ contract BalancerOracleTest is Test {

// weighted average of the first recorded oracle price and the current spot price
// weighted by the time since the last update
uint256 spotAverage = (
(price_1 * (_default.secs - skipTime)) + (getSpotPrice(address(_default.pair), _default.token) * skipTime)
) / _default.secs;
uint256 spotAverage =
((price_1 * (_default.secs - skipTime)) + (getSpotPrice(address(_default.pair), _default.token) * skipTime)) / _default.secs;

assertApproxEqRel(spotAverage, oracle.getPrice(), 0.01 ether, "price variance too large");
}
Expand All @@ -187,13 +186,9 @@ contract BalancerOracleTest is Test {
: (balances[0] * weights[1]).divWadDown(balances[1] * weights[0]);
}

function swap(address pool, address tokenIn, address tokenOut, uint256 amountIn, address sender)
internal
returns (uint256 amountOut)
{
function swap(address pool, address tokenIn, address tokenOut, uint256 amountIn, address sender) internal returns (uint256 amountOut) {
bytes32 poolId = IBalancerTwapOracle(pool).getPoolId();
IVault.SingleSwap memory singleSwap =
IVault.SingleSwap(poolId, IVault.SwapKind.GIVEN_IN, IAsset(tokenIn), IAsset(tokenOut), amountIn, "");
IVault.SingleSwap memory singleSwap = IVault.SingleSwap(poolId, IVault.SwapKind.GIVEN_IN, IAsset(tokenIn), IAsset(tokenOut), amountIn, "");

IVault.FundManagement memory funds = IVault.FundManagement(sender, false, payable(sender), false);

Expand Down
32 changes: 10 additions & 22 deletions test/OptionsToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
(uint256 paymentAmount,,,) = optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params));

// verify options tokens were transferred
Expand All @@ -123,12 +122,8 @@ contract OptionsTokenTest is Test {
assertEqDecimal(paymentToken.balanceOf(address(this)), 0, 18, "user still has payment tokens");
uint256 paymentFee1 = expectedPaymentAmount.mulDivDown(feeBPS_[0], 10000);
uint256 paymentFee2 = expectedPaymentAmount - paymentFee1;
assertEqDecimal(
paymentToken.balanceOf(feeRecipients_[0]), paymentFee1, 18, "fee recipient 1 didn't receive payment tokens"
);
assertEqDecimal(
paymentToken.balanceOf(feeRecipients_[1]), paymentFee2, 18, "fee recipient 2 didn't receive payment tokens"
);
assertEqDecimal(paymentToken.balanceOf(feeRecipients_[0]), paymentFee1, 18, "fee recipient 1 didn't receive payment tokens");
assertEqDecimal(paymentToken.balanceOf(feeRecipients_[1]), paymentFee2, 18, "fee recipient 2 didn't receive payment tokens");
assertEqDecimal(paymentAmount, expectedPaymentAmount, 18, "exercise returned wrong value");
}

Expand All @@ -147,8 +142,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
vm.expectRevert(bytes4(keccak256("BalancerOracle__BelowMinPrice()")));
optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params));
}
Expand All @@ -168,8 +162,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
(uint256 paidAmount,,,) = optionsToken.exercise(amount, address(this), address(exerciser), abi.encode(params));

// update multiplier
Expand Down Expand Up @@ -201,8 +194,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens which should fail
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount - 1, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount - 1, deadline: type(uint256).max});
vm.expectRevert(DiscountExercise.Exercise__SlippageTooHigh.selector);
optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params));
}
Expand All @@ -225,8 +217,7 @@ contract OptionsTokenTest is Test {
oracle.setParams(ORACLE_SECS, ORACLE_LARGEST_SAFETY_WINDOW, ORACLE_MIN_PRICE);

// exercise options tokens which should fail
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
vm.expectRevert(BalancerOracle.BalancerOracle__TWAPOracleNotReady.selector);
optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params));
}
Expand All @@ -244,8 +235,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: deadline});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: deadline});
if (amount != 0) {
vm.expectRevert(DiscountExercise.Exercise__PastDeadline.selector);
}
Expand All @@ -263,8 +253,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens which should fail
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
vm.expectRevert(BaseExercise.Exercise__NotOToken.selector);
exerciser.exercise(address(this), amount, recipient, abi.encode(params));
}
Expand All @@ -285,8 +274,7 @@ contract OptionsTokenTest is Test {
paymentToken.mint(address(this), expectedPaymentAmount);

// exercise options tokens which should fail
DiscountExerciseParams memory params =
DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max});
vm.expectRevert(OptionsToken.OptionsToken__NotExerciseContract.selector);
optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params));
}
Expand Down
3 changes: 1 addition & 2 deletions test/ThenaOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ contract ThenaOracleTest is Test {
// wait
skip(skipTime);

uint256 expectedMinPrice =
(price_1 * (_default.secs - skipTime) + getSpotPrice(_default.pair, _default.token) * skipTime) / _default.secs;
uint256 expectedMinPrice = (price_1 * (_default.secs - skipTime) + getSpotPrice(_default.pair, _default.token) * skipTime) / _default.secs;

assertGeDecimal(oracle.getPrice(), expectedMinPrice, 18, "price variation too large");
}
Expand Down
6 changes: 1 addition & 5 deletions test/mocks/MockBalancerTwapOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ contract MockVault is IVault {
tokens = _tokens;
}

function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request)
external
payable
override
{}
function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) external payable override {}

function getPool(bytes32 poolId) external view override returns (address, PoolSpecialization) {}

Expand Down
14 changes: 2 additions & 12 deletions test/mocks/MockUniswapPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,7 @@ contract MockUniswapPool is IUniswapV3Pool {
external
view
override
returns (
uint128 _liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
)
returns (uint128 _liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)
{}

function observations(uint256 index)
Expand All @@ -122,11 +116,7 @@ contract MockUniswapPool is IUniswapV3Pool {
returns (uint128 amount0, uint128 amount1)
{}

function burn(int24 tickLower, int24 tickUpper, uint128 amount)
external
override
returns (uint256 amount0, uint256 amount1)
{}
function burn(int24 tickLower, int24 tickUpper, uint128 amount) external override returns (uint256 amount0, uint256 amount1) {}

function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data)
external
Expand Down