/
PrimitiveEngine.sol
420 lines (369 loc) · 16.8 KB
/
PrimitiveEngine.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.6;
import "./libraries/Margin.sol";
import "./libraries/ReplicationMath.sol";
import "./libraries/Reserve.sol";
import "./libraries/SafeCast.sol";
import "./libraries/Transfers.sol";
import "./libraries/Units.sol";
import "./interfaces/callback/IPrimitiveCreateCallback.sol";
import "./interfaces/callback/IPrimitiveDepositCallback.sol";
import "./interfaces/callback/IPrimitiveLiquidityCallback.sol";
import "./interfaces/callback/IPrimitiveSwapCallback.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IPrimitiveEngine.sol";
import "./interfaces/IPrimitiveFactory.sol";
/// @title Primitive Engine
/// @author Primitive
/// @notice Replicating Market Maker
/// @dev RMM-01
contract PrimitiveEngine is IPrimitiveEngine {
using ReplicationMath for int128;
using Units for uint256;
using SafeCast for uint256;
using Reserve for mapping(bytes32 => Reserve.Data);
using Reserve for Reserve.Data;
using Margin for mapping(address => Margin.Data);
using Margin for Margin.Data;
using Transfers for IERC20;
/// @dev Parameters of each pool
/// @param strike Strike price of pool with stable token decimals
/// @param sigma Implied volatility, with 1e4 decimals such that 10000 = 100%
/// @param maturity Timestamp of pool expiration, in seconds
/// @param lastTimestamp Timestamp of the pool's last update, in seconds
/// @param gamma Multiplied against deltaIn amounts to apply swap fee, gamma = 1 - fee %, scaled up by 1e4
struct Calibration {
uint128 strike;
uint32 sigma;
uint32 maturity;
uint32 lastTimestamp;
uint32 gamma;
}
/// @inheritdoc IPrimitiveEngineView
uint256 public constant override PRECISION = 10**18;
/// @inheritdoc IPrimitiveEngineView
uint256 public constant override BUFFER = 120 seconds;
/// @inheritdoc IPrimitiveEngineView
uint256 public immutable override MIN_LIQUIDITY;
/// @inheritdoc IPrimitiveEngineView
uint256 public immutable override scaleFactorRisky;
/// @inheritdoc IPrimitiveEngineView
uint256 public immutable override scaleFactorStable;
/// @inheritdoc IPrimitiveEngineView
address public immutable override factory;
/// @inheritdoc IPrimitiveEngineView
address public immutable override risky;
/// @inheritdoc IPrimitiveEngineView
address public immutable override stable;
/// @dev Reentrancy guard initialized to state
uint256 private locked = 1;
/// @inheritdoc IPrimitiveEngineView
mapping(bytes32 => Calibration) public override calibrations;
/// @inheritdoc IPrimitiveEngineView
mapping(address => Margin.Data) public override margins;
/// @inheritdoc IPrimitiveEngineView
mapping(bytes32 => Reserve.Data) public override reserves;
/// @inheritdoc IPrimitiveEngineView
mapping(address => mapping(bytes32 => uint256)) public override liquidity;
modifier lock() {
if (locked != 1) revert LockedError();
locked = 2;
_;
locked = 1;
}
/// @notice Deploys an Engine with two tokens, a 'Risky' and 'Stable'
constructor() {
(factory, risky, stable, scaleFactorRisky, scaleFactorStable, MIN_LIQUIDITY) = IPrimitiveFactory(msg.sender)
.args();
}
/// @return Risky token balance of this contract
function balanceRisky() private view returns (uint256) {
(bool success, bytes memory data) = risky.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
if (!success || data.length != 32) revert BalanceError();
return abi.decode(data, (uint256));
}
/// @return Stable token balance of this contract
function balanceStable() private view returns (uint256) {
(bool success, bytes memory data) = stable.staticcall(
abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
if (!success || data.length != 32) revert BalanceError();
return abi.decode(data, (uint256));
}
/// @notice Revert if expected amount does not exceed current balance
function checkRiskyBalance(uint256 expectedRisky) private view {
uint256 actualRisky = balanceRisky();
if (actualRisky < expectedRisky) revert RiskyBalanceError(expectedRisky, actualRisky);
}
/// @notice Revert if expected amount does not exceed current balance
function checkStableBalance(uint256 expectedStable) private view {
uint256 actualStable = balanceStable();
if (actualStable < expectedStable) revert StableBalanceError(expectedStable, actualStable);
}
/// @return blockTimestamp casted as a uint32
function _blockTimestamp() internal view virtual returns (uint32 blockTimestamp) {
// solhint-disable-next-line
blockTimestamp = uint32(block.timestamp);
}
/// @inheritdoc IPrimitiveEngineActions
function updateLastTimestamp(bytes32 poolId) external override lock returns (uint32 lastTimestamp) {
lastTimestamp = _updateLastTimestamp(poolId);
}
/// @notice Sets the lastTimestamp of `poolId` to `block.timestamp`, max value is `maturity`
/// @return lastTimestamp of the pool, used in calculating the time until expiry
function _updateLastTimestamp(bytes32 poolId) internal virtual returns (uint32 lastTimestamp) {
Calibration storage cal = calibrations[poolId];
if (cal.lastTimestamp == 0) revert UninitializedError();
lastTimestamp = _blockTimestamp();
uint32 maturity = cal.maturity;
if (lastTimestamp > maturity) lastTimestamp = maturity; // if expired, set to the maturity
cal.lastTimestamp = lastTimestamp; // set state
emit UpdateLastTimestamp(poolId);
}
/// @inheritdoc IPrimitiveEngineActions
function create(
uint128 strike,
uint32 sigma,
uint32 maturity,
uint32 gamma,
uint256 riskyPerLp,
uint256 delLiquidity,
bytes calldata data
)
external
override
lock
returns (
bytes32 poolId,
uint256 delRisky,
uint256 delStable
)
{
(uint256 factor0, uint256 factor1) = (scaleFactorRisky, scaleFactorStable);
poolId = keccak256(abi.encodePacked(address(this), strike, sigma, maturity, gamma));
if (calibrations[poolId].lastTimestamp != 0) revert PoolDuplicateError();
if (sigma > 1e7 || sigma < 1) revert SigmaError(sigma);
if (strike == 0) revert StrikeError(strike);
if (delLiquidity <= MIN_LIQUIDITY) revert MinLiquidityError(delLiquidity);
if (riskyPerLp > PRECISION / factor0 || riskyPerLp == 0) revert RiskyPerLpError(riskyPerLp);
if (gamma > Units.PERCENTAGE || gamma < 9000) revert GammaError(gamma);
Calibration memory cal = Calibration({
strike: strike,
sigma: sigma,
maturity: maturity,
lastTimestamp: _blockTimestamp(),
gamma: gamma
});
if (cal.lastTimestamp > cal.maturity) revert PoolExpiredError();
uint32 tau = cal.maturity - cal.lastTimestamp; // time until expiry
delStable = ReplicationMath.getStableGivenRisky(0, factor0, factor1, riskyPerLp, cal.strike, cal.sigma, tau);
delRisky = (riskyPerLp * delLiquidity) / PRECISION; // riskyDecimals * 1e18 decimals / 1e18 = riskyDecimals
delStable = (delStable * delLiquidity) / PRECISION;
if (delRisky == 0 || delStable == 0) revert CalibrationError(delRisky, delStable);
calibrations[poolId] = cal; // state update
uint256 amount = delLiquidity - MIN_LIQUIDITY;
liquidity[msg.sender][poolId] += amount; // burn min liquidity, at cost of msg.sender
reserves[poolId].allocate(delRisky, delStable, delLiquidity, cal.lastTimestamp); // state update
(uint256 balRisky, uint256 balStable) = (balanceRisky(), balanceStable());
IPrimitiveCreateCallback(msg.sender).createCallback(delRisky, delStable, data);
checkRiskyBalance(balRisky + delRisky);
checkStableBalance(balStable + delStable);
emit Create(msg.sender, cal.strike, cal.sigma, cal.maturity, cal.gamma, delRisky, delStable, amount);
}
// ===== Margin =====
/// @inheritdoc IPrimitiveEngineActions
function deposit(
address recipient,
uint256 delRisky,
uint256 delStable,
bytes calldata data
) external override lock {
if (delRisky == 0 && delStable == 0) revert ZeroDeltasError();
margins[recipient].deposit(delRisky, delStable); // state update
uint256 balRisky;
uint256 balStable;
if (delRisky != 0) balRisky = balanceRisky();
if (delStable != 0) balStable = balanceStable();
IPrimitiveDepositCallback(msg.sender).depositCallback(delRisky, delStable, data); // agnostic payment
if (delRisky != 0) checkRiskyBalance(balRisky + delRisky);
if (delStable != 0) checkStableBalance(balStable + delStable);
emit Deposit(msg.sender, recipient, delRisky, delStable);
}
/// @inheritdoc IPrimitiveEngineActions
function withdraw(
address recipient,
uint256 delRisky,
uint256 delStable
) external override lock {
if (delRisky == 0 && delStable == 0) revert ZeroDeltasError();
margins.withdraw(delRisky, delStable); // state update
if (delRisky != 0) IERC20(risky).safeTransfer(recipient, delRisky);
if (delStable != 0) IERC20(stable).safeTransfer(recipient, delStable);
emit Withdraw(msg.sender, recipient, delRisky, delStable);
}
// ===== Liquidity =====
/// @inheritdoc IPrimitiveEngineActions
function allocate(
bytes32 poolId,
address recipient,
uint256 delRisky,
uint256 delStable,
bool fromMargin,
bytes calldata data
) external override lock returns (uint256 delLiquidity) {
if (delRisky == 0 || delStable == 0) revert ZeroDeltasError();
Reserve.Data storage reserve = reserves[poolId];
if (reserve.blockTimestamp == 0) revert UninitializedError();
uint32 timestamp = _blockTimestamp();
uint256 liquidity0 = (delRisky * reserve.liquidity) / uint256(reserve.reserveRisky);
uint256 liquidity1 = (delStable * reserve.liquidity) / uint256(reserve.reserveStable);
delLiquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
if (delLiquidity == 0) revert ZeroLiquidityError();
liquidity[recipient][poolId] += delLiquidity; // increase position liquidity
reserve.allocate(delRisky, delStable, delLiquidity, timestamp); // increase reserves and liquidity
if (fromMargin) {
margins.withdraw(delRisky, delStable); // removes tokens from `msg.sender` margin account
} else {
(uint256 balRisky, uint256 balStable) = (balanceRisky(), balanceStable());
IPrimitiveLiquidityCallback(msg.sender).allocateCallback(delRisky, delStable, data); // agnostic payment
checkRiskyBalance(balRisky + delRisky);
checkStableBalance(balStable + delStable);
}
emit Allocate(msg.sender, recipient, poolId, delRisky, delStable, delLiquidity);
}
/// @inheritdoc IPrimitiveEngineActions
function remove(bytes32 poolId, uint256 delLiquidity)
external
override
lock
returns (uint256 delRisky, uint256 delStable)
{
if (delLiquidity == 0) revert ZeroLiquidityError();
Reserve.Data storage reserve = reserves[poolId];
if (reserve.blockTimestamp == 0) revert UninitializedError();
(delRisky, delStable) = reserve.getAmounts(delLiquidity);
liquidity[msg.sender][poolId] -= delLiquidity; // state update
reserve.remove(delRisky, delStable, delLiquidity, _blockTimestamp());
margins[msg.sender].deposit(delRisky, delStable);
emit Remove(msg.sender, poolId, delRisky, delStable, delLiquidity);
}
struct SwapDetails {
address recipient;
bool riskyForStable;
bool fromMargin;
bool toMargin;
uint32 timestamp;
bytes32 poolId;
uint256 deltaIn;
uint256 deltaOut;
}
/// @inheritdoc IPrimitiveEngineActions
function swap(
address recipient,
bytes32 poolId,
bool riskyForStable,
uint256 deltaIn,
uint256 deltaOut,
bool fromMargin,
bool toMargin,
bytes calldata data
) external override lock {
if (deltaIn == 0) revert DeltaInError();
if (deltaOut == 0) revert DeltaOutError();
SwapDetails memory details = SwapDetails({
recipient: recipient,
poolId: poolId,
deltaIn: deltaIn,
deltaOut: deltaOut,
riskyForStable: riskyForStable,
fromMargin: fromMargin,
toMargin: toMargin,
timestamp: _blockTimestamp()
});
uint32 lastTimestamp = _updateLastTimestamp(details.poolId); // updates lastTimestamp of `poolId`
if (details.timestamp > lastTimestamp + BUFFER) revert PoolExpiredError(); // 120s buffer to allow final swaps
int128 invariantX64 = invariantOf(details.poolId); // stored in memory to perform the invariant check
{
// swap scope, avoids stack too deep errors
Calibration memory cal = calibrations[details.poolId];
Reserve.Data storage reserve = reserves[details.poolId];
uint32 tau = cal.maturity - cal.lastTimestamp;
uint256 deltaInWithFee = (details.deltaIn * cal.gamma) / Units.PERCENTAGE; // amount * (1 - fee %)
uint256 adjustedRisky;
uint256 adjustedStable;
if (details.riskyForStable) {
adjustedRisky = uint256(reserve.reserveRisky) + deltaInWithFee;
adjustedStable = uint256(reserve.reserveStable) - deltaOut;
} else {
adjustedRisky = uint256(reserve.reserveRisky) - deltaOut;
adjustedStable = uint256(reserve.reserveStable) + deltaInWithFee;
}
adjustedRisky = (adjustedRisky * PRECISION) / reserve.liquidity;
adjustedStable = (adjustedStable * PRECISION) / reserve.liquidity;
int128 invariantAfter = ReplicationMath.calcInvariant(
scaleFactorRisky,
scaleFactorStable,
adjustedRisky,
adjustedStable,
cal.strike,
cal.sigma,
tau
);
if (invariantX64 > invariantAfter) revert InvariantError(invariantX64, invariantAfter);
reserve.swap(details.riskyForStable, details.deltaIn, details.deltaOut, details.timestamp); // state update
}
if (details.riskyForStable) {
if (details.toMargin) {
margins[details.recipient].deposit(0, details.deltaOut);
} else {
IERC20(stable).safeTransfer(details.recipient, details.deltaOut); // optimistic transfer out
}
if (details.fromMargin) {
margins.withdraw(details.deltaIn, 0); // pay for swap
} else {
uint256 balRisky = balanceRisky();
IPrimitiveSwapCallback(msg.sender).swapCallback(details.deltaIn, 0, data); // agnostic transfer in
checkRiskyBalance(balRisky + details.deltaIn);
}
} else {
if (details.toMargin) {
margins[details.recipient].deposit(details.deltaOut, 0);
} else {
IERC20(risky).safeTransfer(details.recipient, details.deltaOut); // optimistic transfer out
}
if (details.fromMargin) {
margins.withdraw(0, details.deltaIn); // pay for swap
} else {
uint256 balStable = balanceStable();
IPrimitiveSwapCallback(msg.sender).swapCallback(0, details.deltaIn, data); // agnostic transfer in
checkStableBalance(balStable + details.deltaIn);
}
}
emit Swap(
msg.sender,
details.recipient,
details.poolId,
details.riskyForStable,
details.deltaIn,
details.deltaOut
);
}
// ===== View =====
/// @inheritdoc IPrimitiveEngineView
function invariantOf(bytes32 poolId) public view override returns (int128 invariant) {
Calibration memory cal = calibrations[poolId];
uint32 tau = cal.maturity - cal.lastTimestamp; // cal maturity can never be less than lastTimestamp
(uint256 riskyPerLiquidity, uint256 stablePerLiquidity) = reserves[poolId].getAmounts(PRECISION); // 1e18 liquidity
invariant = ReplicationMath.calcInvariant(
scaleFactorRisky,
scaleFactorStable,
riskyPerLiquidity,
stablePerLiquidity,
cal.strike,
cal.sigma,
tau
);
}
}