p2pix-smart-contracts/contracts/p2pix.sol

889 lines
27 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
/// ______ __
/// .-----.|__ |.-----.|__|.--.--.
/// | _ || __|| _ || ||_ _|
/// | __||______|| __||__||__.__|
/// |__| |__|
///
import { Owned } from "./lib/auth/Owned.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
// solhint-disable no-empty-blocks
using DT for DT.Lock;
using DT for DT.LockStatus;
/// ███ Constants ██████████████████████████████████████████████████████████
/// @dev The bitmask of `sellerBalance` entry.
uint256 private constant BITMASK_SB_ENTRY = (1 << 94) - 1;
/// @dev The bit position of `pixTarget` in `sellerBalance`.
uint256 private constant BITPOS_PIXTARGET = 95;
/// @dev The bit position of `valid` in `sellerBalance`.
uint256 private constant BITPOS_VALID = 255;
/// @dev The bitmask of all 256 bits of `sellerBalance` except for the last one.
uint256 private constant BITMASK_VALID = (1 << 255) - 1;
/// @dev The scalar of BRZ token.
uint256 public constant WAD = 1e18;
/// ███ Storage ████████████████████████████████████████████████████████████
IReputation public reputation;
/// @dev Default blocks that lock will hold tokens.
uint256 public defaultLockBlocks;
uint256 public lockCounter;
/// @dev List of Locks.
mapping(uint256 => 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,
uint256[] calldata expiredLocks
) public nonReentrant returns (uint256) {
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 cCounter = lockCounter + 1;
if (
mapLocks[cCounter].expirationBlock >= block.number
) revert NotExpired();
DT.Lock memory l = DT.Lock(
k,
cCounter,
_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, cCounter, l, t, k);
lockCounter++;
// Halt execution and output `lockID`.
return cCounter;
} else {
if (l.amount <= 1e2 ether) {
_addLock(bal, _amount, cCounter, l, t, k);
lockCounter++;
// Halt execution and output `lockID`.
return cCounter;
} 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, cCounter, l, t, k);
lockCounter++;
// Halt execution and output `lockID`.
return cCounter;
}
}
}
/// @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 `relayerPremium`.
/// @dev Function sighash: 0x4e1389ed.
function release(
uint256 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(uint256[] 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,
uint256 amount,
uint256[] calldata expiredLocks
) public nonReentrant {
unlockExpired(expiredLocks);
if (getValid(msg.sender, token) == true) {
setValidState(token, false);
}
uint256 key = _castAddrToKey(msg.sender);
_decBal(
(sellerBalance[key][token] & BITMASK_SB_ENTRY),
amount,
token,
key
);
// safeTransfer tokens to seller
SafeTransferLib.safeTransfer(
token,
msg.sender,
amount
);
emit DepositWithdrawn(
msg.sender,
address(token),
amount
);
}
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,
uint256 _lockID,
DT.Lock memory _l,
ERC20 _t,
uint256 _k
) internal {
mapLocks[_lockID] = _l;
_decBal(_bal, _amount, _t, _k);
emit LockAdded(
_l.buyerAddress,
_lockID,
_l.sellerKey,
_l.amount
);
}
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 _decBal(
uint256 _bal,
uint256 _amount,
ERC20 _t,
uint256 _k
) private {
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;
}
function getBalance(address seller, ERC20 token)
public
view
returns (uint256 bal)
{
// bal =
// sellerBalance[_castAddrToKey(seller)][token] &
// BITMASK_SB_ENTRY;
assembly {
for {
/* */
} iszero(0x0) {
/* */
} {
mstore(0x00, shl(0xC, seller))
mstore(0x20, sellerBalance.slot)
let sbkslot := keccak256(0x00, 0x40)
mstore(0x00, token)
mstore(0x20, sbkslot)
bal := and(
BITMASK_SB_ENTRY,
sload(keccak256(0x00, 0x40))
)
break
}
}
}
function getValid(address seller, ERC20 token)
public
view
returns (bool valid)
{
// uint256 b = sellerBalance[
// _castAddrToKey(seller)
// ][token];
// ] >> BITPOS_VALID) & BITMASK_SB_ENTRY;
assembly {
for {
/* */
} iszero(0x0) {
/* */
} {
mstore(0x00, shl(0xC, seller))
mstore(0x20, sellerBalance.slot)
let sbkslot := keccak256(0x00, 0x40)
mstore(0x00, token)
mstore(0x20, sbkslot)
valid := and(
BITMASK_SB_ENTRY,
shr(
BITPOS_VALID,
sload(keccak256(0x00, 0x40))
)
)
break
}
}
}
function getPixTarget(address seller, ERC20 token)
public
view
returns (uint160 pixTarget)
{
// pixTarget = uint160(
// sellerBalance[_castAddrToKey(seller)][token] >>
// BITPOS_PIXTARGET
// );
assembly {
for {
/* */
} iszero(0) {
/* */
} {
mstore(0, shl(12, seller))
mstore(32, sellerBalance.slot)
let sbkslot := keccak256(0, 64)
mstore(0, token)
mstore(32, sbkslot)
pixTarget := shr(
BITPOS_PIXTARGET,
sload(keccak256(0, 64))
)
break
}
}
}
function getBalances(
address[] memory sellers,
ERC20 token
) external view returns (uint256[] memory) {
uint256 j;
uint256 len = sellers.length;
uint256[] memory balances = new uint256[](len);
while (j < len) {
uint256 bal = getBalance(sellers[j], token);
balances[j] = bal;
unchecked {
++j;
}
}
return balances;
}
/// @notice External getter that returns the status of a lockIDs array.
/// @dev Call will not revert if provided with an empty array as parameter.
/// @dev Function sighash: 0x49ef8448
function getLocksStatus(uint256[] memory ids)
external
view
returns (uint256[] memory, DT.LockStatus[] memory)
{
if (ids.length == 0) {
uint256[] memory null1 = new uint256[](0);
DT.LockStatus[]
memory null2 = new DT.LockStatus[](0);
return (null1, null2);
}
uint256 c;
uint256 len = ids.length;
uint256[] memory sortedIDs = new uint256[](len);
DT.LockStatus[] memory status = new DT.LockStatus[](
len
);
unchecked {
for (c; c < len; ) {
if (mapLocks[ids[c]].sellerKey == 0x0) {
sortedIDs[c] = ids[c];
status[c] = type(DT.LockStatus).min;
++c;
} else if (mapLocks[ids[c]].amount == 0x0) {
sortedIDs[c] = ids[c];
status[c] = type(DT.LockStatus).max;
++c;
} else if (
mapLocks[ids[c]].expirationBlock <
block.number
) {
sortedIDs[c] = ids[c];
status[c] = DT.LockStatus.Expired;
++c;
} else {
sortedIDs[c] = ids[c];
status[c] = DT.LockStatus.Active;
++c;
}
}
}
return (sortedIDs, status);
}
/// @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;
assembly {
_key := shl(12, _addr)
}
}
function _castKeyToAddr(uint256 _key)
public
pure
returns (address _addr)
{
// _addr = address(uint160(uint256(_key >> 12)));
assembly {
_addr := shr(12, _key)
}
}
}