Skip to content
Open
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
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"entry": ["solidity/ts/compile.ts", "solidity/ts/gas-costs.ts", "solidity/ts/tests/**/*.ts", "solidity/ts/fuzz/**/*.ts", "solidity/ts/types/bun-test.d.ts", "solidity/ts/types/index.d.ts", "ui/ts/tests/**/*.ts", "ui/ts/tests/**/*.tsx", "ui/ts/index.ts", "ui/build/vendor.mts", "ui/build/tests.mts"],
"project": ["solidity/ts/**/*.ts", "ui/ts/**/*.ts", "ui/ts/**/*.tsx", "ui/build/**/*.mts"],
"ignoreDependencies": ["better-typescript-lib"],
"ignoreDependencies": ["better-typescript-lib", "@zoltar/shared"],
"ignoreIssues": {
"solidity/ts/**/*.ts": ["unlisted"]
},
Expand Down
160 changes: 154 additions & 6 deletions solidity/contracts/peripherals/SecurityPoolOracleCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { ReputationToken } from '../ReputationToken.sol';
import { ISecurityPool } from './interfaces/ISecurityPool.sol';

// price oracle
uint256 constant PRICE_VALID_FOR_SECONDS = 1 hours;
uint256 constant PRICE_VALID_FOR_SECONDS = 5 minutes;
uint256 constant PRICE_PRECISION = 1e18;
uint256 constant ORACLE_BUDGET_BPS = 10000;
uint256 constant MAX_OPERATION_VALID_FOR_SECONDS = 5 minutes;

