diff --git a/core/contracts/BaseWithdrawPool.sol b/core/contracts/BaseWithdrawPool.sol new file mode 100644 index 0000000..5e3a3c9 --- /dev/null +++ b/core/contracts/BaseWithdrawPool.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import "./libraries/MathHelper.sol"; +import "./interfaces/IEndpoint.sol"; +import "./Verifier.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/IERC20Base.sol"; +import "./libraries/ERC20Helper.sol"; +import "./common/Constants.sol"; + +abstract contract BaseWithdrawPool is EIP712Upgradeable, OwnableUpgradeable { + using ERC20Helper for IERC20Base; + using MathSD21x18 for int128; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function _initialize(address _clearinghouse, address _verifier) + internal + initializer + { + __Ownable_init(); + clearinghouse = _clearinghouse; + verifier = _verifier; + } + + address internal clearinghouse; + + address internal verifier; + + // submitted withdrawal idxs + mapping(uint64 => bool) public markedIdxs; + + // collected withdrawal fees in native token decimals + mapping(uint32 => int128) public fees; + + uint64 public minIdx; + + function submitFastWithdrawal( + uint64 idx, + bytes calldata transaction, + bytes[] calldata signatures + ) public { + require(!markedIdxs[idx], "Withdrawal already submitted"); + require(idx > minIdx, "idx too small"); + markedIdxs[idx] = true; + + Verifier v = Verifier(verifier); + v.requireValidTxSignatures(transaction, idx, signatures); + + IEndpoint.SignedWithdrawCollateral memory signedTx = abi.decode( + transaction[1:], + (IEndpoint.SignedWithdrawCollateral) + ); + + IERC20Base token = getToken(signedTx.tx.productId); + + address sendTo = address(uint160(bytes20(signedTx.tx.sender))); + uint128 transferAmount = signedTx.tx.amount; + + require(transferAmount <= INT128_MAX, ERR_CONVERSION_OVERFLOW); + + int128 fee = fastWithdrawalFeeAmount( + token, + signedTx.tx.productId, + transferAmount + ); + + require(transferAmount > uint128(fee), "Fee larger than balance"); + transferAmount -= uint128(fee); + fees[signedTx.tx.productId] += fee; + + handleWithdrawTransfer(token, sendTo, transferAmount); + } + + function submitWithdrawal( + IERC20Base token, + address sendTo, + uint128 amount, + uint64 idx + ) public { + require(msg.sender == clearinghouse); + + if (markedIdxs[idx]) { + return; + } + markedIdxs[idx] = true; + // set minIdx to most recent withdrawal submitted by sequencer + minIdx = idx; + + handleWithdrawTransfer(token, sendTo, amount); + } + + function fastWithdrawalFeeAmount( + IERC20Base token, + uint32 productId, + uint128 amount + ) public view returns (int128) { + uint8 decimals = token.decimals(); + require(decimals <= MAX_DECIMALS); + int256 multiplier = int256(10**(MAX_DECIMALS - uint8(decimals))); + int128 amountX18 = int128(amount) * int128(multiplier); + + int128 proportionalFeeX18 = FAST_WITHDRAWAL_FEE_RATE.mul(amountX18); + int128 minFeeX18 = 5 * + IClearinghouse(clearinghouse).getWithdrawFee(productId); + + int128 feeX18 = MathHelper.max(proportionalFeeX18, minFeeX18); + return feeX18 / int128(multiplier); + } + + function removeLiquidity( + uint32 productId, + uint128 amount, + address sendTo + ) external onlyOwner { + handleWithdrawTransfer(getToken(productId), sendTo, amount); + } + + function checkMarkedIdxs(uint64[] calldata idxs) + public + view + returns (bool[] memory) + { + bool[] memory marked = new bool[](idxs.length); + for (uint256 i = 0; i < idxs.length; i++) { + marked[i] = markedIdxs[idxs[i]]; + } + return marked; + } + + function checkProductBalances(uint32[] calldata productIds) + public + view + returns (uint256[] memory) + { + uint256[] memory balances = new uint256[](productIds.length); + for (uint256 i = 0; i < productIds.length; i++) { + IERC20Base token = getToken(productIds[i]); + balances[i] = token.balanceOf(address(this)); + } + return balances; + } + + function handleWithdrawTransfer( + IERC20Base token, + address to, + uint128 amount + ) internal virtual { + token.safeTransfer(to, uint256(amount)); + } + + function safeTransferFrom( + IERC20Base token, + address from, + uint256 amount + ) internal virtual { + token.safeTransferFrom(from, address(this), amount); + } + + function getToken(uint32 productId) internal view returns (IERC20Base) { + IERC20Base token = IERC20Base(spotEngine().getConfig(productId).token); + require(address(token) != address(0)); + return token; + } + + function spotEngine() internal view returns (ISpotEngine) { + return + ISpotEngine( + IClearinghouse(clearinghouse).getEngineByType( + IProductEngine.EngineType.SPOT + ) + ); + } +} diff --git a/core/contracts/Clearinghouse.sol b/core/contracts/Clearinghouse.sol index 9b6288f..5a2a15f 100644 --- a/core/contracts/Clearinghouse.sol +++ b/core/contracts/Clearinghouse.sol @@ -16,7 +16,7 @@ import "./interfaces/engine/IPerpEngine.sol"; import "./EndpointGated.sol"; import "./interfaces/IEndpoint.sol"; import "./ClearinghouseStorage.sol"; -import "./WithdrawPool.sol"; +import "./BaseWithdrawPool.sol"; interface IProxyManager { function getProxyManagerHelper() external view returns (address); @@ -92,6 +92,7 @@ contract Clearinghouse is EndpointGated, ClearinghouseStorage, IClearinghouse { if (health == (type(int128).min)) { return health; } + health += perpEngine.getHealthContribution(subaccount, healthType); uint256 _spreads = spreads; while (_spreads != 0) { @@ -143,8 +144,6 @@ contract Clearinghouse is EndpointGated, ClearinghouseStorage, IClearinghouse { .mul(spotCoreRisk.price + perpCoreRisk.price) .mul(spreadPenalty - existingPenalty); } - - health += perpEngine.getHealthContribution(subaccount, healthType); } function registerProduct(uint32 productId) external { @@ -419,7 +418,7 @@ contract Clearinghouse is EndpointGated, ClearinghouseStorage, IClearinghouse { uint64 idx ) internal virtual { token.safeTransfer(withdrawPool, uint256(amount)); - WithdrawPool(withdrawPool).submitWithdrawal(token, to, amount, idx); + BaseWithdrawPool(withdrawPool).submitWithdrawal(token, to, amount, idx); } function _balanceOf(address token) internal view virtual returns (uint128) { @@ -553,10 +552,36 @@ contract Clearinghouse is EndpointGated, ClearinghouseStorage, IClearinghouse { ISpotEngine spotEngine = ISpotEngine( address(engineByType[IProductEngine.EngineType.SPOT]) ); - spotEngine.updateBalance(QUOTE_PRODUCT_ID, V_ACCOUNT, txn.quoteAmount); - spotEngine.updateBalance(QUOTE_PRODUCT_ID, X_ACCOUNT, -txn.quoteAmount); - spotEngine.updateBalance(VLP_PRODUCT_ID, V_ACCOUNT, txn.vlpAmount); - spotEngine.updateBalance(VLP_PRODUCT_ID, X_ACCOUNT, -txn.vlpAmount); + IPerpEngine perpEngine = IPerpEngine( + address(engineByType[IProductEngine.EngineType.PERP]) + ); + if (address(spotEngine) == address(productToEngine[txn.productId])) { + spotEngine.updateBalance( + txn.productId, + V_ACCOUNT, + txn.baseAmount, + txn.quoteAmount + ); + spotEngine.updateBalance( + txn.productId, + X_ACCOUNT, + -txn.baseAmount, + -txn.quoteAmount + ); + } else { + perpEngine.updateBalance( + txn.productId, + V_ACCOUNT, + txn.baseAmount, + txn.quoteAmount + ); + perpEngine.updateBalance( + txn.productId, + X_ACCOUNT, + -txn.baseAmount, + -txn.quoteAmount + ); + } } function burnLpAndTransfer(IEndpoint.BurnLpAndTransfer calldata txn) diff --git a/core/contracts/ClearinghouseLiq.sol b/core/contracts/ClearinghouseLiq.sol index 856e3e3..a559513 100644 --- a/core/contracts/ClearinghouseLiq.sol +++ b/core/contracts/ClearinghouseLiq.sol @@ -679,7 +679,10 @@ contract ClearinghouseLiq is // it's ok to let initial health become 0 require(!isAboveInitial(txn.liquidatee), ERR_LIQUIDATED_TOO_MUCH); - require(!isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + require( + txn.sender == V_ACCOUNT || !isUnderInitial(txn.sender), + ERR_SUBACCT_HEALTH + ); insurance += v.liquidationFees; diff --git a/core/contracts/Endpoint.sol b/core/contracts/Endpoint.sol index a9cbcff..1a7481a 100644 --- a/core/contracts/Endpoint.sol +++ b/core/contracts/Endpoint.sol @@ -69,24 +69,8 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { mapping(uint32 => int128) internal priceX18; address internal offchainExchange; - string internal constant LIQUIDATE_SUBACCOUNT_SIGNATURE = - "LiquidateSubaccount(bytes32 sender,bytes32 liquidatee,uint32 productId,bool isEncodedSpread,int128 amount,uint64 nonce)"; - string internal constant TRANSFER_QUOTE_SIGNATURE = - "TransferQuote(bytes32 sender,bytes32 recipient,uint128 amount,uint64 nonce)"; - string internal constant WITHDRAW_COLLATERAL_SIGNATURE = - "WithdrawCollateral(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; - string internal constant MINT_LP_SIGNATURE = - "MintLp(bytes32 sender,uint32 productId,uint128 amountBase,uint128 quoteAmountLow,uint128 quoteAmountHigh,uint64 nonce)"; - string internal constant BURN_LP_SIGNATURE = - "BurnLp(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; - string internal constant MINT_VLP_SIGNATURE = - "MintVlp(bytes32 sender,uint128 quoteAmount,uint64 nonce)"; - string internal constant BURN_VLP_SIGNATURE = - "BurnVlp(bytes32 sender,uint128 vlpAmount,uint64 nonce)"; - string internal constant LINK_SIGNER_SIGNATURE = - "LinkSigner(bytes32 sender,bytes32 signer,uint64 nonce)"; - IVerifier private verifier; + address internal vertexGateway; function initialize( address _sanctions, @@ -134,7 +118,9 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { function requireSubaccount(bytes32 subaccount) private view { require( - subaccount == X_ACCOUNT || (subaccountIds[subaccount] != 0), + subaccount == X_ACCOUNT || + subaccount == V_ACCOUNT || + (subaccountIds[subaccount] != 0), ERR_REQUIRES_DEPOSIT ); } @@ -204,6 +190,13 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { ); } + function computeDigest( + TransactionType txType, + bytes calldata transactionBody + ) internal view virtual returns (bytes32) { + return verifier.computeDigest(txType, transactionBody); + } + function safeTransferFrom( IERC20Base token, address from, @@ -281,6 +274,9 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { ) public { require(bytes(referralCode).length != 0); require(!RiskHelper.isIsolatedSubaccount(subaccount), ERR_UNAUTHORIZED); + if (subaccount == V_ACCOUNT) { + require(productId == 0, ERR_UNAUTHORIZED); + } address sender = address(bytes20(subaccount)); @@ -323,6 +319,11 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { slowModeConfig = _slowModeConfig; } + function setVertexGateway(address _vertexGateway) external onlyOwner { + require(vertexGateway == address(0), "already set"); + vertexGateway = _vertexGateway; + } + function requireUnsanctioned(address sender) internal view virtual { require(!sanctions.isSanctioned(sender), ERR_WALLET_SANCTIONED); } @@ -365,8 +366,19 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { } else if (txType == TransactionType.RebalanceXWithdraw) { require(sender == owner()); } else { - chargeSlowModeFee(_getQuote(), sender); - slowModeFees += SLOW_MODE_FEE; + // xrpl, xrpl-testnet, xrpl-devnet + if ( + sender == vertexGateway && + txType == TransactionType.LinkSigner && + (block.chainid == 1440000 || + block.chainid == 1449000 || + block.chainid == 1440002) + ) { + sender = address(this); + } else { + chargeSlowModeFee(_getQuote(), sender); + slowModeFees += SLOW_MODE_FEE; + } } SlowModeConfig memory _slowModeConfig = slowModeConfig; @@ -530,26 +542,16 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { transaction[1:], (SignedLiquidateSubaccount) ); - validateNonce(signedTx.tx.sender, signedTx.tx.nonce); - validateSignature( - signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(LIQUIDATE_SUBACCOUNT_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.liquidatee, - signedTx.tx.productId, - signedTx.tx.isEncodedSpread, - signedTx.tx.amount, - signedTx.tx.nonce - ) - ) - ), - signedTx.signature - ); - requireSubaccount(signedTx.tx.sender); - chargeFee(signedTx.tx.sender, LIQUIDATION_FEE); + if (signedTx.tx.sender != V_ACCOUNT) { + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + validateSignature( + signedTx.tx.sender, + _hashTypedDataV4(computeDigest(txType, transaction[1:])), + signedTx.signature + ); + requireSubaccount(signedTx.tx.sender); + chargeFee(signedTx.tx.sender, LIQUIDATION_FEE); + } clearinghouse.liquidateSubaccount(signedTx.tx); } else if (txType == TransactionType.WithdrawCollateral) { SignedWithdrawCollateral memory signedTx = abi.decode( @@ -559,17 +561,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(WITHDRAW_COLLATERAL_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.productId, - signedTx.tx.amount, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); chargeFee( @@ -642,19 +634,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(MINT_LP_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.productId, - signedTx.tx.amountBase, - signedTx.tx.quoteAmountLow, - signedTx.tx.quoteAmountHigh, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); @@ -667,17 +647,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(BURN_LP_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.productId, - signedTx.tx.amount, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); @@ -690,16 +660,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(MINT_VLP_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.quoteAmount, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); @@ -713,16 +674,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(BURN_VLP_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.vlpAmount, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); @@ -738,16 +690,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { validateNonce(signedTx.tx.sender, signedTx.tx.nonce); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(LINK_SIGNER_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.signer, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); linkedSigners[signedTx.tx.sender] = address( @@ -763,17 +706,7 @@ contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable { _recordSubaccount(signedTx.tx.recipient); validateSignature( signedTx.tx.sender, - _hashTypedDataV4( - keccak256( - abi.encode( - keccak256(bytes(TRANSFER_QUOTE_SIGNATURE)), - signedTx.tx.sender, - signedTx.tx.recipient, - signedTx.tx.amount, - signedTx.tx.nonce - ) - ) - ), + _hashTypedDataV4(computeDigest(txType, transaction[1:])), signedTx.signature ); validateNonce(signedTx.tx.sender, signedTx.tx.nonce); diff --git a/core/contracts/OffchainExchange.sol b/core/contracts/OffchainExchange.sol index 847d541..9fc8061 100644 --- a/core/contracts/OffchainExchange.sol +++ b/core/contracts/OffchainExchange.sol @@ -928,22 +928,7 @@ contract OffchainExchange is uint32 productId, bool taker ) public view returns (int128) { - FeeRates memory userFeeRates = feeRates[ - address(uint160(bytes20(subaccount))) - ][productId]; - if (userFeeRates.isNonDefault == 0) { - // use the default fee rates. - if (block.chainid == 80094 || block.chainid == 80084) { - // defaults for Berachain maker: 2bps / taker: 5bps - userFeeRates = FeeRates( - 200_000_000_000_000, - 500_000_000_000_000, - 1 - ); - } else { - userFeeRates = FeeRates(0, 200_000_000_000_000, 1); - } - } + FeeRates memory userFeeRates = _getUserFeeRates(subaccount, productId); return taker ? userFeeRates.takerRateX18 : userFeeRates.makerRateX18; } @@ -951,14 +936,38 @@ contract OffchainExchange is public view returns (int128, int128) + { + FeeRates memory userFeeRates = _getUserFeeRates(subaccount, productId); + return (userFeeRates.takerRateX18, userFeeRates.makerRateX18); + } + + function _getUserFeeRates(bytes32 subaccount, uint32 productId) + private + view + returns (FeeRates memory) { FeeRates memory userFeeRates = feeRates[ address(uint160(bytes20(subaccount))) ][productId]; - if (userFeeRates.isNonDefault == 0) { - // use the default fee rates. + + uint96 subName = uint96((uint256(subaccount) << 160) >> 160); + + if ((subName & MASK_6_BYTES) == 0x666F78696679000000000000) { + // defaults for "foxify". maker: 0bps / taker: 7.5bps + userFeeRates = FeeRates(0, 750_000_000_000_000, 1); + } else if ((subName & MASK_6_BYTES) == 0x66756E646564000000000000) { + // defaults for "funded". maker: 7.5bps / taker: 7.5bps + userFeeRates = FeeRates( + 750_000_000_000_000, + 750_000_000_000_000, + 1 + ); + } else if ((subName & MASK_6_BYTES) == 0x706572706965000000000000) { + // defaults for "perpie". maker: 0bps / taker: 4bps + userFeeRates = FeeRates(0, 400_000_000_000_000, 1); + } else if (userFeeRates.isNonDefault == 0) { if (block.chainid == 80094 || block.chainid == 80084) { - // defaults for Berachain maker: 2bps / taker: 5bps + // defaults for Berachain. maker: 2bps / taker: 5bps userFeeRates = FeeRates( 200_000_000_000_000, 500_000_000_000_000, @@ -968,7 +977,8 @@ contract OffchainExchange is userFeeRates = FeeRates(0, 200_000_000_000_000, 1); } } - return (userFeeRates.takerRateX18, userFeeRates.makerRateX18); + + return userFeeRates; } function updateFeeRates( diff --git a/core/contracts/Verifier.sol b/core/contracts/Verifier.sol index 8746f0d..cd2f9f4 100644 --- a/core/contracts/Verifier.sol +++ b/core/contracts/Verifier.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "./common/Errors.sol"; import "./libraries/MathHelper.sol"; import "./interfaces/IVerifier.sol"; +import "./interfaces/IEndpoint.sol"; contract Verifier is EIP712Upgradeable, OwnableUpgradeable, IVerifier { Point[8] internal pubkeys; @@ -14,6 +15,23 @@ contract Verifier is EIP712Upgradeable, OwnableUpgradeable, IVerifier { bool[256] internal isAggregatePubkeyLatest; uint256 internal nSigner; + string internal constant LIQUIDATE_SUBACCOUNT_SIGNATURE = + "LiquidateSubaccount(bytes32 sender,bytes32 liquidatee,uint32 productId,bool isEncodedSpread,int128 amount,uint64 nonce)"; + string internal constant TRANSFER_QUOTE_SIGNATURE = + "TransferQuote(bytes32 sender,bytes32 recipient,uint128 amount,uint64 nonce)"; + string internal constant WITHDRAW_COLLATERAL_SIGNATURE = + "WithdrawCollateral(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; + string internal constant MINT_LP_SIGNATURE = + "MintLp(bytes32 sender,uint32 productId,uint128 amountBase,uint128 quoteAmountLow,uint128 quoteAmountHigh,uint64 nonce)"; + string internal constant BURN_LP_SIGNATURE = + "BurnLp(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; + string internal constant MINT_VLP_SIGNATURE = + "MintVlp(bytes32 sender,uint128 quoteAmount,uint64 nonce)"; + string internal constant BURN_VLP_SIGNATURE = + "BurnVlp(bytes32 sender,uint128 vlpAmount,uint64 nonce)"; + string internal constant LINK_SIGNER_SIGNATURE = + "LinkSigner(bytes32 sender,bytes32 signer,uint64 nonce)"; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -277,4 +295,130 @@ contract Verifier is EIP712Upgradeable, OwnableUpgradeable, IVerifier { ERR_INVALID_SIGNATURE ); } + + function computeDigest( + IEndpoint.TransactionType txType, + bytes calldata transactionBody + ) public pure returns (bytes32) { + bytes32 digest; + + if (txType == IEndpoint.TransactionType.LiquidateSubaccount) { + IEndpoint.SignedLiquidateSubaccount memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedLiquidateSubaccount) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(LIQUIDATE_SUBACCOUNT_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.liquidatee, + signedTx.tx.productId, + signedTx.tx.isEncodedSpread, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.WithdrawCollateral) { + IEndpoint.SignedWithdrawCollateral memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedWithdrawCollateral) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(WITHDRAW_COLLATERAL_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.MintLp) { + IEndpoint.SignedMintLp memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedMintLp) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(MINT_LP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amountBase, + signedTx.tx.quoteAmountLow, + signedTx.tx.quoteAmountHigh, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.BurnLp) { + IEndpoint.SignedBurnLp memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedBurnLp) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(BURN_LP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.MintVlp) { + IEndpoint.SignedMintVlp memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedMintVlp) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(MINT_VLP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.quoteAmount, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.BurnVlp) { + IEndpoint.SignedBurnVlp memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedBurnVlp) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(BURN_VLP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.vlpAmount, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.LinkSigner) { + IEndpoint.SignedLinkSigner memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedLinkSigner) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(LINK_SIGNER_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.signer, + signedTx.tx.nonce + ) + ); + } else if (txType == IEndpoint.TransactionType.TransferQuote) { + IEndpoint.SignedTransferQuote memory signedTx = abi.decode( + transactionBody, + (IEndpoint.SignedTransferQuote) + ); + digest = keccak256( + abi.encode( + keccak256(bytes(TRANSFER_QUOTE_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.recipient, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ); + } else { + revert(); + } + + return digest; + } } diff --git a/core/contracts/VertexGateway.sol b/core/contracts/VertexGateway.sol new file mode 100644 index 0000000..3e78d4b --- /dev/null +++ b/core/contracts/VertexGateway.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import "./Endpoint.sol"; +import "./interfaces/IOffchainExchange.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./common/Errors.sol"; +import "./common/Constants.sol"; +import "./libraries/ERC20Helper.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/RippleBase58.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./interfaces/IEndpoint.sol"; +import "./interfaces/IERC20Base.sol"; + +import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import {IInterchainTokenService} from "@axelar-network/interchain-token-service/contracts/interfaces/IInterchainTokenService.sol"; + +contract VertexGateway is EIP712Upgradeable, OwnableUpgradeable { + bytes32 internal constant EXECUTE_SUCCESS = + keccak256("its-execute-success"); + using ERC20Helper for IERC20Base; + using RippleBase58 for bytes; + + struct Config { + bytes32 tokenId; + address token; + } + + address internal endpoint; + address internal axelarGateway; + address internal axelarGasService; + address payable internal interchainTokenService; + string public sourceChain; + + mapping(uint32 => Config) public configs; + mapping(address => bytes32) internal tokenIds; + mapping(address => bytes) public rippleAddresses; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _endpoint, + address _axelarGateway, + address _axelarGasService, + address payable _interchainTokenService, + string calldata _sourceChain + ) external initializer { + __Ownable_init(); + endpoint = _endpoint; + axelarGateway = _axelarGateway; + axelarGasService = _axelarGasService; + interchainTokenService = _interchainTokenService; + sourceChain = _sourceChain; + } + + modifier onlyIts() { + require( + msg.sender == interchainTokenService, + "Not InterchainTokenService" + ); + _; + } + + function equal(string memory a, string memory b) + internal + pure + returns (bool) + { + return + bytes(a).length == bytes(b).length && + keccak256(bytes(a)) == keccak256(bytes(b)); + } + + function addProduct( + uint32 productId, + bytes32 tokenId, + address token + ) external onlyOwner { + configs[productId] = Config({tokenId: tokenId, token: token}); + tokenIds[token] = tokenId; + } + + function execute( + bytes32 commandId, + string calldata _sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + require(equal(sourceChain, _sourceChain), "Not authored sourceChain"); + bytes32 payloadHash = keccak256(payload); + require( + IAxelarGateway(axelarGateway).validateContractCall( + commandId, + _sourceChain, + sourceAddress, + payloadHash + ), + "Not approved by gateway." + ); + address sender = bytes(sourceAddress).decodeFromRippleAddress(); + bytes12 subaccountName; + address signer; + (subaccountName, signer) = abi.decode(payload, (bytes12, address)); + bytes32 subaccount = bytes32(abi.encodePacked(sender, subaccountName)); + + linkSigner(subaccount, signer); + } + + function executeWithInterchainToken( + bytes32 commandId, + string calldata _sourceChain, + bytes calldata sourceAddress, + bytes calldata payload, + bytes32 tokenId, + address token, + uint256 amount + ) external onlyIts returns (bytes32) { + require(equal(sourceChain, _sourceChain), "Not authored sourceChain"); + address sender = sourceAddress.decodeFromRippleAddress(); + rippleAddresses[sender] = sourceAddress; + bytes12 subaccountName; + address signer; + uint32 productId; + (subaccountName, productId, signer) = abi.decode( + payload, + (bytes12, uint32, address) + ); + bytes32 subaccount = bytes32(abi.encodePacked(sender, subaccountName)); + require(configs[productId].token == token, "product mismatched"); + IERC20Base(token).approve(endpoint, amount); + IEndpoint(endpoint).depositCollateralWithReferral( + subaccount, + productId, + uint128(amount), + "VertexGateway" + ); + + if (signer != address(0)) { + linkSigner(subaccount, signer); + } + + return EXECUTE_SUCCESS; + } + + function linkSigner(bytes32 sender, address signer) internal { + bytes32 signerSubaccount = bytes32(uint256(uint160(signer)) << 96); + IEndpoint.LinkSigner memory linkSigner = IEndpoint.LinkSigner( + sender, + signerSubaccount, + IEndpoint(endpoint).getNonce(signer) + ); + bytes memory linkSignerTx = abi.encodePacked( + uint8(19), + abi.encode(linkSigner) + ); + Endpoint(endpoint).submitSlowModeTransaction(linkSignerTx); + } + + function isNativeWallet(address wallet) external view returns (bool) { + return rippleAddresses[wallet].length == 0; + } + + function withdraw( + IERC20Base token, + address to, + uint256 amount + ) external { + token.safeTransferFrom(msg.sender, address(this), amount); + token.approve(interchainTokenService, amount); + bytes32 tokenId = tokenIds[address(token)]; + bytes memory rippleAddress = rippleAddresses[to]; + IInterchainTokenService(interchainTokenService).interchainTransfer{ + value: uint256(uint128(ONE)) + }( + tokenId, + sourceChain, + rippleAddress, + amount, + "", + uint256(uint128(ONE)) + ); + } +} diff --git a/core/contracts/WithdrawPool.sol b/core/contracts/WithdrawPool.sol index 53953f8..a61fc67 100644 --- a/core/contracts/WithdrawPool.sol +++ b/core/contracts/WithdrawPool.sol @@ -10,171 +10,10 @@ import "./interfaces/engine/ISpotEngine.sol"; import "./interfaces/IERC20Base.sol"; import "./libraries/ERC20Helper.sol"; import "./common/Constants.sol"; +import "./BaseWithdrawPool.sol"; -contract WithdrawPool is EIP712Upgradeable, OwnableUpgradeable { - using ERC20Helper for IERC20Base; - using MathSD21x18 for int128; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - function initialize(address _clearinghouse, address _verifier) - external - initializer - { - __Ownable_init(); - clearinghouse = _clearinghouse; - verifier = _verifier; - } - - address internal clearinghouse; - - address internal verifier; - - // submitted withdrawal idxs - mapping(uint64 => bool) public markedIdxs; - - // collected withdrawal fees in native token decimals - mapping(uint32 => int128) public fees; - - uint64 public minIdx; - - function submitFastWithdrawal( - uint64 idx, - bytes calldata transaction, - bytes[] calldata signatures - ) public { - require(!markedIdxs[idx], "Withdrawal already submitted"); - require(idx > minIdx, "idx too small"); - markedIdxs[idx] = true; - - Verifier v = Verifier(verifier); - v.requireValidTxSignatures(transaction, idx, signatures); - - IEndpoint.SignedWithdrawCollateral memory signedTx = abi.decode( - transaction[1:], - (IEndpoint.SignedWithdrawCollateral) - ); - - IERC20Base token = getToken(signedTx.tx.productId); - - address sendTo = address(uint160(bytes20(signedTx.tx.sender))); - uint128 transferAmount = signedTx.tx.amount; - - require(transferAmount <= INT128_MAX, ERR_CONVERSION_OVERFLOW); - - int128 fee = fastWithdrawalFeeAmount( - token, - signedTx.tx.productId, - transferAmount - ); - - require(transferAmount > uint128(fee), "Fee larger than balance"); - transferAmount -= uint128(fee); - fees[signedTx.tx.productId] += fee; - - handleWithdrawTransfer(token, sendTo, transferAmount); - } - - function submitWithdrawal( - IERC20Base token, - address sendTo, - uint128 amount, - uint64 idx - ) public { - require(msg.sender == clearinghouse); - - if (markedIdxs[idx]) { - return; - } - markedIdxs[idx] = true; - // set minIdx to most recent withdrawal submitted by sequencer - minIdx = idx; - - handleWithdrawTransfer(token, sendTo, amount); - } - - function fastWithdrawalFeeAmount( - IERC20Base token, - uint32 productId, - uint128 amount - ) public view returns (int128) { - uint8 decimals = token.decimals(); - require(decimals <= MAX_DECIMALS); - int256 multiplier = int256(10**(MAX_DECIMALS - uint8(decimals))); - int128 amountX18 = int128(amount) * int128(multiplier); - - int128 proportionalFeeX18 = FAST_WITHDRAWAL_FEE_RATE.mul(amountX18); - int128 minFeeX18 = 5 * - IClearinghouse(clearinghouse).getWithdrawFee(productId); - - int128 feeX18 = MathHelper.max(proportionalFeeX18, minFeeX18); - return feeX18 / int128(multiplier); - } - - function removeLiquidity( - uint32 productId, - uint128 amount, - address sendTo - ) external onlyOwner { - handleWithdrawTransfer(getToken(productId), sendTo, amount); - } - - function checkMarkedIdxs(uint64[] calldata idxs) - public - view - returns (bool[] memory) - { - bool[] memory marked = new bool[](idxs.length); - for (uint256 i = 0; i < idxs.length; i++) { - marked[i] = markedIdxs[idxs[i]]; - } - return marked; - } - - function checkProductBalances(uint32[] calldata productIds) - public - view - returns (uint256[] memory) - { - uint256[] memory balances = new uint256[](productIds.length); - for (uint256 i = 0; i < productIds.length; i++) { - IERC20Base token = getToken(productIds[i]); - balances[i] = token.balanceOf(address(this)); - } - return balances; - } - - function handleWithdrawTransfer( - IERC20Base token, - address to, - uint128 amount - ) internal virtual { - token.safeTransfer(to, uint256(amount)); - } - - function safeTransferFrom( - IERC20Base token, - address from, - uint256 amount - ) internal virtual { - token.safeTransferFrom(from, address(this), amount); - } - - function getToken(uint32 productId) internal view returns (IERC20Base) { - IERC20Base token = IERC20Base(spotEngine().getConfig(productId).token); - require(address(token) != address(0)); - return token; - } - - function spotEngine() internal view returns (ISpotEngine) { - return - ISpotEngine( - IClearinghouse(clearinghouse).getEngineByType( - IProductEngine.EngineType.SPOT - ) - ); +contract WithdrawPool is BaseWithdrawPool { + function initialize(address _clearinghouse, address _verifier) external { + _initialize(_clearinghouse, _verifier); } } diff --git a/core/contracts/WithdrawPoolRipple.sol b/core/contracts/WithdrawPoolRipple.sol new file mode 100644 index 0000000..1181a83 --- /dev/null +++ b/core/contracts/WithdrawPoolRipple.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./libraries/MathHelper.sol"; +import "./interfaces/IEndpoint.sol"; +import "./Verifier.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/IERC20Base.sol"; +import "./libraries/ERC20Helper.sol"; +import "./common/Constants.sol"; +import "./BaseWithdrawPool.sol"; +import "./VertexGateway.sol"; + +contract WithdrawPoolRipple is BaseWithdrawPool { + using ERC20Helper for IERC20Base; + address internal vertexGateway; + + function initialize( + address _clearinghouse, + address _verifier, + address _vertexGateway + ) external { + _initialize(_clearinghouse, _verifier); + vertexGateway = _vertexGateway; + } + + function handleWithdrawTransfer( + IERC20Base token, + address to, + uint128 amount + ) internal override { + if (VertexGateway(vertexGateway).isNativeWallet(to)) { + token.safeTransfer(to, uint256(amount)); + } else { + token.approve(vertexGateway, uint256(amount)); + VertexGateway(vertexGateway).withdraw(token, to, uint256(amount)); + } + } +} diff --git a/core/contracts/common/Constants.sol b/core/contracts/common/Constants.sol index 6a3042c..3eef412 100644 --- a/core/contracts/common/Constants.sol +++ b/core/contracts/common/Constants.sol @@ -41,3 +41,5 @@ int256 constant MIN_DEPOSIT_AMOUNT = 5 * ONE; uint32 constant MAX_ISOLATED_SUBACCOUNTS_PER_ADDRESS = 10; uint32 constant VLP_PRODUCT_ID = 153; + +uint96 constant MASK_6_BYTES = 0xFFFFFFFFFFFF000000000000; diff --git a/core/contracts/interfaces/IERC20Base.sol b/core/contracts/interfaces/IERC20Base.sol index d77b3cd..3703dba 100644 --- a/core/contracts/interfaces/IERC20Base.sol +++ b/core/contracts/interfaces/IERC20Base.sol @@ -37,4 +37,6 @@ interface IERC20Base { returns (bool); function balanceOf(address account) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); } diff --git a/core/contracts/interfaces/IEndpoint.sol b/core/contracts/interfaces/IEndpoint.sol index df9dbc4..6743648 100644 --- a/core/contracts/interfaces/IEndpoint.sol +++ b/core/contracts/interfaces/IEndpoint.sol @@ -157,8 +157,9 @@ interface IEndpoint { } struct RebalanceVlp { + uint32 productId; + int128 baseAmount; int128 quoteAmount; - int128 vlpAmount; } struct LinkSigner { diff --git a/core/contracts/interfaces/IVerifier.sol b/core/contracts/interfaces/IVerifier.sol index 72dcb8f..785ce82 100644 --- a/core/contracts/interfaces/IVerifier.sol +++ b/core/contracts/interfaces/IVerifier.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; +import "./IEndpoint.sol"; interface IVerifier { function requireValidSignature( @@ -17,4 +18,9 @@ interface IVerifier { bytes32 digest, bytes memory signature ) external pure; + + function computeDigest( + IEndpoint.TransactionType txType, + bytes calldata transactionBody + ) external view returns (bytes32); } diff --git a/core/contracts/libraries/RippleBase58.sol b/core/contracts/libraries/RippleBase58.sol new file mode 100644 index 0000000..23eb29b --- /dev/null +++ b/core/contracts/libraries/RippleBase58.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +// revised from https://github.com/storyicon/base58-solidity + +library RippleBase58 { + bytes constant ALPHABET = + "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"; + + function decode(bytes memory data_) internal pure returns (bytes memory) { + unchecked { + uint256 zero = 49; + uint256 b58sz = data_.length; + uint256 zcount = 0; + for (uint256 i = 0; i < b58sz && uint8(data_[i]) == zero; i++) { + zcount++; + } + uint256 t; + uint256 c; + bool f; + bytes memory binu = new bytes(2 * (((b58sz * 8351) / 6115) + 1)); + uint32[] memory outi = new uint32[]((b58sz + 3) / 4); + for (uint256 i = 0; i < data_.length; i++) { + bytes1 r = data_[i]; + (c, f) = indexOf(ALPHABET, r); + require(f, "invalid base58 digit"); + for (int256 k = int256(outi.length) - 1; k >= 0; k--) { + t = uint64(outi[uint256(k)]) * 58 + c; + c = t >> 32; + outi[uint256(k)] = uint32(t & 0xffffffff); + } + } + uint64 mask = uint64(b58sz % 4) * 8; + if (mask == 0) { + mask = 32; + } + mask -= 8; + uint256 outLen = 0; + for (uint256 j = 0; j < outi.length; j++) { + while (mask < 32) { + binu[outLen] = bytes1(uint8(outi[j] >> mask)); + outLen++; + if (mask < 8) { + break; + } + mask -= 8; + } + mask = 24; + } + for (uint256 msb = zcount; msb < binu.length; msb++) { + if (binu[msb] > 0) { + return slice(binu, msb - zcount, outLen); + } + } + + return slice(binu, 0, outLen); + } + } + + function decodeFromRippleAddress(bytes memory data) + internal + pure + returns (address) + { + uint160 result; + bytes memory a = decode(data); + for (uint256 i = 0; i + 4 < a.length; i++) { + result = result * 256 + uint8(a[i]); + } + return address(result); + } + + /** + * @notice slice is used to slice the given byte, returns the bytes in the range of [start_, end_) + * @param data_ raw data, passed in as bytes. + * @param start_ start index. + * @param end_ end index. + * @return slice data + */ + function slice( + bytes memory data_, + uint256 start_, + uint256 end_ + ) internal pure returns (bytes memory) { + unchecked { + bytes memory ret = new bytes(end_ - start_); + for (uint256 i = 0; i < end_ - start_; i++) { + ret[i] = data_[i + start_]; + } + return ret; + } + } + + /** + * @notice indexOf is used to find where char_ appears in data_. + * @param data_ raw data, passed in as bytes. + * @param char_ target byte. + * @return index, and whether the search was successful. + */ + function indexOf(bytes memory data_, bytes1 char_) + internal + pure + returns (uint256, bool) + { + unchecked { + for (uint256 i = 0; i < data_.length; i++) { + if (data_[i] == char_) { + return (i, true); + } + } + return (0, false); + } + } +} diff --git a/core/package.json b/core/package.json index 8326bb1..feeff6e 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "vertex-core", - "version": "3.2.0", + "version": "3.3.0", "license": "UNLICENSED", "description": "EVM implementation of Vertex", "scripts": { diff --git a/lba/package.json b/lba/package.json index 884c5af..40745ec 100644 --- a/lba/package.json +++ b/lba/package.json @@ -1,6 +1,6 @@ { "name": "vertex-lba", - "version": "3.2.0", + "version": "3.3.0", "license": "UNLICENSED", "description": "Vertex LBA contracts", "scripts": {