pragma solidity ^0.8.26; pragma experimental ABIEncoderV2; import "./Interfaces/IERC20.sol"; import "./Interfaces/IElasticERC20.sol"; import "./Libraries/structs.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol"; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; // Upgradeable import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; interface IVerifier { function verify(bytes calldata, bytes32[] calldata) external view returns (bool); function getVerificationKeyHash() external pure returns (bytes32); } interface IOracle { function getCost( uint256 _amount, address _chainlinkFeed, address _xftPool ) external view returns (uint256); function chainlinkPrice(address _chainlinkFeed) external view returns (uint256); } interface IWETH9 is IERC20{ function deposit() external payable; function withdraw(uint256 _amount) external; } interface IMomiji { function publish(bytes calldata _proof, Batch calldata _batch) external; } contract Momiji is Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable, EIP712Upgradeable { address constant _weth9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; uint256 constant MAX_FIELD_VALUE = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000; uint256 constant FIELD_MODULUS = MAX_FIELD_VALUE + 1; bytes32 constant ZERO_VALUE = 0x016a430aa58685aba1311244a973a3bc358859da86784be51094368e8fb6f720; bytes32 constant ZERO_UTXO_ROOT = 0x11d25ff6aa8a431fbce8e8d9a87a2d7986adf38e724fbe47f15752d0931f14d8; bytes32 constant INITIAL_STATE = 0x06f93f503e77fcdcacfe622e66adc639b63e8c0083f5cab5d71d461aa4562c92; bytes32 constant COINBASE_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000003; bytes32 constant BROADCASTER_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000002; bytes32 constant PROVER_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000001; bytes32 constant NO_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000000; IWETH9 constant weth9 = IWETH9(_weth9); IVerifier public verifier; IElasticERC20 public token; IOracle public oracle; ISwapRouter public swapRouter; bytes32[] public validRoots; bytes32 public txKeyHash; bytes32 public txWrapperKeyHash; bytes32 public recursiveKeyHash; uint256 public STATE_DEPTH; uint256 public MAX_ITEMS; uint256 public MAX_UTXOS; bytes32 public merkleRoot; bytes32 public histRoot; // Training Wheels uint256 public dailyCap; uint256 public dailyMint; uint256 public lastCapReset; uint256 public burnPercentageSwap; event EncryptedUTXOBroadcast(bytes32 indexed _oldRoot, bytes32 indexed _utxoId, EncryptedUTXO _encryptedUTXO); event TransactionBroadcast(TransactionWithProof _tx, bytes32 indexed _merkleRoot, bytes32 indexed _txId); event TransactionPublish(Transaction _tx, bytes32 indexed _merkleRoot, bytes32 indexed _txId); event BatchPublish(uint256 indexed _batchNumber, bytes32 _oldRoot, bytes32 indexed _newRoot, bytes32 indexed _oracle, bytes32[20] _historicPath); event BroadcastAddress(bytes32 indexed _merkleRoot, string indexed _address); // _address is a libp2p address // Training Wheels event DailyCapChanged(uint256 indexed _oldCap, uint256 indexed _newCap); event BurnPercentageChanged(uint256 indexed _oldPercentage, uint256 indexed _newPercentage); mapping(bytes32 => bool) public nullifierHashes; mapping(bytes32 => bytes32) public utxoPrevRoots; mapping(bytes32 => bytes32) public relay; uint256 public batchNumber; address public xftPool; address public xft; uint24 public fee; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize( IVerifier _verifier, address _token, bytes32 _txKeyHash, bytes32 _txWrapperKeyHash, bytes32 _recursiveKeyHash, address _xftPool ) public initializer { __Pausable_init(); __Ownable_init(msg.sender); __UUPSUpgradeable_init(); verifier = _verifier; token = IElasticERC20(_token); xft = _token; txKeyHash = _txKeyHash; txWrapperKeyHash = _txWrapperKeyHash; recursiveKeyHash = _recursiveKeyHash; xftPool = _xftPool; swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); STATE_DEPTH = 20; MAX_ITEMS = 16; MAX_UTXOS = 16; merkleRoot = ZERO_VALUE; validRoots.push(merkleRoot); histRoot = INITIAL_STATE; fee = 3000; burnPercentageSwap = 5; dailyCap = 1; __EIP712_init("Momiji", "1"); } function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} modifier nonreentrant() { assembly { if tload(0) { revert(0, 0) } tstore(0, 1) } _; assembly { tstore(0, 0) } } receive() external payable { if (msg.sender != _weth9) { msg.sender.call{value: msg.value}(""); } } fallback() external payable { if (msg.sender != _weth9) { msg.sender.call{value: msg.value}(""); } } // Training Wheels function updateCircuits(bytes32[] calldata _circuits) public onlyOwner { txKeyHash = _circuits[0]; txWrapperKeyHash = _circuits[1]; recursiveKeyHash = _circuits[2]; verifier = IVerifier(_toAddress(_circuits[3])); } // Training Wheels function emergencyPause() public onlyOwner { _pause(); } // Training Wheels function emergencyUnpause() public onlyOwner { _unpause(); } // Training Wheels function changeCap(uint256 _cap) public onlyOwner { require(_cap < 1000, "momiji.changeCap: Cannot exceed 1000 bps"); emit DailyCapChanged(dailyCap, _cap); dailyCap = _cap; } // Training Wheels function changeBurnPercentage(uint256 _percentage) public onlyOwner { require(_percentage < 100, "momiji.changeBurnPercentage: Cannot exceed 100%"); emit BurnPercentageChanged(burnPercentageSwap, _percentage); burnPercentageSwap = _percentage; } // Training Wheels function changePools(address[] calldata _addrs) public onlyOwner { swapRouter = ISwapRouter(_addrs[0]); xftPool = _addrs[1]; xft = _addrs[2]; token = IElasticERC20(_addrs[2]); } function getSpentNullifiers(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) { uint256 nullifierHashesLength = _nullifierHashes.length; spent = new bool[](nullifierHashesLength); for (uint256 i; i < nullifierHashesLength; i++) { if (isSpent(_nullifierHashes[i])) { spent[i] = true; } } } function getValidRoots() public view returns (bytes32[] memory) { return validRoots; } function getValidRootAtIndex(uint256 _index) public view returns (bytes32) { return validRoots[_index]; } function getValidRootsPaginated(uint256 _start, uint256 _end) public view returns (bytes32[] memory) { require(_start < _end, "Invalid range"); require(_end < validRoots.length, "Invalid end index"); bytes32[] memory paginatedRoots = new bytes32[](_end - _start); uint256 paginatedIndex = 0; for (uint256 i = _start; i <= _end; i++) { paginatedRoots[paginatedIndex] = validRoots[i]; paginatedIndex++; } return paginatedRoots; } function isSpent(bytes32 _nullifierHash) public view returns (bool) { return nullifierHashes[_nullifierHash]; } function ecRecover(bytes32 _hash, bytes calldata _signature) public pure returns (address) { return ECDSA.recover(_hash, _signature); } function _toAddress(bytes32 _address) internal pure returns (address) { return address(uint160(uint256(_address))); } function _fromAddress(address _address) internal pure returns (bytes32) { return bytes32(uint256(uint160(_address))); } function hashTypedDataV4(Deposit calldata _deposit) public view returns (bytes32) { return _hashTypedDataV4(keccak256(abi.encode( keccak256("DepositHash(bytes32 pi_hash)"), _deposit.pi_hash ))); } function broadcastTransaction(TransactionWithProof calldata _tx, bytes32 _recipient) public nonreentrant { bytes32 _signatureHash; if (_tx.transaction.amount > 0) { _signatureHash = _hashTypedDataV4(keccak256(abi.encode( keccak256("DepositHash(bytes32 pi_hash)"), _tx.transaction.deposit.pi_hash ))); } else { _signatureHash = 0; } bytes32 _piHash = hashCircuitInputsForTx(_tx.transaction, _signatureHash); require(relay[_piHash] == 0, "momiji.broadcast: already broadcasted"); for (uint256 i = 0; i < 16; i++) { if (_tx.transaction.recipients[i] == BROADCASTER_PAYMENT) relay[_piHash] = _recipient; } emit TransactionBroadcast(_tx, merkleRoot, _piHash); } function broadcastAddress(string calldata _address) public nonreentrant { emit BroadcastAddress(merkleRoot, _address); } function _swapForETH(uint256 _swapAmount, uint160 _priceLimit) internal returns (uint256 _amountOut) { token.approve(address(swapRouter), _swapAmount); _amountOut = swapRouter.exactInputSingle( ISwapRouter.ExactInputSingleParams({ tokenIn: xft, tokenOut: _weth9, fee: fee, recipient: address(this), deadline: block.timestamp, amountIn: _swapAmount, amountOutMinimum: 0, sqrtPriceLimitX96: _priceLimit }) ); weth9.withdraw(_amountOut); return _amountOut; } function verifyProof( bytes calldata _proof, Batch calldata _batch, bytes32 _accumulator ) public view returns (bool) { bytes32[] memory publicInputs = new bytes32[](17); publicInputs[0] = prepareBatchPublicInputs(_batch, _accumulator); for (uint256 i = 0; i < 16; i++) publicInputs[i + 1] = _batch.aggregation_object[i]; return verifier.verify(_proof, publicInputs); } function _publishDeposit( Transaction calldata _tx, bytes32 _signatureHash ) internal { address depositor = ecRecover(_signatureHash, _tx.deposit.signature); require(token.balanceOf(depositor) >= uint256(_tx.amount), "momiji._publishDeposit: insufficient balance"); token.burn(depositor, uint256(_tx.amount)); } function _handleUTXOs(Transaction calldata _tx) internal { for (uint256 j = 0; j < 16; j++) { if (_tx.nullifier_hashes[j] != ZERO_VALUE) { require(!nullifierHashes[_tx.nullifier_hashes[j]], 'momiji._publishWithdraw: nullifier spent'); nullifierHashes[_tx.nullifier_hashes[j]] = true; } if (_tx.commitments[j] != ZERO_VALUE) { require(utxoPrevRoots[_tx.commitments[j]] == 0, 'momiji._publishWithdraw: utxo exists'); utxoPrevRoots[_tx.commitments[j]] = merkleRoot; emit EncryptedUTXOBroadcast( merkleRoot, _tx.uids[j], _tx.encrypted_utxo[j] ); } if (_tx.commitments_in[j] != ZERO_VALUE) require(utxoPrevRoots[_tx.commitments_in[j]] != 0, "momiji.publish: commitment doesn't exist"); } } function _createPayments( Transaction calldata _tx, Payment[] memory _payments, bytes32 _piHash, uint256 _txIndex ) internal { for (uint256 j = 0; j < MAX_UTXOS; j++) { bytes32 _recipient = ZERO_VALUE; if (_tx.recipients[j] != ZERO_VALUE) { if (_tx.recipients[j] == BROADCASTER_PAYMENT) { _recipient = relay[_piHash]; } else { _recipient = _tx.recipients[j]; } uint256 _swapAmount = uint256(_tx.swap_amounts[j]); uint256 _withdrawalAmount = uint256(_tx.withdrawals[j]) - _swapAmount; _swapAmount = _swapAmount * (100 - burnPercentageSwap) / 100; uint160 _priceLimit = (_tx.price_limit == ZERO_VALUE) ? 0 : uint160(uint256(_tx.price_limit)); _payments[(_txIndex * MAX_UTXOS) + j] = Payment(_recipient, _withdrawalAmount, _swapAmount, _priceLimit); } } } function _handlePayments(Payment[] memory _payments, uint256 _txCount) internal { uint256 _paymentsCount = 1; uint256 _xftForSwap; uint160 _priceLimit; bytes32[] memory _recipients = new bytes32[](_txCount * MAX_UTXOS + 1); Payment memory _payment; for (uint256 i = 0; i < _txCount; i++) { for (uint256 j = 0; j < MAX_UTXOS; j++) { _payment = _payments[i * MAX_UTXOS + j]; if (_payment.recipient != ZERO_VALUE && _payment.recipient != NO_PAYMENT) { if (_payment.recipient == COINBASE_PAYMENT) _payment.recipient = _fromAddress(block.coinbase); if (_payment.recipient == PROVER_PAYMENT) _payment.recipient = _fromAddress(msg.sender); if (_priceLimit < _payment.price_limit) _priceLimit = _payment.price_limit; uint256 _recipientIndex; bytes32 _thisRecipient = _payment.recipient; assembly { _recipientIndex := tload(_thisRecipient) } if (_recipientIndex == 0) { _recipients[_paymentsCount] = _payment.recipient; _recipientIndex = i * MAX_UTXOS + j; assembly { tstore(_thisRecipient, _recipientIndex) } _paymentsCount++; } else { if (_payment.withdrawalAmount > 0) _payments[_recipientIndex].withdrawalAmount += _payment.withdrawalAmount; if (_payment.swapAmount > 0) _payments[_recipientIndex].swapAmount += _payment.swapAmount; _payments[i * MAX_UTXOS + j] = Payment(NO_PAYMENT, 0, 0, 0); } _xftForSwap += _payment.swapAmount; } } } // Training Wheels uint256 _now = block.timestamp; if (_now - lastCapReset > 86400) { lastCapReset = _now; dailyMint = 0; } uint256 _amountOut = 0; if (_xftForSwap > 0) { // Training Wheels require(_xftForSwap + dailyMint <= (dailyCap * token.totalSupply() / 1000), "momiji._handlePayments: daily cap exceeded"); dailyMint += _xftForSwap; token.mint(address(this), _xftForSwap); _amountOut = _swapForETH(_xftForSwap, _priceLimit); for (uint256 i = 1; i < _paymentsCount; i++) { if (_recipients[i] != NO_PAYMENT) { uint256 _thisPaymentIndex; bytes32 _thisRecipient = _recipients[i]; assembly { _thisPaymentIndex := tload(_thisRecipient) } _payment = _payments[uint256(_thisPaymentIndex)]; uint256 _amountOutShare; if (_payment.swapAmount > 0) { _amountOutShare = _payment.swapAmount * _amountOut / _xftForSwap; payable(_toAddress(_recipients[i])).transfer(_amountOutShare); } uint256 _xftAmountOut = _payment.withdrawalAmount; if (_xftAmountOut > 0) { // Training Wheels require(_xftAmountOut + dailyMint <= (dailyCap * token.totalSupply() / 1000), "momiji._handlePayments: daily cap exceeded"); dailyMint += _xftAmountOut; token.mint(_toAddress(_recipients[i]), _xftAmountOut); } } } } } function _getSignatureHash(Transaction calldata _transaction) public view returns (bytes32 _signatureHash) { if (_transaction.amount > 0) { _signatureHash = _hashTypedDataV4(keccak256(abi.encode( keccak256("DepositHash(bytes32 pi_hash)"), hashCircuitInputsForTxWithoutDeposit(_transaction) ))); } else { _signatureHash = 0; } return _signatureHash; } function _accumulatePublicInputs( bytes32 _previousAccumulator, bytes32 _publicInputsHash ) public view returns (bytes32) { bytes32[] memory _hash = new bytes32[](4); _hash[0] = _previousAccumulator; _hash[1] = _publicInputsHash; _hash[2] = txKeyHash; _hash[3] = (_previousAccumulator == ZERO_VALUE) ? txWrapperKeyHash : recursiveKeyHash; return _hashAndMod(_hash); } function _updateState(Batch memory _batch) internal { validRoots.push(_batch.new_root); merkleRoot = _batch.new_root; histRoot = _batch.new_hist_root; batchNumber++; } // This should always revert before the verify step function simulatePublish(Batch calldata _batch) public returns (bytes memory returnData) { (bool success, returnData) = address(this).call(abi.encodeWithSelector(IMomiji.publish.selector, "0x", _batch)); } function publish(bytes calldata _proof, Batch calldata _batch) public nonreentrant whenNotPaused { require(batchNumber < 2 ** STATE_DEPTH, "momiji.publish: state depth reached"); require(_batch.tx_key_hash == txKeyHash, 'momiji.publish: invalid tx keyHash'); require(_batch.recursive_key_hash == recursiveKeyHash, 'momiji.publish: invalid recursive keyHash'); require(_batch.old_hist_root == histRoot, 'momiji.publish: invalid historic root'); uint256 _txCount = _batch.transactions.length; bytes32 _accumulator = ZERO_VALUE; Payment[] memory _payments = new Payment[](MAX_ITEMS * MAX_UTXOS); for (uint256 i = 0; i < _txCount; i++) { require(_batch.transactions[i].utxo_root != ZERO_VALUE, 'momiji.publish: tx must not be empty'); require(_batch.transactions[i].utxo_root != ZERO_UTXO_ROOT, 'momiji.publish: tx utxos must not be empty'); require(uint256(_batch.transactions[i].timestamp) % 60 == 0, 'momiji.publish: timestamp must be divisible by 60'); require(_batch.transactions[i].timestamp < bytes32(block.timestamp), 'momiji.publish: tx not yet valid'); require(uint256(_batch.transactions[i].deadline) % 60 == 0, 'momiji.publish: deadline must be divisible by 60'); require(_batch.transactions[i].deadline > bytes32(block.timestamp), 'momiji.publish: tx has expired'); bytes32 _signatureHash = _getSignatureHash(_batch.transactions[i]); bytes32 _publicInputsHash = hashCircuitInputsForTx(_batch.transactions[i], _signatureHash); _accumulator = _accumulatePublicInputs(_accumulator, _publicInputsHash); if (_signatureHash > 0) _publishDeposit(_batch.transactions[i], _signatureHash); _handleUTXOs(_batch.transactions[i]); _createPayments(_batch.transactions[i], _payments, _publicInputsHash, i); emit TransactionPublish(_batch.transactions[i], merkleRoot, _publicInputsHash); } _handlePayments(_payments, _txCount); _updateState(_batch); emit BatchPublish(batchNumber, merkleRoot, _batch.new_root, _batch.oracle, _batch.historic_path); require(_proof.length > 0, 'momiji.publish: no proof'); require(verifyProof(_proof, _batch, _accumulator), 'momiji.publish: invalid proof'); } function prepareBatchPublicInputs( Batch calldata _batch, bytes32 _accumulator ) public view returns (bytes32) { bytes32[] memory _hash = new bytes32[](26); _hash[0] = _batch.new_root; _hash[1] = _batch.old_hist_root; _hash[2] = _batch.new_hist_root; _hash[3] = _accumulator; _hash[4] = _batch.tx_key_hash; _hash[5] = _batch.recursive_key_hash; for (uint256 i = 0; i < 20; i++) { _hash[6 + i] = _batch.historic_path[i]; } return _hashAndMod(_hash); } function hashCircuitInputsForTx(Transaction calldata _tx, bytes32 _signatureHash) public view returns (bytes32) { bytes32[] memory _hashInputs = new bytes32[](53); _hashInputs[0] = _tx.current_root; _hashInputs[1] = _tx.utxo_root; _hashInputs[2] = _tx.amount; _hashInputs[3] = hashContractOnlyInputsForTx(_tx, _signatureHash); for (uint256 i = 0; i < 16; i++) { _hashInputs[4] = bytes32(uint256(_hashInputs[4]) + uint256(_tx.withdrawals[i])); _hashInputs[5 + i] = _tx.commitments_in[i]; _hashInputs[21 + i] = _tx.commitments[i]; _hashInputs[37 + i] = _tx.nullifier_hashes[i]; } return _hashAndMod(_hashInputs); } function hashContractOnlyInputsForTx(Transaction calldata _tx, bytes32 _signatureHash) public view returns (bytes32) { bytes32[] memory _hashInputs = new bytes32[](100); _hashInputs[0] = _tx.timestamp; _hashInputs[1] = _tx.deadline; _hashInputs[2] = bytes32(uint256(_signatureHash) % FIELD_MODULUS); _hashInputs[3] = _tx.price_limit; for (uint256 i = 0; i < 16; i++) { _hashInputs[4 + i] = _tx.recipients[i]; _hashInputs[20 + i] = _tx.swap_amounts[i]; _hashInputs[36 + i] = _tx.uids[i]; _hashInputs[52 + (i * 3)] = _tx.encrypted_utxo[i].secret; _hashInputs[53 + (i * 3)] = _tx.encrypted_utxo[i].amount; _hashInputs[54 + (i * 3)] = _tx.encrypted_utxo[i].data; } return _hashAndMod(_hashInputs); } function hashCircuitInputsForTxWithoutDeposit(Transaction calldata _tx) public view returns (bytes32) { bytes32[] memory _hashInputs = new bytes32[](53); _hashInputs[0] = _tx.current_root; _hashInputs[1] = _tx.utxo_root; _hashInputs[2] = _tx.amount; _hashInputs[3] = hashContractOnlyInputsForTxWithoutDeposit(_tx); for (uint256 i = 0; i < 16; i++) { _hashInputs[4] = bytes32(uint256(_hashInputs[4]) + uint256(_tx.withdrawals[i])); _hashInputs[5 + i] = _tx.commitments_in[i]; _hashInputs[21 + i] = _tx.commitments[i]; _hashInputs[37 + i] = _tx.nullifier_hashes[i]; } return _hashAndMod(_hashInputs); } function hashContractOnlyInputsForTxWithoutDeposit(Transaction calldata _tx) public view returns (bytes32) { bytes32[] memory _hashInputs = new bytes32[](99); _hashInputs[0] = _tx.timestamp; _hashInputs[1] = _tx.deadline; _hashInputs[2] = _tx.price_limit; for (uint256 i = 0; i < 16; i++) { _hashInputs[3 + i] = _tx.recipients[i]; _hashInputs[19 + i] = _tx.swap_amounts[i]; _hashInputs[35 + i] = _tx.uids[i]; _hashInputs[51 + (i * 3)] = _tx.encrypted_utxo[i].secret; _hashInputs[52 + (i * 3)] = _tx.encrypted_utxo[i].amount; _hashInputs[53 + (i * 3)] = _tx.encrypted_utxo[i].data; } return _hashAndMod(_hashInputs); } function _hashAndMod(bytes32[] memory _hashes) internal view returns (bytes32) { bytes32 _hash = keccak256(abi.encodePacked(_hashes)); return bytes32(uint256(_hash) % FIELD_MODULUS); } }