diff --git a/contracts/Gateway.sol b/contracts/Gateway.sol index c368a60..7974448 100644 --- a/contracts/Gateway.sol +++ b/contracts/Gateway.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; +import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import {GatewaySettingManager} from './GatewaySettingManager.sol'; import {IGateway, IERC20} from './interfaces/IGateway.sol'; @@ -11,15 +12,18 @@ import {IGateway, IERC20} from './interfaces/IGateway.sol'; * @notice This contract serves as a gateway for creating orders and managing settlements. */ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { + using ECDSA for bytes32; struct fee { uint256 protocolFee; uint256 liquidityProviderAmount; } - mapping(bytes32 => Order) private order; + mapping(bytes32 => OrderOut) private orderOut; + mapping(bytes32 => OrderIn) private orderIn; mapping(address => uint256) private _nonce; - uint256[50] private __gap; mapping(address => mapping(address => uint256)) private balance; + mapping(bytes32 => bool) private processedOrders; + uint256[50] private __gap; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -57,7 +61,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { * @param _amount The amount to be checked. */ modifier isValidAmount(uint256 _amount) { - require(_amount != 0, 'AmountIsZero'); + require(_amount > 0, 'AmountIsZero'); _; } @@ -95,7 +99,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { _handler(_token, _amount, _refundAddress, _senderFeeRecipient, _senderFee); // validate messageHash - require(bytes(messageHash).length != 0, 'InvalidMessageHash'); + require(bytes(messageHash).length > 0, 'InvalidMessageHash'); // transfer token from msg.sender to contract IERC20(_token).transferFrom(msg.sender, address(this), _amount + _senderFee); @@ -108,7 +112,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { // update transaction uint256 _protocolFee = (_amount * protocolFeePercent) / MAX_BPS; - order[orderId] = Order({ + orderOut[orderId] = OrderOut({ sender: msg.sender, token: _token, senderFeeRecipient: _senderFeeRecipient, @@ -123,9 +127,9 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { // emit order created event emit OrderCreated( - order[orderId].sender, + orderOut[orderId].sender, _token, - order[orderId].amount, + orderOut[orderId].amount, _protocolFee, orderId, _rate, @@ -150,7 +154,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { ) internal isTokenApproved(_token) isValidAmount(_amount) view { require(_refundAddress != address(0), 'ThrowZeroAddress'); - if (_senderFee != 0) { + if (_senderFee > 0) { require(_senderFeeRecipient != address(0), 'InvalidSenderFeeRecipient'); } } @@ -158,46 +162,46 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { /* ################################################################## AGGREGATOR FUNCTIONS ################################################################## */ - /** @dev See {settle-IGateway}. */ - function settle( + /** @dev See {settleOrderOut-IGateway}. */ + function settleOrderOut( bytes32 _splitOrderId, bytes32 _orderId, - address _liquidityProvider, + address _provider, uint64 _settlePercent ) external onlyAggregator returns (bool) { // ensure the transaction has not been fulfilled - require(!order[_orderId].isFulfilled, 'OrderFulfilled'); - require(!order[_orderId].isRefunded, 'OrderRefunded'); + require(!orderOut[_orderId].isFulfilled, 'OrderFulfilled'); + require(!orderOut[_orderId].isRefunded, 'OrderRefunded'); // load the token into memory - address token = order[_orderId].token; + address token = orderOut[_orderId].token; // subtract sum of amount based on the input _settlePercent - order[_orderId].currentBPS -= _settlePercent; + orderOut[_orderId].currentBPS -= _settlePercent; - if (order[_orderId].currentBPS == 0) { + if (orderOut[_orderId].currentBPS == 0) { // update the transaction to be fulfilled - order[_orderId].isFulfilled = true; + orderOut[_orderId].isFulfilled = true; - if (order[_orderId].senderFee != 0) { + if (orderOut[_orderId].senderFee > 0) { // transfer sender fee - IERC20(order[_orderId].token).transfer( - order[_orderId].senderFeeRecipient, - order[_orderId].senderFee + IERC20(orderOut[_orderId].token).transfer( + orderOut[_orderId].senderFeeRecipient, + orderOut[_orderId].senderFee ); // emit event emit SenderFeeTransferred( - order[_orderId].senderFeeRecipient, - order[_orderId].senderFee + orderOut[_orderId].senderFeeRecipient, + orderOut[_orderId].senderFee ); } } // transfer to liquidity provider - uint256 liquidityProviderAmount = (order[_orderId].amount * _settlePercent) / MAX_BPS; - order[_orderId].amount -= liquidityProviderAmount; + uint256 liquidityProviderAmount = (orderOut[_orderId].amount * _settlePercent) / MAX_BPS; + orderOut[_orderId].amount -= liquidityProviderAmount; uint256 protocolFee = (liquidityProviderAmount * protocolFeePercent) / MAX_BPS; liquidityProviderAmount -= protocolFee; @@ -205,37 +209,37 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { // transfer protocol fee IERC20(token).transfer(treasuryAddress, protocolFee); - IERC20(token).transfer(_liquidityProvider, liquidityProviderAmount); + IERC20(token).transfer(_provider, liquidityProviderAmount); // emit settled event - emit OrderSettled(_splitOrderId, _orderId, _liquidityProvider, _settlePercent); + emit OrderSettledOut(_splitOrderId, _orderId, _provider, _settlePercent); return true; } - /** @dev See {refund-IGateway}. */ - function refund(uint256 _fee, bytes32 _orderId) external onlyAggregator returns (bool) { + /** @dev See {refundOrder-IGateway}. */ + function refundOrder(uint256 _fee, bytes32 _orderId) external onlyAggregator returns (bool) { // ensure the transaction has not been fulfilled - require(!order[_orderId].isFulfilled, 'OrderFulfilled'); - require(!order[_orderId].isRefunded, 'OrderRefunded'); - require(order[_orderId].protocolFee >= _fee, 'FeeExceedsProtocolFee'); + require(!orderOut[_orderId].isFulfilled, 'OrderFulfilled'); + require(!orderOut[_orderId].isRefunded, 'OrderRefunded'); + require(orderOut[_orderId].protocolFee >= _fee, 'FeeExceedsProtocolFee'); if (_fee > 0) { // transfer refund fee to the treasury - IERC20(order[_orderId].token).transfer(treasuryAddress, _fee); + IERC20(orderOut[_orderId].token).transfer(treasuryAddress, _fee); } // reset state values - order[_orderId].isRefunded = true; - order[_orderId].currentBPS = 0; + orderOut[_orderId].isRefunded = true; + orderOut[_orderId].currentBPS = 0; // deduct fee from order amount - uint256 refundAmount = order[_orderId].amount - _fee; + uint256 refundAmount = orderOut[_orderId].amount - _fee; // transfer refund amount and sender fee to the refund address - IERC20(order[_orderId].token).transfer( - order[_orderId].refundAddress, - refundAmount + order[_orderId].senderFee + IERC20(orderOut[_orderId].token).transfer( + orderOut[_orderId].refundAddress, + refundAmount + orderOut[_orderId].senderFee ); // emit refunded event @@ -253,12 +257,63 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { return true; } + /** @dev See {settledOrderIn-IGateway}. */ + function settleOrderIn( + bytes32 _orderId, + bytes memory _signature, + address _provider, + address _sender, + address _token, + uint256 _amount + ) external onlyAggregator isValidAmount(_amount) { + require(!processedOrders[_orderId], "Order already processed"); + require(_provider != address(0), "Invalid provider address"); + require(_sender != address(0), "Invalid sender address"); + + // Verify signature + bytes32 messageHash = keccak256(abi.encodePacked(_orderId, _provider, _sender, _token, _amount)); + bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); + address recoveredAddress = ethSignedMessageHash.recover(_signature); + require(recoveredAddress == _provider, "Invalid signature"); + uint256 _protocolFee = (_amount * protocolFeePercent) / MAX_BPS; + // Note: There is no need for checks for token supported as the balance will be 0 if the token is not supported + require(balance[_token][_provider] >= _amount + _protocolFee, "Insufficient balance"); + + // Mark order as processed + processedOrders[_orderId] = true; + + // Update balances + balance[_token][_provider] -= (_amount + _protocolFee); + + orderIn[_orderId] = OrderIn({ + amount: _amount, + provider: _provider, + sender: _sender, + token: _token, + orderId: _orderId + }); + + + // transfer to sender + IERC20(_token).transfer(_sender, _amount); + if (_protocolFee > 0) { + IERC20(_token).transfer(treasuryAddress, _protocolFee); + } + + emit OrderSettledIn(_provider, _sender, _amount, _token, _orderId); + } + /* ################################################################## VIEW CALLS ################################################################## */ - /** @dev See {getOrderInfo-IGateway}. */ - function getOrderInfo(bytes32 _orderId) external view returns (Order memory) { - return order[_orderId]; + /** @dev See {getOrderInfoOut-IGateway}. */ + function getOrderInfoOut(bytes32 _orderId) external view returns (OrderOut memory) { + return orderOut[_orderId]; + } + + /** @dev See {getOrderInfoIn-IGateway}. */ + function getOrderInfoIn(bytes32 _orderId) external view returns (OrderIn memory) { + return orderIn[_orderId]; } /** @dev See {isTokenSupported-IGateway}. */ @@ -276,4 +331,9 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { function getBalance(address _token, address _provider) external view returns (uint256) { return balance[_token][_provider]; } + + /** See {isOrderProcessed-IGateway} */ + function isOrderProcessed(bytes32 _orderId) external view returns (bool) { + return processedOrders[_orderId]; + } } diff --git a/contracts/interfaces/IGateway.sol b/contracts/interfaces/IGateway.sol index 62da392..d0f4444 100644 --- a/contracts/interfaces/IGateway.sol +++ b/contracts/interfaces/IGateway.sol @@ -31,13 +31,13 @@ interface IGateway { ); /** - * @notice Emitted when an aggregator settles a transaction. + * @notice Emitted when an order is settled out. * @param splitOrderId The ID of the split order. * @param orderId The ID of the order. * @param liquidityProvider The address of the liquidity provider. * @param settlePercent The percentage at which the transaction is settled. */ - event OrderSettled( + event OrderSettledOut( bytes32 splitOrderId, bytes32 indexed orderId, address indexed liquidityProvider, @@ -66,11 +66,20 @@ interface IGateway { */ event Deposit(address indexed sender, address indexed token, uint256 indexed amount); + /** + * @dev Emitted when an order is settled in. + * @param provider The address of the provider. + * @param senderAddress The address of the sender. + * @param amount The address of the deposited token. + * @param token The amount of the deposit. + * @param orderId The ID of the order. + */ + event OrderSettledIn(address indexed provider, address indexed senderAddress, uint256 indexed amount, address token, bytes32 orderId); /* ################################################################## STRUCTS ################################################################## */ /** - * @notice Struct representing an order. + * @notice Struct representing an order out. * @param sender The address of the sender. * @param token The address of the token. * @param senderFeeRecipient The address of the sender fee recipient. @@ -82,7 +91,7 @@ interface IGateway { * @param currentBPS The current basis points. * @param amount The amount of the order. */ - struct Order { + struct OrderOut { address sender; address token; address senderFeeRecipient; @@ -95,6 +104,22 @@ interface IGateway { uint256 amount; } + /** + * @notice Struct representing an order settled in. + * @param amount The amount of the order. + * @param provider The address of the provider. + * @param sender The address of the sender. + * @param token The address of the token. + * @param orderid The ID of the order. + */ + struct OrderIn { + uint256 amount; + address provider; + address sender; + address token; + bytes32 orderId; + } + /* ################################################################## EXTERNAL CALLS ################################################################## */ @@ -125,17 +150,17 @@ interface IGateway { ) external returns (bytes32 _orderId); /** - * @notice Settles a transaction and distributes fees accordingly. + * @notice Settles an order out transaction and distributes fees accordingly. * @param _splitOrderId The ID of the split order. * @param _orderId The ID of the transaction. - * @param _liquidityProvider The address of the liquidity provider. + * @param _provider The address of the liquidity provider. * @param _settlePercent The rate at which the transaction is settled. * @return bool The settlement is successful. */ - function settle( + function settleOrderOut( bytes32 _splitOrderId, bytes32 _orderId, - address _liquidityProvider, + address _provider, uint64 _settlePercent ) external returns (bool); @@ -147,7 +172,7 @@ interface IGateway { * @param _orderId The ID of the transaction. * @return bool the refund is successful. */ - function refund(uint256 _fee, bytes32 _orderId) external returns (bool); + function refundOrder(uint256 _fee, bytes32 _orderId) external returns (bool); /** * @notice Allow a provider to deposit an asset into Gateway. @@ -161,6 +186,25 @@ interface IGateway { */ function deposit(address _token, uint256 _amount) external returns (bool); + + /** + * @notice Allow aggregator to settle an order coming in. + * @param _orderId The ID of the transaction. + * @param _signature The signature of the provider. + * @param _provider The address of the provider. + * @param _senderAddress The address of the sender. + * @param _token The address of the asset. + * @param _amount The amount to be transferred. + */ + function settleOrderIn( + bytes32 _orderId, + bytes memory _signature, + address _provider, + address _senderAddress, + address _token, + uint256 _amount + ) external; + /** * @notice Checks if a token is supported by Gateway. * @param _token The address of the token to check. @@ -169,11 +213,18 @@ interface IGateway { function isTokenSupported(address _token) external view returns (bool); /** - * @notice Gets the details of an order. + * @notice Gets the details of an off ramp order. + * @param _orderId The ID of the order. + * @return Order The order details. + */ + function getOrderInfoOut(bytes32 _orderId) external view returns (OrderOut memory); + + /** + * @notice Gets the details of an on ramp order. * @param _orderId The ID of the order. * @return Order The order details. */ - function getOrderInfo(bytes32 _orderId) external view returns (Order memory); + function getOrderInfoIn(bytes32 _orderId) external view returns (OrderIn memory); /** * @notice Gets the fee details of Gateway. @@ -189,4 +240,11 @@ interface IGateway { * @return uint256 The provider's balance. */ function getBalance(address _asset, address _provider) external view returns (uint256); + + /** + * @notice Gets order processed status. + * @param _orderId The ID of the order. + * @return bool The order processed status. + */ + function isOrderProcessed(bytes32 _orderId) external view returns (bool); } diff --git a/package.json b/package.json index 74ed042..3dc1249 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "chai": "^4.3.7", "dotenv": "^16.0.2", "ethers": "^5.7.2", - "hardhat": "^2.14.0", + "hardhat": "^2.22.12", "hardhat-deploy": "^0.11.34", "hardhat-gas-reporter": "^1.0.9", "solidity-coverage": "^0.8.2", diff --git a/scripts/manualUpgrade.ts b/scripts/manualUpgrade.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/gateway/gateway.createorder.test.js b/test/gateway/gateway.createorder.test.js index 176a1d7..d6627e1 100644 --- a/test/gateway/gateway.createorder.test.js +++ b/test/gateway/gateway.createorder.test.js @@ -146,7 +146,7 @@ describe("Gateway create order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await this.gateway.getOrderInfo(orderId); + ] = await this.gateway.getOrderInfoOut(orderId); // expect sender balance to increase by sender fee expect(await this.mockUSDT.balanceOf(this.sender.address)).to.eq( ZERO_AMOUNT @@ -237,7 +237,7 @@ describe("Gateway create order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await this.gateway.getOrderInfo(orderId); + ] = await this.gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(ZERO_ADDRESS); expect(this.token).to.eq(ZERO_ADDRESS); @@ -312,7 +312,7 @@ describe("Gateway create order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await this.gateway.getOrderInfo(orderId); + ] = await this.gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(ZERO_ADDRESS); expect(this.token).to.eq(ZERO_ADDRESS); @@ -387,7 +387,7 @@ describe("Gateway create order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await this.gateway.getOrderInfo(orderId); + ] = await this.gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(ZERO_ADDRESS); expect(this.token).to.eq(ZERO_ADDRESS); @@ -461,7 +461,7 @@ describe("Gateway create order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await this.gateway.getOrderInfo(orderId); + ] = await this.gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(ZERO_ADDRESS); expect(this.token).to.eq(ZERO_ADDRESS); diff --git a/test/gateway/gateway.settleOrderIn.test.js b/test/gateway/gateway.settleOrderIn.test.js new file mode 100644 index 0000000..83eab68 --- /dev/null +++ b/test/gateway/gateway.settleOrderIn.test.js @@ -0,0 +1,515 @@ +const { ethers } = require("hardhat"); +const { BigNumber } = require("@ethersproject/bignumber"); +const { gatewayFixture } = require("../fixtures/gateway.js"); +const { + deployContract, + ZERO_AMOUNT, + Events, + mockMintDeposit, + assertBalance, + assertDepositBalance, +} = require("../utils/utils.manager.js"); +const { expect } = require("chai"); + +describe("Gateway Onramp Order settlement", function () { + beforeEach(async function () { + [ + this.deployer, + this.treasuryAddress, + this.aggregator, + this.alice, + this.bob, + this.Eve, + this.hacker, + ...this.accounts + ] = await ethers.getSigners(); + + ({ gateway, mockUSDT } = await gatewayFixture()); + + this.mockDAI = await deployContract("MockUSDT"); + + this.mockUSDT = mockUSDT; + this.gateway = gateway; + + this.depositAmount = ethers.utils.parseEther("1000000"); + + await mockMintDeposit(gateway, this.alice, mockUSDT, this.depositAmount); + await mockMintDeposit(gateway, this.Eve, mockUSDT, this.depositAmount); + await mockMintDeposit( + gateway, + this.alice, + this.mockDAI, + this.depositAmount + ); + await mockMintDeposit(gateway, this.Eve, this.mockDAI, this.depositAmount); + + await assertBalance( + this.mockUSDT, + this.mockDAI, + this.alice.address, + this.depositAmount + ); + await assertBalance( + this.mockUSDT, + this.mockDAI, + this.bob.address, + ZERO_AMOUNT + ); + await assertBalance( + this.mockUSDT, + this.mockDAI, + this.Eve.address, + this.depositAmount + ); + + const token = ethers.utils.formatBytes32String("token"); + + await expect( + this.gateway + .connect(this.deployer) + .settingManagerBool(token, this.mockUSDT.address, BigNumber.from(1)) + ) + .to.emit(this.gateway, Events.Gateway.SettingManagerBool) + .withArgs(token, this.mockUSDT.address, BigNumber.from(1)); + + const treasury = ethers.utils.formatBytes32String("treasury"); + + await expect( + gateway + .connect(this.deployer) + .updateProtocolAddress(treasury, this.treasuryAddress.address) + ).to.emit(gateway, Events.Gateway.ProtocolAddressUpdated); + + const aggregator = ethers.utils.formatBytes32String("aggregator"); + + await expect( + gateway + .connect(this.deployer) + .updateProtocolAddress(aggregator, this.aggregator.address) + ).to.emit(gateway, Events.Gateway.ProtocolAddressUpdated); + }); + + it("Should settle onramp assets from provider to sender", async function () { + const orderId = ethers.utils.formatBytes32String("order1"); + const amount = ethers.utils.parseEther("1000"); + + // Create the message hash + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + // Sign the message + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + // Check initial balances + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.alice.address, + ZERO_AMOUNT + ); + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.bob.address, + ZERO_AMOUNT + ); + + await expect( + this.gateway + .connect(this.alice) + .deposit(this.mockUSDT.address, this.depositAmount) + ) + .to.emit(this.gateway, Events.Gateway.Deposit) + .withArgs( + this.alice.address, + this.mockUSDT.address, + BigNumber.from(this.depositAmount) + ); + + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.alice.address, + this.depositAmount + ); + // Perform the settling order + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ) + .to.emit(this.gateway, Events.Gateway.OrderSettledIn) + .withArgs( + this.alice.address, + this.bob.address, + amount, + this.mockUSDT.address, + orderId + ); + + // Check final balances + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.alice.address, + this.depositAmount.sub(amount) + ); + + const getOrderInfoIn = await this.gateway.getOrderInfoIn(orderId); + expect(getOrderInfoIn.amount).to.eq(amount); + expect(getOrderInfoIn.sender).to.eq(this.bob.address); + expect(getOrderInfoIn.provider).to.eq(this.alice.address); + expect(getOrderInfoIn.token).to.eq(this.mockUSDT.address); + expect(getOrderInfoIn.orderId).to.eq(orderId); + + await assertBalance(this.mockUSDT, this.mockUSDT, this.bob.address, amount); + }); + + it("Should fail if the order is already processed", async function () { + const orderId = ethers.utils.formatBytes32String("order1"); + const amount = ethers.utils.parseEther("1000"); + + await expect( + this.gateway + .connect(this.alice) + .deposit(this.mockUSDT.address, this.depositAmount) + ) + .to.emit(this.gateway, Events.Gateway.Deposit) + .withArgs( + this.alice.address, + this.mockUSDT.address, + BigNumber.from(this.depositAmount) + ); + + // Create the message hash + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + // Sign the message + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + // Perform the settling + await this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ); + + // Try to perform the settling again + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("Order already processed"); + }); + + it("Should fail if the signature is invalid", async function () { + const orderId = ethers.utils.formatBytes32String("order1"); + const amount = ethers.utils.parseEther("1000"); + + // Create the message hash + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + // Sign the message with a different account + const signature = await this.hacker.signMessage( + ethers.utils.arrayify(messageHash) + ); + + // Try to perform the settling with the invalid signature + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("Invalid signature"); + }); + + it("Should fail if the provider has insufficient balance", async function () { + const orderId = ethers.utils.formatBytes32String("order1"); + const amount = ethers.utils.parseEther("2000000"); // More than the deposit amount + + // Create the message hash + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + // Sign the message + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + // Try to perform the settling with insufficient balance + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("Insufficient balance"); + }); + + it("Should fail when provider address is zero", async function () { + const orderId = ethers.utils.formatBytes32String("order2"); + const amount = ethers.utils.parseEther("1000"); + const zeroAddress = ethers.constants.AddressZero; + + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [orderId, zeroAddress, this.bob.address, this.mockUSDT.address, amount] + ); + + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + zeroAddress, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("Invalid provider address"); + }); + + it("Should revert when sender address is zero", async function () { + const orderId = ethers.utils.formatBytes32String("zeroSenderOrder"); + const amount = ethers.utils.parseEther("1"); + const zeroAddress = ethers.constants.AddressZero; + + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [orderId, this.alice.address, zeroAddress, this.mockUSDT.address, amount] + ); + + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + zeroAddress, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("Invalid sender address"); + }); + + it("Should correctly calculate and transfer protocol fees", async function () { + const orderId = ethers.utils.formatBytes32String("feeOrder"); + const amount = ethers.utils.parseEther("1000"); + const newProtocolFeePercent = 250; // 2.5% + + // Set a new protocol fee + await this.gateway + .connect(this.deployer) + .updateProtocolFee(newProtocolFeePercent); + + // Create the message hash + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + // Sign the message + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + // Get initial balances + const initialTreasuryBalance = await this.mockUSDT.balanceOf( + this.treasuryAddress.address + ); + const initialBobBalance = await this.mockUSDT.balanceOf(this.bob.address); + + await expect( + this.gateway + .connect(this.alice) + .deposit(this.mockUSDT.address, this.depositAmount) + ) + .to.emit(this.gateway, Events.Gateway.Deposit) + .withArgs( + this.alice.address, + this.mockUSDT.address, + BigNumber.from(this.depositAmount) + ); + + const initialAliceBalance = await this.gateway.getBalance( + this.mockUSDT.address, + this.alice.address + ); + + // Perform the settling order + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.emit(this.gateway, "OrderSettledIn"); + const feeDetails = await this.gateway.getFeeDetails(); + // Calculate expected fee + const expectedFee = amount.mul(feeDetails[0]).div(feeDetails[1]); + + // Check final balances + const finalTreasuryBalance = await this.mockUSDT.balanceOf( + this.treasuryAddress.address + ); + const finalAliceBalance = await this.gateway.getBalance( + this.mockUSDT.address, + this.alice.address + ); + const finalBobBalance = await this.mockUSDT.balanceOf(this.bob.address); + + expect(finalTreasuryBalance).to.equal( + initialTreasuryBalance.add(expectedFee) + ); + expect(finalAliceBalance).to.equal( + initialAliceBalance.sub(amount.add(expectedFee)) + ); + + expect(finalBobBalance).to.equal(initialBobBalance.add(amount)); + + // validate treasury balance + expect(await this.mockUSDT.balanceOf(this.treasuryAddress.address)).to.eq( + initialTreasuryBalance.add(expectedFee) + ); + }); + + it("Should revert when a non-aggregator calls settle onramp order", async function () { + const orderId = ethers.utils.formatBytes32String("nonAggregatorOrder"); + const amount = ethers.utils.parseEther("1"); + + await this.gateway + .connect(this.alice) + .deposit(this.mockUSDT.address, amount); + + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + await expect( + this.gateway.connect(this.alice).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("OnlyAggregator"); + }); + + it("Should revert when amount is zero", async function () { + const orderId = ethers.utils.formatBytes32String("zeroAmountOrder"); + const amount = BigNumber.from(0); + + const messageHash = ethers.utils.solidityKeccak256( + ["bytes32", "address", "address", "address", "uint256"], + [ + orderId, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount, + ] + ); + + const signature = await this.alice.signMessage( + ethers.utils.arrayify(messageHash) + ); + + await expect( + this.gateway.connect(this.aggregator).settleOrderIn( + orderId, + signature, + this.alice.address, + this.bob.address, + this.mockUSDT.address, + amount + ) + ).to.be.revertedWith("AmountIsZero"); + }); + +}); diff --git a/test/gateway/gateway.settleOrder.test.js b/test/gateway/gateway.settleOrderOut.test.js similarity index 93% rename from test/gateway/gateway.settleOrder.test.js rename to test/gateway/gateway.settleOrderOut.test.js index 309e538..baf1163 100644 --- a/test/gateway/gateway.settleOrder.test.js +++ b/test/gateway/gateway.settleOrderOut.test.js @@ -13,7 +13,7 @@ const { } = require("../utils/utils.manager.js"); const { expect } = require("chai"); -describe("Gateway settle order", function () { +describe("Gateway offramp order", function () { beforeEach(async function () { [ this.deployer, @@ -174,7 +174,7 @@ describe("Gateway settle order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await gateway.getOrderInfo(orderId); + ] = await gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(this.sender.address); expect(this.token).to.eq(mockUSDT.address); @@ -196,10 +196,13 @@ describe("Gateway settle order", function () { expect( await gateway - .connect(this.aggregator) - .settle(orderId, orderId, this.liquidityProvider.address, MAX_BPS) + .connect(this.aggregator)["settleOrderOut(bytes32,bytes32,address,uint64)"]( + orderId, + orderId, + this.liquidityProvider.address, + MAX_BPS) ) - .to.emit(gateway, Events.Gateway.OrderSettled) + .to.emit(gateway, Events.Gateway.OrderSettledOut) .withArgs(orderId, orderId, this.liquidityProvider.address, MAX_BPS); expect(await mockUSDT.balanceOf(this.liquidityProvider.address)).to.eq( @@ -281,7 +284,7 @@ describe("Gateway settle order", function () { this.refundAddress, this.currentBPS, this.amount, - ] = await gateway.getOrderInfo(orderId); + ] = await gateway.getOrderInfoOut(orderId); expect(this.seller).to.eq(this.sender.address); expect(this.token).to.eq(mockUSDT.address); @@ -304,10 +307,13 @@ describe("Gateway settle order", function () { // =================== Create Order =================== expect( await gateway - .connect(this.aggregator) - .settle(orderId, orderId, this.liquidityProvider.address, MAX_BPS) + .connect(this.aggregator)["settleOrderOut(bytes32,bytes32,address,uint64)"]( + orderId, + orderId, + this.liquidityProvider.address, + MAX_BPS) ) - .to.emit(gateway, Events.Gateway.OrderSettled) + .to.emit(gateway, Events.Gateway.OrderSettledOut) .withArgs(orderId, orderId, this.liquidityProvider.address, MAX_BPS); expect(await mockUSDT.balanceOf(this.liquidityProvider.address)).to.eq( diff --git a/test/utils/utils.manager.js b/test/utils/utils.manager.js index 4524cf3..9047373 100644 --- a/test/utils/utils.manager.js +++ b/test/utils/utils.manager.js @@ -29,12 +29,13 @@ const Errors = { const Events = { Gateway: { OrderCreated: "OrderCreated", - OrderSettled: "OrderSettled", + OrderSettledOut: "OrderSettledOut", OrderRefunded: "OrderRefunded", SettingManagerBool: "SettingManagerBool", ProtocolFeeUpdated: "ProtocolFeeUpdated", ProtocolAddressUpdated: "ProtocolAddressUpdated", Deposit: "Deposit", + OrderSettledIn: "OrderSettledIn", }, };