enum OperationType {
Liquidation,
Expand Down Expand Up @@ -49,6 +51,14 @@ contract SecurityPoolOracleCoordinator {
bool public immutable timeType;
bool public immutable trackDisputes;
address public immutable protocolFeeRecipient;
uint256 public immutable priceRoundBudgetMultiplierBps;
uint256 public immutable escalationHaltMultiplierBps;
uint256 public immutable maxSettlementBaseFeeMultiplierBps;
uint256 public immutable minLiquidationPriceDistanceBps;
uint256 public pendingReportMaxSettlementBaseFee;
uint256 public priceRoundId;
uint256 public priceRoundMaxNotional;
uint256 public priceRoundConsumedNotional;

event PriceReported(uint256 reportId, uint256 price);
event StagedOperationQueued(
Expand Down Expand Up @@ -87,7 +97,11 @@ contract SecurityPoolOracleCoordinator {
uint16 _multiplier,
bool _timeType,
bool _trackDisputes,
address _protocolFeeRecipient
address _protocolFeeRecipient,
uint256 _priceRoundBudgetMultiplierBps,
uint256 _escalationHaltMultiplierBps,
uint256 _maxSettlementBaseFeeMultiplierBps,
uint256 _minLiquidationPriceDistanceBps
) {
reputationToken = _reputationToken;
openOracle = _openOracle;
Expand All @@ -103,6 +117,14 @@ contract SecurityPoolOracleCoordinator {
timeType = _timeType;
trackDisputes = _trackDisputes;
protocolFeeRecipient = _protocolFeeRecipient;
require(_priceRoundBudgetMultiplierBps > 0, 'price budget multiplier is zero');
require(_escalationHaltMultiplierBps > 0, 'escalation halt multiplier is zero');
priceRoundBudgetMultiplierBps = _priceRoundBudgetMultiplierBps;
escalationHaltMultiplierBps = _escalationHaltMultiplierBps;
require(_maxSettlementBaseFeeMultiplierBps >= ORACLE_BUDGET_BPS, 'base fee multiplier too low');
require(_minLiquidationPriceDistanceBps <= ORACLE_BUDGET_BPS, 'liquidation distance too high');
maxSettlementBaseFeeMultiplierBps = _maxSettlementBaseFeeMultiplierBps;
minLiquidationPriceDistanceBps = _minLiquidationPriceDistanceBps;
}

function setSecurityPool(ISecurityPool _securityPool) public {
Expand All @@ -123,11 +145,12 @@ contract SecurityPoolOracleCoordinator {
require(pendingReportId == 0, 'Already pending request');
uint256 ethCost = getRequestPriceEthCost();
require(msg.value >= ethCost, 'not big enough eth bounty');
uint256 escalationHalt = reputationToken.totalSupply() / 100000;
uint256 escalationHalt = (exactToken1Report * escalationHaltMultiplierBps) / ORACLE_BUDGET_BPS;
uint256 settlerReward = block.basefee * 2 * gasConsumedOpenOracleReportPrice;
require(exactToken1Report <= type(uint128).max, 'exactToken1Report too large');
require(escalationHalt <= type(uint128).max, 'escalation halt too large');
require(settlerReward <= type(uint96).max, 'settler reward too large');
pendingReportMaxSettlementBaseFee = (block.basefee * maxSettlementBaseFeeMultiplierBps) / ORACLE_BUDGET_BPS;

OpenOracle.CreateReportParams memory reportparams = OpenOracle.CreateReportParams({
exactToken1Report: uint128(exactToken1Report),
Expand Down Expand Up @@ -167,6 +190,11 @@ contract SecurityPoolOracleCoordinator {
require(msg.sender == address(openOracle), 'only open oracle can call');
require(reportId == pendingReportId, 'not report created by us');
pendingReportId = 0;
if (block.basefee > pendingReportMaxSettlementBaseFee) {
pendingReportMaxSettlementBaseFee = 0;
return;
}
pendingReportMaxSettlementBaseFee = 0;
if (amount1 == 0 || amount2 == 0) {
return;
}
Expand All @@ -176,22 +204,49 @@ contract SecurityPoolOracleCoordinator {
}
lastSettlementTimestamp = block.timestamp;
lastPrice = price;
priceRoundId++;
priceRoundConsumedNotional = 0;
priceRoundMaxNotional =
(exactToken1Report * PRICE_PRECISION * priceRoundBudgetMultiplierBps) /
price /
ORACLE_BUDGET_BPS;
emit PriceReported(reportId, lastPrice);
if (pendingOperationSlotId != 0) {
// TODO we maybe should allow executing couple operations?
uint256 operationId = pendingOperationSlotId;
pendingOperationSlotId = 0;
StagedOperation memory stagedOperation = stagedOperations[operationId];
if (stagedOperation.initiatorVault == address(0)) return;
if (block.timestamp > stagedOperation.queuedAt + settlementTime + stagedOperation.validForSeconds) {
_consumeActiveStagedOperation(operationId);
stagedOperations[operationId].initiatorVault = address(0);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'staged operation expired');
return;
}
executeStagedOperation(operationId);
}
}

function isPriceValid() public view returns (bool) {
return isPriceFresh();
}

function isPriceFresh() public view returns (bool) {
return
lastPrice > 0 &&
lastSettlementTimestamp != 0 &&
lastSettlementTimestamp + PRICE_VALID_FOR_SECONDS > block.timestamp;
}

function getPriceRoundRemainingNotional() public view returns (uint256) {
if (priceRoundMaxNotional <= priceRoundConsumedNotional) return 0;
return priceRoundMaxNotional - priceRoundConsumedNotional;
}

function isPriceUsable() public view returns (bool) {
return isPriceFresh() && getPriceRoundRemainingNotional() > 0;
}

function requestPriceIfNeededAndStageOperation(
OperationType operation,
address targetVault,
Expand All @@ -202,6 +257,7 @@ contract SecurityPoolOracleCoordinator {
require(amount > 0, 'need to do non zero operation');
}
require(validForSeconds > 0, 'operation timeout must be positive');
require(validForSeconds <= MAX_OPERATION_VALID_FOR_SECONDS, 'operation timeout too long');
if (operation != OperationType.Liquidation) {
require(targetVault == msg.sender, 'self operation target must match initiator');
}
Expand Down Expand Up @@ -236,18 +292,25 @@ contract SecurityPoolOracleCoordinator {

uint256 retained = 0; // amount to retain from msg.value (cost incurred)

if (isPriceValid()) {
if (
isPriceFresh() && _getOperationNotional(stagedOperations[operationId]) <= getPriceRoundRemainingNotional()
) {
emit StagedOperationQueued(operationId, operation, msg.sender, targetVault, amount, false);
executeStagedOperation(operationId);
// no cost when price is valid
} else if (pendingOperationSlotId == 0) {
} else if (pendingReportId == 0 && pendingOperationSlotId == 0) {
pendingOperationSlotId = operationId;
emit StagedOperationQueued(operationId, operation, msg.sender, targetVault, amount, true);
uint256 ethCost = getRequestPriceEthCost();
require(msg.value >= ethCost, 'not enough eth to request price');
retained += ethCost;
// Forward exactly ethCost to requestPrice to create the report
this.requestPrice{ value: ethCost }();
} else if (pendingReportId == 0) {
emit StagedOperationQueued(operationId, operation, msg.sender, targetVault, amount, false);
} else if (pendingOperationSlotId == 0) {
pendingOperationSlotId = operationId;
emit StagedOperationQueued(operationId, operation, msg.sender, targetVault, amount, true);
} else {
emit StagedOperationQueued(operationId, operation, msg.sender, targetVault, amount, false);
// This is intentional: only one staged operation is marked as the auto-execute
Expand All @@ -267,13 +330,30 @@ contract SecurityPoolOracleCoordinator {
function executeStagedOperation(uint256 operationId) public {
StagedOperation memory stagedOperation = stagedOperations[operationId];
require(stagedOperation.initiatorVault != address(0), 'no such operation');
require(isPriceValid(), 'price is not valid to execute');
require(isPriceFresh(), 'price is not valid to execute');
require(
block.timestamp <= stagedOperation.queuedAt + settlementTime + stagedOperation.validForSeconds,
'staged operation expired'
);
_consumeActiveStagedOperation(operationId);
stagedOperations[operationId].initiatorVault = address(0);
uint256 operationNotional = _getOperationNotional(stagedOperation);
if (operationNotional > getPriceRoundRemainingNotional()) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'oracle budget exceeded');
return;
}
if (
stagedOperation.operation == OperationType.Liquidation &&
!_isLiquidationBeyondMinPriceDistance(stagedOperation)
) {
emit ExecutedStagedOperation(
operationId,
stagedOperation.operation,
false,
'liquidation too close to threshold'
);
return;
}
if (stagedOperation.operation == OperationType.Liquidation) {
try
securityPool.performLiquidation(
Expand All @@ -286,6 +366,7 @@ contract SecurityPoolOracleCoordinator {
stagedOperation.snapshotDenominator
)
{
_consumePriceRoundNotional(operationNotional);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, true, '');
} catch Error(string memory reason) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, reason);
Expand All @@ -296,6 +377,7 @@ contract SecurityPoolOracleCoordinator {
}
} else if (stagedOperation.operation == OperationType.WithdrawRep) {
try securityPool.performWithdrawRep(stagedOperation.initiatorVault, stagedOperation.amount) {
_consumePriceRoundNotional(operationNotional);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, true, '');
} catch Error(string memory reason) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, reason);
Expand All @@ -306,6 +388,7 @@ contract SecurityPoolOracleCoordinator {
}
} else {
try securityPool.performSetSecurityBondsAllowance(stagedOperation.initiatorVault, stagedOperation.amount) {
_consumePriceRoundNotional(operationNotional);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, true, '');
} catch Error(string memory reason) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, reason);
Expand All @@ -317,6 +400,71 @@ contract SecurityPoolOracleCoordinator {
}
}

function _consumePriceRoundNotional(uint256 notional) private {
priceRoundConsumedNotional += notional;
}

function _getOperationNotional(StagedOperation memory stagedOperation) private view returns (uint256) {
if (stagedOperation.operation == OperationType.Liquidation) {
uint256 debtToMove = _getLiquidationDebtToMove(stagedOperation);
uint256 repToMove = _getLiquidationRepToMove(stagedOperation, debtToMove);
uint256 repEthValue = _repToEthNotional(repToMove);
return debtToMove > repEthValue ? debtToMove : repEthValue;
}
if (stagedOperation.operation == OperationType.WithdrawRep) {
uint256 price = lastPrice;
if (price == 0 || stagedOperation.amount == 0) return 0;
uint256 numerator = stagedOperation.amount * PRICE_PRECISION;
return (numerator - 1) / price + 1;
}
if (stagedOperation.amount <= stagedOperation.snapshotTargetAllowance) return 0;
return stagedOperation.amount - stagedOperation.snapshotTargetAllowance;
}

function _getSnapshotVaultRep(StagedOperation memory stagedOperation) private pure returns (uint256) {
if (stagedOperation.snapshotDenominator == 0) {
return stagedOperation.snapshotTargetOwnership / PRICE_PRECISION;
}
return
(stagedOperation.snapshotTargetOwnership * stagedOperation.snapshotTotalRep) /
stagedOperation.snapshotDenominator;
}

function _getLiquidationDebtToMove(StagedOperation memory stagedOperation) private pure returns (uint256) {
return
stagedOperation.amount > stagedOperation.snapshotTargetAllowance
? stagedOperation.snapshotTargetAllowance
: stagedOperation.amount;
}

function _getLiquidationRepToMove(
StagedOperation memory stagedOperation,
uint256 debtToMove
) private pure returns (uint256) {
if (stagedOperation.snapshotTargetAllowance == 0 || debtToMove == 0) return 0;
return (debtToMove * _getSnapshotVaultRep(stagedOperation)) / stagedOperation.snapshotTargetAllowance;
}

function _repToEthNotional(uint256 repAmount) private view returns (uint256) {
uint256 price = lastPrice;
if (price == 0 || repAmount == 0) return 0;
uint256 numerator = repAmount * PRICE_PRECISION;
return (numerator - 1) / price + 1;
}

function _isLiquidationBeyondMinPriceDistance(StagedOperation memory stagedOperation) private view returns (bool) {
if (minLiquidationPriceDistanceBps == 0) return true;
uint256 snapshotTargetAllowance = stagedOperation.snapshotTargetAllowance;
if (snapshotTargetAllowance == 0) return false;
uint256 currentPrice = lastPrice;
if (currentPrice == 0) return false;
uint256 vaultRep = _getSnapshotVaultRep(stagedOperation);
uint256 securityMultiplier = securityPool.securityMultiplier();
uint256 thresholdPrice = (vaultRep * PRICE_PRECISION) / (snapshotTargetAllowance * securityMultiplier);
if (currentPrice <= thresholdPrice) return false;
return ((currentPrice - thresholdPrice) * ORACLE_BUDGET_BPS) / currentPrice >= minLiquidationPriceDistanceBps;
}

function getPendingOperationSlot() public view returns (StagedOperation memory) {
return stagedOperations[pendingOperationSlotId];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ contract PriceOracleManagerAndOperatorQueuerFactory {
bool public immutable timeType;
bool public immutable trackDisputes;
address public immutable protocolFeeRecipient;
uint256 public immutable priceRoundBudgetMultiplierBps;
uint256 public immutable escalationHaltMultiplierBps;
uint256 public immutable maxSettlementBaseFeeMultiplierBps;
uint256 public immutable minLiquidationPriceDistanceBps;

constructor(
IWeth9 _weth,
Expand All @@ -34,7 +38,11 @@ contract PriceOracleManagerAndOperatorQueuerFactory {
uint16 _multiplier,
bool _timeType,
bool _trackDisputes,
address _protocolFeeRecipient
address _protocolFeeRecipient,
uint256 _priceRoundBudgetMultiplierBps,
uint256 _escalationHaltMultiplierBps,
uint256 _maxSettlementBaseFeeMultiplierBps,
uint256 _minLiquidationPriceDistanceBps
) {
weth = _weth;
gasConsumedOpenOracleReportPrice = _gasConsumedOpenOracleReportPrice;
Expand All @@ -48,6 +56,10 @@ contract PriceOracleManagerAndOperatorQueuerFactory {
timeType = _timeType;
trackDisputes = _trackDisputes;
protocolFeeRecipient = _protocolFeeRecipient;
priceRoundBudgetMultiplierBps = _priceRoundBudgetMultiplierBps;
escalationHaltMultiplierBps = _escalationHaltMultiplierBps;
maxSettlementBaseFeeMultiplierBps = _maxSettlementBaseFeeMultiplierBps;
minLiquidationPriceDistanceBps = _minLiquidationPriceDistanceBps;
}

function deployPriceOracleManagerAndOperatorQueuer(
Expand All @@ -70,7 +82,11 @@ contract PriceOracleManagerAndOperatorQueuerFactory {
multiplier,
timeType,
trackDisputes,
protocolFeeRecipient
protocolFeeRecipient,
priceRoundBudgetMultiplierBps,
escalationHaltMultiplierBps,
maxSettlementBaseFeeMultiplierBps,
minLiquidationPriceDistanceBps
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ contract UniformPriceDualCapBatchAuctionFactory {
address owner,
bytes32 salt
) external returns (UniformPriceDualCapBatchAuction) {
return new UniformPriceDualCapBatchAuction{ salt: salt }(owner);
bytes memory initCode = abi.encodePacked(type(UniformPriceDualCapBatchAuction).creationCode, abi.encode(owner));
address auction;
assembly {
auction := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
require(auction != address(0), 'auction deploy failed');
return UniformPriceDualCapBatchAuction(auction);
}
}
Loading
Loading