// SPDX-License-Identifier: MIT pragma solidity 0.8.9; /// ______ __ /// .-----.|__ |.-----.|__|.--.--. /// | _ || __|| _ || ||_ _| /// | __||______|| __||__||__.__| /// |__| |__| /// import { Owned } from "./lib/auth/Owned.sol"; // import { Counters } from "./lib/utils/Counters.sol"; import { ERC20, SafeTransferLib } from "./lib/utils/SafeTransferLib.sol"; import { IReputation } from "./lib/interfaces/IReputation.sol"; import { MerkleProofLib as Merkle } from "./lib/utils/MerkleProofLib.sol"; import { ReentrancyGuard } from "./lib/utils/ReentrancyGuard.sol"; import { EventAndErrors } from "./EventAndErrors.sol"; import { DataTypes as DT } from "./DataTypes.sol"; contract P2PIX is EventAndErrors, Owned(msg.sender), ReentrancyGuard { // solhint-disable use-forbidden-name // solhint-disable no-inline-assembly // using Counters for Counters.Counter; using DT for DT.Lock; /// ███ Constants ██████████████████████████████████████████████████████████ /// @dev The bitmask of `sellerBalance` entry. uint256 constant private BITMASK_SB_ENTRY = (1 << 94) - 1; /// @dev The bit position of `pixTarget` in `sellerBalance`. uint256 constant private BITPOS_PIXTARGET = 95; /// @dev The bit position of `valid` in `sellerBalance`. uint256 constant private BITPOS_VALID = 255; /// @dev The bitmask of all 256 bits of `sellerBalance` except for the last one. uint256 constant private BITMASK_VALID = (1 << 255) - 1; /// @dev The scalar of BRZ token. uint256 constant public WAD = 1e18; /// ███ Storage ████████████████████████████████████████████████████████████ IReputation public reputation; // Counters.Counter public depositCount; /// @dev Default blocks that lock will hold tokens. uint256 public defaultLockBlocks; uint256 public lockCounter; /// @dev List of Locks. mapping(bytes32 => DT.Lock) public mapLocks; /// @dev Seller casted to key => Seller's allowlist merkleroot. mapping(uint256 => bytes32) public sellerAllowList; /// @dev Stores an relayer's last computed credit. mapping(uint256 => uint256) public userRecord; /// @dev List of valid Bacen signature addresses mapping(uint256 => bool) public validBacenSigners; /// @dev List of Pix transactions already signed. mapping(bytes32 => bool) public usedTransactions; /// @dev Tokens allowed to serve as the underlying amount of a deposit. mapping(ERC20 => bool) public allowedERC20s; // BITS LAYOUT // `uint96` [0...94] := balance // `uint160` [95...254] := pixTarget // `bool` [255] := valid /// @dev `balance` max. value = 10**26. /// @dev `pixTarget` keys are restricted to 160 bits. mapping(uint256 => mapping(ERC20 => uint256)) public sellerBalance; /// ███ Constructor ████████████████████████████████████████████████████████ constructor( uint256 defaultBlocks, address[] memory validSigners, IReputation _reputation, address[] memory tokens, bool[] memory tokenStates ) payable { setDefaultLockBlocks(defaultBlocks); setReputation(_reputation); setValidSigners(validSigners); tokenSettings(tokens, tokenStates); } /// ███ Public FX ██████████████████████████████████████████████████████████ /// @notice Creates a deposit order based on a seller's /// offer of an amount of ERC20 tokens. /// @dev Seller needs to send his tokens to the P2PIX smart contract. /// @param _pixTarget Pix key destination provided by the offer's seller. /// @param allowlistRoot Optional allow list merkleRoot update `bytes32` value. /// as the deposit identifier. /// @dev Function sighash: 0xbfe07da6. function deposit( address _token, uint96 _amount, uint160 _pixTarget, bool _valid, bytes32 allowlistRoot ) public { ERC20 t = ERC20(_token); uint256 k = _castAddrToKey(msg.sender); if(_pixTarget == 0) revert EmptyPixTarget(); if (!allowedERC20s[t]) revert TokenDenied(); uint256 _sellerBalance = sellerBalance[k][t]; uint256 currBal = _sellerBalance & BITMASK_SB_ENTRY; if ((currBal + _amount) > 1e8 ether) revert MaxBalExceeded(); setReentrancyGuard(); if (allowlistRoot != 0) { setRoot(msg.sender, allowlistRoot); } uint256 amountCasted; uint256 pixTargetCasted; uint256 validCasted; (amountCasted, pixTargetCasted, validCasted) = _castToUint(_amount, _pixTarget, _valid); sellerBalance[k][t] = (currBal + amountCasted) | (pixTargetCasted << BITPOS_PIXTARGET) | (validCasted << BITPOS_VALID); SafeTransferLib.safeTransferFrom( t, msg.sender, address(this), _amount ); clearReentrancyGuard(); emit DepositAdded( msg.sender, _token, _amount ); } /// @notice Enables seller to invalidate future /// locks made to his/her token offering order. /// @dev This function does not affect any ongoing active locks. /// @dev Function sighash: 0x72fada5c. function setValidState(ERC20 token, bool state) public { uint256 key = _castAddrToKey(msg.sender); uint256 _sellerBalance = sellerBalance[key][token]; if (_sellerBalance != 0) { uint256 _valid; assembly { _valid := state } _sellerBalance = (_sellerBalance & BITMASK_VALID) | (_valid << BITPOS_VALID); sellerBalance[key][token] = _sellerBalance; emit ValidSet( msg.sender, address(token), state ); } else revert NotInitialized(); } /// @notice Public method designed to lock an remaining amount of /// the deposit order of a seller. /// @dev This method can be performed either by: /// - An user allowed via the seller's allowlist; /// - An user with enough userRecord to lock the wished amount; /// @dev There can only exist a lock per each `_amount` partitioned /// from the total `remaining` value. /// @dev Locks can only be performed in valid orders. /// @param _buyerAddress The address of the buyer of a `_depositID`. /// @param _relayerTarget Target address entitled to the `relayerPremium`. /// @param _relayerPremium The refund/premium owed to a relayer. /// @param _amount The deposit's remaining amount wished to be locked. /// @param merkleProof This value should be: /// - Provided as a pass if the `msg.sender` is in the seller's allowlist; /// - Left empty otherwise; /// @param expiredLocks An array of `bytes32` identifiers to be /// provided so to unexpire locks using this transaction gas push. /// @return lockID The `bytes32` value returned as the lock identifier. /// @dev Function sighash: 0x03aaf306. function lock( address _seller, address _token, address _buyerAddress, address _relayerTarget, uint256 _relayerPremium, uint256 _amount, bytes32[] calldata merkleProof, bytes32[] calldata expiredLocks ) public nonReentrant returns (bytes32 lockID) { unlockExpired(expiredLocks); ERC20 t = ERC20(_token); if (!getValid(_seller, t)) revert InvalidDeposit(); uint256 bal = getBalance(_seller, t); if (bal < _amount) revert NotEnoughTokens(); uint256 k = _castAddrToKey(_seller); uint256 cachedCounter = lockCounter + 1; lockID = keccak256( abi.encodePacked( lockCounter, _amount, _buyerAddress ) ); if (mapLocks[lockID].expirationBlock >= block.number) revert NotExpired(); DT.Lock memory l = DT.Lock( k, cachedCounter, _relayerPremium, _amount, (block.number + defaultLockBlocks), uint160(sellerBalance[k][t] >> BITPOS_PIXTARGET), _buyerAddress, _relayerTarget, msg.sender, address(t) ); if (merkleProof.length != 0) { merkleVerify( merkleProof, sellerAllowList[k], msg.sender ); _addLock( bal, _amount, lockID, l, t, k); lockCounter++; // Halt execution and output `lockID`. return lockID; } else { if (l.amount <= 1e2 ether) { _addLock( bal, _amount, lockID, l, t, k); lockCounter++; // Halt execution and output `lockID`. return lockID; } else { uint256 userCredit = userRecord[ _castAddrToKey(msg.sender) ]; uint256 spendLimit; (spendLimit) = _limiter(userCredit / WAD); if (l.amount > (spendLimit * WAD) || l.amount > 1e6 ether) revert AmountNotAllowed(); _addLock( bal, _amount, lockID, l, t, k); lockCounter++; // Halt execution and output `lockID`. return lockID; } } } /// @notice Lock release method that liquidate lock /// orders and distributes relayer fees. /// @dev This method can be called by any public actor /// as long the signature provided is valid. /// @dev `relayerPremium` gets splitted equaly /// if `relayerTarget` addresses differ. /// @dev If the `msg.sender` of this method and `l.relayerAddress` are the same, /// `msg.sender` accrues both l.amount and l.relayerPremium as userRecord credit. /// In case of they differing: /// - `lock` caller gets accrued with `l.amount` as userRecord credit; /// - `release` caller gets accrued with `l.relayerPremium` as userRecord credit; /// @param _relayerTarget Target address entitled to the `relayerPremim`. /// @dev Function sighash: 0x4e1389ed. function release( bytes32 lockID, address _relayerTarget, bytes32 pixTimestamp, bytes32 r, bytes32 s, uint8 v ) public nonReentrant { DT.Lock storage l = mapLocks[lockID]; if (l.amount == 0) revert AlreadyReleased(); if (l.expirationBlock < block.number) revert LockExpired(); bytes32 message = keccak256( abi.encodePacked( l.pixTarget, l.amount, pixTimestamp ) ); bytes32 messageDigest = keccak256( abi.encodePacked( "\x19Ethereum Signed Message:\n32", message ) ); if (usedTransactions[message] == true) revert TxAlreadyUsed(); uint256 signer = _castAddrToKey( ecrecover(messageDigest, v, r, s) ); if (!validBacenSigners[signer]) revert InvalidSigner(); ERC20 t = ERC20(l.token); // We cache values before zeroing them out. uint256 lockAmount = l.amount; uint256 totalAmount = (lockAmount - l.relayerPremium); l.amount = 0; l.expirationBlock = 0; usedTransactions[message] = true; if (msg.sender != l.relayerAddress) { userRecord[_castAddrToKey(msg.sender)] += l .relayerPremium; userRecord[ _castAddrToKey(l.relayerAddress) ] += lockAmount; } else { userRecord[_castAddrToKey(msg.sender)] += (l .relayerPremium + lockAmount); } SafeTransferLib.safeTransfer( t, l.buyerAddress, totalAmount ); // Method doesn't check for zero address. if (l.relayerPremium != 0) { if (_relayerTarget != l.relayerTarget) { SafeTransferLib.safeTransfer( t, l.relayerTarget, (l.relayerPremium >> 1) ); SafeTransferLib.safeTransfer( t, _relayerTarget, (l.relayerPremium >> 1) ); } else { SafeTransferLib.safeTransfer( t, _relayerTarget, l.relayerPremium ); } } emit LockReleased(l.buyerAddress, lockID, lockAmount); } /// @notice Unlocks expired locks. /// @dev Triggered in the callgraph by both `lock` and `withdraw` functions. /// @dev This method can also have any public actor as its `tx.origin`. /// @dev For each successfull unexpired lock recovered, /// `userRecord[_castAddrToKey(l.relayerAddress)]` is decreased by half of its value. /// @dev Function sighash: 0x8e2749d6. function unlockExpired( bytes32[] calldata lockIDs ) public { uint256 i; uint256 locksSize = lockIDs.length; for (i; i < locksSize; ) { DT.Lock storage l = mapLocks[lockIDs[i]]; _notExpired(l); uint256 _sellerBalance = sellerBalance[l.sellerKey][ERC20(l.token)] & BITMASK_SB_ENTRY; if((_sellerBalance + l.amount) > 1e8 ether) revert MaxBalExceeded(); sellerBalance[l.sellerKey][ERC20(l.token)] += l.amount; l.amount = 0; uint256 userKey = _castAddrToKey( l.relayerAddress ); uint256 _newUserRecord = (userRecord[userKey] >> 1); if (_newUserRecord <= 1e2 ether) { userRecord[userKey] = 1e2 ether; } else { userRecord[userKey] = _newUserRecord; } emit LockReturned(l.buyerAddress, lockIDs[i]); unchecked { ++i; } } assembly { if lt(i, locksSize) { // LoopOverflow() mstore(0x00, 0xdfb035c9) revert(0x1c, 0x04) } } } /// @notice Seller's expired deposit fund sweeper. /// @dev A seller may use this method to recover /// tokens from expired deposits. /// @dev Function sighash: 0x36317972. function withdraw( ERC20 token, bytes32[] calldata expiredLocks ) public nonReentrant returns(uint256 _sellerBalance) { unlockExpired(expiredLocks); if (getValid(msg.sender, token) == true ) { setValidState(token, false); } uint256 key = _castAddrToKey(msg.sender); sellerBalance[key][token] -= (_sellerBalance = sellerBalance[key][token] & BITMASK_SB_ENTRY); if(_sellerBalance == 0) revert InvalidDeposit(); // safeTransfer tokens to seller SafeTransferLib.safeTransfer( token, msg.sender, _sellerBalance ); emit DepositWithdrawn( msg.sender, address(token), _sellerBalance ); } function setRoot( address addr, bytes32 merkleroot ) public { if (addr == msg.sender) { sellerAllowList[ _castAddrToKey(addr) ] = merkleroot; emit RootUpdated(addr, merkleroot); } else revert OnlySeller(); } /// ███ Owner Only █████████████████████████████████████████████████████████ /// @dev Contract's underlying balance withdraw method. /// @dev Function sighash: 0x5fd8c710. function withdrawBalance() external onlyOwner { uint256 balance = address(this).balance; SafeTransferLib.safeTransferETH(msg.sender, balance); emit FundsWithdrawn(msg.sender, balance); } function setReputation( IReputation _reputation ) public onlyOwner { assembly { sstore(reputation.slot, _reputation) } emit ReputationUpdated(address(_reputation)); } function setDefaultLockBlocks( uint256 _blocks ) public onlyOwner { assembly { sstore(defaultLockBlocks.slot, _blocks) } emit LockBlocksUpdated(_blocks); } function setValidSigners( address[] memory _validSigners ) public onlyOwner { unchecked { uint256 i; uint256 len = _validSigners.length; for (i; i < len; ) { uint256 key = _castAddrToKey( _validSigners[i] ); validBacenSigners[key] = true; ++i; } } emit ValidSignersUpdated(_validSigners); } function tokenSettings( address[] memory _tokens, bool[] memory _states ) public onlyOwner { /* Yul Impl */ assembly { // first 32 bytes eq to array's length let tLen := mload(_tokens) // NoTokens() if iszero(tLen) { mstore(0x00, 0xdf957883) revert(0x1c, 0x04) } // LengthMismatch() if iszero(eq(tLen, mload(_states))) { mstore(0x00, 0xff633a38) revert(0x1c, 0x04) } let tLoc := add(_tokens, 0x20) let sLoc := add(_states, 0x20) for { let end := add(tLoc, shl(5, tLen)) } iszero(eq(tLoc, end)) { tLoc := add(tLoc, 0x20) sLoc := add(sLoc, 0x20) } { // cache hashmap entry in scratch space mstore(0x00, mload(tLoc)) mstore(0x20, allowedERC20s.slot) let mapSlot := keccak256(0x00, 0x40) sstore(mapSlot, mload(sLoc)) log3( 0, 0, // AllowedERC20Updated(address, bool) 0x5d6e86e5341d57a92c49934296c51542a25015c9b1782a1c2722a940131c3d9a, mload(tLoc), mload(sLoc) ) } } } /// ███ Helper FX ██████████████████████████████████████████████████████████ // solhint-disable-next-line no-empty-blocks receive() external payable {} /// @notice Private view auxiliar logic that reverts /// on a not expired lock passed as argument of the function. /// @dev Called exclusively by the `unlockExpired` method. /// @dev Function sighash: 0x74e2a0bb. function _notExpired(DT.Lock storage _l) private view { if (_l.expirationBlock > block.number) revert NotExpired(); if (_l.amount == 0) revert AlreadyReleased(); } function _addLock( uint256 _bal, uint256 _amount, bytes32 _lockID, DT.Lock memory _l, ERC20 _t, uint256 _k ) internal { mapLocks[_lockID] = _l; assembly { if iszero( iszero( or( iszero(_bal), lt( sub( _bal, _amount), 0x0 )))) { // DecOverflow() mstore(0x00, 0xce3a3d37) revert(0x1c, 0x04) } } // we can directly dec from packed uint entry value sellerBalance[_k][_t] -= _amount; emit LockAdded( _l.buyerAddress, _lockID, _l.sellerKey, _l.amount ); /// @todo // assembly { // // sstore(mp.slot, _l) // mstore(0x00, _k) // mstore(0x20, sellerBalance.slot) // let sbkslot := keccak256(0x00, 0x40) // mstore(0x00, _t) // mstore(0x20, sbkslot) // let sbslot := keccak256(0x00,0x40) // let oldsb := sload(sbslot) // sstore( // sbslot, // or( // or( // _newBal, // shr( // oldsb, // BITPOS_PIXTARGET // )), // and( // shr( // oldsb, // BITPOS_VALID // ), // BITMASK_SB_ENTRY // ) // ) // ) // } } /// @notice Private view auxiliar logic that encodes/returns /// the `bytes32` identifier of an lock. /// @dev reverts on a not expired lock with the same ID passed /// as argument of the function. /// @dev Called exclusively by the `lock` method. /// @dev Function sighash: 0x3fc5fb52. // function _encodeLockID( // uint256 _lockCounter, // uint256 _amount, // address _buyerAddress // ) private view returns (bytes32 _lockID) { // _lockID = keccak256( // abi.encodePacked( // _lockCounter, // _amount, // _buyerAddress // ) // ); // if (mapLocks[_lockID].expirationBlock >= block.number) // revert NotExpired(); // } function merkleVerify( bytes32[] calldata _merkleProof, bytes32 root, address _addr ) private pure { if ( !Merkle.verify( _merkleProof, root, bytes32(uint256(uint160(_addr))) ) ) revert AddressDenied(); } function _limiter( uint256 _userCredit ) internal view returns (uint256 _spendLimit) { // enconde the fx sighash and args bytes memory encodedParams = abi.encodeWithSelector( IReputation.limiter.selector, _userCredit ); // cast the uninitialized return values to memory bool success; uint256 returnSize; uint256 returnValue; // perform staticcall from the stack w yul assembly { success := staticcall( // gas 0x7530, // address sload(reputation.slot), // argsOffset add(encodedParams, 0x20), // argsSize mload(encodedParams), // retOffset 0x00, // retSize 0x20 ) returnSize := returndatasize() returnValue := mload(0x00) _spendLimit := returnValue // reverts if call does not succeed. if iszero(success) { mstore(0x00, 0xe10bf1cc) revert(0x1c, 0x04) } } } function _castToUint( uint96 _amount, uint160 _pixTarget, bool _valid) private pure returns ( uint256 _amountCasted, uint256 _pixTargetCasted, uint256 _validCasted ) { assembly { _amountCasted := _amount _pixTargetCasted := _pixTarget _validCasted := _valid } } function getBalance( address seller, ERC20 token ) public view returns(uint256 bal) { bal = sellerBalance[ _castAddrToKey(seller) ][token] & BITMASK_SB_ENTRY; } function getValid( address seller, ERC20 token ) public view returns(bool valid) { uint256 b = (sellerBalance[ _castAddrToKey(seller) ][token] >> BITPOS_VALID) & BITMASK_SB_ENTRY; assembly { valid := b } } function getPixTarget( address seller, ERC20 token ) public view returns(uint160 pixTarget) { pixTarget = uint160( sellerBalance[ _castAddrToKey(seller) ][token] >> BITPOS_PIXTARGET ); } /// @notice Public method that handles `address` /// to `uint256` safe type casting. /// @dev Function sighash: 0x4b2ae980. function _castAddrToKey( address _addr ) public pure returns (uint256 _key) { _key = uint256(uint160(address(_addr))) << 12; } function _castKeyToAddr( uint256 _key ) public pure returns (address _addr) { _addr = address(uint160(uint256(_key >> 12))); } }