diff --git a/package.json b/package.json index 22fb579..44c3dac 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "viem": "2.38.3" }, "scripts": { - "setup": "npm ci --ignore-scripts && npm run contracts", - "contracts": "cd solidity && npm ci --ignore-scripts && npm run compile", - "test": "cd solidity && npm run test" + "setup": "npm ci --ignore-scripts && npm run setup-contracts", + "setup-contracts": "cd solidity && npm run setup", + "compile-contracts": "cd solidity && npm run compile-contracts", + "test": "cd solidity && npm run test", + "test-peripherals": "cd solidity && npm run compile-contracts && npm run test-peripherals" } } diff --git a/solidity/contracts/ReputationToken.sol b/solidity/contracts/ReputationToken.sol index 38ebdec..40efd44 100644 --- a/solidity/contracts/ReputationToken.sol +++ b/solidity/contracts/ReputationToken.sol @@ -5,7 +5,7 @@ import './ERC20.sol'; contract ReputationToken is ERC20 { - address public zoltar; + address public immutable zoltar; constructor(address _zoltar) ERC20('Reputation', 'REP') { zoltar = _zoltar; diff --git a/solidity/contracts/peripherals/Auction.sol b/solidity/contracts/peripherals/Auction.sol new file mode 100644 index 0000000..69da1cb --- /dev/null +++ b/solidity/contracts/peripherals/Auction.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; +uint256 constant AUCTION_TIME = 1 weeks; + +contract Auction { + mapping(address => uint256) public purchasedRep; + uint256 public totalRepPurchased; + uint256 public repAvailable; + uint256 public auctionStarted; + uint256 public ethAmountToBuy; + bool public finalized; + address immutable owner; + + event Participated(address user, uint256 repAmount, uint256 ethAmount, uint256 totalRepPurchased); + event FinalizedAuction(address user, uint256 repAmount, uint256 ethAmount); + event AuctionStarted(uint256 ethAmountToBuy, uint256 repAvailable); + + constructor(address _owner) { + owner = _owner; + } + + function participate(uint256 repToBuy) public payable { + require(auctionStarted > 0, 'Auction needs to have started'); + require(!finalized, 'Already finalized'); + require(msg.value > 0, 'need to invest with eth!'); + require(address(this).balance <= ethAmountToBuy, 'attempting to overfund'); + require(totalRepPurchased + repToBuy <= repAvailable, 'attempt to buy too much rep'); + purchasedRep[msg.sender] = repToBuy; // todo, currently anyone can buy with any price + totalRepPurchased += repToBuy; + emit Participated(msg.sender, repToBuy, msg.value, totalRepPurchased); + } + + function startAuction(uint256 _ethAmountToBuy, uint256 _repAvailable) public { + require(auctionStarted == 0, 'Already started!'); + auctionStarted = block.timestamp; + ethAmountToBuy = _ethAmountToBuy; + repAvailable = _repAvailable; + emit AuctionStarted(ethAmountToBuy, repAvailable); + } + + function finalizeAuction() public { + //require(block.timestamp > auctionStarted + AUCTION_TIME, 'Auction needs to have ended first'); // caller checks + require(msg.sender == owner, 'Only owner can finalize'); + require(!finalized, 'Already finalized'); + finalized = true; + (bool sent, ) = payable(owner).call{value: address(this).balance}(''); + require(sent, 'Failed to send Ether'); + } +} diff --git a/solidity/contracts/peripherals/CompleteSet.sol b/solidity/contracts/peripherals/CompleteSet.sol new file mode 100644 index 0000000..64f5ae2 --- /dev/null +++ b/solidity/contracts/peripherals/CompleteSet.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +import '../ERC20.sol'; + +contract CompleteSet is ERC20 { + + address public immutable securityPool; + + constructor(address _securityPool) ERC20('CompleteSet', 'CS') { + securityPool = _securityPool; + } + + function mint(address account, uint256 value) external { + require(msg.sender == securityPool, 'Not securityPool'); + _mint(account, value); + } + + function burn(address account, uint256 value) external { + require(msg.sender == securityPool, 'Not securityPool'); + _burn(account, value); + } + + function splitSomehow() external { + // we need to somehow split this in a way that balances are maintained + } +} diff --git a/solidity/contracts/peripherals/PriceOracleManagerAndOperatorQueuer.sol b/solidity/contracts/peripherals/PriceOracleManagerAndOperatorQueuer.sol new file mode 100644 index 0000000..eb5606a --- /dev/null +++ b/solidity/contracts/peripherals/PriceOracleManagerAndOperatorQueuer.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +import { IWeth9 } from './interfaces/IWeth9.sol'; +import { OpenOracle } from './openOracle/OpenOracle.sol'; +import { ReputationToken } from '../ReputationToken.sol'; +import { ISecurityPool } from './interfaces/ISecurityPool.sol'; + +// price oracle +uint256 constant PRICE_VALID_FOR_SECONDS = 1 hours; + +enum OperationType { + Liquidation, + WithdrawRep, + SetSecurityBondsAllowance +} + +uint256 constant gasConsumedOpenOracleReportPrice = 100000; //TODO +uint32 constant gasConsumedSettlement = 1000000; //TODO + +IWeth9 constant WETH = IWeth9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + +struct QueuedOperation { + OperationType operation; + address initiatorVault; + address targetVault; + uint256 amount; +} + +contract PriceOracleManagerAndOperatorQueuer { + uint256 public pendingReportId; + uint256 public queuedPendingOperationId; + uint256 public lastSettlementTimestamp; + uint256 public lastPrice; // (REP * PRICE_PRECISION) / ETH; + ReputationToken immutable reputationToken; + ISecurityPool public immutable securityPool; + OpenOracle public immutable openOracle; + + event PriceReported(uint256 reportId, uint256 price); + event ExecutetedQueuedOperation(uint256 operationId, OperationType operation, bool success); + + // operation queuing + uint256 public previousQueuedOperationId; + mapping(uint256 => QueuedOperation) public queuedOperations; + + constructor(OpenOracle _openOracle, ISecurityPool _securityPool, ReputationToken _reputationToken) { + reputationToken = _reputationToken; + securityPool = _securityPool; + openOracle = _openOracle; + } + + function setRepEthPrice(uint256 _lastPrice) public { + require(msg.sender == address(securityPool), 'only security pool can set'); + lastPrice = _lastPrice; + } + + function getRequestPriceEthCost() public view returns (uint256) {// todo, probably something else + // https://github.com/j0i0m0b0o/openOracleBase/blob/feeTokenChange/src/OpenOracle.sol#L100 + uint256 ethCost = block.basefee * 4 * (gasConsumedSettlement + gasConsumedOpenOracleReportPrice); // todo, probably something else + return ethCost; + } + function requestPrice() public payable { + require(pendingReportId == 0, 'Already pending request'); + // https://github.com/j0i0m0b0o/openOracleBase/blob/feeTokenChange/src/OpenOracle.sol#L100 + uint256 ethCost = getRequestPriceEthCost();// todo, probably something else + require(msg.value >= ethCost, 'not big enough eth bounty'); + + // TODO, research more on how to set these params + OpenOracle.CreateReportParams memory reportparams = OpenOracle.CreateReportParams({ + exactToken1Report: 26392439800,//block.basefee * 200 / lastPrice, // initial oracle liquidity in token1 + escalationHalt: reputationToken.totalSupply() / 100000, // amount of token1 past which escalation stops but disputes can still happen + settlerReward: block.basefee * 2 * gasConsumedOpenOracleReportPrice, // eth paid to settler in wei + token1Address: address(reputationToken), // address of token1 in the oracle report instance + settlementTime: 15 * 12,//~15 blocks // report instance can settle if no disputes within this timeframe + disputeDelay: 0, // time disputes must wait after every new report + protocolFee: 0, // fee paid to protocolFeeRecipient. 1000 = 0.01% + token2Address: address(WETH), // address of token2 in the oracle report instance + callbackGasLimit: gasConsumedSettlement, // gas the settlement callback must use + feePercentage: 10000, // 0.1% atm, TODO,// fee paid to previous reporter. 1000 = 0.01% + multiplier: 140, // amount by which newAmount1 must increase versus old amount1. 140 = 1.4x + timeType: true, // true for block timestamp, false for block number + trackDisputes: false, // true keeps a readable dispute history for smart contracts + keepFee: false, // true means initial reporter keeps the initial reporter reward. if false, it goes to protocolFeeRecipient + callbackContract: address(this), // contract address for settle to call back into + callbackSelector: this.openOracleReportPrice.selector, // method in the callbackContract you want called. + protocolFeeRecipient: address(0x0), // address that receives protocol fees and initial reporter rewards if keepFee set to false + feeToken: true //if true, protocol fees + fees paid to previous reporter are in tokenToSwap. if false, in not(tokenToSwap) + }); //typically if feeToken true, fees are paid in less valuable token, if false, fees paid in more valuable token + + pendingReportId = openOracle.createReportInstance{value: ethCost}(reportparams); + } + function openOracleReportPrice(uint256 reportId, uint256 price, uint256, address, address) external { + require(msg.sender == address(openOracle), 'only open oracle can call'); + require(reportId == pendingReportId, 'not report created by us'); + pendingReportId = 0; + lastSettlementTimestamp = block.timestamp; + lastPrice = price; + emit PriceReported(reportId, lastPrice); + if (queuedPendingOperationId != 0) { // todo we maybe should allow executing couple operations? + executeQueuedOperation(queuedPendingOperationId); + queuedPendingOperationId = 0; + } + } + + function isPriceValid() public view returns (bool) { + return lastSettlementTimestamp + PRICE_VALID_FOR_SECONDS > block.timestamp; + } + + function requestPriceIfNeededAndQueueOperation(OperationType operation, address targetVault, uint256 amount) public payable { + require(amount > 0, 'need to do non zero operation'); + previousQueuedOperationId++; + queuedOperations[previousQueuedOperationId] = QueuedOperation({ + operation: operation, + initiatorVault: msg.sender, + targetVault: targetVault, + amount: amount + }); + if (isPriceValid()) { + executeQueuedOperation(previousQueuedOperationId); + } else if (queuedPendingOperationId == 0) { + queuedPendingOperationId = previousQueuedOperationId; + requestPrice(); + } + } + + function executeQueuedOperation(uint256 operationId) public { + require(queuedOperations[operationId].amount > 0, 'no such operation or already executed'); + require(isPriceValid()); + // todo, we should allow these operations here to fail, but solidity try catch doesnt work inside the same contract + if (queuedOperations[operationId].operation == OperationType.Liquidation) { + try securityPool.performLiquidation(queuedOperations[operationId].initiatorVault, queuedOperations[operationId].targetVault, queuedOperations[operationId].amount) { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, true); + } catch { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, false); + } + } else if(queuedOperations[operationId].operation == OperationType.WithdrawRep) { + try securityPool.performWithdrawRep(queuedOperations[operationId].initiatorVault, queuedOperations[operationId].amount) { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, true); + } catch { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, false); + } + } else { + try securityPool.performSetSecurityBondsAllowance(queuedOperations[operationId].initiatorVault, queuedOperations[operationId].amount) { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, true); + } catch { + emit ExecutetedQueuedOperation(operationId, queuedOperations[operationId].operation, false); + } + } + queuedOperations[operationId].amount = 0; + } +} diff --git a/solidity/contracts/peripherals/SecurityPool.sol b/solidity/contracts/peripherals/SecurityPool.sol new file mode 100644 index 0000000..91fcc80 --- /dev/null +++ b/solidity/contracts/peripherals/SecurityPool.sol @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +import { Auction } from './Auction.sol'; +import { Zoltar } from '../Zoltar.sol'; +import { ReputationToken } from '../ReputationToken.sol'; +import { CompleteSet } from './CompleteSet.sol'; +import { PriceOracleManagerAndOperatorQueuer } from './PriceOracleManagerAndOperatorQueuer.sol'; +import { ISecurityPool, SecurityVault, SystemState, QuestionOutcome, ISecurityPoolFactory } from './interfaces/ISecurityPool.sol'; +import { OpenOracle } from './openOracle/OpenOracle.sol'; +import { SecurityPoolUtils } from './SecurityPoolUtils.sol'; + +// Security pool for one question, one universe, one denomination (ETH) +contract SecurityPool is ISecurityPool { + uint56 public immutable questionId; + uint192 public immutable universeId; + + Zoltar public immutable zoltar; + uint256 public securityBondAllowance; + uint256 public completeSetCollateralAmount; // amount of eth that is backing complete sets, `address(this).balance - completeSetCollateralAmount` are the fees belonging to REP pool holders + uint256 public poolOwnershipDenominator; + uint256 public repAtFork; + uint256 public migratedRep; + uint256 public securityMultiplier; + + uint256 public feesAccrued; + uint256 public lastUpdatedFeeAccumulator; + uint256 public currentRetentionRate; + + uint256 public securityPoolForkTriggeredTimestamp; + + mapping(address => SecurityVault) public securityVaults; + mapping(address => bool) public claimedAuctionProceeds; + + ISecurityPool[3] public children; + ISecurityPool immutable public parent; + + uint256 public truthAuctionStarted; + SystemState public systemState; + + CompleteSet public immutable completeSet; + Auction public immutable truthAuction; + ReputationToken public repToken; + ISecurityPoolFactory public immutable securityPoolFactory; + + PriceOracleManagerAndOperatorQueuer public priceOracleManagerAndOperatorQueuer; + OpenOracle public openOracle; + + event SecurityBondAllowanceChange(address vault, uint256 from, uint256 to); + event PerformWithdrawRep(address vault, uint256 amount); + event PoolRetentionRateChanged(uint256 retentionRate); + event ForkSecurityPool(uint256 repAtFork); + event MigrateVault(address vault, QuestionOutcome outcome, uint256 poolOwnership, uint256 securityBondAllowance); + event TruthAuctionStarted(uint256 completeSetCollateralAmount, uint256 repMigrated, uint256 repAtFork); + event TruthAuctionFinalized(); + event ClaimAuctionProceeds(address vault, uint256 amount, uint256 poolOwnershipAmount, uint256 poolOwnershipDenominator); + event MigrateRepFromParent(address vault, uint256 parentSecurityBondAllowance, uint256 parentpoolOwnership); + event DepositRep(address vault, uint256 repAmount, uint256 poolOwnership); + + modifier isOperational { + (,, uint256 forkTime) = zoltar.universes(universeId); + require(forkTime == 0, 'Zoltar has forked'); + require(systemState == SystemState.Operational, 'System is not operational'); + _; + } + + constructor(ISecurityPoolFactory _securityPoolFactory, OpenOracle _openOracle, ISecurityPool _parent, Zoltar _zoltar, uint192 _universeId, uint56 _questionId, uint256 _securityMultiplier) { + universeId = _universeId; + securityPoolFactory = _securityPoolFactory; + questionId = _questionId; + securityMultiplier = _securityMultiplier; + zoltar = _zoltar; + parent = _parent; + openOracle = _openOracle; + if (address(parent) == address(0x0)) { // origin universe never does truthAuction + systemState = SystemState.Operational; + } else { + systemState = SystemState.ForkMigration; + truthAuction = new Auction{ salt: bytes32(uint256(0x1)) }(address(this)); + } + // todo, we can probably do these smarter so that we don't need migration + completeSet = new CompleteSet{ salt: bytes32(uint256(0x1)) }(address(this)); + } + + function setStartingParams(uint256 _currentRetentionRate, uint256 _repEthPrice, uint256 _completeSetCollateralAmount) public { + require(msg.sender == address(securityPoolFactory), 'only callable by securityPoolFactory'); + lastUpdatedFeeAccumulator = block.timestamp; + currentRetentionRate = _currentRetentionRate; + completeSetCollateralAmount = _completeSetCollateralAmount; + (repToken,,) = zoltar.universes(universeId); + priceOracleManagerAndOperatorQueuer = new PriceOracleManagerAndOperatorQueuer{ salt: bytes32(uint256(0x1)) }(openOracle, this, repToken); + priceOracleManagerAndOperatorQueuer.setRepEthPrice(_repEthPrice); + } + + function updateCollateralAmount() public { + (uint64 endTime,,,) = zoltar.questions(questionId); + uint256 clampedCurrentTimestamp = block.timestamp > endTime ? endTime : block.timestamp; + uint256 clampedLastUpdatedFeeAccumulator = lastUpdatedFeeAccumulator > endTime ? endTime : lastUpdatedFeeAccumulator; + uint256 timeDelta = clampedCurrentTimestamp - clampedLastUpdatedFeeAccumulator; + if (timeDelta == 0) return; + + uint256 newCompleteSetCollateralAmount = completeSetCollateralAmount * SecurityPoolUtils.rpow(currentRetentionRate, timeDelta, SecurityPoolUtils.PRICE_PRECISION) / SecurityPoolUtils.PRICE_PRECISION; + feesAccrued += completeSetCollateralAmount - newCompleteSetCollateralAmount; + completeSetCollateralAmount = newCompleteSetCollateralAmount; + lastUpdatedFeeAccumulator = block.timestamp; + } + + function updateRetentionRate() public { + if (securityBondAllowance == 0) return; + if (systemState != SystemState.Operational) return; // if system state is not operational do not change fees + currentRetentionRate = SecurityPoolUtils.calculateRetentionRate(completeSetCollateralAmount, securityBondAllowance); + emit PoolRetentionRateChanged(currentRetentionRate); + } + + // I wonder if we want to delay the payments and smooth them out to avoid flashloan attacks? + function updateVaultFees(address vault) public { + updateCollateralAmount(); + require(feesAccrued >= securityVaults[vault].feeAccumulator, 'fee accumulator too high? should not happen'); + uint256 accumulatorDiff = feesAccrued - securityVaults[vault].feeAccumulator; + uint256 fees = (securityVaults[vault].securityBondAllowance * accumulatorDiff) / SecurityPoolUtils.PRICE_PRECISION; + securityVaults[vault].feeAccumulator = feesAccrued; + securityVaults[vault].unpaidEthFees += fees; + } + + function redeemFees(address vault) public { + uint256 fees = securityVaults[vault].unpaidEthFees; + securityVaults[vault].unpaidEthFees = 0; + (bool sent, ) = payable(vault).call{ value: fees }(''); + require(sent, 'Failed to send Ether'); + } + + //////////////////////////////////////// + // withdrawing rep + //////////////////////////////////////// + + function performWithdrawRep(address vault, uint256 repAmount) public isOperational { + require(msg.sender == address(priceOracleManagerAndOperatorQueuer), 'only priceOracleManagerAndOperatorQueuer can call'); + require(priceOracleManagerAndOperatorQueuer.isPriceValid(), 'no valid price'); + uint256 oldRep = poolOwnershipToRep(securityVaults[vault].poolOwnership); + require(oldRep >= repAmount, 'cannot withdraw that much'); + require((oldRep - repAmount) * SecurityPoolUtils.PRICE_PRECISION >= securityVaults[vault].securityBondAllowance * priceOracleManagerAndOperatorQueuer.lastPrice(), 'Local Security Bond Alowance broken'); + require((repToken.balanceOf(address(this)) - repAmount) * SecurityPoolUtils.PRICE_PRECISION >= securityBondAllowance * priceOracleManagerAndOperatorQueuer.lastPrice(), 'Global Security Bond Alowance broken'); + + uint256 poolOwnership = repToPoolOwnership(repAmount); + securityVaults[vault].poolOwnership -= poolOwnership; + repToken.transfer(vault, repAmount); + poolOwnershipDenominator -= poolOwnership; + require(oldRep - repAmount >= SecurityPoolUtils.MIN_REP_DEPOSIT || oldRep - repAmount == 0, 'min deposit requirement'); + emit PerformWithdrawRep(vault, repAmount); + } + + function repToPoolOwnership(uint256 repAmount) public view returns (uint256) { + uint256 totalRep = repToken.balanceOf(address(this)); + if (poolOwnershipDenominator == 0 || totalRep == 0) return repAmount * SecurityPoolUtils.PRICE_PRECISION; + return repAmount * poolOwnershipDenominator / totalRep; + } + + function poolOwnershipToRep(uint256 poolOwnership) public view returns (uint256) { + uint256 totalRep = repToken.balanceOf(address(this)); + if (poolOwnershipDenominator == 0) return 0; + return poolOwnership * totalRep / poolOwnershipDenominator; + } + + // todo, an owner can save their vault from liquidation if they deposit REP after the liquidation price query is triggered, we probably want to lock the vault from deposits if this has been triggered? + function depositRep(uint256 repAmount) public isOperational { + uint256 poolOwnership = repToPoolOwnership(repAmount); + poolOwnershipDenominator += poolOwnership; + repToken.transferFrom(msg.sender, address(this), repAmount); + securityVaults[msg.sender].poolOwnership += poolOwnership; + require(poolOwnershipToRep(securityVaults[msg.sender].poolOwnership) >= SecurityPoolUtils.MIN_REP_DEPOSIT, 'min deposit requirement'); + emit DepositRep(msg.sender, repAmount, securityVaults[msg.sender].poolOwnership); + } + + //////////////////////////////////////// + // liquidating vault + //////////////////////////////////////// + + //price = (amount1 * PRICE_PRECISION) / amount2; + // price = REP * PRICE_PRECISION / ETH + // liquidation moves share of debt and rep to another pool which need to remain non-liquidable + // this is currently very harsh, as we steal all the rep and debt from the pool + function performLiquidation(address callerVault, address targetVaultAddress, uint256 debtAmount) public isOperational { + /* require(msg.sender == address(priceOracleManagerAndOperatorQueuer), 'only priceOracleManagerAndOperatorQueuer can call'); + require(priceOracleManagerAndOperatorQueuer.isPriceValid(), 'no valid price'); + updateVaultFees(targetVaultAddress); + updateVaultFees(callerVault); + uint256 vaultsSecurityBondAllowance = securityVaults[targetVaultAddress].securityBondAllowance; + uint256 vaultsRepDeposit = securityVaults[targetVaultAddress].poolOwnership * repToken.balanceOf(address(this)) / poolOwnershipDenominator; + require(vaultsSecurityBondAllowance * securityMultiplier * priceOracleManagerAndOperatorQueuer.lastPrice() > vaultsRepDeposit * PRICE_PRECISION, 'vault need to be liquidable'); + + uint256 debtToMove = debtAmount > securityVaults[callerVault].securityBondAllowance ? securityVaults[callerVault].securityBondAllowance : debtAmount; + require(debtToMove > 0, 'no debt to move'); + uint256 repToMove = securityVaults[callerVault].poolOwnership * repToken.balanceOf(address(this)) / poolOwnershipDenominator * debtToMove / securityVaults[callerVault].securityBondAllowance; + require((securityVaults[callerVault].securityBondAllowance+debtToMove) * securityMultiplier * priceOracleManagerAndOperatorQueuer.lastPrice() <= (securityVaults[callerVault].poolOwnership + repToMove) * PRICE_PRECISION, 'New pool would be liquidable!'); + securityVaults[targetVaultAddress].securityBondAllowance -= debtToMove; + securityVaults[targetVaultAddress].poolOwnership -= repToMove * poolOwnershipDenominator / repToken.balanceOf(address(this)); + + securityVaults[callerVault].securityBondAllowance += debtToMove; + securityVaults[callerVault].poolOwnership += repToMove * poolOwnershipDenominator / repToken.balanceOf(address(this)); + + require(securityVaults[targetVaultAddress].poolOwnership > MIN_REP_DEPOSIT * repToken.balanceOf(address(this)) / poolOwnershipDenominator || securityVaults[targetVaultAddress].poolOwnership == 0, 'min deposit requirement'); + require(securityVaults[targetVaultAddress].securityBondAllowance > MIN_SECURITY_BOND_DEBT || securityVaults[targetVaultAddress].securityBondAllowance == 0, 'min deposit requirement'); + require(securityVaults[callerVault].securityBondAllowance > MIN_SECURITY_BOND_DEBT || securityVaults[callerVault].securityBondAllowance == 0, 'min deposit requirement'); + */} + + //////////////////////////////////////// + // set security bond allowance + //////////////////////////////////////// + + function performSetSecurityBondsAllowance(address callerVault, uint256 amount) public isOperational { + updateVaultFees(callerVault); + require(msg.sender == address(priceOracleManagerAndOperatorQueuer), 'only priceOracleManagerAndOperatorQueuer can call'); + require(priceOracleManagerAndOperatorQueuer.isPriceValid(), 'no valid price'); + require(poolOwnershipToRep(securityVaults[callerVault].poolOwnership) * SecurityPoolUtils.PRICE_PRECISION > amount * priceOracleManagerAndOperatorQueuer.lastPrice()); + require(repToken.balanceOf(address(this)) * SecurityPoolUtils.PRICE_PRECISION > amount * priceOracleManagerAndOperatorQueuer.lastPrice()); + uint256 oldAllowance = securityVaults[callerVault].securityBondAllowance; + securityBondAllowance += amount; + securityBondAllowance -= oldAllowance; + //require(securityBondAllowance >= completeSetCollateralAmount, 'minted too many complete sets to allow this'); + securityVaults[callerVault].securityBondAllowance = amount; + //require(securityVaults[callerVault].securityBondAllowance > MIN_SECURITY_BOND_DEBT || securityVaults[callerVault].securityBondAllowance == 0, 'min deposit requirement'); + emit SecurityBondAllowanceChange(callerVault, oldAllowance, amount); + updateRetentionRate(); + } + + //////////////////////////////////////// + // Complete Sets + //////////////////////////////////////// + function createCompleteSet() payable public isOperational { + require(msg.value > 0, 'need to send eth'); + require(systemState == SystemState.Operational, 'system is not Operational'); //todo, we want to be able to create complete sets in the children right away, figure accounting out + updateCollateralAmount(); + require(securityBondAllowance - completeSetCollateralAmount >= msg.value, 'no capacity to create that many sets'); + uint256 amountToMint = completeSet.totalSupply() == completeSetCollateralAmount ? msg.value : msg.value * completeSet.totalSupply() / completeSetCollateralAmount; + completeSet.mint(msg.sender, amountToMint); + completeSetCollateralAmount += msg.value; + updateRetentionRate(); + } + + function redeemCompleteSet(uint256 amount) public isOperational { + require(systemState == SystemState.Operational, 'system is not Operational'); // todo, we want to allow people to exit, but for accounting purposes that is difficult but maybe there's a way? + updateCollateralAmount(); + // takes in complete set and releases security bond and eth + uint256 ethValue = amount * completeSetCollateralAmount / completeSet.totalSupply(); + completeSet.burn(msg.sender, amount); + completeSetCollateralAmount -= ethValue; + updateRetentionRate(); + (bool sent, ) = payable(msg.sender).call{value: ethValue}(''); + require(sent, 'Failed to send Ether'); + } + + /* + function redeemShare() isOperational public { + require(zoltar.isFinalized(universeId, questionId), 'Question has not finalized!'); + //convertes yes,no or invalid share to 1 eth each, depending on market outcome + } + */ + + //////////////////////////////////////// + // FORKING (migrate vault (oi+rep), truth truthAuction) + //////////////////////////////////////// + function forkSecurityPool() public { + (,, uint256 forkTime) = zoltar.universes(universeId); + require(forkTime > 0, 'Zoltar needs to have forked before Security Pool can do so'); + require(systemState == SystemState.Operational, 'System needs to be operational to trigger fork'); + require(securityPoolForkTriggeredTimestamp == 0, 'fork already triggered'); + require(!zoltar.isFinalized(universeId, questionId), 'question has been finalized already'); + systemState = SystemState.PoolForked; + securityPoolForkTriggeredTimestamp = block.timestamp; + repAtFork = repToken.balanceOf(address(this)); + // TODO, handle case where parent repAtFork == 0 + emit ForkSecurityPool(repAtFork); + repToken.approve(address(zoltar), repAtFork); + zoltar.splitRep(universeId); + // we could pay the caller basefee*2 out of Open interest we have? + } + + // migrates vault into outcome universe after fork + function migrateVault(QuestionOutcome outcome) public { // called on parent + require(systemState == SystemState.PoolForked, 'Pool needs to have forked'); + require(block.timestamp <= securityPoolForkTriggeredTimestamp + SecurityPoolUtils.MIGRATION_TIME , 'migration time passed'); + require(securityVaults[msg.sender].poolOwnership > 0, 'Vault has no rep to migrate'); + updateVaultFees(msg.sender); + emit MigrateVault(msg.sender, outcome, securityVaults[msg.sender].poolOwnership, securityVaults[msg.sender].securityBondAllowance); + if (address(children[uint8(outcome)]) == address(0x0)) { + // first vault migrater creates new pool and transfers all REP to it + uint192 childUniverseId = (universeId << 2) + uint192(outcome) + 1; + children[uint8(outcome)] = securityPoolFactory.deploySecurityPool(openOracle, this, zoltar, childUniverseId, questionId, securityMultiplier, currentRetentionRate, priceOracleManagerAndOperatorQueuer.lastPrice(), 0); + ReputationToken childReputationToken = children[uint8(outcome)].repToken(); + childReputationToken.transfer(address(children[uint8(outcome)]), childReputationToken.balanceOf(address(this))); + } + children[uint256(outcome)].migrateRepFromParent(msg.sender); + + // migrate open interest + (bool sent, ) = payable(msg.sender).call{ value: completeSetCollateralAmount * securityVaults[msg.sender].poolOwnership / poolOwnershipDenominator }(''); + require(sent, 'Failed to send Ether'); + + securityVaults[msg.sender].poolOwnership = 0; + securityVaults[msg.sender].securityBondAllowance = 0; + } + + function migrateRepFromParent(address vault) public { // called on children + require(msg.sender == address(parent), 'only parent can migrate'); + updateVaultFees(vault); + parent.updateCollateralAmount(); + (uint256 parentpoolOwnership, uint256 parentSecurityBondAllowance, , ) = parent.securityVaults(vault); + emit MigrateRepFromParent(vault, parentSecurityBondAllowance, parentpoolOwnership); + securityVaults[vault].securityBondAllowance = parentSecurityBondAllowance; + securityBondAllowance += parentSecurityBondAllowance; + + securityVaults[vault].poolOwnership = parentpoolOwnership * repToken.balanceOf(address(this)) / parent.poolOwnershipDenominator(); + migratedRep += securityVaults[vault].poolOwnership; // poolOwnership equal to REP amounts at this point + + // migrate completeset collateral amount incrementally as we want this portion to start paying fees right away, but stop paying fees in the parent system + // TODO, handle case where parent repAtFork == 0 + require(parent.repAtFork() > 0, 'parent needs to have rep at fork'); + completeSetCollateralAmount += parent.completeSetCollateralAmount() * parentpoolOwnership / parent.repAtFork(); + securityVaults[vault].feeAccumulator = feesAccrued; + } + + function startTruthAuction() public { + require(systemState == SystemState.ForkMigration, 'System needs to be in migration'); + require(block.timestamp > securityPoolForkTriggeredTimestamp + SecurityPoolUtils.MIGRATION_TIME, 'migration time needs to pass first'); + require(truthAuctionStarted == 0, 'Auction already started'); + systemState = SystemState.ForkTruthAuction; + truthAuctionStarted = block.timestamp; + parent.updateCollateralAmount(); + uint256 parentCollateral = parent.completeSetCollateralAmount(); + completeSetCollateralAmount = parentCollateral; // update to the real one, and not only to migrated amount + emit TruthAuctionStarted(completeSetCollateralAmount, migratedRep, parent.repAtFork()); + if (migratedRep >= parent.repAtFork()) { + // we have acquired all the ETH already, no need for truthAuction + _finalizeTruthAuction(0); + } else { + // we need to buy all the collateral that is missing (did not migrate) + uint256 ethToBuy = parentCollateral - parentCollateral * migratedRep / parent.repAtFork(); + truthAuction.startAuction(ethToBuy, parent.repAtFork() - parent.repAtFork() / SecurityPoolUtils.MAX_AUCTION_VAULT_HAIRCUT_DIVISOR); // sell all but very small amount of REP for ETH. We cannot sell all for accounting purposes, as `poolOwnershipDenominator` cannot be infinite + } + } + + function _finalizeTruthAuction(uint256 repPurchased) private { + require(systemState == SystemState.ForkTruthAuction, 'Auction need to have started'); + truthAuction.finalizeAuction(); // this sends the eth back + systemState = SystemState.Operational; + uint256 repAvailable = parent.repAtFork(); + poolOwnershipDenominator = repAvailable * migratedRep / (repAvailable - repPurchased); + updateRetentionRate(); + } + + function finalizeTruthAuction() public { + require(block.timestamp > truthAuctionStarted + SecurityPoolUtils.AUCTION_TIME, 'truthAuction still ongoing'); + _finalizeTruthAuction(truthAuction.totalRepPurchased()); + } + + receive() external payable { + // needed for Truth Auction to send ETH back + } + + // accounts the purchased REP from truthAuction to the vault + // we should also move a share of bad debt in the system to this vault + function claimAuctionProceeds(address vault) public { + require(claimedAuctionProceeds[vault] == false, 'Already Claimed'); + require(truthAuction.finalized(), 'Auction needs to be finalized'); + claimedAuctionProceeds[vault] = true; + uint256 amount = truthAuction.purchasedRep(vault); + uint256 poolOwnershipAmount = repToPoolOwnership(amount); + securityVaults[vault].poolOwnership += poolOwnershipAmount; // no need to add to poolOwnershipDenominator as its already accounted + emit ClaimAuctionProceeds(vault, amount, poolOwnershipAmount, poolOwnershipDenominator); + //todo, we should give the auction buyers the securitbond debt of attackers? + } + + // todo, missing feature to get rep back after market finalization + // todo, missing redeeming yes/no/invalid poolOwnership to eth after finalization +} diff --git a/solidity/contracts/peripherals/SecurityPoolFactory.sol b/solidity/contracts/peripherals/SecurityPoolFactory.sol new file mode 100644 index 0000000..18a7272 --- /dev/null +++ b/solidity/contracts/peripherals/SecurityPoolFactory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; +import { SecurityPool } from './SecurityPool.sol'; +import { ISecurityPool, ISecurityPoolFactory } from './interfaces/ISecurityPool.sol'; +import { OpenOracle } from './openOracle/OpenOracle.sol'; +import { Zoltar } from '../Zoltar.sol'; + +contract SecurityPoolFactory is ISecurityPoolFactory { + event DeploySecurityPool(ISecurityPool securityPool, OpenOracle openOracle, ISecurityPool parent, Zoltar zoltar, uint192 universeId, uint56 questionId, uint256 securityMultiplier, uint256 currentRetentionRate, uint256 startingRepEthPrice, uint256 completeSetCollateralAmount); + function deploySecurityPool(OpenOracle openOracle, ISecurityPool parent, Zoltar zoltar, uint192 universeId, uint56 questionId, uint256 securityMultiplier, uint256 currentRetentionRate, uint256 startingRepEthPrice, uint256 completeSetCollateralAmount) external returns (ISecurityPool securityPoolAddress) { + securityPoolAddress = new SecurityPool{salt: bytes32(uint256(0x1))}(this, openOracle, parent, zoltar, universeId, questionId, securityMultiplier); + securityPoolAddress.setStartingParams(currentRetentionRate, startingRepEthPrice, completeSetCollateralAmount); + emit DeploySecurityPool(securityPoolAddress, openOracle, parent, zoltar, universeId, questionId, securityMultiplier, currentRetentionRate, startingRepEthPrice, completeSetCollateralAmount); + } +} diff --git a/solidity/contracts/peripherals/SecurityPoolUtils.sol b/solidity/contracts/peripherals/SecurityPoolUtils.sol new file mode 100644 index 0000000..d5b42df --- /dev/null +++ b/solidity/contracts/peripherals/SecurityPoolUtils.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +library SecurityPoolUtils { + uint256 constant MIGRATION_TIME = 8 weeks; + uint256 constant AUCTION_TIME = 1 weeks; + + // fees + uint256 constant PRICE_PRECISION = 1e18; + + uint256 constant MAX_RETENTION_RATE = 999_999_996_848_000_000; // ≈90% yearly + uint256 constant MIN_RETENTION_RATE = 999_999_977_880_000_000; // ≈50% yearly + uint256 constant RETENTION_RATE_DIP = 80; // 80% utilization + + // smallest vaults + uint256 constant MIN_SECURITY_BOND_DEBT = 1 ether; // 1 eth + uint256 constant MIN_REP_DEPOSIT = 10 ether; // 10 rep + + function rpow(uint256 x, uint256 n, uint256 baseUnit) external pure returns (uint256 z) { + z = n % 2 != 0 ? x : baseUnit; + for (n /= 2; n != 0; n /= 2) { + x = (x * x) / baseUnit; + if (n % 2 != 0) { + z = (z * x) / baseUnit; + } + } + } + + function calculateRetentionRate(uint256 completeSetCollateralAmount, uint256 securityBondAllowance) external pure returns (uint256 z) { + uint256 utilization = (completeSetCollateralAmount * 100) / securityBondAllowance; + if (utilization <= RETENTION_RATE_DIP) { + // first slope: 0% -> RETENTION_RATE_DIP% + uint256 utilizationRatio = (utilization * PRICE_PRECISION) / RETENTION_RATE_DIP; + uint256 slopeSpan = MAX_RETENTION_RATE - MIN_RETENTION_RATE; + return MAX_RETENTION_RATE - (slopeSpan * utilizationRatio) / PRICE_PRECISION; + } else if (utilization <= 100) { + // second slope: RETENTION_RATE_DIP% -> 100% + uint256 slopeSpan = MAX_RETENTION_RATE - MIN_RETENTION_RATE; + return MIN_RETENTION_RATE + (slopeSpan * (100 - utilization) * PRICE_PRECISION / (100 - RETENTION_RATE_DIP)) / PRICE_PRECISION; + } else { + // clamp to MIN_RETENTION_RATE if utilization > 100% + return MIN_RETENTION_RATE; + } + } + + // auction + uint256 constant MAX_AUCTION_VAULT_HAIRCUT_DIVISOR = 1_000_000; +} diff --git a/solidity/contracts/peripherals/interfaces/IAugur.sol b/solidity/contracts/peripherals/interfaces/IAugur.sol new file mode 100644 index 0000000..76c6941 --- /dev/null +++ b/solidity/contracts/peripherals/interfaces/IAugur.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +enum TokenType { + ReputationToken, + DisputeCrowdsourcer, + ParticipationToken +} + +enum MarketType { + YES_NO, + CATEGORICAL, + SCALAR +} + +interface IAugur { + function createChildUniverse(bytes32 _parentPayoutDistributionHash, uint256[] memory _parentPayoutNumerators) external returns (address); + function isKnownUniverse(address _universe) external view returns (bool); + function trustedCashTransfer(address _from, address _to, uint256 _amount) external returns (bool); + function isTrustedSender(address _address) external returns (bool); + function onCategoricalMarketCreated(uint256 _endTime, string memory _extraInfo, address _market, address _marketCreator, address _designatedReporter, uint256 _feePerCashInAttoCash, bytes32[] memory _outcomes) external returns (bool); + function onYesNoMarketCreated(uint256 _endTime, string memory _extraInfo, address _market, address _marketCreator, address _designatedReporter, uint256 _feePerCashInAttoCash) external returns (bool); + function onScalarMarketCreated(uint256 _endTime, string memory _extraInfo, address _market, address _marketCreator, address _designatedReporter, uint256 _feePerCashInAttoCash, int256[] memory _prices, uint256 _numTicks) external returns (bool); + function logInitialReportSubmitted(address _universe, address _reporter, address _market, address _initialReporter, uint256 _amountStaked, bool _isDesignatedReporter, uint256[] memory _payoutNumerators, string memory _description, uint256 _nextWindowStartTime, uint256 _nextWindowEndTime) external returns (bool); + function disputeCrowdsourcerCreated(address _universe, address _market, address _disputeCrowdsourcer, uint256[] memory _payoutNumerators, uint256 _size, uint256 _disputeRound) external returns (bool); + function logDisputeCrowdsourcerContribution(address _universe, address _reporter, address _market, address _disputeCrowdsourcer, uint256 _amountStaked, string memory description, uint256[] memory _payoutNumerators, uint256 _currentStake, uint256 _stakeRemaining, uint256 _disputeRound) external returns (bool); + function logDisputeCrowdsourcerCompleted(address _universe, address _market, address _disputeCrowdsourcer, uint256[] memory _payoutNumerators, uint256 _nextWindowStartTime, uint256 _nextWindowEndTime, bool _pacingOn, uint256 _totalRepStakedInPayout, uint256 _totalRepStakedInMarket, uint256 _disputeRound) external returns (bool); + function logInitialReporterRedeemed(address _universe, address _reporter, address _market, uint256 _amountRedeemed, uint256 _repReceived, uint256[] memory _payoutNumerators) external returns (bool); + function logDisputeCrowdsourcerRedeemed(address _universe, address _reporter, address _market, uint256 _amountRedeemed, uint256 _repReceived, uint256[] memory _payoutNumerators) external returns (bool); + function logMarketFinalized(address _universe, uint256[] memory _winningPayoutNumerators) external returns (bool); + function logMarketMigrated(address _market, address _originalUniverse) external returns (bool); + function logReportingParticipantDisavowed(address _universe, address _market) external returns (bool); + function logMarketParticipantsDisavowed(address _universe) external returns (bool); + function logCompleteSetsPurchased(address _universe, address _market, address _account, uint256 _numCompleteSets) external returns (bool); + function logCompleteSetsSold(address _universe, address _market, address _account, uint256 _numCompleteSets, uint256 _fees) external returns (bool); + function logMarketOIChanged(address _universe, address _market) external returns (bool); + function logTradingProceedsClaimed(address _universe, address _sender, address _market, uint256 _outcome, uint256 _numShares, uint256 _numPayoutTokens, uint256 _fees) external returns (bool); + function logUniverseForked(address _forkingMarket) external returns (bool); + function logReputationTokensTransferred(address _universe, address _from, address _to, uint256 _value, uint256 _fromBalance, uint256 _toBalance) external returns (bool); + function logReputationTokensBurned(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logReputationTokensMinted(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logShareTokensBalanceChanged(address _account, address _market, uint256 _outcome, uint256 _balance) external returns (bool); + function logDisputeCrowdsourcerTokensTransferred(address _universe, address _from, address _to, uint256 _value, uint256 _fromBalance, uint256 _toBalance) external returns (bool); + function logDisputeCrowdsourcerTokensBurned(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logDisputeCrowdsourcerTokensMinted(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logDisputeWindowCreated(address _disputeWindow, uint256 _id, bool _initial) external returns (bool); + function logParticipationTokensRedeemed(address universe, address _sender, uint256 _attoParticipationTokens, uint256 _feePayoutShare) external returns (bool); + function logTimestampSet(uint256 _newTimestamp) external returns (bool); + function logInitialReporterTransferred(address _universe, address _market, address _from, address _to) external returns (bool); + function logMarketTransferred(address _universe, address _from, address _to) external returns (bool); + function logParticipationTokensTransferred(address _universe, address _from, address _to, uint256 _value, uint256 _fromBalance, uint256 _toBalance) external returns (bool); + function logParticipationTokensBurned(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logParticipationTokensMinted(address _universe, address _target, uint256 _amount, uint256 _totalSupply, uint256 _balance) external returns (bool); + function logMarketRepBondTransferred(address _universe, address _from, address _to) external returns (bool); + function logWarpSyncDataUpdated(address _universe, uint256 _warpSyncHash, uint256 _marketEndTime) external returns (bool); + function isKnownFeeSender(address _feeSender) external view returns (bool); + function lookup(bytes32 _key) external view returns (address); + function getTimestamp() external view returns (uint256); + function getMaximumMarketEndDate() external returns (uint256); + function isKnownMarket(address _market) external view returns (bool); + function derivePayoutDistributionHash(uint256[] memory _payoutNumerators, uint256 _numTicks, uint256 numOutcomes) external view returns (bytes32); + function logValidityBondChanged(uint256 _validityBond) external returns (bool); + function logDesignatedReportStakeChanged(uint256 _designatedReportStake) external returns (bool); + function logNoShowBondChanged(uint256 _noShowBond) external returns (bool); + function logReportingFeeChanged(uint256 _reportingFee) external returns (bool); + function getUniverseForkIndex(address _universe) external view returns (uint256); + + event MarketCreated(address indexed universe, uint256 endTime, string extraInfo, address market, address indexed marketCreator, address designatedReporter, uint256 feePerCashInAttoCash, int256[] prices, MarketType marketType, uint256 numTicks, bytes32[] outcomes, uint256 noShowBond, uint256 timestamp); + event InitialReportSubmitted(address indexed universe, address indexed reporter, address indexed market, address initialReporter, uint256 amountStaked, bool isDesignatedReporter, uint256[] payoutNumerators, string description, uint256 nextWindowStartTime, uint256 nextWindowEndTime, uint256 timestamp); + event DisputeCrowdsourcerCreated(address indexed universe, address indexed market, address disputeCrowdsourcer, uint256[] payoutNumerators, uint256 size, uint256 disputeRound); + event DisputeCrowdsourcerContribution(address indexed universe, address indexed reporter, address indexed market, address disputeCrowdsourcer, uint256 amountStaked, string description, uint256[] payoutNumerators, uint256 currentStake, uint256 stakeRemaining, uint256 disputeRound, uint256 timestamp); + event DisputeCrowdsourcerCompleted(address indexed universe, address indexed market, address disputeCrowdsourcer, uint256[] payoutNumerators, uint256 nextWindowStartTime, uint256 nextWindowEndTime, bool pacingOn, uint256 totalRepStakedInPayout, uint256 totalRepStakedInMarket, uint256 disputeRound, uint256 timestamp); + event InitialReporterRedeemed(address indexed universe, address indexed reporter, address indexed market, address initialReporter, uint256 amountRedeemed, uint256 repReceived, uint256[] payoutNumerators, uint256 timestamp); + event DisputeCrowdsourcerRedeemed(address indexed universe, address indexed reporter, address indexed market, address disputeCrowdsourcer, uint256 amountRedeemed, uint256 repReceived, uint256[] payoutNumerators, uint256 timestamp); + event ReportingParticipantDisavowed(address indexed universe, address indexed market, address reportingParticipant); + event MarketParticipantsDisavowed(address indexed universe, address indexed market); + event MarketFinalized(address indexed universe, address indexed market, uint256 timestamp, uint256[] winningPayoutNumerators); + event MarketMigrated(address indexed market, address indexed originalUniverse, address indexed newUniverse); + event UniverseForked(address indexed universe, address forkingMarket); + event UniverseCreated(address indexed parentUniverse, address indexed childUniverse, uint256[] payoutNumerators, uint256 creationTimestamp); + event CompleteSetsPurchased(address indexed universe, address indexed market, address indexed account, uint256 numCompleteSets, uint256 timestamp); + event CompleteSetsSold(address indexed universe, address indexed market, address indexed account, uint256 numCompleteSets, uint256 fees, uint256 timestamp); + event TradingProceedsClaimed(address indexed universe, address indexed sender, address market, uint256 outcome, uint256 numShares, uint256 numPayoutTokens, uint256 fees, uint256 timestamp); + event TokensTransferred(address indexed universe, address token, address indexed from, address indexed to, uint256 value, TokenType tokenType, address market); + event TokensMinted(address indexed universe, address indexed token, address indexed target, uint256 amount, TokenType tokenType, address market, uint256 totalSupply); + event TokensBurned(address indexed universe, address indexed token, address indexed target, uint256 amount, TokenType tokenType, address market, uint256 totalSupply); + event TokenBalanceChanged(address indexed universe, address indexed owner, address token, TokenType tokenType, address market, uint256 balance, uint256 outcome); + event DisputeWindowCreated(address indexed universe, address disputeWindow, uint256 startTime, uint256 endTime, uint256 id, bool initial); + event InitialReporterTransferred(address indexed universe, address indexed market, address from, address to); + event MarketTransferred(address indexed universe, address indexed market, address from, address to); + event MarketOIChanged(address indexed universe, address indexed market, uint256 marketOI); + event ParticipationTokensRedeemed(address indexed universe, address indexed disputeWindow, address indexed account, uint256 attoParticipationTokens, uint256 feePayoutShare, uint256 timestamp); + event TimestampSet(uint256 newTimestamp); + event ValidityBondChanged(address indexed universe, uint256 validityBond); + event DesignatedReportStakeChanged(address indexed universe, uint256 designatedReportStake); + event NoShowBondChanged(address indexed universe, uint256 noShowBond); + event ReportingFeeChanged(address indexed universe, uint256 reportingFee); + event ShareTokenBalanceChanged(address indexed universe, address indexed account, address indexed market, uint256 outcome, uint256 balance); + event MarketRepBondTransferred(address indexed universe, address market, address from, address to); + event WarpSyncDataUpdated(address indexed universe, uint256 warpSyncHash, uint256 marketEndTime); + + event RegisterContract(address contractAddress, bytes32 key); + event FinishDeployment(); +} diff --git a/solidity/contracts/peripherals/interfaces/ISecurityPool.sol b/solidity/contracts/peripherals/interfaces/ISecurityPool.sol new file mode 100644 index 0000000..4058cbf --- /dev/null +++ b/solidity/contracts/peripherals/interfaces/ISecurityPool.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +import { Zoltar } from '../../Zoltar.sol'; +import { OpenOracle } from "../openOracle/OpenOracle.sol"; +import { Auction } from "../Auction.sol"; +import { CompleteSet } from "../CompleteSet.sol"; +import { ReputationToken } from "../../ReputationToken.sol"; +import { PriceOracleManagerAndOperatorQueuer } from "../PriceOracleManagerAndOperatorQueuer.sol"; + +struct SecurityVault { + uint256 poolOwnership; + uint256 securityBondAllowance; + uint256 unpaidEthFees; + uint256 feeAccumulator; +} + +enum SystemState { + Operational, + PoolForked, + ForkMigration, + ForkTruthAuction +} + +enum QuestionOutcome { + Invalid, + Yes, + No +} + +interface ISecurityPool { + + // -------- View Functions -------- + function questionId() external view returns (uint56); + function universeId() external view returns (uint192); + function zoltar() external view returns (Zoltar); + function securityBondAllowance() external view returns (uint256); + function completeSetCollateralAmount() external view returns (uint256); + function poolOwnershipDenominator() external view returns (uint256); + function repAtFork() external view returns (uint256); + function migratedRep() external view returns (uint256); + function securityMultiplier() external view returns (uint256); + function feesAccrued() external view returns (uint256); + function lastUpdatedFeeAccumulator() external view returns (uint256); + function currentRetentionRate() external view returns (uint256); + function securityPoolForkTriggeredTimestamp() external view returns (uint256); + function securityVaults(address vault) external view returns (uint256 poolOwnership, uint256 securityBondAllowance, uint256 unpaidEthFees, uint256 feeAccumulator); + function claimedAuctionProceeds(address vault) external view returns (bool); + function children(uint256 index) external view returns (ISecurityPool); + function parent() external view returns (ISecurityPool); + function truthAuctionStarted() external view returns (uint256); + function systemState() external view returns (SystemState); + function completeSet() external view returns (CompleteSet); + function truthAuction() external view returns (Auction); + function repToken() external view returns (ReputationToken); + function securityPoolFactory() external view returns (ISecurityPoolFactory); + function priceOracleManagerAndOperatorQueuer() external view returns (PriceOracleManagerAndOperatorQueuer); + function openOracle() external view returns (OpenOracle); + + function repToPoolOwnership(uint256 repAmount) external view returns (uint256); + function poolOwnershipToRep(uint256 poolOwnership) external view returns (uint256); + + // -------- Mutative Functions -------- + function setStartingParams(uint256 currentRetentionRate, uint256 repEthPrice, uint256 completeSetCollateralAmount) external; + + function updateCollateralAmount() external; + function updateRetentionRate() external; + function updateVaultFees(address vault) external; + function redeemFees(address vault) external; + + function performWithdrawRep(address vault, uint256 repAmount) external; + function depositRep(uint256 repAmount) external; + function performLiquidation(address callerVault, address targetVaultAddress, uint256 debtAmount) external; + function performSetSecurityBondsAllowance(address callerVault, uint256 amount) external; + + function createCompleteSet() external payable; + function redeemCompleteSet(uint256 amount) external; + + function forkSecurityPool() external; + function migrateVault(QuestionOutcome outcome) external; + function migrateRepFromParent(address vault) external; + function startTruthAuction() external; + function finalizeTruthAuction() external; + function claimAuctionProceeds(address vault) external; + + receive() external payable; +} + +interface ISecurityPoolFactory { + function deploySecurityPool(OpenOracle openOracle, ISecurityPool parent, Zoltar zoltar, uint192 universeId, uint56 questionId, uint256 securityMultiplier, uint256 currentRetentionRate, uint256 startingRepEthPrice, uint256 completeSetCollateralAmount) external returns (ISecurityPool securityPoolAddress); +} diff --git a/solidity/contracts/peripherals/interfaces/IWeth9.sol b/solidity/contracts/peripherals/interfaces/IWeth9.sol new file mode 100644 index 0000000..208747c --- /dev/null +++ b/solidity/contracts/peripherals/interfaces/IWeth9.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNICENSE +pragma solidity 0.8.30; + +interface IWeth9 { + // ERC-20 metadata + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + + // ERC-20 standard events + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + + // WETH-specific events + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + // ERC-20 storage mappings + function balanceOf(address account) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + // WETH functions + function deposit() external payable; + function withdraw(uint wad) external; + function totalSupply() external view returns (uint); + function approve(address guy, uint wad) external returns (bool); + function transfer(address dst, uint wad) external returns (bool); + function transferFrom(address src, address dst, uint wad) external returns (bool); +} + diff --git a/solidity/contracts/peripherals/openOracle/OpenOracle.sol b/solidity/contracts/peripherals/openOracle/OpenOracle.sol new file mode 100644 index 0000000..f7e6056 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/OpenOracle.sol @@ -0,0 +1,878 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ReentrancyGuard} from "./openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {IERC20} from "./openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "./openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title OpenOracle + * @notice A trust-free price oracle that uses an escalating auction mechanism + * @dev This contract enables price discovery through economic incentives where + * expiration serves as evidence of a good price with appropriate parameters + * @author OpenOracle Team + * @custom:version 0.1.6 + * @custom:documentation https://openprices.gitbook.io/openoracle-docs + */ +contract OpenOracle is ReentrancyGuard { + using SafeERC20 for IERC20; + + // Custom errors for gas optimization + error InvalidInput(string parameter); + error InsufficientAmount(string resource); + error AlreadyProcessed(string action); + error InvalidTiming(string action); + error OutOfBounds(string parameter); + error TokensCannotBeSame(); + error NoReportToDispute(); + error EthTransferFailed(); + error CallToArbSysFailed(); + error InvalidAmount2(string parameter); + error InvalidStateHash(string parameter); + error InvalidGasLimit(); + + // Constants + uint256 public constant PRICE_PRECISION = 1e18; + uint256 public constant PERCENTAGE_PRECISION = 1e7; + uint256 public constant MULTIPLIER_PRECISION = 100; + uint256 public constant SETTLEMENT_WINDOW = 60; // 60 seconds for testing + uint256 public constant SETTLEMENT_WINDOW_BLOCKS = 1350; // 5 minutes @ 4.5 blocks per second on Arbitrum + + // State variables + uint256 public nextReportId = 1; + + mapping(uint256 => ReportMeta) public reportMeta; + mapping(uint256 => ReportStatus) public reportStatus; + mapping(address => mapping(address => uint256)) public protocolFees; + mapping(address => uint256) public accruedProtocolFees; + mapping(uint256 => extraReportData) public extraData; + mapping(uint256 => mapping(uint256 => disputeRecord)) public disputeHistory; + + struct disputeRecord { + uint256 amount1; + uint256 amount2; + address tokenToSwap; + uint48 reportTimestamp; + } + + struct extraReportData { + bytes32 stateHash; + address callbackContract; + uint32 numReports; + uint32 callbackGasLimit; + bytes4 callbackSelector; + address protocolFeeRecipient; + bool trackDisputes; + bool keepFee; + bool feeToken; + } + + // Type declarations + struct ReportMeta { + uint256 exactToken1Report; + uint256 escalationHalt; + uint256 fee; + uint256 settlerReward; + address token1; + uint48 settlementTime; + address token2; + bool timeType; + uint24 feePercentage; + uint24 protocolFee; + uint16 multiplier; + uint24 disputeDelay; + } + + struct ReportStatus { + uint256 currentAmount1; + uint256 currentAmount2; + uint256 price; + address payable currentReporter; + uint48 reportTimestamp; + uint48 settlementTimestamp; + address payable initialReporter; + uint48 lastReportOppoTime; + bool disputeOccurred; + bool isDistributed; + } + + //initial reporter reward is paid to the initial reporter and is msg.value - settlerReward + struct CreateReportParams { + uint256 exactToken1Report; // initial oracle liquidity in token1 + uint256 escalationHalt; // amount of token1 past which escalation stops but disputes can still happen + uint256 settlerReward; // eth paid to settler in wei + address token1Address; // address of token1 in the oracle report instance + uint48 settlementTime; // report instance can settle if no disputes within this timeframe + uint24 disputeDelay; // time disputes must wait after every new report + uint24 protocolFee; // fee paid to protocolFeeRecipient. 1000 = 0.01% + address token2Address; // address of token2 in the oracle report instance + uint32 callbackGasLimit; // gas the settlement callback must use + uint24 feePercentage; // fee paid to previous reporter. 1000 = 0.01% + uint16 multiplier; // amount by which newAmount1 must increase versus old amount1. 140 = 1.4x + bool timeType; // true for block timestamp, false for block number + bool trackDisputes; // true keeps a readable dispute history for smart contracts + bool keepFee; // true means initial reporter keeps the initial reporter reward if disputed. if false, it goes to protocolFeeRecipient if disputed + address callbackContract; // contract address for settle to call back into + bytes4 callbackSelector; // method in the callbackContract you want called. + address protocolFeeRecipient; // address that receives protocol fees and initial reporter rewards if keepFee set to false + bool feeToken; //if true, protocol fees + fees paid to previous reporter are in tokenToSwap. if false, in not(tokenToSwap) + } //typically if feeToken true, fees are paid in less valuable token, if false, fees paid in more valuable token + + // Events + event ReportInstanceCreated( + uint256 indexed reportId, + address indexed token1Address, + address indexed token2Address, + uint256 feePercentage, + uint256 multiplier, + uint256 exactToken1Report, + uint256 ethFee, + address creator, + uint256 settlementTime, + uint256 escalationHalt, + uint256 disputeDelay, + uint256 protocolFee, + uint256 settlerReward, + bool timeType, + address callbackContract, + bytes4 callbackSelector, + bool trackDisputes, + uint256 callbackGasLimit, + bool keepFee, + bytes32 stateHash, + uint256 blockTimestamp, + bool feeToken + ); + + event InitialReportSubmitted( + uint256 indexed reportId, + address reporter, + uint256 amount1, + uint256 amount2, + address indexed token1Address, + address indexed token2Address, + uint256 swapFee, + uint256 protocolFee, + uint256 settlementTime, + uint256 disputeDelay, + uint256 escalationHalt, + bool timeType, + address callbackContract, + bytes4 callbackSelector, + bool trackDisputes, + uint256 callbackGasLimit, + bytes32 stateHash, + uint256 blockTimestamp + ); + + event ReportDisputed( + uint256 indexed reportId, + address disputer, + uint256 newAmount1, + uint256 newAmount2, + address indexed token1Address, + address indexed token2Address, + uint256 swapFee, + uint256 protocolFee, + uint256 settlementTime, + uint256 disputeDelay, + uint256 escalationHalt, + bool timeType, + address callbackContract, + bytes4 callbackSelector, + bool trackDisputes, + uint256 callbackGasLimit, + bytes32 stateHash, + uint256 blockTimestamp + ); + + event ReportSettled(uint256 indexed reportId, uint256 price, uint256 settlementTimestamp, uint256 blockTimestamp); + + event SettlementCallbackExecuted(uint256 indexed reportId, address indexed callbackContract, bool success); + + constructor() ReentrancyGuard() {} + + /** + * @notice Withdraws accumulated protocol fees for a specific token + * @param tokenToGet The token address to withdraw fees for + */ + function getProtocolFees(address tokenToGet) external nonReentrant returns (uint256) { + uint256 amount = protocolFees[msg.sender][tokenToGet]; + if (amount > 0) { + protocolFees[msg.sender][tokenToGet] = 0; + _transferTokens(tokenToGet, address(this), msg.sender, amount); + return amount; + } + } + + /** + * @notice Withdraws accumulated protocol fees in ETH + */ + function getETHProtocolFees() external nonReentrant returns (uint256) { + uint256 amount = accruedProtocolFees[msg.sender]; + if (amount > 0) { + accruedProtocolFees[msg.sender] = 0; + (bool success,) = payable(msg.sender).call{value: amount}(""); + if (!success) revert EthTransferFailed(); + return amount; + } + } + + /** + * @notice Settles a report after the settlement time has elapsed + * @param reportId The unique identifier for the report to settle + * @return price The final settled price + * @return settlementTimestamp The timestamp when the report was settled + */ + function settle(uint256 reportId) external nonReentrant returns (uint256 price, uint256 settlementTimestamp) { + ReportStatus storage status = reportStatus[reportId]; + ReportMeta storage meta = reportMeta[reportId]; + + if (meta.timeType) { + if (block.timestamp < status.reportTimestamp + meta.settlementTime) { + revert InvalidTiming("settlement"); + } + } else { + if (_getBlockNumber() < status.reportTimestamp + meta.settlementTime) { + revert InvalidTiming("settlement"); + } + } + + if (status.reportTimestamp == 0) revert InvalidInput("no initial report"); + + if (status.isDistributed) { + return status.isDistributed ? (status.price, status.settlementTimestamp) : (0, 0); + } + + uint256 settlerReward = meta.settlerReward; + uint256 reporterReward = meta.fee; + + status.isDistributed = true; + status.settlementTimestamp = meta.timeType ? uint48(block.timestamp) : _getBlockNumber(); + emit ReportSettled(reportId, status.price, status.settlementTimestamp, block.timestamp); + + extraReportData storage extra = extraData[reportId]; + + _transferTokens(meta.token1, address(this), status.currentReporter, status.currentAmount1); + _transferTokens(meta.token2, address(this), status.currentReporter, status.currentAmount2); + + if (extra.callbackContract != address(0) && extra.callbackSelector != bytes4(0)) { + // Prepare callback data + bytes memory callbackData = abi.encodeWithSelector( + extra.callbackSelector, reportId, status.price, status.settlementTimestamp, meta.token1, meta.token2 + ); + + // Execute callback with gas limit. Revert if not enough gas supplied to attempt callback fully. + // Using low-level call to handle failures gracefully + if (gasleft() < ((64 * extra.callbackGasLimit + 62) / 63) + 100000) revert InvalidGasLimit(); + (bool success,) = extra.callbackContract.call{gas: extra.callbackGasLimit}(callbackData); + + // Emit event regardless of bool success + emit SettlementCallbackExecuted(reportId, extra.callbackContract, success); + } + + // other external calls below (check-effect-interaction pattern) + + if (status.disputeOccurred) { + if (extraData[reportId].keepFee) { + _sendEth(status.initialReporter, reporterReward); + } else { + accruedProtocolFees[extra.protocolFeeRecipient] += reporterReward; + } + } else { + _sendEth(status.initialReporter, reporterReward); + } + + _sendEth(payable(msg.sender), settlerReward); + + return status.isDistributed ? (status.price, status.settlementTimestamp) : (0, 0); + } + + /** + * @notice Gets the settlement data for a settled report + * @param reportId The unique identifier for the report + * @return price The settled price + * @return settlementTimestamp The timestamp when the report was settled + */ + function getSettlementData(uint256 reportId) external view returns (uint256 price, uint256 settlementTimestamp) { + ReportStatus storage status = reportStatus[reportId]; + if (!status.isDistributed) revert AlreadyProcessed("not settled"); + return (status.price, status.settlementTimestamp); + } + + /** + * @notice Creates a new report instance for price discovery. Backwards-compatible (timeType true) + * @param token1Address Address of the first token + * @param token2Address Address of the second token + * @param exactToken1Report Exact amount of token1 required for reports + * @param feePercentage Fee in thousandths of basis points (3000 = 3bps) + * @param multiplier Multiplier in percentage points (110 = 1.1x) + * @param settlementTime Time in seconds before report can be settled + * @param escalationHalt Threshold where multiplier drops to 100 + * @param disputeDelay Delay in seconds before disputes are allowed + * @param protocolFee Protocol fee in thousandths of basis points + * @param settlerReward Reward for settling the report in wei + * @return reportId The unique identifier for the created report + */ + function createReportInstance( + address token1Address, + address token2Address, + uint256 exactToken1Report, + uint24 feePercentage, + uint16 multiplier, + uint48 settlementTime, + uint256 escalationHalt, + uint24 disputeDelay, + uint24 protocolFee, + uint256 settlerReward + ) external payable returns (uint256 reportId) { + CreateReportParams memory params = CreateReportParams({ + token1Address: token1Address, + token2Address: token2Address, + exactToken1Report: exactToken1Report, + feePercentage: feePercentage, + multiplier: multiplier, + settlementTime: settlementTime, + escalationHalt: escalationHalt, + disputeDelay: disputeDelay, + protocolFee: protocolFee, + settlerReward: settlerReward, + timeType: true, + callbackContract: address(0), + callbackSelector: bytes4(0), + trackDisputes: false, + callbackGasLimit: 0, + keepFee: true, + protocolFeeRecipient: msg.sender, + feeToken: true + }); + return _createReportInstance(params); + } + + //new function. full control over timeType. true = seconds, false = blocks + // not backwards compatible to previous createReportInstance (different function argument order!!) + function createReportInstance(CreateReportParams calldata params) external payable returns (uint256 reportId) { + return _createReportInstance(params); + } + + function _createReportInstance(CreateReportParams memory params) internal returns (uint256 reportId) { + if (msg.value <= 100) revert InsufficientAmount("fee"); + if (params.exactToken1Report == 0) revert InvalidInput("token amount"); + if (params.token1Address == params.token2Address) revert TokensCannotBeSame(); + if (params.settlementTime < params.disputeDelay) revert InvalidTiming("settlement vs dispute delay"); + if (msg.value <= params.settlerReward) revert InsufficientAmount("settler reward fee"); + if (params.feePercentage == 0) revert InvalidInput("feePercentage 0"); + if (params.feePercentage + params.protocolFee > 1e7) revert InvalidInput("sum of fees"); + reportId = nextReportId++; + + ReportMeta storage meta = reportMeta[reportId]; + meta.token1 = params.token1Address; + meta.token2 = params.token2Address; + meta.exactToken1Report = params.exactToken1Report; + meta.feePercentage = params.feePercentage; + meta.multiplier = params.multiplier; + meta.settlementTime = params.settlementTime; + meta.fee = msg.value - params.settlerReward; + meta.escalationHalt = params.escalationHalt; + meta.disputeDelay = params.disputeDelay; + meta.protocolFee = params.protocolFee; + meta.settlerReward = params.settlerReward; + meta.timeType = params.timeType; + + extraReportData storage extra = extraData[reportId]; + extra.callbackContract = params.callbackContract; + extra.callbackSelector = params.callbackSelector; + extra.trackDisputes = params.trackDisputes; + extra.callbackGasLimit = params.callbackGasLimit; + extra.keepFee = params.keepFee; + extra.protocolFeeRecipient = params.protocolFeeRecipient; + extra.feeToken = params.feeToken; + + bytes32 stateHash = keccak256( + abi.encodePacked( + keccak256(abi.encodePacked(params.timeType)), + keccak256(abi.encodePacked(params.settlementTime)), + keccak256(abi.encodePacked(params.disputeDelay)), + keccak256(abi.encodePacked(params.callbackContract)), + keccak256(abi.encodePacked(params.callbackSelector)), + keccak256(abi.encodePacked(params.callbackGasLimit)), + keccak256(abi.encodePacked(params.keepFee)), + keccak256(abi.encodePacked(params.feePercentage)), + keccak256(abi.encodePacked(params.protocolFee)), + keccak256(abi.encodePacked(params.settlerReward)), + keccak256(abi.encodePacked(meta.fee)), + keccak256(abi.encodePacked(params.trackDisputes)), + keccak256(abi.encodePacked(params.multiplier)), + keccak256(abi.encodePacked(params.escalationHalt)), + keccak256(abi.encodePacked(params.feeToken)), + keccak256(abi.encodePacked(msg.sender)), + keccak256(abi.encodePacked(_getBlockNumber())), + keccak256(abi.encodePacked(uint48(block.timestamp))) + ) + ); + + extra.stateHash = stateHash; + + emit ReportInstanceCreated( + reportId, + params.token1Address, + params.token2Address, + params.feePercentage, + params.multiplier, + params.exactToken1Report, + msg.value, + msg.sender, + params.settlementTime, + params.escalationHalt, + params.disputeDelay, + params.protocolFee, + params.settlerReward, + params.timeType, + params.callbackContract, + params.callbackSelector, + params.trackDisputes, + params.callbackGasLimit, + params.keepFee, + stateHash, + block.timestamp, + params.feeToken + ); + return reportId; + } + + /** + * @notice Submits the initial price report for a given report ID + * @param reportId The unique identifier for the report + * @param amount1 Amount of token1 (must equal exactToken1Report) + * @param amount2 Amount of token2 for the price ratio + * @dev Tokens are pulled from msg.sender and will be returned to msg.sender when settled + */ + function submitInitialReport(uint256 reportId, uint256 amount1, uint256 amount2, bytes32 stateHash) external { + _submitInitialReport(reportId, amount1, amount2, stateHash, msg.sender); + } + + /** + * @notice Submits the initial price report with a custom reporter address + * @param reportId The unique identifier for the report + * @param amount1 Amount of token1 (must equal exactToken1Report) + * @param amount2 Amount of token2 for the price ratio + * @param reporter The address that will receive tokens back when settled + * @dev Tokens are pulled from msg.sender but will be returned to reporter address + * @dev This overload enables contracts to submit reports on behalf of users + */ + function submitInitialReport( + uint256 reportId, + uint256 amount1, + uint256 amount2, + bytes32 stateHash, + address reporter + ) external { + _submitInitialReport(reportId, amount1, amount2, stateHash, reporter); + } + + /** + * @notice Submits the initial price report for a given report ID + * @param reportId The unique identifier for the report + * @param amount1 Amount of token1 (must equal exactToken1Report) + * @param amount2 Amount of token2 for the price ratio + * @param reporter The address that will receive tokens back when settled + */ + function _submitInitialReport( + uint256 reportId, + uint256 amount1, + uint256 amount2, + bytes32 stateHash, + address reporter + ) internal { + if (reportStatus[reportId].currentReporter != address(0)) revert AlreadyProcessed("report submitted"); + + ReportMeta storage meta = reportMeta[reportId]; + ReportStatus storage status = reportStatus[reportId]; + extraReportData storage extra = extraData[reportId]; + + if (reportId >= nextReportId) revert InvalidInput("report id"); + if (amount1 != meta.exactToken1Report) revert InvalidInput("token1 amount"); + if (amount2 == 0) revert InvalidInput("token2 amount"); + if (extra.stateHash != stateHash) revert InvalidStateHash("state hash"); //TODO; commented as its bit hard for ethsimulate testsuite + if (reporter == address(0)) revert InvalidInput("reporter address"); + + _transferTokens(meta.token1, msg.sender, address(this), amount1); + _transferTokens(meta.token2, msg.sender, address(this), amount2); + + status.currentAmount1 = amount1; + status.currentAmount2 = amount2; + status.currentReporter = payable(reporter); + status.initialReporter = payable(reporter); + status.reportTimestamp = meta.timeType ? uint48(block.timestamp) : _getBlockNumber(); + status.price = (amount1 * PRICE_PRECISION) / amount2; + status.lastReportOppoTime = meta.timeType ? _getBlockNumber() : uint48(block.timestamp); + + if (extra.trackDisputes) { + disputeHistory[reportId][0].amount1 = amount1; + disputeHistory[reportId][0].amount2 = amount2; + disputeHistory[reportId][0].reportTimestamp = status.reportTimestamp; + extra.numReports = 1; + } + + emit InitialReportSubmitted( + reportId, + reporter, + amount1, + amount2, + meta.token1, + meta.token2, + meta.feePercentage, + meta.protocolFee, + meta.settlementTime, + meta.disputeDelay, + meta.escalationHalt, + meta.timeType, + extra.callbackContract, + extra.callbackSelector, + extra.trackDisputes, + extra.callbackGasLimit, + stateHash, + block.timestamp + ); + } + + //backwards-compatible function + function disputeAndSwap( + uint256 reportId, + address tokenToSwap, + uint256 newAmount1, + uint256 newAmount2, + uint256 amt2Expected, + bytes32 stateHash + ) external nonReentrant { + _disputeAndSwap(reportId, tokenToSwap, newAmount1, newAmount2, msg.sender, amt2Expected, stateHash); + } + + function disputeAndSwap( + uint256 reportId, + address tokenToSwap, + uint256 newAmount1, + uint256 newAmount2, + address disputer, + uint256 amt2Expected, + bytes32 stateHash + ) external nonReentrant { + _disputeAndSwap(reportId, tokenToSwap, newAmount1, newAmount2, disputer, amt2Expected, stateHash); + } + + /** + * @notice Disputes an existing report and swaps tokens to update the price + * @param reportId The unique identifier for the report to dispute + * @param tokenToSwap The token being swapped (token1 or token2) + * @param newAmount1 New amount of token1 after the dispute + * @param newAmount2 New amount of token2 after the dispute + */ + function _disputeAndSwap( + uint256 reportId, + address tokenToSwap, + uint256 newAmount1, + uint256 newAmount2, + address disputer, + uint256 amt2Expected, + bytes32 stateHash + ) internal { + _preValidate( + newAmount1, + reportStatus[reportId].currentAmount1, + reportMeta[reportId].multiplier, + reportMeta[reportId].escalationHalt + ); + + ReportMeta storage meta = reportMeta[reportId]; + ReportStatus storage status = reportStatus[reportId]; + + _validateDispute(reportId, tokenToSwap, newAmount1, newAmount2, meta, status); + if (status.currentAmount2 != amt2Expected) revert InvalidAmount2("amount2 doesn't match expectation"); + if (stateHash != extraData[reportId].stateHash) revert InvalidStateHash("state hash"); + if (disputer == address(0)) revert InvalidInput("disputer address"); + + address protocolFeeRecipient = extraData[reportId].protocolFeeRecipient; + bool feeToken = extraData[reportId].feeToken; + + if (tokenToSwap == meta.token1) { + _handleToken1Swap(meta, status, newAmount2, disputer, protocolFeeRecipient, feeToken); + } else if (tokenToSwap == meta.token2) { + _handleToken2Swap(meta, status, newAmount2, protocolFeeRecipient, feeToken); + } else { + revert InvalidInput("token to swap"); + } + + // Update the report status after the dispute and swap + status.currentAmount1 = newAmount1; + status.currentAmount2 = newAmount2; + status.currentReporter = payable(disputer); + status.reportTimestamp = meta.timeType ? uint48(block.timestamp) : _getBlockNumber(); + status.price = (newAmount1 * PRICE_PRECISION) / newAmount2; + status.disputeOccurred = true; + status.lastReportOppoTime = meta.timeType ? _getBlockNumber() : uint48(block.timestamp); + + if (extraData[reportId].trackDisputes) { + uint32 nextIndex = extraData[reportId].numReports; + disputeHistory[reportId][nextIndex].amount1 = newAmount1; + disputeHistory[reportId][nextIndex].amount2 = newAmount2; + disputeHistory[reportId][nextIndex].reportTimestamp = status.reportTimestamp; + disputeHistory[reportId][nextIndex].tokenToSwap = tokenToSwap; + extraData[reportId].numReports = nextIndex + 1; + } + + emit ReportDisputed( + reportId, + disputer, + newAmount1, + newAmount2, + meta.token1, + meta.token2, + meta.feePercentage, + meta.protocolFee, + meta.settlementTime, + meta.disputeDelay, + meta.escalationHalt, + meta.timeType, + extraData[reportId].callbackContract, + extraData[reportId].callbackSelector, + extraData[reportId].trackDisputes, + extraData[reportId].callbackGasLimit, + stateHash, + block.timestamp + ); + } + + function _preValidate(uint256 newAmount1, uint256 oldAmount1, uint256 multiplier, uint256 escalationHalt) + internal + pure + { + uint256 expectedAmount1; + + if (escalationHalt > oldAmount1) { + expectedAmount1 = (oldAmount1 * multiplier) / MULTIPLIER_PRECISION; + } else { + expectedAmount1 = oldAmount1 + 1; + } + + if (newAmount1 != expectedAmount1) { + if (escalationHalt <= oldAmount1) { + revert OutOfBounds("escalation halted"); + } else { + revert InvalidInput("new amount"); + } + } + } + + /** + * @dev Validates that a dispute is valid according to the oracle rules + */ + function _validateDispute( + uint256 reportId, + address tokenToSwap, + uint256 newAmount1, + uint256 newAmount2, + ReportMeta storage meta, + ReportStatus storage status + ) internal view { + if (reportId >= nextReportId) revert InvalidInput("report id"); + if (newAmount1 == 0 || newAmount2 == 0) revert InvalidInput("token amounts"); + if (status.currentReporter == address(0)) revert NoReportToDispute(); + if (meta.timeType) { + if (block.timestamp > status.reportTimestamp + meta.settlementTime) { + revert InvalidTiming("dispute period expired"); + } + } else { + if (_getBlockNumber() > status.reportTimestamp + meta.settlementTime) { + revert InvalidTiming("dispute period expired"); + } + } + if (status.isDistributed) revert AlreadyProcessed("report settled"); + if (tokenToSwap != meta.token1 && tokenToSwap != meta.token2) revert InvalidInput("token to swap"); + if (meta.timeType) { + if (block.timestamp < status.reportTimestamp + meta.disputeDelay) revert InvalidTiming("dispute too early"); + } else { + if (_getBlockNumber() < status.reportTimestamp + meta.disputeDelay) { + revert InvalidTiming("dispute too early"); + } + } + + uint256 oldAmount1 = status.currentAmount1; + uint256 oldPrice = (oldAmount1 * PRICE_PRECISION) / status.currentAmount2; + uint256 feeSum = uint256(meta.feePercentage) + uint256(meta.protocolFee); + uint256 feeBoundary = (oldPrice * feeSum) / PERCENTAGE_PRECISION; + uint256 lowerBoundary = (oldPrice * PERCENTAGE_PRECISION) / (PERCENTAGE_PRECISION + feeSum); + uint256 upperBoundary = oldPrice + feeBoundary; + uint256 newPrice = (newAmount1 * PRICE_PRECISION) / newAmount2; + + if (newPrice >= lowerBoundary && newPrice <= upperBoundary) { + revert OutOfBounds("price within boundaries"); + } + } + + /** + * @dev Handles token swaps when token1 is being swapped during a dispute + */ + function _handleToken1Swap( + ReportMeta storage meta, + ReportStatus storage status, + uint256 newAmount2, + address disputer, + address protocolFeeRecipient, + bool feeToken + ) internal { + uint256 oldAmount1 = status.currentAmount1; + uint256 oldAmount2 = status.currentAmount2; + + if (feeToken) { + uint256 fee = (oldAmount1 * meta.feePercentage) / PERCENTAGE_PRECISION; + uint256 protocolFee = (oldAmount1 * meta.protocolFee) / PERCENTAGE_PRECISION; + + protocolFees[protocolFeeRecipient][meta.token1] += protocolFee; + + uint256 requiredToken1Contribution = meta.escalationHalt > oldAmount1 + ? (oldAmount1 * meta.multiplier) / MULTIPLIER_PRECISION + : oldAmount1 + 1; + + uint256 netToken2Contribution = newAmount2 >= oldAmount2 ? newAmount2 - oldAmount2 : 0; + uint256 netToken2Receive = newAmount2 < oldAmount2 ? oldAmount2 - newAmount2 : 0; + + if (netToken2Contribution > 0) { + IERC20(meta.token2).safeTransferFrom(msg.sender, address(this), netToken2Contribution); + } + + if (netToken2Receive > 0) { + IERC20(meta.token2).safeTransfer(disputer, netToken2Receive); + } + + IERC20(meta.token1).safeTransferFrom( + msg.sender, address(this), requiredToken1Contribution + oldAmount1 + fee + protocolFee + ); + IERC20(meta.token1).safeTransfer(status.currentReporter, 2 * oldAmount1 + fee); + } else { + uint256 fee = (oldAmount2 * meta.feePercentage) / PERCENTAGE_PRECISION; + uint256 protocolFee = (oldAmount2 * meta.protocolFee) / PERCENTAGE_PRECISION; + + protocolFees[protocolFeeRecipient][meta.token2] += protocolFee; + + uint256 requiredToken1Contribution = meta.escalationHalt > oldAmount1 + ? (oldAmount1 * meta.multiplier) / MULTIPLIER_PRECISION + : oldAmount1 + 1; + + uint256 netToken2Contribution = + newAmount2 + protocolFee + fee >= oldAmount2 ? newAmount2 + protocolFee + fee - oldAmount2 : 0; + uint256 netToken2Receive = + newAmount2 + protocolFee + fee < oldAmount2 ? oldAmount2 - newAmount2 - protocolFee - fee : 0; + + if (netToken2Contribution > 0) { + IERC20(meta.token2).safeTransferFrom(msg.sender, address(this), netToken2Contribution); + } + + if (netToken2Receive > 0) { + IERC20(meta.token2).safeTransfer(disputer, netToken2Receive); + } + + IERC20(meta.token1).safeTransferFrom(msg.sender, address(this), requiredToken1Contribution + oldAmount1); + + IERC20(meta.token1).safeTransfer(status.currentReporter, 2 * oldAmount1); + IERC20(meta.token2).safeTransfer(status.currentReporter, fee); + } + } + + /** + * @dev Handles token swaps when token2 is being swapped during a dispute + */ + function _handleToken2Swap( + ReportMeta storage meta, + ReportStatus storage status, + uint256 newAmount2, + address protocolFeeRecipient, + bool feeToken + ) internal { + uint256 oldAmount1 = status.currentAmount1; + uint256 oldAmount2 = status.currentAmount2; + + if (feeToken) { + uint256 fee = (oldAmount2 * meta.feePercentage) / PERCENTAGE_PRECISION; + uint256 protocolFee = (oldAmount2 * meta.protocolFee) / PERCENTAGE_PRECISION; + + protocolFees[protocolFeeRecipient][meta.token2] += protocolFee; + + uint256 requiredToken1Contribution = meta.escalationHalt > oldAmount1 + ? (oldAmount1 * meta.multiplier) / MULTIPLIER_PRECISION + : oldAmount1 + 1; + + uint256 netToken1Contribution = + requiredToken1Contribution > (oldAmount1) ? requiredToken1Contribution - (oldAmount1) : 0; + + if (netToken1Contribution > 0) { + IERC20(meta.token1).safeTransferFrom(msg.sender, address(this), netToken1Contribution); + } + + IERC20(meta.token2).safeTransferFrom(msg.sender, address(this), newAmount2 + oldAmount2 + fee + protocolFee); + IERC20(meta.token2).safeTransfer(status.currentReporter, 2 * oldAmount2 + fee); + } else { + uint256 fee = (oldAmount1 * meta.feePercentage) / PERCENTAGE_PRECISION; + uint256 protocolFee = (oldAmount1 * meta.protocolFee) / PERCENTAGE_PRECISION; + + protocolFees[protocolFeeRecipient][meta.token1] += protocolFee; + + uint256 requiredToken1Contribution = meta.escalationHalt > oldAmount1 + ? (oldAmount1 * meta.multiplier) / MULTIPLIER_PRECISION + : oldAmount1 + 1; + + requiredToken1Contribution += fee + protocolFee; + uint256 netToken1Contribution = + requiredToken1Contribution > (oldAmount1) ? requiredToken1Contribution - (oldAmount1) : 0; + + if (netToken1Contribution > 0) { + IERC20(meta.token1).safeTransferFrom(msg.sender, address(this), netToken1Contribution); + } + + IERC20(meta.token2).safeTransferFrom(msg.sender, address(this), newAmount2 + oldAmount2); + IERC20(meta.token2).safeTransfer(status.currentReporter, 2 * oldAmount2); + + IERC20(meta.token1).safeTransfer(status.currentReporter, fee); + } + } + + /** + * @dev Internal function to handle token transfers + */ + function _transferTokens(address token, address from, address to, uint256 amount) internal { + if (amount == 0) return; // Gas optimization: skip zero transfers + + if (from == address(this)) { + IERC20(token).safeTransfer(to, amount); + } else { + IERC20(token).safeTransferFrom(from, to, amount); + } + } + + /** + * @dev Internal function to send ETH to a recipient + */ + function _sendEth(address payable recipient, uint256 amount) internal { + if (amount == 0) return; // Gas optimization: skip zero transfers + + (bool success,) = recipient.call{value: amount}(""); + if (!success) { + (bool success2,) = payable(address(0)).call{value: amount}(""); + if (!success2) { + //do nothing so at least erc20 can move + } + } + } + + /** + * @dev Gets the current block number (returns L1 block number for L1 deployment) + */ + function _getBlockNumber() internal view returns (uint48) { + uint256 id; + assembly { + id := chainid() + } + + return uint48(block.number); + } +} + diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC1363.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC1363.sol new file mode 100644 index 0000000..7bf3e1f --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC1363.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC1363.sol) + +pragma solidity >=0.6.2; + +import {IERC20} from "./IERC20.sol"; +import {IERC165} from "./IERC165.sol"; + +/** + * @title IERC1363 + * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363]. + * + * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract + * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. + */ +interface IERC1363 is IERC20, IERC165 { + /* + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === + * bytes4(keccak256('transferAndCall(address,uint256)')) ^ + * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) + */ + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferAndCall(address to, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferFromAndCall(address from, address to, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function approveAndCall(address spender, uint256 value) external returns (bool); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @param data Additional data with no specified format, sent in call to `spender`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool); +} diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC165.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC165.sol new file mode 100644 index 0000000..d2c99a5 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC165.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC165.sol) + +pragma solidity >=0.4.16; + +import {IERC165} from "../utils/introspection/IERC165.sol"; diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC20.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC20.sol new file mode 100644 index 0000000..078e9ec --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC20.sol) + +pragma solidity >=0.4.16; + +import {IERC20} from "../token/ERC20/IERC20.sol"; diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/IERC20.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/IERC20.sol new file mode 100644 index 0000000..b493743 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol) + +pragma solidity >=0.4.16; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol new file mode 100644 index 0000000..388bf99 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "../IERC20.sol"; +import {IERC1363} from "../../../interfaces/IERC1363.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC-20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + /** + * @dev An operation with an ERC-20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20 token, address to, uint256 value) internal { + if (!_safeTransfer(token, to, value, true)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + if (!_safeTransferFrom(token, from, to, value, true)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) { + return _safeTransfer(token, to, value, false); + } + + /** + * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) { + return _safeTransferFrom(token, from, to, value, false); + } + + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + * + * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" + * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using + * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract + * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. + */ + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 oldAllowance = token.allowance(address(this), spender); + forceApprove(token, spender, oldAllowance + value); + } + + /** + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no + * value, non-reverting calls are assumed to be successful. + * + * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" + * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using + * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract + * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. + */ + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { + unchecked { + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); + } + } + + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval + * to be set to zero before setting it to a non-zero value, such as USDT. + * + * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function + * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being + * set here. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + if (!_safeApprove(token, spender, value, false)) { + if (!_safeApprove(token, spender, 0, true)) revert SafeERC20FailedOperation(address(token)); + if (!_safeApprove(token, spender, value, true)) revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when + * targeting contracts. + * + * Reverts if the returned value is other than `true`. + */ + function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + safeTransfer(token, to, value); + } else if (!token.transferAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target + * has no code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when + * targeting contracts. + * + * Reverts if the returned value is other than `true`. + */ + function transferFromAndCallRelaxed( + IERC1363 token, + address from, + address to, + uint256 value, + bytes memory data + ) internal { + if (to.code.length == 0) { + safeTransferFrom(token, from, to, value); + } else if (!token.transferFromAndCall(from, to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targeting contracts. + * + * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}. + * Oppositely, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall} + * once without retrying, and relies on the returned value to be true. + * + * Reverts if the returned value is other than `true`. + */ + function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + forceApprove(token, to, value); + } else if (!token.approveAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Imitates a Solidity `token.transfer(to, value)` call, relaxing the requirement on the return value: the + * return value is optional (but if data is returned, it must not be false). + * + * @param token The token targeted by the call. + * @param to The recipient of the tokens + * @param value The amount of token to transfer + * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. + */ + function _safeTransfer(IERC20 token, address to, uint256 value, bool bubble) private returns (bool success) { + bytes4 selector = IERC20.transfer.selector; + + assembly ("memory-safe") { + let fmp := mload(0x40) + mstore(0x00, selector) + mstore(0x04, and(to, shr(96, not(0)))) + mstore(0x24, value) + success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20) + // if call success and return is true, all is good. + // otherwise (not success or return is not true), we need to perform further checks + if iszero(and(success, eq(mload(0x00), 1))) { + // if the call was a failure and bubble is enabled, bubble the error + if and(iszero(success), bubble) { + returndatacopy(fmp, 0x00, returndatasize()) + revert(fmp, returndatasize()) + } + // if the return value is not true, then the call is only successful if: + // - the token address has code + // - the returndata is empty + success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) + } + mstore(0x40, fmp) + } + } + + /** + * @dev Imitates a Solidity `token.transferFrom(from, to, value)` call, relaxing the requirement on the return + * value: the return value is optional (but if data is returned, it must not be false). + * + * @param token The token targeted by the call. + * @param from The sender of the tokens + * @param to The recipient of the tokens + * @param value The amount of token to transfer + * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. + */ + function _safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value, + bool bubble + ) private returns (bool success) { + bytes4 selector = IERC20.transferFrom.selector; + + assembly ("memory-safe") { + let fmp := mload(0x40) + mstore(0x00, selector) + mstore(0x04, and(from, shr(96, not(0)))) + mstore(0x24, and(to, shr(96, not(0)))) + mstore(0x44, value) + success := call(gas(), token, 0, 0x00, 0x64, 0x00, 0x20) + // if call success and return is true, all is good. + // otherwise (not success or return is not true), we need to perform further checks + if iszero(and(success, eq(mload(0x00), 1))) { + // if the call was a failure and bubble is enabled, bubble the error + if and(iszero(success), bubble) { + returndatacopy(fmp, 0x00, returndatasize()) + revert(fmp, returndatasize()) + } + // if the return value is not true, then the call is only successful if: + // - the token address has code + // - the returndata is empty + success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) + } + mstore(0x40, fmp) + mstore(0x60, 0) + } + } + + /** + * @dev Imitates a Solidity `token.approve(spender, value)` call, relaxing the requirement on the return value: + * the return value is optional (but if data is returned, it must not be false). + * + * @param token The token targeted by the call. + * @param spender The spender of the tokens + * @param value The amount of token to transfer + * @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. + */ + function _safeApprove(IERC20 token, address spender, uint256 value, bool bubble) private returns (bool success) { + bytes4 selector = IERC20.approve.selector; + + assembly ("memory-safe") { + let fmp := mload(0x40) + mstore(0x00, selector) + mstore(0x04, and(spender, shr(96, not(0)))) + mstore(0x24, value) + success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20) + // if call success and return is true, all is good. + // otherwise (not success or return is not true), we need to perform further checks + if iszero(and(success, eq(mload(0x00), 1))) { + // if the call was a failure and bubble is enabled, bubble the error + if and(iszero(success), bubble) { + returndatacopy(fmp, 0x00, returndatasize()) + revert(fmp, returndatasize()) + } + // if the return value is not true, then the call is only successful if: + // - the token address has code + // - the returndata is empty + success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) + } + mstore(0x40, fmp) + } + } +} diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/ReentrancyGuard.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/ReentrancyGuard.sol new file mode 100644 index 0000000..6e44894 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/ReentrancyGuard.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol) + +pragma solidity ^0.8.20; + +import {StorageSlot} from "./StorageSlot.sol"; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at, + * consider using {ReentrancyGuardTransient} instead. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + * + * IMPORTANT: Deprecated. This storage-based reentrancy guard will be removed and replaced + * by the {ReentrancyGuardTransient} variant in v6.0. + * + * @custom:stateless + */ +abstract contract ReentrancyGuard { + using StorageSlot for bytes32; + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant REENTRANCY_GUARD_STORAGE = + 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; + + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + + constructor() { + _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + /** + * @dev A `view` only version of {nonReentrant}. Use to block view functions + * from being called, preventing reading from inconsistent contract state. + * + * CAUTION: This is a "view" modifier and does not change the reentrancy + * status. Use it only on view functions. For payable or non-payable functions, + * use the standard {nonReentrant} modifier instead. + */ + modifier nonReentrantView() { + _nonReentrantBeforeView(); + _; + } + + function _nonReentrantBeforeView() private view { + if (_reentrancyGuardEntered()) { + revert ReentrancyGuardReentrantCall(); + } + } + + function _nonReentrantBefore() private { + // On the first call to nonReentrant, _status will be NOT_ENTERED + _nonReentrantBeforeView(); + + // Any calls to nonReentrant after this point will fail + _reentrancyGuardStorageSlot().getUint256Slot().value = ENTERED; + } + + function _nonReentrantAfter() private { + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED; + } + + /** + * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a + * `nonReentrant` function in the call stack. + */ + function _reentrancyGuardEntered() internal view returns (bool) { + return _reentrancyGuardStorageSlot().getUint256Slot().value == ENTERED; + } + + function _reentrancyGuardStorageSlot() internal pure virtual returns (bytes32) { + return REENTRANCY_GUARD_STORAGE; + } +} diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/StorageSlot.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/StorageSlot.sol new file mode 100644 index 0000000..aebb105 --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/StorageSlot.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC-1967 implementation slot: + * ```solidity + * contract ERC1967 { + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * TIP: Consider using this library along with {SlotDerivation}. + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct Int256Slot { + int256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Int256Slot` with member `value` located at `slot`. + */ + function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + assembly ("memory-safe") { + r.slot := store.slot + } + } + + /** + * @dev Returns a `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + assembly ("memory-safe") { + r.slot := store.slot + } + } +} diff --git a/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/introspection/IERC165.sol b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/introspection/IERC165.sol new file mode 100644 index 0000000..be1932f --- /dev/null +++ b/solidity/contracts/peripherals/openOracle/openzeppelin/contracts/utils/introspection/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol) + +pragma solidity >=0.4.16; + +/** + * @dev Interface of the ERC-165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[ERC]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/solidity/package.json b/solidity/package.json index cf2ff80..6fa2b2a 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -3,9 +3,10 @@ "type": "module", "description": "Zoltar Contracts", "scripts": { - "contracts": "npm ci --ignore-scripts && npm run compile", - "compile": "tsc --project tsconfig-compile.json && node ./js/compile.js", - "test": "npx tsc && node --test $npm_config_testargs" + "setup": "npm ci --ignore-scripts && npm run compile-contracts", + "compile-contracts": "tsc --project tsconfig-compile.json && node ./js/compile.js", + "test": "npx tsc && node --test", + "test-peripherals": "npx tsc && node --test ./js/tests/testPeripherals.js" }, "keywords": [], "author": "", diff --git a/solidity/ts/tests/testPeripherals.ts b/solidity/ts/tests/testPeripherals.ts new file mode 100644 index 0000000..5c4b38e --- /dev/null +++ b/solidity/ts/tests/testPeripherals.ts @@ -0,0 +1,196 @@ +import { describe, beforeEach, test } from 'node:test' +import { getMockedEthSimulateWindowEthereum, MockWindowEthereum } from '../testsuite/simulator/MockWindowEthereum.js' +import { createWriteClient, WriteClient } from '../testsuite/simulator/utils/viem.js' +import { DAY, GENESIS_REPUTATION_TOKEN, TEST_ADDRESSES } from '../testsuite/simulator/utils/constants.js' +import { approximatelyEqual, contractExists, getChildUniverseId, getERC20Balance, getETHBalance, getReportBond, getRepTokenAddress, setupTestAccounts } from '../testsuite/simulator/utils/utilities.js' +import { addressString } from '../testsuite/simulator/utils/bigint.js' +import { createCompleteSet, forkSecurityPool, getCompleteSetAddress, getCompleteSetCollateralAmount, getLastPrice, getPriceOracleManagerAndOperatorQueuer, getSecurityBondAllowance, OperationType, redeemCompleteSet, migrateVault, getSecurityPoolAddress, getMigratedRep, getSystemState, startTruthAuction, getCurrentRetentionRate, getTruthAuction, getEthAmountToBuy, participateAuction, finalizeTruthAuction, claimAuctionProceeds, getSecurityVault, getPoolOwnershipDenominator, poolOwnershipToRep } from '../testsuite/simulator/utils/peripherals.js' +import assert from 'node:assert' +import { SystemState } from '../testsuite/simulator/types/peripheralTypes.js' +import { getDeployments } from '../testsuite/simulator/utils/deployments.js' +import { createTransactionExplainer } from '../testsuite/simulator/utils/transactionExplainer.js' +import { approveAndDepositRep, deployPeripherals, deployZoltarAndCreateMarket, genesisUniverse, MAX_RETENTION_RATE, PRICE_PRECISION, questionId, requestPrice, securityMultiplier, triggerFork } from '../testsuite/simulator/utils/peripheralsTestUtils.js' + +describe('Peripherals Contract Test Suite', () => { + let mockWindow: MockWindowEthereum + let securityPoolAddress: `0x${ string }` + let client: WriteClient + let startBalance: bigint + let reportBond: bigint + const repDeposit = 100n * 10n ** 18n + let priceOracleManagerAndOperatorQueuer: `0x${ string }` + + beforeEach(async () => { + mockWindow = getMockedEthSimulateWindowEthereum() + mockWindow.setAfterTransactionSendCallBack(createTransactionExplainer(getDeployments(genesisUniverse, questionId, securityMultiplier))) + client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) + //await mockWindow.setStartBLock(mockWindow.getTime) + await setupTestAccounts(mockWindow) + startBalance = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address) + const currentTimestamp = BigInt(Math.floor((await mockWindow.getTime()).getTime() / 1000)) + await deployZoltarAndCreateMarket(client, currentTimestamp + DAY / 2n) + await deployPeripherals(client) + await approveAndDepositRep(client, repDeposit) + securityPoolAddress = getSecurityPoolAddress(addressString(0x0n), genesisUniverse, questionId, securityMultiplier) + reportBond = await getReportBond(client) + priceOracleManagerAndOperatorQueuer = await getPriceOracleManagerAndOperatorQueuer(client, securityPoolAddress) + }) + + test('can deposit rep and withdraw it', async () => { + await requestPrice(client, mockWindow, priceOracleManagerAndOperatorQueuer, OperationType.WithdrawRep, client.account.address, repDeposit) + assert.strictEqual(await getLastPrice(client, priceOracleManagerAndOperatorQueuer), 1n * PRICE_PRECISION, 'Price was not set!') + assert.strictEqual(await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), securityPoolAddress), 0n, 'Did not empty security pool of rep') + assert.strictEqual(await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address), startBalance - reportBond, 'Did not get rep back') + }) + + test('can set security bonds allowance, mint complete sets and fork happily' , async () => { + const securityPoolAllowance = repDeposit / 4n + assert.strictEqual(await getCurrentRetentionRate(client, securityPoolAddress), MAX_RETENTION_RATE, 'retention rate was not at max'); + await requestPrice(client, mockWindow, priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, securityPoolAllowance) + assert.strictEqual(await getLastPrice(client, priceOracleManagerAndOperatorQueuer), 1n * PRICE_PRECISION, 'Price was not set!') + assert.strictEqual(await getSecurityBondAllowance(client, securityPoolAddress), securityPoolAllowance, 'Security pool allowance was not set correctly') + + const openInterestAmount = 1n * 10n ** 18n + const maxGasFees = openInterestAmount /4n + const ethBalance = await getETHBalance(client, client.account.address) + await createCompleteSet(client, securityPoolAddress, openInterestAmount) + assert.ok(await getCurrentRetentionRate(client, securityPoolAddress) < MAX_RETENTION_RATE, 'retention rate did not decrease after minting complete sets'); + const completeSetAddress = getCompleteSetAddress(securityPoolAddress) + const completeSetBalance = await getERC20Balance(client, completeSetAddress, client.account.address) + assert.strictEqual(openInterestAmount, completeSetBalance, 'Did not create enough complete sets') + assert.ok(ethBalance - await getETHBalance(client, client.account.address) > maxGasFees, 'Did not lose eth to create complete sets') + assert.strictEqual(await getCompleteSetCollateralAmount(client, securityPoolAddress), openInterestAmount, 'contract did not record the amount correctly') + await redeemCompleteSet(client, securityPoolAddress, openInterestAmount) + assert.ok(ethBalance - await getETHBalance(client, client.account.address) < maxGasFees, 'Did not get ETH back from complete sets') + assert.strictEqual(await getERC20Balance(client, completeSetAddress, client.account.address), 0n, 'Did not lose complete sets') + assert.strictEqual(await getCurrentRetentionRate(client, securityPoolAddress), MAX_RETENTION_RATE, 'retention rate was not at max after zero complete sets'); + + // forking + await createCompleteSet(client, securityPoolAddress, openInterestAmount) + const repBalance = await getERC20Balance(client, getRepTokenAddress(genesisUniverse), securityPoolAddress) + await triggerFork(client, mockWindow, questionId) + await forkSecurityPool(client, securityPoolAddress) + assert.strictEqual(await getSystemState(client, securityPoolAddress), SystemState.PoolForked, 'Parent is forked') + assert.strictEqual(0n, await getERC20Balance(client, getRepTokenAddress(genesisUniverse), securityPoolAddress), 'Parents original rep is gone') + await migrateVault(client, securityPoolAddress, QuestionOutcome.Yes) + const yesUniverse = getChildUniverseId(genesisUniverse, QuestionOutcome.Yes) + const yesSecurityPool = getSecurityPoolAddress(securityPoolAddress, yesUniverse, questionId, securityMultiplier) + assert.strictEqual(await getCurrentRetentionRate(client, securityPoolAddress), await getCurrentRetentionRate(client, yesSecurityPool), 'Parent and childs retention rate should be equal') + + assert.strictEqual(await getSystemState(client, yesSecurityPool), SystemState.ForkMigration, 'Fork Migration need to start') + const migratedRep = await getMigratedRep(client, yesSecurityPool) + assert.strictEqual(migratedRep, repBalance, 'correct amount rep migrated') + assert.ok(await contractExists(client, yesSecurityPool), 'Did not create YES security pool') + await mockWindow.advanceTime(8n * 7n * DAY + DAY) + await startTruthAuction(client, yesSecurityPool) + assert.strictEqual(await getSystemState(client, yesSecurityPool), SystemState.Operational, 'System should be operational again') + assert.strictEqual(await getCompleteSetCollateralAmount(client, yesSecurityPool), openInterestAmount, 'child contract did not record the amount correctly') + }) + test('two security pools with disagreement', async () => { + const openInterestAmount = 1n * 10n ** 18n + const securityPoolAllowance = repDeposit / 4n + await requestPrice(client, mockWindow, priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, securityPoolAllowance) + const attackerClient = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0) + await approveAndDepositRep(attackerClient, repDeposit) + await requestPrice(attackerClient, mockWindow, priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, securityPoolAllowance) + + const repBalanceInGenesisPool = await getERC20Balance(client, getRepTokenAddress(genesisUniverse), securityPoolAddress) + assert.strictEqual(repBalanceInGenesisPool, 2n * repDeposit, 'After two deposits, the system should have 2 x repDeposit worth of REP') + assert.strictEqual(await getSecurityBondAllowance(client, securityPoolAddress), 2n * securityPoolAllowance, 'Security bond allowance should be 2x') + assert.strictEqual(await getPoolOwnershipDenominator(client, securityPoolAddress), repBalanceInGenesisPool * PRICE_PRECISION, 'Pool ownership denominator should equal `pool balance * PRICE_PRECISION` prior fork') + + const openInterestHolder = createWriteClient(mockWindow, TEST_ADDRESSES[2], 0) + await createCompleteSet(openInterestHolder, securityPoolAddress, openInterestAmount) + const completeSetAddress = getCompleteSetAddress(securityPoolAddress) + const completeSetBalance = await getERC20Balance(client, completeSetAddress, addressString(TEST_ADDRESSES[2])) + assert.strictEqual(openInterestAmount, completeSetBalance, 'Did not create enough complete sets') + + await triggerFork(client, mockWindow, questionId) + await forkSecurityPool(client, securityPoolAddress) + + // we migrate to yes + const yesUniverse = getChildUniverseId(genesisUniverse, QuestionOutcome.Yes) + const yesSecurityPool = getSecurityPoolAddress(securityPoolAddress, yesUniverse, questionId, securityMultiplier) + await migrateVault(client, securityPoolAddress, QuestionOutcome.Yes) + const migratedRepInYes = await getMigratedRep(client, yesSecurityPool) + assert.strictEqual(repBalanceInGenesisPool / 2n, migratedRepInYes, 'half migrated to yes') + assert.strictEqual(await getERC20Balance(client, getRepTokenAddress(yesUniverse), yesSecurityPool), repBalanceInGenesisPool, 'yes has all the rep') + + // attacker migrated to No + const noUniverse = getChildUniverseId(genesisUniverse, QuestionOutcome.No) + const noSecurityPool = getSecurityPoolAddress(securityPoolAddress, noUniverse, questionId, securityMultiplier) + await migrateVault(attackerClient, securityPoolAddress, QuestionOutcome.No) + const migratedRepInNo = await getMigratedRep(client, noSecurityPool) + assert.strictEqual(repBalanceInGenesisPool / 2n, migratedRepInNo, 'half migrated to no') + assert.strictEqual(await getERC20Balance(client, getRepTokenAddress(noUniverse), noSecurityPool), repBalanceInGenesisPool, 'no has all the rep') + + // auction + await mockWindow.advanceTime(8n * 7n * DAY + DAY) + await startTruthAuction(client, yesSecurityPool) + assert.strictEqual(await getSystemState(client, yesSecurityPool), SystemState.ForkTruthAuction, 'Auction started') + await startTruthAuction(client, noSecurityPool) + assert.strictEqual(await getSystemState(client, noSecurityPool), SystemState.ForkTruthAuction, 'Auction started') + const yesAuction = getTruthAuction(yesSecurityPool) + const noAuction = getTruthAuction(noSecurityPool) + + const ethToBuyInYes = await getEthAmountToBuy(client, yesAuction) + const ethToBuyInNo = await getEthAmountToBuy(client, noAuction) + assert.strictEqual(ethToBuyInYes, openInterestAmount / 2n, 'Need to buy half of open interest') + assert.strictEqual(ethToBuyInNo, openInterestAmount / 2n, 'Need to buy half of open interest') + + // participate yes auction by buying quarter of all REP (this is a open interest and rep holder happy case where REP holders win 50%) + const yesAuctionParticipant = createWriteClient(mockWindow, TEST_ADDRESSES[3], 0) + await participateAuction(yesAuctionParticipant, yesAuction, repBalanceInGenesisPool / 4n, openInterestAmount / 2n) + + // participate yes auction by buying 3/4 of all REP (this is a open interest happy case where REP holders lose happy case where REP holders lose 50%) + const noAuctionParticipant = createWriteClient(mockWindow, TEST_ADDRESSES[4], 0) + await participateAuction(noAuctionParticipant, noAuction, repBalanceInGenesisPool * 3n / 4n, openInterestAmount / 2n) + + await mockWindow.advanceTime(7n * DAY + DAY) + + await finalizeTruthAuction(client, yesSecurityPool) + await finalizeTruthAuction(client, noSecurityPool) + + assert.strictEqual(await getSystemState(client, yesSecurityPool), SystemState.Operational, 'Yes System should be operational again') + assert.strictEqual(await getSystemState(client, noSecurityPool), SystemState.Operational, 'No System should be operational again') + assert.strictEqual(await getCompleteSetCollateralAmount(client, yesSecurityPool), openInterestAmount, 'yes child contract did not record the amount correctly') + assert.strictEqual(await getCompleteSetCollateralAmount(client, noSecurityPool), openInterestAmount, 'no child contract did not record the amount correctly') + + await claimAuctionProceeds(client, yesSecurityPool, yesAuctionParticipant.account.address) + await claimAuctionProceeds(client, noSecurityPool, noAuctionParticipant.account.address) + + // yes status + const yesAuctionParticipantVault = await getSecurityVault(client, yesSecurityPool, yesAuctionParticipant.account.address) + console.log(yesAuctionParticipantVault) + const yesAuctionParticipantRep = await poolOwnershipToRep(client, yesSecurityPool, yesAuctionParticipantVault.repDepositShare) + approximatelyEqual(yesAuctionParticipantRep, repBalanceInGenesisPool / 4n, 1000n, 'yes auction participant did not get ownership of rep they bought') + + const originalYesVault = await getSecurityVault(client, yesSecurityPool, client.account.address) + const originalYesVaultRep = await poolOwnershipToRep(client, yesSecurityPool, originalYesVault.repDepositShare) + approximatelyEqual(originalYesVaultRep, repBalanceInGenesisPool * 3n / 4n, 1000n, 'original yes vault holder should hold rest 3/4 of rep') + assert.strictEqual((await getSecurityVault(client, yesSecurityPool, attackerClient.account.address)).repDepositShare, 0n, 'attacker should have zero as they did not migrate to yes') + + // no status + const noAuctionParticipantVault = await getSecurityVault(client, noSecurityPool, noAuctionParticipant.account.address) + const noAuctionParticipantRep = await poolOwnershipToRep(client, noSecurityPool, noAuctionParticipantVault.repDepositShare) + approximatelyEqual(noAuctionParticipantRep, repBalanceInGenesisPool * 3n / 4n, 1000n, 'no auction participant did not get ownership of rep they bought') + + const originalNoVault = await getSecurityVault(client, noSecurityPool, attackerClient.account.address) + const originalNoVaultRep = await poolOwnershipToRep(client, noSecurityPool, originalNoVault.repDepositShare) + approximatelyEqual(originalNoVaultRep, repBalanceInGenesisPool * 1n / 4n, 1000n, 'original no vault holder should hold rest 1/4 of rep') + assert.strictEqual((await getSecurityVault(client, noSecurityPool, client.account.address)).repDepositShare, 0n, 'client should have zero as they did not migrate to no') + }) + + //test('can liquidate', async () => { + // add liquidation test + //}) + + //test('cannot mint over or withdraw too much rep', async () => { + // add complete sets minting test where price has changed so we can no longer mint + //}) + + //test('test that fees subtract balances', async () => { + // add liquidation test + //}) + +}) diff --git a/solidity/ts/testsuite/simulator/types/peripheralTypes.ts b/solidity/ts/testsuite/simulator/types/peripheralTypes.ts new file mode 100644 index 0000000..cde4aa1 --- /dev/null +++ b/solidity/ts/testsuite/simulator/types/peripheralTypes.ts @@ -0,0 +1,6 @@ +export enum SystemState { + Operational, + PoolForked, + ForkMigration, + ForkTruthAuction, +} diff --git a/solidity/ts/testsuite/simulator/utils/constants.ts b/solidity/ts/testsuite/simulator/utils/constants.ts index 9be5f80..10b5252 100644 --- a/solidity/ts/testsuite/simulator/utils/constants.ts +++ b/solidity/ts/testsuite/simulator/utils/constants.ts @@ -15,6 +15,7 @@ export const PROXY_DEPLOYER_ADDRESS = 0x7a0d94f55792c434d74a40883c6ed8545e406d12 export const VITALIK = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045n export const REP_BOND = 10n**18n export const BURN_ADDRESS = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeFn +export const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' // Testing export const TEST_ADDRESSES = [ diff --git a/solidity/ts/testsuite/simulator/utils/deployments.ts b/solidity/ts/testsuite/simulator/utils/deployments.ts new file mode 100644 index 0000000..5fa2e44 --- /dev/null +++ b/solidity/ts/testsuite/simulator/utils/deployments.ts @@ -0,0 +1,100 @@ +import { peripherals_interfaces_IAugur_IAugur, IERC20_IERC20, peripherals_interfaces_IWeth9_IWeth9, peripherals_Auction_Auction, peripherals_CompleteSet_CompleteSet, peripherals_openOracle_OpenOracle_OpenOracle, peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer, peripherals_SecurityPool_SecurityPool, peripherals_SecurityPoolFactory_SecurityPoolFactory, ReputationToken_ReputationToken, Zoltar_Zoltar, peripherals_SecurityPoolUtils_SecurityPoolUtils } from '../../../types/contractArtifact.js' +import { QuestionOutcome } from '../types/types.js' +import { addressString } from './bigint.js' +import { ETHEREUM_LOGS_LOGGER_ADDRESS, GENESIS_REPUTATION_TOKEN, TEST_ADDRESSES, WETH_ADDRESS } from './constants.js' +import { Deployment } from './logExplaining.js' +import { getCompleteSetAddress, getOpenOracleAddress, getPriceOracleManagerAndOperatorQueuerAddress, getSecurityPoolAddress, getSecurityPoolFactoryAddress, getSecurityPoolUtilsAddress, getTruthAuction } from './peripherals.js' +import { getChildUniverseId, getRepTokenAddress, getZoltarAddress } from './utilities.js' + +const getDeploymentsForUniverse = (universeId: bigint, securityPoolAddress: `0x${ string }`, repTokenAddress: `0x${ string }`, priceOracleManagerAndOperatorQueuerAddress: `0x${ string }`, completeSetAddress: `0x${ string }`, auction: `0x${ string }`): Deployment[] => [ + { + abi: ReputationToken_ReputationToken.abi, + deploymentName: `RepV2-U${ universeId }`, + address: repTokenAddress + }, { + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + deploymentName: `PriceOracleManagerAndOperatorQueuer U${ universeId }`, + address: priceOracleManagerAndOperatorQueuerAddress + }, { + abi: peripherals_SecurityPool_SecurityPool.abi, + deploymentName: `ETH SecurityPool U${ universeId }`, + address: securityPoolAddress + }, { + abi: peripherals_CompleteSet_CompleteSet.abi, + deploymentName: `CompleteSet U${ universeId }`, + address: completeSetAddress + }, { + abi: peripherals_Auction_Auction.abi, + deploymentName: `Truth Auction U${ universeId }`, + address: auction + } +] as const + +export const getDeployments = (genesisUniverse: bigint, questionId: bigint, securityMultiplier: bigint): Deployment[] => { + const securityPoolAddress = getSecurityPoolAddress(addressString(0x0n), genesisUniverse, questionId, securityMultiplier) + const repToken = addressString(GENESIS_REPUTATION_TOKEN) + const priceOracleManagerAndOperatorQueuerAddress = getPriceOracleManagerAndOperatorQueuerAddress(securityPoolAddress, repToken) + const completeSetAddress = getCompleteSetAddress(securityPoolAddress) + const truthAuction = getTruthAuction(securityPoolAddress) + + const oucomes = [QuestionOutcome.Invalid, QuestionOutcome.No, QuestionOutcome.Yes] + + const getChildAddresses = (parentSecurityPoolAddress: `0x${ string }`, parentUniverseId: bigint): Deployment[] => { + return oucomes.flatMap((outcome) => { + const universeId = getChildUniverseId(parentUniverseId, outcome) + const securityPoolAddress = getSecurityPoolAddress(parentSecurityPoolAddress, universeId, questionId, securityMultiplier) + const priceOracleManagerAndOperatorQueuerAddress = getPriceOracleManagerAndOperatorQueuerAddress(securityPoolAddress, getRepTokenAddress(universeId)) + const completeSetAddress = getCompleteSetAddress(securityPoolAddress) + const truthAuction = getTruthAuction(securityPoolAddress) + return getDeploymentsForUniverse(universeId, securityPoolAddress, getRepTokenAddress(universeId), priceOracleManagerAndOperatorQueuerAddress, completeSetAddress, truthAuction) + }) + } + + return [ + ...getDeploymentsForUniverse(genesisUniverse, securityPoolAddress, getRepTokenAddress(genesisUniverse), priceOracleManagerAndOperatorQueuerAddress, completeSetAddress, truthAuction), + ...getChildAddresses(securityPoolAddress, genesisUniverse), // children + ...oucomes.flatMap((outcome) => getChildAddresses(getSecurityPoolAddress(securityPoolAddress, genesisUniverse, questionId, securityMultiplier), getChildUniverseId(genesisUniverse, outcome))), // grand children + { + abi: Zoltar_Zoltar.abi, + deploymentName: 'Zoltar', + address: getZoltarAddress(), + }, { + abi: peripherals_SecurityPoolFactory_SecurityPoolFactory.abi, + deploymentName: 'SecurityPoolFactory', + address: getSecurityPoolFactoryAddress() + }, { + abi: peripherals_openOracle_OpenOracle_OpenOracle.abi, + deploymentName: 'OpenOracle', + address: getOpenOracleAddress() + }, { + abi: peripherals_interfaces_IWeth9_IWeth9.abi, + deploymentName: 'WETH', + address: WETH_ADDRESS + }, { + abi: peripherals_interfaces_IAugur_IAugur.abi, + deploymentName: 'Augur', + address: '0x23916a8f5c3846e3100e5f587ff14f3098722f5d' + }, { + abi: IERC20_IERC20.abi, + deploymentName: 'ETH', + address: addressString(ETHEREUM_LOGS_LOGGER_ADDRESS) + }, { + abi: undefined, + deploymentName: 'Micah Deployer', + address: `0x7a0d94f55792c434d74a40883c6ed8545e406d12` + }, { + abi: peripherals_SecurityPoolUtils_SecurityPoolUtils.abi, + deploymentName: 'Security Pool Utils', + address: getSecurityPoolUtilsAddress() + }, { + abi: undefined, + deploymentName: 'Augur V2 Genesis', + address: '0x49244BD018Ca9fd1f06ecC07B9E9De773246e5AA' + }, + ...TEST_ADDRESSES.map((testAddress, index) => ({ + abi: undefined, + deploymentName: `Test EOA(${ index + 1 })`, + address: addressString(testAddress) + } as const)) + ] as const +} diff --git a/solidity/ts/testsuite/simulator/utils/logExplaining.ts b/solidity/ts/testsuite/simulator/utils/logExplaining.ts index e8edf29..50aeb98 100644 --- a/solidity/ts/testsuite/simulator/utils/logExplaining.ts +++ b/solidity/ts/testsuite/simulator/utils/logExplaining.ts @@ -13,7 +13,7 @@ interface DecodedLog { args: Record | undefined } -function safeDecodeEventLog(parameters: { abi: Abi; data: `0x${string}`; topics: [`0x${string}`, ...`0x${string}`[]] | [] }): DecodedLog | undefined { +function safeDecodeEventLog(parameters: { abi: Abi; data: `0x${ string }`; topics: [`0x${ string }`, ...`0x${ string }`[]] | [] }): DecodedLog | undefined { try { const result = decodeEventLog(parameters) as unknown if (typeof result === 'object' && result !== null && 'eventName' in result && 'args' in result) return result as DecodedLog diff --git a/solidity/ts/testsuite/simulator/utils/peripherals.ts b/solidity/ts/testsuite/simulator/utils/peripherals.ts new file mode 100644 index 0000000..ff72855 --- /dev/null +++ b/solidity/ts/testsuite/simulator/utils/peripherals.ts @@ -0,0 +1,514 @@ +import 'viem/window' +import { encodeDeployData, getContractAddress, getCreate2Address, keccak256, numberToBytes, ReadContractReturnType, toHex } from 'viem' +import { ReadClient, WriteClient } from './viem.js' +import { PROXY_DEPLOYER_ADDRESS, WETH_ADDRESS } from './constants.js' +import { addressString, bytes32String } from './bigint.js' +import { getZoltarAddress } from './utilities.js' +import { mainnet } from 'viem/chains' +import { SystemState } from '../types/peripheralTypes.js' +import { peripherals_Auction_Auction, peripherals_CompleteSet_CompleteSet, peripherals_openOracle_OpenOracle_OpenOracle, peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer, peripherals_SecurityPool_SecurityPool, peripherals_SecurityPoolFactory_SecurityPoolFactory, peripherals_SecurityPoolUtils_SecurityPoolUtils } from '../../../types/contractArtifact.js' +import { QuestionOutcome } from '../types/types.js' + +export async function ensureProxyDeployerDeployed(client: WriteClient): Promise { + const deployerBytecode = await client.getCode({ address: addressString(PROXY_DEPLOYER_ADDRESS)}) + if (deployerBytecode === '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3') return + const ethSendHash = await client.sendTransaction({ to: '0x4c8d290a1b368ac4728d83a9e8321fc3af2b39b1', amount: 10000000000000000n }) + await client.waitForTransactionReceipt({ hash: ethSendHash }) + const deployHash = await client.sendRawTransaction({ serializedTransaction: '0xf87e8085174876e800830186a08080ad601f80600e600039806000f350fe60003681823780368234f58015156014578182fd5b80825250506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' }) + await client.waitForTransactionReceipt({ hash: deployHash }) +} + +export function getOpenOracleAddress() { + const bytecode: `0x${ string }` = `0x${ peripherals_openOracle_OpenOracle_OpenOracle.evm.bytecode.object }` + return getContractAddress({ bytecode, from: addressString(PROXY_DEPLOYER_ADDRESS), opcode: 'CREATE2', salt: numberToBytes(0) }) +} + +export const isOpenOracleDeployed = async (client: ReadClient) => { + const expectedDeployedBytecode: `0x${ string }` = `0x${ peripherals_openOracle_OpenOracle_OpenOracle.evm.deployedBytecode.object }` + const address = getOpenOracleAddress() + const deployedBytecode = await client.getCode({ address }) + return deployedBytecode === expectedDeployedBytecode +} + +export function getSecurityPoolUtilsAddress() { + const bytecode: `0x${ string }` = `0x${ peripherals_SecurityPoolUtils_SecurityPoolUtils.evm.bytecode.object }` + return getContractAddress({ bytecode, from: addressString(PROXY_DEPLOYER_ADDRESS), opcode: 'CREATE2', salt: numberToBytes(0) }) +} + +export const isSecurityPoolUtilsDeployed = async (client: ReadClient) => { + const expectedDeployedBytecode: `0x${ string }` = `0x${ peripherals_SecurityPoolUtils_SecurityPoolUtils.evm.deployedBytecode.object }` + const address = getOpenOracleAddress() + const deployedBytecode = await client.getCode({ address }) + return deployedBytecode === expectedDeployedBytecode +} + +export const ensureOpenOracleDeployed = async (client: WriteClient) => { + await ensureProxyDeployerDeployed(client) + if (await isOpenOracleDeployed(client)) return + const bytecode: `0x${ string }` = `0x${ peripherals_openOracle_OpenOracle_OpenOracle.evm.bytecode.object }` + const hash = await client.sendTransaction({ to: addressString(PROXY_DEPLOYER_ADDRESS), data: bytecode } as const) + await client.waitForTransactionReceipt({ hash }) +} + +export const ensureSecurityPoolUtilsDeployed = async (client: WriteClient) => { + await ensureProxyDeployerDeployed(client) + if (await isSecurityPoolUtilsDeployed(client)) return + const bytecode: `0x${ string }` = `0x${ peripherals_SecurityPoolUtils_SecurityPoolUtils.evm.bytecode.object }` + const hash = await client.sendTransaction({ to: addressString(PROXY_DEPLOYER_ADDRESS), data: bytecode } as const) + await client.waitForTransactionReceipt({ hash }) +} + +export const applyLibraries = (bytecode: string): `0x${ string }` => { + const securityPoolUtils = keccak256(toHex('contracts/peripherals/SecurityPoolUtils.sol:SecurityPoolUtils')).slice(2, 36) + return `0x${ bytecode.replaceAll(`__$${ securityPoolUtils }$__`, getSecurityPoolUtilsAddress().slice(2).toLocaleLowerCase()) }` +} + +export const isSecurityPoolFactoryDeployed = async (client: ReadClient) => { + const address = getSecurityPoolFactoryAddress() + return await client.getCode({ address }) === applyLibraries(peripherals_SecurityPoolFactory_SecurityPoolFactory.evm.deployedBytecode.object) +} + +export function getSecurityPoolFactoryAddress() { + return getContractAddress({ bytecode: applyLibraries(peripherals_SecurityPoolFactory_SecurityPoolFactory.evm.bytecode.object), from: addressString(PROXY_DEPLOYER_ADDRESS), opcode: 'CREATE2', salt: numberToBytes(0) }) +} + +export const ensureSecurityPoolFactoryDeployed = async (client: WriteClient) => { + await ensureProxyDeployerDeployed(client) + await ensureSecurityPoolUtilsDeployed(client) + if (await isSecurityPoolFactoryDeployed(client)) return + const hash = await client.sendTransaction({ to: addressString(PROXY_DEPLOYER_ADDRESS), data: applyLibraries(peripherals_SecurityPoolFactory_SecurityPoolFactory.evm.bytecode.object) } as const) + await client.waitForTransactionReceipt({ hash }) +} + +export const deploySecurityPool = async (client: WriteClient, openOracle: `0x${ string }`, universeId: bigint, questionId: bigint, securityMultiplier: bigint, startingRetentionRate: bigint, startingRepEthPrice: bigint, completeSetCollateralAmount: bigint) => { + return await client.writeContract({ + chain: mainnet, + abi: peripherals_SecurityPoolFactory_SecurityPoolFactory.abi, + functionName: 'deploySecurityPool', + address: getSecurityPoolFactoryAddress(), + args: [openOracle, addressString(0x0n), getZoltarAddress(), universeId, questionId, securityMultiplier, startingRetentionRate, startingRepEthPrice, completeSetCollateralAmount] + }) +} + +export const depositRep = async (client: WriteClient, securityPoolAddress: `0x${ string }`, amount: bigint) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'depositRep', + address: securityPoolAddress, + args: [amount] + }) +} + +export const getPriceOracleManagerAndOperatorQueuer = async (client: ReadClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'priceOracleManagerAndOperatorQueuer', + address: securityPoolAddress, + args: [] + }) as `0x${ string }` +} + +export enum OperationType { + Liquidation = 0, + WithdrawRep = 1, + SetSecurityBondsAllowance = 2 +} + +export const requestPriceIfNeededAndQueueOperation = async (client: WriteClient, priceOracleManagerAndOperatorQueuer: `0x${ string }`, operation: OperationType, targetVault: `0x${ string }`, amount: bigint) => { + const ethCost = await getRequestPriceEthCost(client, priceOracleManagerAndOperatorQueuer) * 2n; + return await client.writeContract({ + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + functionName: 'requestPriceIfNeededAndQueueOperation', + address: priceOracleManagerAndOperatorQueuer, + args: [operation, targetVault, amount], + value: ethCost, + }) +} + +export const getPendingReportId = async (client: ReadClient, priceOracleManagerAndOperatorQueuer: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + functionName: 'pendingReportId', + address: priceOracleManagerAndOperatorQueuer, + args: [] + }) as bigint +} + +interface ExtraReportData { + stateHash: `0x${ string }` + callbackContract: `0x${ string }` + numReports: number + callbackGasLimit: number + callbackSelector: `0x${ string }` + protocolFeeRecipient: `0x${ string }` + trackDisputes: boolean + keepFee: boolean + feeToken: boolean +} + +export const getOpenOracleExtraData = async (client: ReadClient, extraDataId: bigint): Promise => { + const result = await client.readContract({ + abi: peripherals_openOracle_OpenOracle_OpenOracle.abi, + functionName: 'extraData', + address: getOpenOracleAddress(), + args: [extraDataId] + }) as ReadContractReturnType + + const [ + stateHash, + callbackContract, + numReports, + callbackGasLimit, + callbackSelector, + protocolFeeRecipient, + trackDisputes, + keepFee, + feeToken + ] = result as [ + `0x${ string }`, + `0x${ string }`, + bigint, + bigint, + `0x${ string }`, + `0x${ string }`, + boolean, + boolean, + boolean + ] + + return { + stateHash, + callbackContract, + numReports: Number(numReports), + callbackGasLimit: Number(callbackGasLimit), + callbackSelector, + protocolFeeRecipient, + trackDisputes, + keepFee, + feeToken + } +} + +export const openOracleSubmitInitialReport = async (client: WriteClient, reportId: bigint, amount1: bigint, amount2: bigint, stateHash: `0x${ string }`) => { + return await client.writeContract({ + abi: peripherals_openOracle_OpenOracle_OpenOracle.abi, + functionName: 'submitInitialReport', + address: getOpenOracleAddress(), + args: [reportId, amount1, amount2, stateHash] + }) +} + +export const openOracleSettle = async (client: WriteClient, reportId: bigint) => { + return await client.writeContract({ + abi: peripherals_openOracle_OpenOracle_OpenOracle.abi, + functionName: 'settle', + address: getOpenOracleAddress(), + gas: 10000000n, //needed because of gas() opcode being used + args: [reportId] + }) +} + +export const getRequestPriceEthCost = async (client: ReadClient, priceOracleManagerAndOperatorQueuer: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + functionName: 'getRequestPriceEthCost', + address: priceOracleManagerAndOperatorQueuer, + args: [] + }) as bigint +} + +export const wrapWeth = async (client: WriteClient, amount: bigint) => { + const wethAbi = [{ + type: 'function', + name: 'deposit', + stateMutability: 'payable', + inputs: [], + outputs: [] + }] + return await client.writeContract({ + abi: wethAbi, + address: WETH_ADDRESS, + functionName: 'deposit', + value: amount + }) +} + +export interface ReportMeta { + exactToken1Report: bigint + escalationHalt: bigint + fee: bigint + settlerReward: bigint + token1: `0x${ string }` + settlementTime: number + token2: `0x${ string }` + timeType: boolean + feePercentage: number + protocolFee: number + multiplier: number + disputeDelay: number +} + +export const getOpenOracleReportMeta = async (client: ReadClient, reportId: bigint): Promise => { + const reportMetaData = await client.readContract({ + abi: peripherals_openOracle_OpenOracle_OpenOracle.abi, + functionName: 'reportMeta', + address: getOpenOracleAddress(), + args: [reportId] + }) + + const [ + exactToken1Report, + escalationHalt, + fee, + settlerReward, + token1, + settlementTime, + token2, + timeType, + feePercentage, + protocolFee, + multiplier, + disputeDelay + ] = reportMetaData + + return { + exactToken1Report, + escalationHalt, + fee, + settlerReward, + token1, + settlementTime, + token2, + timeType, + feePercentage, + protocolFee, + multiplier, + disputeDelay + } +} + +export const createCompleteSet = async (client: WriteClient, securityPoolAddress: `0x${ string }`, completeSetsToCreate: bigint) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'createCompleteSet', + address: securityPoolAddress, + args: [], + value: completeSetsToCreate, + }) +} + +export const redeemCompleteSet = async (client: WriteClient, securityPoolAddress: `0x${ string }`, completeSetsToRedeem: bigint) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'redeemCompleteSet', + address: securityPoolAddress, + args: [completeSetsToRedeem], + }) +} + +export const getSecurityBondAllowance = async (client: ReadClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'securityBondAllowance', + address: securityPoolAddress, + args: [] + }) as bigint +} + +export const getCompleteSetCollateralAmount = async (client: ReadClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'completeSetCollateralAmount', + address: securityPoolAddress, + args: [] + }) as bigint +} + +export const getLastPrice = async (client: ReadClient, priceOracleManagerAndOperatorQueuer: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + functionName: 'lastPrice', + address: priceOracleManagerAndOperatorQueuer, + args: [] + }) as bigint +} + +export const forkSecurityPool = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'forkSecurityPool', + address: securityPoolAddress, + args: [], + }) +} + +export const migrateVault = async (client: WriteClient, securityPoolAddress: `0x${ string }`, outcome: QuestionOutcome) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'migrateVault', + address: securityPoolAddress, + args: [Number(outcome)], + }) +} + +export const startTruthAuction = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'startTruthAuction', + address: securityPoolAddress, + args: [], + }) +} + +export const finalizeTruthAuction = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'finalizeTruthAuction', + address: securityPoolAddress, + args: [], + }) +} + +export const claimAuctionProceeds = async (client: WriteClient, securityPoolAddress: `0x${ string }`, vault: `0x${ string }`) => { + return await client.writeContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'claimAuctionProceeds', + address: securityPoolAddress, + args: [vault], + }) +} + +export function getSecurityPoolAddress( + parent: `0x${ string }`, + universeId: bigint, + questionId: bigint, + securityMultiplier: bigint, +) : `0x${ string }` { + const initCode = encodeDeployData({ + abi: peripherals_SecurityPool_SecurityPool.abi, + bytecode: applyLibraries(peripherals_SecurityPool_SecurityPool.evm.bytecode.object), + args: [getSecurityPoolFactoryAddress(), getOpenOracleAddress(), parent, getZoltarAddress(), universeId, questionId, securityMultiplier] + }) + return getCreate2Address({ from: getSecurityPoolFactoryAddress(), salt: bytes32String(1n), bytecodeHash: keccak256(initCode) }) +} + +export function getPriceOracleManagerAndOperatorQueuerAddress(securityPool: `0x${ string }`, repToken: `0x${ string }`): `0x${ string }` { + const initCode = encodeDeployData({ + abi: peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.abi, + bytecode: `0x${ peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer.evm.bytecode.object }`, + args: [getOpenOracleAddress(), securityPool, repToken] + }) + return getCreate2Address({ from: securityPool, salt: bytes32String(1n), bytecodeHash: keccak256(initCode) }) +} + +export function getCompleteSetAddress(securityPool: `0x${ string }`): `0x${ string }` { + const initCode = encodeDeployData({ + abi: peripherals_CompleteSet_CompleteSet.abi, + bytecode: `0x${ peripherals_CompleteSet_CompleteSet.evm.bytecode.object }`, + args: [securityPool] + }) + return getCreate2Address({ from: securityPool, salt: bytes32String(1n), bytecodeHash: keccak256(initCode) }) +} + +export function getTruthAuction(securityPool: `0x${ string }`): `0x${ string }` { + const initCode = encodeDeployData({ + abi: peripherals_Auction_Auction.abi, + bytecode: `0x${ peripherals_Auction_Auction.evm.bytecode.object }`, + args: [securityPool] + }) + return getCreate2Address({ from: securityPool, salt: bytes32String(1n), bytecodeHash: keccak256(initCode) }) +} + +export const participateAuction = async (client: WriteClient, auctionAddress: `0x${ string }`, repToBuy: bigint, ethToInvest: bigint) => { + return await client.writeContract({ + abi: peripherals_Auction_Auction.abi, + functionName: 'participate', + address: auctionAddress, + args: [repToBuy], + value: ethToInvest + }) +} +export const getEthAmountToBuy = async (client: WriteClient, auctionAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_Auction_Auction.abi, + functionName: 'ethAmountToBuy', + address: auctionAddress, + args: [], + }) +} + +export const getMigratedRep = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'migratedRep', + address: securityPoolAddress, + args: [], + }) +} + +export const getSystemState = async (client: WriteClient, securityPoolAddress: `0x${ string }`): Promise => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'systemState', + address: securityPoolAddress, + args: [], + }) +} + +export const getCurrentRetentionRate = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'currentRetentionRate', + address: securityPoolAddress, + args: [], + }) +} + +export const getSecurityVault = async (client: WriteClient, securityPoolAddress: `0x${ string }`, securityVault: `0x${ string }`) => { + const vault = await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'securityVaults', + address: securityPoolAddress, + args: [securityVault], + }) + const [ + repDepositShare, + securityBondAllowance, + unpaidEthFees, + feeAccumulator, + ] = vault + + return { + repDepositShare, + securityBondAllowance, + unpaidEthFees, + feeAccumulator, + } +} + +export const getPoolOwnershipDenominator = async (client: WriteClient, securityPoolAddress: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'poolOwnershipDenominator', + address: securityPoolAddress, + args: [], + }) +} + +export const poolOwnershipToRep = async (client: WriteClient, securityPoolAddress: `0x${ string }`, poolOwnership: bigint) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'poolOwnershipToRep', + address: securityPoolAddress, + args: [poolOwnership], + }) +} + +export const repToPoolOwnership = async (client: WriteClient, securityPoolAddress: `0x${ string }`, repAmount: bigint) => { + return await client.readContract({ + abi: peripherals_SecurityPool_SecurityPool.abi, + functionName: 'repToPoolOwnership', + address: securityPoolAddress, + args: [repAmount], + }) +} diff --git a/solidity/ts/testsuite/simulator/utils/peripheralsTestUtils.ts b/solidity/ts/testsuite/simulator/utils/peripheralsTestUtils.ts new file mode 100644 index 0000000..8b93b45 --- /dev/null +++ b/solidity/ts/testsuite/simulator/utils/peripheralsTestUtils.ts @@ -0,0 +1,96 @@ +import { MockWindowEthereum } from '../MockWindowEthereum.js' +import { QuestionOutcome } from '../types/types.js' +import { addressString } from './bigint.js' +import { DAY, GENESIS_REPUTATION_TOKEN, WETH_ADDRESS } from './constants.js' +import { deploySecurityPool, depositRep, ensureOpenOracleDeployed, ensureSecurityPoolFactoryDeployed, getOpenOracleAddress, getOpenOracleExtraData, getOpenOracleReportMeta, getPendingReportId, getSecurityPoolAddress, isOpenOracleDeployed, isSecurityPoolFactoryDeployed, openOracleSettle, openOracleSubmitInitialReport, OperationType, requestPriceIfNeededAndQueueOperation, wrapWeth } from './peripherals.js' +import { approveToken, contractExists, createQuestion, dispute, ensureZoltarDeployed, getERC20Balance, getQuestionData, getUniverseData, getZoltarAddress, isZoltarDeployed, reportOutcome } from './utilities.js' +import { WriteClient } from './viem.js' +import assert from 'node:assert' + +export const genesisUniverse = 0n +export const questionId = 1n +export const securityMultiplier = 2n +export const startingRepEthPrice = 1n +export const completeSetCollateralAmount = 0n +export const PRICE_PRECISION = 10n ** 18n +export const MAX_RETENTION_RATE = 999_999_996_848_000_000n // ≈90% yearly + +export const deployZoltarAndCreateMarket = async (client: WriteClient, questionEndTime: bigint) => { + await ensureZoltarDeployed(client) + const isDeployed = await isZoltarDeployed(client) + assert.ok(isDeployed, `Zoltar Not Deployed!`) + const zoltar = getZoltarAddress() + await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), zoltar) + await createQuestion(client, genesisUniverse, questionEndTime, 'test') + return await getQuestionData(client, questionId) +} + +export const deployPeripherals = async (client: WriteClient) => { + await ensureOpenOracleDeployed(client) + assert.ok(await isOpenOracleDeployed(client), 'Open Oracle Not Deployed!') + const openOracle = getOpenOracleAddress() + await ensureSecurityPoolFactoryDeployed(client) + assert.ok(await isSecurityPoolFactoryDeployed(client), 'Security Pool Factory Not Deployed!') + await deploySecurityPool(client, openOracle, genesisUniverse, questionId, securityMultiplier, MAX_RETENTION_RATE, startingRepEthPrice, completeSetCollateralAmount) + assert.ok(await contractExists(client, getSecurityPoolAddress(addressString(0x0n), genesisUniverse, questionId, securityMultiplier)), 'security pool not deployed') +} + +export const approveAndDepositRep = async (client: WriteClient, repDeposit: bigint) => { + const securityPoolAddress = getSecurityPoolAddress(addressString(0x0n), genesisUniverse, questionId, securityMultiplier) + assert.ok(await contractExists(client, securityPoolAddress), 'security pool not deployed') + + const startBalance = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), securityPoolAddress) + await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), securityPoolAddress) + await depositRep(client, securityPoolAddress, repDeposit) + + const newBalance = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), securityPoolAddress) + assert.strictEqual(newBalance, startBalance + repDeposit, 'Did not deposit rep') +} + +export const triggerFork = async(client: WriteClient, mockWindow: MockWindowEthereum, questionId: bigint) => { + await ensureZoltarDeployed(client) + await mockWindow.advanceTime(DAY) + const initialOutcome = QuestionOutcome.Yes + await reportOutcome(client, genesisUniverse, questionId, initialOutcome) + const disputeOutcome = QuestionOutcome.No + await dispute(client, genesisUniverse, questionId, disputeOutcome) + const invalidUniverseId = 1n + const yesUniverseId = 2n + const noUniverseId = 3n + return { + invalidUniverseData: await getUniverseData(client, invalidUniverseId), + yesUniverseData: await getUniverseData(client, yesUniverseId), + noUniverseData: await getUniverseData(client, noUniverseId) + } +} + +export const requestPrice = async(client: WriteClient, mockWindow: MockWindowEthereum, priceOracleManagerAndOperatorQueuer: `0x${ string }`, operation: OperationType, targetVault: `0x${ string }`, amount: bigint) => { + await requestPriceIfNeededAndQueueOperation(client, priceOracleManagerAndOperatorQueuer, operation, targetVault, amount) + + const pendingReportId = await getPendingReportId(client, priceOracleManagerAndOperatorQueuer) + if (pendingReportId === 0n) { + // operation already executed + return + } + assert.ok(pendingReportId > 0, 'Operation is not queued') + + const reportMeta = await getOpenOracleReportMeta(client, pendingReportId) + + // initial report + const amount1 = reportMeta.exactToken1Report + const amount2 = amount1 + + await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), getOpenOracleAddress()) + await approveToken(client, WETH_ADDRESS, getOpenOracleAddress()) + await wrapWeth(client, amount2) + const wethBalance = await getERC20Balance(client, WETH_ADDRESS, client.account.address) + assert.strictEqual(wethBalance, amount2, 'Did not wrap weth') + + const stateHash = (await getOpenOracleExtraData(client, pendingReportId)).stateHash + await openOracleSubmitInitialReport(client, pendingReportId, amount1, amount2, stateHash) + + await mockWindow.advanceTime(DAY) + + await openOracleSettle(client, pendingReportId) +} + diff --git a/solidity/ts/testsuite/simulator/utils/viem.ts b/solidity/ts/testsuite/simulator/utils/viem.ts index 24955b1..4ade2d7 100644 --- a/solidity/ts/testsuite/simulator/utils/viem.ts +++ b/solidity/ts/testsuite/simulator/utils/viem.ts @@ -2,7 +2,6 @@ import { createPublicClient, createWalletClient, custom, EIP1193Provider, http, import 'viem/window' import { mainnet } from 'viem/chains' import { addressString } from './bigint.js' -export type AccountAddress = `0x${ string }` const DEFAULT_HTTP = 'https://ethereum.dark.florist'