|
// SPDX-License-Identifier: MIT |
|
pragma solidity ^0.8.24; |
|
|
|
interface IERC20Minimal { |
|
function balanceOf(address account) external view returns (uint256); |
|
function transfer(address to, uint256 value) external returns (bool); |
|
function transferFrom(address from, address to, uint256 value) external returns (bool); |
|
} |
|
|
|
interface IERC1155ReceiverMinimal { |
|
function onERC1155Received( |
|
address operator, |
|
address from, |
|
uint256 id, |
|
uint256 value, |
|
bytes calldata data |
|
) external returns (bytes4); |
|
|
|
function onERC1155BatchReceived( |
|
address operator, |
|
address from, |
|
uint256[] calldata ids, |
|
uint256[] calldata values, |
|
bytes calldata data |
|
) external returns (bytes4); |
|
} |
|
|
|
/// @title Symmetric fully-collateralized, oracle-free options |
|
/// @notice Draft reference implementation of the exchange-right primitive. |
|
/// @dev Amounts of claims/residuals use 1e18 option units. Pool unit amounts are |
|
/// expressed in the underlying ERC-20's native decimals per 1e18 option units. |
|
contract SymmetricOptions { |
|
uint256 public constant UNIT = 1e18; |
|
uint256 private constant MAX_CASCADE_STEPS = 64; |
|
|
|
struct Market { |
|
address assetA; |
|
address assetB; |
|
uint64 expiry; |
|
uint64 exerciseWindow; |
|
bool exists; |
|
} |
|
|
|
struct Pool { |
|
uint256 marketId; |
|
bool collateralIsA; |
|
uint256 collateralUnit; |
|
uint256 exerciseUnit; |
|
uint256 totalClaims; |
|
uint256 totalResiduals; |
|
uint256 rawUnits; |
|
uint256 accruedExercise; |
|
uint256 firstOpenPosition; |
|
bool exists; |
|
} |
|
|
|
struct Position { |
|
address owner; |
|
uint256 targetPoolId; |
|
uint256 backingPoolId; |
|
uint256 linkedUnits; |
|
uint256 rawUnits; |
|
uint256 proceedsA; |
|
uint256 proceedsB; |
|
bool closed; |
|
} |
|
|
|
mapping(uint256 => Market) public markets; |
|
mapping(uint256 => Pool) public pools; |
|
mapping(uint256 => Position) public positions; |
|
|
|
uint256 public marketCount; |
|
uint256 public poolCount; |
|
uint256 public positionCount; |
|
|
|
mapping(address => mapping(uint256 => uint256)) private _balances; |
|
mapping(address => mapping(address => bool)) public isApprovedForAll; |
|
|
|
bool private _entered; |
|
|
|
event MarketCreated( |
|
uint256 indexed marketId, |
|
address indexed assetA, |
|
address indexed assetB, |
|
uint64 expiry, |
|
uint64 exerciseWindow |
|
); |
|
event PoolCreated( |
|
uint256 indexed poolId, |
|
uint256 indexed marketId, |
|
bool collateralIsA, |
|
uint256 collateralUnit, |
|
uint256 exerciseUnit |
|
); |
|
event RawMinted(uint256 indexed poolId, address indexed writer, uint256 units, uint256 residualUnits); |
|
event LinkedMinted( |
|
uint256 indexed positionId, |
|
uint256 indexed targetPoolId, |
|
uint256 indexed backingPoolId, |
|
address writer, |
|
uint256 units |
|
); |
|
event Exercised(uint256 indexed poolId, address indexed exerciser, uint256 units); |
|
event BackingUpgraded(uint256 indexed positionId, uint256 units); |
|
event RawUnwound(uint256 indexed poolId, address indexed writer, uint256 units); |
|
event LinkedUnwound(uint256 indexed positionId, uint256 units); |
|
event PositionRawUnwound(uint256 indexed positionId, uint256 units); |
|
event ResidualRedeemed( |
|
uint256 indexed poolId, |
|
address indexed holder, |
|
uint256 residualUnits, |
|
uint256 collateralOut, |
|
uint256 exerciseOut |
|
); |
|
event PositionProceedsWithdrawn(uint256 indexed positionId, address indexed owner, uint256 amountA, uint256 amountB); |
|
event PositionClosed(uint256 indexed positionId); |
|
|
|
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); |
|
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); |
|
event ApprovalForAll(address indexed account, address indexed operator, bool approved); |
|
event URI(string value, uint256 indexed id); |
|
|
|
error Reentrancy(); |
|
error InvalidMarket(); |
|
error InvalidPool(); |
|
error InvalidPosition(); |
|
error InvalidPhase(); |
|
error InvalidAmount(); |
|
error IncompatibleBacking(); |
|
error PoolFullyExercised(); |
|
error Slippage(); |
|
error InsufficientBalance(); |
|
error NotAuthorized(); |
|
error UnsafeToken(); |
|
error CascadeTooDeep(); |
|
|
|
modifier nonReentrant() { |
|
if (_entered) revert Reentrancy(); |
|
_entered = true; |
|
_; |
|
_entered = false; |
|
} |
|
|
|
function createMarket( |
|
address assetA, |
|
address assetB, |
|
uint64 expiry, |
|
uint64 exerciseWindow |
|
) external returns (uint256 marketId) { |
|
if (assetA == address(0) || assetB == address(0) || assetA == assetB) revert InvalidMarket(); |
|
if (expiry <= block.timestamp || exerciseWindow == 0) revert InvalidMarket(); |
|
|
|
marketId = ++marketCount; |
|
markets[marketId] = Market({ |
|
assetA: assetA, |
|
assetB: assetB, |
|
expiry: expiry, |
|
exerciseWindow: exerciseWindow, |
|
exists: true |
|
}); |
|
|
|
emit MarketCreated(marketId, assetA, assetB, expiry, exerciseWindow); |
|
} |
|
|
|
function createPool( |
|
uint256 marketId, |
|
bool collateralIsA, |
|
uint256 collateralUnit, |
|
uint256 exerciseUnit |
|
) external returns (uint256 poolId) { |
|
Market storage market = markets[marketId]; |
|
if (!market.exists) revert InvalidMarket(); |
|
if (collateralUnit == 0 || exerciseUnit == 0) revert InvalidAmount(); |
|
|
|
poolId = ++poolCount; |
|
pools[poolId] = Pool({ |
|
marketId: marketId, |
|
collateralIsA: collateralIsA, |
|
collateralUnit: collateralUnit, |
|
exerciseUnit: exerciseUnit, |
|
totalClaims: 0, |
|
totalResiduals: 0, |
|
rawUnits: 0, |
|
accruedExercise: 0, |
|
firstOpenPosition: 1, |
|
exists: true |
|
}); |
|
|
|
emit PoolCreated(poolId, marketId, collateralIsA, collateralUnit, exerciseUnit); |
|
} |
|
|
|
/// @notice Writes raw-backed claims. If the raw residual pool has drifted, |
|
/// the writer must also deposit a proportional slice of accrued exercise asset. |
|
function mintRaw( |
|
uint256 poolId, |
|
uint256 units, |
|
uint256 maxExerciseDeposit |
|
) external nonReentrant returns (uint256 residualUnits, uint256 exerciseDeposit) { |
|
Pool storage pool = _requirePool(poolId); |
|
_requireBeforeSettlement(pool.marketId); |
|
if (units == 0) revert InvalidAmount(); |
|
|
|
if (pool.rawUnits == 0) { |
|
if (pool.totalResiduals != 0 || pool.accruedExercise != 0) revert PoolFullyExercised(); |
|
residualUnits = units; |
|
} else { |
|
exerciseDeposit = _mulDivUp(units, pool.accruedExercise, pool.rawUnits); |
|
residualUnits = (units * pool.totalResiduals) / pool.rawUnits; |
|
if (residualUnits == 0) revert InvalidAmount(); |
|
} |
|
if (exerciseDeposit > maxExerciseDeposit) revert Slippage(); |
|
|
|
_pullToken(_collateralToken(poolId), msg.sender, _unitAmount(units, pool.collateralUnit)); |
|
_pullToken(_exerciseToken(poolId), msg.sender, exerciseDeposit); |
|
|
|
pool.rawUnits += units; |
|
pool.totalClaims += units; |
|
pool.totalResiduals += residualUnits; |
|
|
|
_mint(msg.sender, claimId(poolId), units); |
|
_mint(msg.sender, residualId(poolId), residualUnits); |
|
|
|
emit RawMinted(poolId, msg.sender, units, residualUnits); |
|
} |
|
|
|
/// @notice Writes target claims backed by already-held claims of an eligible neighbor pool. |
|
function mintLinked( |
|
uint256 targetPoolId, |
|
uint256 backingPoolId, |
|
uint256 units |
|
) external nonReentrant returns (uint256 positionId) { |
|
Pool storage target = _requirePool(targetPoolId); |
|
_requirePool(backingPoolId); |
|
_requireBeforeSettlement(target.marketId); |
|
if (units == 0) revert InvalidAmount(); |
|
_requireEligibleBacking(targetPoolId, backingPoolId); |
|
|
|
_transfer1155(msg.sender, address(this), claimId(backingPoolId), units, ""); |
|
|
|
target.totalClaims += units; |
|
_mint(msg.sender, claimId(targetPoolId), units); |
|
|
|
positionId = ++positionCount; |
|
positions[positionId] = Position({ |
|
owner: msg.sender, |
|
targetPoolId: targetPoolId, |
|
backingPoolId: backingPoolId, |
|
linkedUnits: units, |
|
rawUnits: 0, |
|
proceedsA: 0, |
|
proceedsB: 0, |
|
closed: false |
|
}); |
|
|
|
emit LinkedMinted(positionId, targetPoolId, backingPoolId, msg.sender, units); |
|
} |
|
|
|
/// @notice Exercises claims during the market's exercise window. |
|
function exercise(uint256 poolId, uint256 units) external nonReentrant { |
|
Pool storage pool = _requirePool(poolId); |
|
_requireExerciseWindow(pool.marketId); |
|
if (units == 0) revert InvalidAmount(); |
|
|
|
_burn(msg.sender, claimId(poolId), units); |
|
pool.totalClaims -= units; |
|
|
|
_pullToken(_exerciseToken(poolId), msg.sender, _unitAmount(units, pool.exerciseUnit)); |
|
_sourceCollateral(poolId, units, 0); |
|
_pushToken(_collateralToken(poolId), msg.sender, _unitAmount(units, pool.collateralUnit)); |
|
|
|
emit Exercised(poolId, msg.sender, units); |
|
} |
|
|
|
/// @notice Exercises a position's pledged backing claim and replaces it with raw collateral. |
|
/// This is the Rule W1 capture primitive from the specification. |
|
function upgradeBacking(uint256 positionId, uint256 units) external nonReentrant { |
|
Position storage position = _requirePosition(positionId); |
|
if (position.owner != msg.sender) revert NotAuthorized(); |
|
if (units == 0 || units > position.linkedUnits) revert InvalidAmount(); |
|
|
|
Pool storage target = pools[position.targetPoolId]; |
|
Pool storage backing = pools[position.backingPoolId]; |
|
_requireExerciseWindow(target.marketId); |
|
|
|
position.linkedUnits -= units; |
|
position.rawUnits += units; |
|
|
|
_burn(address(this), claimId(position.backingPoolId), units); |
|
backing.totalClaims -= units; |
|
|
|
_pullToken(_exerciseToken(position.backingPoolId), msg.sender, _unitAmount(units, backing.exerciseUnit)); |
|
uint256 released = _sourceCollateral(position.backingPoolId, units, 1); |
|
uint256 required = _unitAmount(units, target.collateralUnit); |
|
if (released < required) revert IncompatibleBacking(); |
|
|
|
_creditPosition(position, _collateralIsAssetA(position.targetPoolId), released - required); |
|
|
|
emit BackingUpgraded(positionId, units); |
|
} |
|
|
|
/// @notice Recombines raw-backed claim and residual units to reclaim raw collateral. |
|
function unwindRaw(uint256 poolId, uint256 units) external nonReentrant { |
|
Pool storage pool = _requirePool(poolId); |
|
_requireBeforeSettlement(pool.marketId); |
|
if (units == 0 || units > pool.rawUnits) revert InvalidAmount(); |
|
|
|
_burn(msg.sender, claimId(poolId), units); |
|
_burn(msg.sender, residualId(poolId), units); |
|
|
|
pool.totalClaims -= units; |
|
pool.totalResiduals -= units; |
|
pool.rawUnits -= units; |
|
|
|
_pushToken(_collateralToken(poolId), msg.sender, _unitAmount(units, pool.collateralUnit)); |
|
|
|
emit RawUnwound(poolId, msg.sender, units); |
|
} |
|
|
|
/// @notice Burns written target claims against still-linked position backing and returns the backing claims. |
|
function unwindLinkedPosition(uint256 positionId, uint256 units) external nonReentrant { |
|
Position storage position = _requirePosition(positionId); |
|
if (position.owner != msg.sender) revert NotAuthorized(); |
|
Pool storage target = pools[position.targetPoolId]; |
|
_requireBeforeSettlement(target.marketId); |
|
if (units == 0 || units > position.linkedUnits) revert InvalidAmount(); |
|
|
|
position.linkedUnits -= units; |
|
_burn(msg.sender, claimId(position.targetPoolId), units); |
|
target.totalClaims -= units; |
|
_transfer1155(address(this), msg.sender, claimId(position.backingPoolId), units, ""); |
|
|
|
emit LinkedUnwound(positionId, units); |
|
} |
|
|
|
/// @notice Burns written target claims against upgraded raw position backing and returns raw collateral. |
|
function unwindRawPosition(uint256 positionId, uint256 units) external nonReentrant { |
|
Position storage position = _requirePosition(positionId); |
|
if (position.owner != msg.sender) revert NotAuthorized(); |
|
Pool storage target = pools[position.targetPoolId]; |
|
_requireBeforeSettlement(target.marketId); |
|
if (units == 0 || units > position.rawUnits) revert InvalidAmount(); |
|
|
|
position.rawUnits -= units; |
|
_burn(msg.sender, claimId(position.targetPoolId), units); |
|
target.totalClaims -= units; |
|
_pushToken(_collateralToken(position.targetPoolId), msg.sender, _unitAmount(units, target.collateralUnit)); |
|
|
|
emit PositionRawUnwound(positionId, units); |
|
} |
|
|
|
/// @notice Post-window pro-rata redemption of raw residuals. |
|
function redeemResidual(uint256 poolId, uint256 residualUnits) external nonReentrant { |
|
Pool storage pool = _requirePool(poolId); |
|
_requireSettlement(pool.marketId); |
|
if (residualUnits == 0 || residualUnits > pool.totalResiduals) revert InvalidAmount(); |
|
|
|
uint256 collateralOut = _mulDivDown(_unitAmount(pool.rawUnits, pool.collateralUnit), residualUnits, pool.totalResiduals); |
|
uint256 exerciseOut = _mulDivDown(pool.accruedExercise, residualUnits, pool.totalResiduals); |
|
|
|
_burn(msg.sender, residualId(poolId), residualUnits); |
|
pool.totalResiduals -= residualUnits; |
|
|
|
uint256 rawUnitsBurned = _mulDivDown(pool.rawUnits, residualUnits, pool.totalResiduals + residualUnits); |
|
pool.rawUnits -= rawUnitsBurned; |
|
pool.accruedExercise -= exerciseOut; |
|
|
|
_pushToken(_collateralToken(poolId), msg.sender, collateralOut); |
|
_pushToken(_exerciseToken(poolId), msg.sender, exerciseOut); |
|
|
|
emit ResidualRedeemed(poolId, msg.sender, residualUnits, collateralOut, exerciseOut); |
|
} |
|
|
|
function withdrawPositionProceeds(uint256 positionId) public nonReentrant { |
|
Position storage position = _requirePosition(positionId); |
|
if (position.owner != msg.sender) revert NotAuthorized(); |
|
|
|
uint256 amountA = position.proceedsA; |
|
uint256 amountB = position.proceedsB; |
|
position.proceedsA = 0; |
|
position.proceedsB = 0; |
|
|
|
Market storage market = markets[pools[position.targetPoolId].marketId]; |
|
_pushToken(market.assetA, msg.sender, amountA); |
|
_pushToken(market.assetB, msg.sender, amountB); |
|
|
|
emit PositionProceedsWithdrawn(positionId, msg.sender, amountA, amountB); |
|
} |
|
|
|
/// @notice Post-window close. Upgraded raw units are reclaimed; unassigned linked backing claims are returned. |
|
function closePosition(uint256 positionId) external nonReentrant { |
|
Position storage position = _requirePosition(positionId); |
|
if (position.owner != msg.sender) revert NotAuthorized(); |
|
if (position.closed) revert InvalidPosition(); |
|
_requireSettlement(pools[position.targetPoolId].marketId); |
|
|
|
position.closed = true; |
|
|
|
uint256 linkedUnits = position.linkedUnits; |
|
uint256 rawUnits = position.rawUnits; |
|
position.linkedUnits = 0; |
|
position.rawUnits = 0; |
|
|
|
if (linkedUnits != 0) { |
|
_transfer1155(address(this), msg.sender, claimId(position.backingPoolId), linkedUnits, ""); |
|
} |
|
if (rawUnits != 0) { |
|
_pushToken( |
|
_collateralToken(position.targetPoolId), |
|
msg.sender, |
|
_unitAmount(rawUnits, pools[position.targetPoolId].collateralUnit) |
|
); |
|
} |
|
|
|
uint256 amountA = position.proceedsA; |
|
uint256 amountB = position.proceedsB; |
|
position.proceedsA = 0; |
|
position.proceedsB = 0; |
|
Market storage market = markets[pools[position.targetPoolId].marketId]; |
|
_pushToken(market.assetA, msg.sender, amountA); |
|
_pushToken(market.assetB, msg.sender, amountB); |
|
|
|
emit PositionProceedsWithdrawn(positionId, msg.sender, amountA, amountB); |
|
emit PositionClosed(positionId); |
|
} |
|
|
|
function balanceOf(address account, uint256 id) public view returns (uint256) { |
|
if (account == address(0)) revert InvalidAmount(); |
|
return _balances[account][id]; |
|
} |
|
|
|
function balanceOfBatch( |
|
address[] calldata accounts, |
|
uint256[] calldata ids |
|
) external view returns (uint256[] memory batchBalances) { |
|
if (accounts.length != ids.length) revert InvalidAmount(); |
|
batchBalances = new uint256[](accounts.length); |
|
for (uint256 i = 0; i < accounts.length; ++i) { |
|
batchBalances[i] = balanceOf(accounts[i], ids[i]); |
|
} |
|
} |
|
|
|
function setApprovalForAll(address operator, bool approved) external { |
|
isApprovedForAll[msg.sender][operator] = approved; |
|
emit ApprovalForAll(msg.sender, operator, approved); |
|
} |
|
|
|
function safeTransferFrom( |
|
address from, |
|
address to, |
|
uint256 id, |
|
uint256 value, |
|
bytes calldata data |
|
) external { |
|
if (msg.sender != from && !isApprovedForAll[from][msg.sender]) revert NotAuthorized(); |
|
_transfer1155(from, to, id, value, data); |
|
} |
|
|
|
function safeBatchTransferFrom( |
|
address from, |
|
address to, |
|
uint256[] calldata ids, |
|
uint256[] calldata values, |
|
bytes calldata data |
|
) external { |
|
if (ids.length != values.length) revert InvalidAmount(); |
|
if (msg.sender != from && !isApprovedForAll[from][msg.sender]) revert NotAuthorized(); |
|
if (to == address(0)) revert InvalidAmount(); |
|
|
|
for (uint256 i = 0; i < ids.length; ++i) { |
|
uint256 id = ids[i]; |
|
uint256 value = values[i]; |
|
if (_balances[from][id] < value) revert InsufficientBalance(); |
|
_balances[from][id] -= value; |
|
_balances[to][id] += value; |
|
} |
|
|
|
emit TransferBatch(msg.sender, from, to, ids, values); |
|
_checkBatchReceiver(from, to, ids, values, data); |
|
} |
|
|
|
function onERC1155Received( |
|
address, |
|
address, |
|
uint256, |
|
uint256, |
|
bytes calldata |
|
) external pure returns (bytes4) { |
|
return IERC1155ReceiverMinimal.onERC1155Received.selector; |
|
} |
|
|
|
function onERC1155BatchReceived( |
|
address, |
|
address, |
|
uint256[] calldata, |
|
uint256[] calldata, |
|
bytes calldata |
|
) external pure returns (bytes4) { |
|
return IERC1155ReceiverMinimal.onERC1155BatchReceived.selector; |
|
} |
|
|
|
function supportsInterface(bytes4 interfaceId) external pure returns (bool) { |
|
return interfaceId == 0x01ffc9a7 || interfaceId == 0xd9b67a26 || interfaceId == 0x4e2312e0; |
|
} |
|
|
|
function claimId(uint256 poolId) public pure returns (uint256) { |
|
return uint256(keccak256(abi.encodePacked("SYMMETRIC_OPTIONS_CLAIM", poolId))); |
|
} |
|
|
|
function residualId(uint256 poolId) public pure returns (uint256) { |
|
return uint256(keccak256(abi.encodePacked("SYMMETRIC_OPTIONS_RESIDUAL", poolId))); |
|
} |
|
|
|
function collateralToken(uint256 poolId) external view returns (address) { |
|
return _collateralToken(poolId); |
|
} |
|
|
|
function exerciseToken(uint256 poolId) external view returns (address) { |
|
return _exerciseToken(poolId); |
|
} |
|
|
|
function isExerciseWindow(uint256 marketId) external view returns (bool) { |
|
Market storage market = markets[marketId]; |
|
return |
|
market.exists && |
|
block.timestamp >= market.expiry && |
|
block.timestamp < market.expiry + market.exerciseWindow; |
|
} |
|
|
|
function isSettlement(uint256 marketId) external view returns (bool) { |
|
Market storage market = markets[marketId]; |
|
return market.exists && block.timestamp >= market.expiry + market.exerciseWindow; |
|
} |
|
|
|
function _sourceCollateral(uint256 poolId, uint256 units, uint256 depth) private returns (uint256 releasedAmount) { |
|
if (depth > MAX_CASCADE_STEPS) revert CascadeTooDeep(); |
|
Pool storage pool = pools[poolId]; |
|
uint256 remaining = units; |
|
|
|
uint256 rawUnits = remaining < pool.rawUnits ? remaining : pool.rawUnits; |
|
if (rawUnits != 0) { |
|
pool.rawUnits -= rawUnits; |
|
pool.accruedExercise += _unitAmount(rawUnits, pool.exerciseUnit); |
|
releasedAmount += _unitAmount(rawUnits, pool.collateralUnit); |
|
remaining -= rawUnits; |
|
} |
|
|
|
while (remaining != 0) { |
|
uint256 positionId = pool.firstOpenPosition; |
|
if (positionId > positionCount) revert InsufficientBalance(); |
|
|
|
Position storage position = positions[positionId]; |
|
if (position.targetPoolId != poolId || position.closed || (position.linkedUnits == 0 && position.rawUnits == 0)) { |
|
pool.firstOpenPosition = positionId + 1; |
|
continue; |
|
} |
|
|
|
uint256 fromRawPosition = remaining < position.rawUnits ? remaining : position.rawUnits; |
|
if (fromRawPosition != 0) { |
|
position.rawUnits -= fromRawPosition; |
|
_creditPosition(position, !_collateralIsAssetA(poolId), _unitAmount(fromRawPosition, pool.exerciseUnit)); |
|
releasedAmount += _unitAmount(fromRawPosition, pool.collateralUnit); |
|
remaining -= fromRawPosition; |
|
continue; |
|
} |
|
|
|
uint256 fromLinked = remaining < position.linkedUnits ? remaining : position.linkedUnits; |
|
if (fromLinked == 0) { |
|
pool.firstOpenPosition = positionId + 1; |
|
continue; |
|
} |
|
|
|
position.linkedUnits -= fromLinked; |
|
Pool storage backing = pools[position.backingPoolId]; |
|
|
|
_burn(address(this), claimId(position.backingPoolId), fromLinked); |
|
backing.totalClaims -= fromLinked; |
|
|
|
uint256 backingReleased = _sourceCollateral(position.backingPoolId, fromLinked, depth + 1); |
|
uint256 targetCollateral = _unitAmount(fromLinked, pool.collateralUnit); |
|
uint256 targetExercise = _unitAmount(fromLinked, pool.exerciseUnit); |
|
uint256 backingExercise = _unitAmount(fromLinked, backing.exerciseUnit); |
|
|
|
if (backingReleased < targetCollateral || targetExercise < backingExercise) revert IncompatibleBacking(); |
|
|
|
releasedAmount += targetCollateral; |
|
_creditPosition(position, _collateralIsAssetA(poolId), backingReleased - targetCollateral); |
|
_creditPosition(position, !_collateralIsAssetA(poolId), targetExercise - backingExercise); |
|
remaining -= fromLinked; |
|
} |
|
} |
|
|
|
function _requireEligibleBacking(uint256 targetPoolId, uint256 backingPoolId) private view { |
|
Pool storage target = pools[targetPoolId]; |
|
Pool storage backing = pools[backingPoolId]; |
|
if (!target.exists || !backing.exists || targetPoolId == backingPoolId) revert IncompatibleBacking(); |
|
if (target.marketId != backing.marketId) revert IncompatibleBacking(); |
|
if (target.collateralIsA != backing.collateralIsA) revert IncompatibleBacking(); |
|
if (backing.collateralUnit < target.collateralUnit) revert IncompatibleBacking(); |
|
if (backing.exerciseUnit > target.exerciseUnit) revert IncompatibleBacking(); |
|
} |
|
|
|
function _requirePool(uint256 poolId) private view returns (Pool storage pool) { |
|
pool = pools[poolId]; |
|
if (!pool.exists) revert InvalidPool(); |
|
} |
|
|
|
function _requirePosition(uint256 positionId) private view returns (Position storage position) { |
|
position = positions[positionId]; |
|
if (position.owner == address(0)) revert InvalidPosition(); |
|
} |
|
|
|
function _requireBeforeSettlement(uint256 marketId) private view { |
|
Market storage market = markets[marketId]; |
|
if (!market.exists || block.timestamp >= market.expiry + market.exerciseWindow) revert InvalidPhase(); |
|
} |
|
|
|
function _requireExerciseWindow(uint256 marketId) private view { |
|
Market storage market = markets[marketId]; |
|
if (!market.exists || block.timestamp < market.expiry || block.timestamp >= market.expiry + market.exerciseWindow) { |
|
revert InvalidPhase(); |
|
} |
|
} |
|
|
|
function _requireSettlement(uint256 marketId) private view { |
|
Market storage market = markets[marketId]; |
|
if (!market.exists || block.timestamp < market.expiry + market.exerciseWindow) revert InvalidPhase(); |
|
} |
|
|
|
function _collateralToken(uint256 poolId) private view returns (address) { |
|
Pool storage pool = pools[poolId]; |
|
Market storage market = markets[pool.marketId]; |
|
return pool.collateralIsA ? market.assetA : market.assetB; |
|
} |
|
|
|
function _exerciseToken(uint256 poolId) private view returns (address) { |
|
Pool storage pool = pools[poolId]; |
|
Market storage market = markets[pool.marketId]; |
|
return pool.collateralIsA ? market.assetB : market.assetA; |
|
} |
|
|
|
function _collateralIsAssetA(uint256 poolId) private view returns (bool) { |
|
return pools[poolId].collateralIsA; |
|
} |
|
|
|
function _creditPosition(Position storage position, bool assetA, uint256 amount) private { |
|
if (amount == 0) return; |
|
if (assetA) { |
|
position.proceedsA += amount; |
|
} else { |
|
position.proceedsB += amount; |
|
} |
|
} |
|
|
|
function _mint(address to, uint256 id, uint256 value) private { |
|
if (to == address(0)) revert InvalidAmount(); |
|
_balances[to][id] += value; |
|
emit TransferSingle(msg.sender, address(0), to, id, value); |
|
} |
|
|
|
function _burn(address from, uint256 id, uint256 value) private { |
|
if (_balances[from][id] < value) revert InsufficientBalance(); |
|
_balances[from][id] -= value; |
|
emit TransferSingle(msg.sender, from, address(0), id, value); |
|
} |
|
|
|
function _transfer1155(address from, address to, uint256 id, uint256 value, bytes memory data) private { |
|
if (to == address(0)) revert InvalidAmount(); |
|
if (_balances[from][id] < value) revert InsufficientBalance(); |
|
_balances[from][id] -= value; |
|
_balances[to][id] += value; |
|
emit TransferSingle(msg.sender, from, to, id, value); |
|
_checkReceiver(from, to, id, value, data); |
|
} |
|
|
|
function _checkReceiver(address from, address to, uint256 id, uint256 value, bytes memory data) private { |
|
if (to.code.length == 0) return; |
|
bytes4 response = IERC1155ReceiverMinimal(to).onERC1155Received(msg.sender, from, id, value, data); |
|
if (response != IERC1155ReceiverMinimal.onERC1155Received.selector) revert UnsafeToken(); |
|
} |
|
|
|
function _checkBatchReceiver( |
|
address from, |
|
address to, |
|
uint256[] calldata ids, |
|
uint256[] calldata values, |
|
bytes calldata data |
|
) private { |
|
if (to.code.length == 0) return; |
|
bytes4 response = IERC1155ReceiverMinimal(to).onERC1155BatchReceived(msg.sender, from, ids, values, data); |
|
if (response != IERC1155ReceiverMinimal.onERC1155BatchReceived.selector) revert UnsafeToken(); |
|
} |
|
|
|
function _pullToken(address token, address from, uint256 amount) private { |
|
if (amount == 0) return; |
|
uint256 beforeBalance = IERC20Minimal(token).balanceOf(address(this)); |
|
(bool ok, bytes memory data) = token.call( |
|
abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, address(this), amount) |
|
); |
|
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert UnsafeToken(); |
|
uint256 afterBalance = IERC20Minimal(token).balanceOf(address(this)); |
|
if (afterBalance - beforeBalance != amount) revert UnsafeToken(); |
|
} |
|
|
|
function _pushToken(address token, address to, uint256 amount) private { |
|
if (amount == 0) return; |
|
uint256 beforeBalance = IERC20Minimal(token).balanceOf(address(this)); |
|
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, amount)); |
|
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert UnsafeToken(); |
|
uint256 afterBalance = IERC20Minimal(token).balanceOf(address(this)); |
|
if (beforeBalance - afterBalance != amount) revert UnsafeToken(); |
|
} |
|
|
|
function _unitAmount(uint256 units, uint256 unitAmount) private pure returns (uint256) { |
|
return _mulDivUp(units, unitAmount, UNIT); |
|
} |
|
|
|
function _mulDivDown(uint256 x, uint256 y, uint256 denominator) private pure returns (uint256) { |
|
return (x * y) / denominator; |
|
} |
|
|
|
function _mulDivUp(uint256 x, uint256 y, uint256 denominator) private pure returns (uint256) { |
|
if (x == 0 || y == 0) return 0; |
|
return ((x * y) - 1) / denominator + 1; |
|
} |
|
} |