Skip to content

Commit 285279a

Browse files
authored
Merge pull request #397 from primitivefinance/release/v1.3.0-beta
Release/v1.3.0 beta
2 parents bafb73a + db1131e commit 285279a

8 files changed

Lines changed: 148 additions & 58 deletions

File tree

contracts/Portfolio.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "./Objective.sol";
66
/**
77
* @title Portfolio
88
* @author Primitive™
9-
* @dev Note: All tokens sent to this contract will be lost.
9+
* @dev All directly transferred tokens to this contract are lost.
1010
* @custom:contributor TomAFrench
1111
*/
1212
abstract contract PortfolioVirtual is Objective {
@@ -25,11 +25,11 @@ abstract contract PortfolioVirtual is Objective {
2525
mstore(0x00, 0x20)
2626

2727
// We load the length of our string (11 bytes, 0x0b in hex) and its
28-
// actual hex value (0x76312e322e302d62657461) using the offset 0x2b.
28+
// actual hex value (0x76312e332e302d62657461) using the offset 0x2b.
2929
// Using this particular offset value will right pad the length at
3030
// the end of the slot and left pad the string at the beginning of
3131
// the next slot, assuring the right ABI format to return a string.
32-
mstore(0x2b, 0x0b76312e322e302d62657461) // "v1.2.0-beta"
32+
mstore(0x2b, 0x0b76312e332e302d62657461) // "v1.3.0-beta"
3333

3434
// Return all the 96 bytes (0x60) of data that was loaded into
3535
// the memory.

contracts/RMM01Portfolio.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ contract RMM01Portfolio is PortfolioVirtual {
112112
uint256 maxInput;
113113
if (sellAsset) {
114114
// invariant: x reserve < 1E18
115-
maxInput = (FixedPointMathLib.WAD - reserveIn).mulWadDown(liquidity);
115+
maxInput =
116+
(FixedPointMathLib.WAD - reserveIn - 1).mulWadDown(liquidity);
116117
} else {
117118
// invariant: y reserve < strikePrice
118119
maxInput = (pools[poolId].params.strikePrice - reserveIn - 1)

contracts/libraries/RMM01Lib.sol

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: AGPL-3.0-only
22
pragma solidity 0.8.19;
33

4-
import "solstat/Invariant.sol";
4+
import "solstat/Gaussian.sol";
55
import {
66
PortfolioPool,
77
Iteration,
@@ -15,6 +15,7 @@ uint256 constant WAD = 1e18;
1515
uint256 constant SQRT_WAD = 1e9;
1616
uint256 constant YEAR = 31556953 seconds;
1717
uint256 constant BISECTION_EPSILON = 0;
18+
uint256 constant BISECTION_ITERATIONS = 256;
1819
int256 constant BISECTION_ERROR = 2;
1920

2021
/**
@@ -29,6 +30,8 @@ library RMM01Lib {
2930

3031
error UndefinedPrice();
3132
error OverflowWad(int256 wad);
33+
error InvalidQuotient(uint256 quotient);
34+
error InvalidDifference(uint256 difference);
3235

3336
/**
3437
* @notice
@@ -80,11 +83,17 @@ library RMM01Lib {
8083
// σ√τ
8184
uint256 volSqrtYearsWad = volatilityWad.mulWadDown(sqrtTauWad);
8285
// y / K
83-
uint256 quotientWad = reserveYPerWad.divWadUp(strikePriceWad); // todo: review rounding direction. Avoids scenarios division truncates to 0.
86+
uint256 quotientWad = reserveYPerWad.divWadUp(strikePriceWad);
87+
if (quotientWad == 0 || quotientWad == WAD) {
88+
revert InvalidQuotient(quotientWad);
89+
}
8490
// Φ⁻¹(y/K)
8591
int256 inverseCdfQuotient = Gaussian.ppf(int256(quotientWad));
8692
// 1 - x
8793
uint256 differenceWad = WAD - reserveXPerWad;
94+
if (differenceWad == 0 || differenceWad == WAD) {
95+
revert InvalidDifference(differenceWad);
96+
}
8897
// Φ⁻¹(1-x)
8998
int256 inverseCdfDifference = Gaussian.ppf(int256(differenceWad));
9099
// k = Φ⁻¹(y/K) - Φ⁻¹(1-x) + σ√τ
@@ -115,13 +124,18 @@ library RMM01Lib {
115124
uint256 timeRemainingSec,
116125
int256 invariant
117126
) internal pure returns (uint256 reserveXPerWad) {
127+
// If y reserves has reached upper bound, x reserves is zero.
128+
if (reserveYPerWad >= strikePriceWad) return 0;
129+
// If y reserves has reached lower bound, x reserves is one.
130+
if (reserveYPerWad == 0) return WAD;
131+
118132
uint256 yearsWad = timeRemainingSec.divWadDown(uint256(YEAR));
119133
// √τ, √τ is scaled to WAD by multiplying by 1E9.
120134
uint256 sqrtTauWad = yearsWad.sqrt() * SQRT_WAD;
121135
// σ√τ
122136
uint256 volSqrtYearsWad = volatilityWad.mulWadDown(sqrtTauWad);
123137
// y / K
124-
uint256 quotientWad = reserveYPerWad.divWadDown(strikePriceWad);
138+
uint256 quotientWad = reserveYPerWad.divWadUp(strikePriceWad);
125139
// Φ⁻¹(y/K)
126140
int256 inverseCdfQuotient = Gaussian.ppf(int256(quotientWad));
127141
// Φ⁻¹(y/K) + σ√τ - k
@@ -154,6 +168,11 @@ library RMM01Lib {
154168
uint256 timeRemainingSec,
155169
int256 invariant
156170
) internal pure returns (uint256 reserveYPerWad) {
171+
// If x reserves has reached upper bound, y reserves is zero.
172+
if (reserveXPerWad >= WAD) return 0;
173+
// If x reserves has reached lower bound, y reserves is equal to the strike price.
174+
if (reserveXPerWad == 0) return strikePriceWad;
175+
157176
uint256 yearsWad = timeRemainingSec.divWadDown(uint256(YEAR));
158177
// √τ, √τ is scaled to WAD by multiplying by 1E9.
159178
uint256 sqrtTauWad = yearsWad.sqrt() * SQRT_WAD;
@@ -172,14 +191,17 @@ library RMM01Lib {
172191
}
173192

174193
/**
175-
* @dev Computes the invariant of the RMM-01 trading function.
194+
* @notice
195+
* Computes the invariant of the RMM-01 trading function.
196+
*
197+
* @dev
198+
* k = Φ⁻¹(y/K) - Φ⁻¹(1-x) + σ√τ
199+
*
176200
* @param self Pool instance.
177201
* @param R_x Quantity of `asset` reserves scaled to WAD units per WAD of liquidity.
178202
* @param R_y Quantity of `quote` reserves scaled to WAD units per WAD of liquidity.
179203
* @param timeRemainingSec Amount of time in seconds until the `self` PortfolioPool is matured.
180-
* @return invariantWad Signed invariant denominated in `quote` tokens, scaled to WAD units.
181-
* @custom:math k = y - KΦ(Φ⁻¹(1-x) - σ√τ)
182-
* @custom:dependency https://github.com/primitivefinance/solstat
204+
* @return invariantWad Invariant of the pool. Does not have an explicit unit denomination.
183205
*/
184206
function invariantOf(
185207
PortfolioPool memory self,
@@ -198,11 +220,18 @@ library RMM01Lib {
198220
}
199221

200222
/**
201-
* @dev Approximation of amount out of tokens given a swap `amountIn`.
202-
* It is not exactly precise to the optimal amount.
223+
* @notice
224+
* Gets the total tokens out given an amount of tokens in for a swap.
225+
*
226+
* @dev
227+
* Approximation of amount out of tokens given a swap of `amountIn`.
228+
*
229+
* @param self PortfolioPool instance.
230+
* @param sellAsset True if `asset` tokens are being sold for `quote` tokens.
203231
* @param amountIn Quantity of tokens in, units are native token decimals.
204-
* @param timestamp Timestamp to use to compute the remaining duration in the Portfolio.
205-
* @custom:error Maximum absolute error of 1e-6.
232+
* @param timestamp Expected timestamp of the swap's execution.
233+
* @param swapper Address of the account that is swapping.
234+
* @return amountOut Quantity of tokens out, units are native token decimals.
206235
*/
207236
function getAmountOut(
208237
PortfolioPool memory self,
@@ -235,8 +264,14 @@ library RMM01Lib {
235264
}
236265

237266
/**
238-
* @notice Fetches the data needed to simulate a swap to compute the output of tokens.
239-
* @dev Does not consider protocol fees, therefore feeAmount could be overestimated since protocol fees are not subtracted.
267+
* @notice
268+
* Fetches the data needed to simulate a swap to compute the output of tokens.
269+
*
270+
* @dev
271+
* Does not consider protocol fees, therefore feeAmount could be overestimated since protocol fees are not subtracted.
272+
* Computes the invariant of the pool with rounded up virtual reserves for the output reserve of a trade.
273+
* This is on purpose so that the invariant is slightly overestimated, which will make sure any rounding errors
274+
* during swaps are advantageous to Portfolio rather than swappers.
240275
*/
241276
function getSwapData(
242277
PortfolioPool memory self,
@@ -356,18 +391,26 @@ library RMM01Lib {
356391
lower,
357392
upper,
358393
BISECTION_EPSILON, // Set to 0 to find the exact dependent reserve which sets the invariant to 0.
359-
256, // Maximum amount of loops to run in bisection.
394+
BISECTION_ITERATIONS, // Maximum amount of loops to run in bisection.
360395
optimizeDependentReserve
361396
);
362397
// Increase dependent reserve per liquidity by 1 to account for precision loss.
363398
adjustedDependentReserve++;
364399
// Return the total adjusted dependent pool reserve for all the liquidity.
365-
nextDep = adjustedDependentReserve.mulWadDown(data.liquidity);
400+
nextDep = adjustedDependentReserve.mulWadDown(data.liquidity); // Truncates product.
366401
}
367402

368403
/**
369-
* @dev Optimized function used in the bisection method to compute the precise dependent reserve.
404+
* @notice
405+
* Function used in the bisection method to compute the precise dependent reserve.
406+
*
407+
* @dev
408+
* Optimizes for the case in which `optimized` is the dependent reserve
409+
* which produces an invariant equal to the previous invariant plus a small error.
410+
* Effectively, finds the dependent reserve which positively changes the invariant by `BISECTION_ERROR`.
411+
*
370412
* @param optimized Dependent reserve in WAD units per 1E18 liquidity.
413+
* @return error The difference between the invariant and the previous invariant plus a small error.
371414
*/
372415
function optimizeDependentReserve(
373416
Bisection memory args,
@@ -387,31 +430,37 @@ library RMM01Lib {
387430
}
388431

389432
/**
390-
* @dev Computes the amount of `asset` and `quote` tokens scaled to WAD units to track per WAD units of liquidity.
433+
* @notice
434+
* Computes the x and y reserves given a price.
435+
*
436+
* @dev
437+
* Computes the amount of `asset` and `quote` tokens per one liquidity unit.
438+
*
439+
* @param self PortfolioPool instance.
391440
* @param priceWad Price of `asset` token scaled to WAD units.
392-
* @param invariantWad Current invariant of the pool in its native WAD units.
441+
* @param invariantWad Current invariant of the pool.
442+
* @return R_x Quantity of `asset` reserves scaled to WAD units per WAD of liquidity.
443+
* @return R_y Quantity of `quote` reserves scaled to WAD units per WAD of liquidity.
393444
*/
394445
function computeReservesWithPrice(
395446
PortfolioPool memory self,
396447
uint256 priceWad,
397448
int128 invariantWad
398449
) internal pure returns (uint256 R_x, uint256 R_y) {
399-
uint256 terminalPriceWad = self.params.strikePrice;
400-
uint256 volatilityFactorWad =
401-
convertPercentageToWad(self.params.volatility);
402-
uint256 timeRemainingSec = self.lastTau(); // uses self.lastTimestamp, is it set?
450+
uint256 volatilityWad = convertPercentageToWad(self.params.volatility);
451+
uint256 timeRemainingSec = self.lastTau(); // Uses self.lastTimestamp; must be set before calling this function.
403452
R_x = getXWithPrice({
404453
prc: priceWad,
405-
stk: terminalPriceWad,
454+
stk: self.params.strikePrice,
406455
vol: self.params.volatility,
407456
tau: timeRemainingSec
408457
});
409-
R_y = Invariant.getY({
410-
R_x: R_x,
411-
stk: terminalPriceWad,
412-
vol: volatilityFactorWad,
413-
tau: timeRemainingSec,
414-
inv: invariantWad
458+
R_y = getReserveYPerWad({
459+
reserveXPerWad: R_x,
460+
strikePriceWad: self.params.strikePrice,
461+
volatilityWad: volatilityWad,
462+
timeRemainingSec: timeRemainingSec,
463+
invariant: invariantWad
415464
});
416465
}
417466

contracts/test/SimpleRegistry.sol

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
12
pragma solidity ^0.8.4;
23

4+
import "../interfaces/IERC20.sol";
5+
import "solmate/auth/Owned.sol";
6+
37
interface PortfolioLike {
48
function setProtocolFee(uint256) external;
59
function claimFee(address, uint256) external;
610
}
711

8-
// Do not use! No protection!
9-
contract SimpleRegistry {
12+
/// @dev Basic registry with a single owner.
13+
contract SimpleRegistry is Owned {
1014
address public controller;
1115

12-
constructor() {
13-
controller = msg.sender;
14-
}
15-
16-
modifier useSelfAsController() {
17-
address prevController = controller;
16+
constructor() Owned(msg.sender) {
1817
controller = address(this);
19-
_;
20-
controller = prevController;
2118
}
2219

23-
function setFee(
24-
address portfolio,
25-
uint256 fee
26-
) public useSelfAsController {
20+
function setFee(address portfolio, uint256 fee) public onlyOwner {
2721
PortfolioLike(portfolio).setProtocolFee(fee);
2822
}
2923

3024
function claimFee(
3125
address portfolio,
3226
address token,
3327
uint256 amount
34-
) public useSelfAsController {
28+
) public onlyOwner {
3529
PortfolioLike(portfolio).claimFee(token, amount);
3630
}
31+
32+
function withdraw(address token, uint256 amount) public onlyOwner {
33+
require(amount > 0, "SimpleRegistry/invalid-amount");
34+
require(
35+
IERC20(token).transfer(msg.sender, amount),
36+
"SimpleRegistry/transfer-failed"
37+
);
38+
}
3739
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@primitivexyz/portfolio",
33
"license": "AGPL-3.0-only",
4-
"version": "v1.2.0-beta",
4+
"version": "v1.3.0-beta",
55
"description": "Onchain protocol for low cost portfolio management using automated market making strategies.",
66
"publishConfig": {
77
"access": "public"

test/TestPortfolioAllocate.t.sol

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ contract TestPortfolioAllocate is Setup {
6969
)
7070
);
7171

72-
subject().multicall{ value: 250 ether }(data);
72+
subject().multicall{value: 250 ether}(data);
7373
}
7474

7575
function test_allocate_recipient_weth()
@@ -81,7 +81,7 @@ contract TestPortfolioAllocate is Setup {
8181
{
8282
vm.deal(actor(), 250 ether);
8383

84-
subject().allocate{ value: 250 ether }(
84+
subject().allocate{value: 250 ether}(
8585
false,
8686
address(0xbeef),
8787
ghost().poolId,
@@ -693,14 +693,13 @@ contract TestPortfolioAllocate is Setup {
693693
defaultConfig
694694
useActor
695695
usePairTokens(10 ether)
696-
allocateSome(uint128(BURNED_LIQUIDITY))
697696
isArmed
698697
{
699698
uint128 liquidity = 1 ether;
700699

701700
subject().allocate(
702701
false,
703-
address(this),
702+
actor(),
704703
ghost().poolId,
705704
liquidity,
706705
type(uint128).max,
@@ -739,7 +738,7 @@ contract TestPortfolioAllocate is Setup {
739738
IPortfolioActions.allocate,
740739
(
741740
true,
742-
address(this),
741+
actor(),
743742
poolId,
744743
maxLiquidity,
745744
type(uint128).max,
@@ -749,9 +748,11 @@ contract TestPortfolioAllocate is Setup {
749748

750749
bytes[] memory res = subject().multicall(data);
751750

752-
(uint256 assetDeallocate,) = abi.decode(res[0], (uint256, uint256));
751+
(uint256 assetDeallocate, uint256 quoteDeallocate) =
752+
abi.decode(res[0], (uint256, uint256));
753753

754-
(uint256 assetAllocate,) = abi.decode(res[1], (uint256, uint256));
754+
(uint256 assetAllocate, uint256 quoteAllocate) =
755+
abi.decode(res[1], (uint256, uint256));
755756

756757
uint256 postAssetBalance =
757758
ghost().asset().to_token().balanceOf(address(actor()));
@@ -764,6 +765,10 @@ contract TestPortfolioAllocate is Setup {
764765
preAssetBalance + assetDeallocate - assetAllocate,
765766
"asset balance"
766767
);
767-
assertEq(postQuoteBalance, preQuoteBalance, "quote balance");
768+
assertEq(
769+
postQuoteBalance,
770+
preQuoteBalance + quoteDeallocate - quoteAllocate,
771+
"quote balance"
772+
);
768773
}
769774
}

0 commit comments

Comments
 (0)