From 5cd74c98f69d8cd81dca39397f973d59e9866d44 Mon Sep 17 00:00:00 2001 From: Prosper Date: Sun, 29 Sep 2024 20:56:17 +0100 Subject: [PATCH 1/5] feat: update with withdraw for providers --- contracts/Gateway.sol | 13 ++ contracts/interfaces/IGateway.sol | 21 +++ test/gateway/gateway.withdraw.test.js | 198 ++++++++++++++++++++++++++ test/utils/utils.manager.js | 1 + 4 files changed, 233 insertions(+) create mode 100644 test/gateway/gateway.withdraw.test.js diff --git a/contracts/Gateway.sol b/contracts/Gateway.sol index b09f51e..d80e5b9 100644 --- a/contracts/Gateway.sol +++ b/contracts/Gateway.sol @@ -302,6 +302,19 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { emit OrderSettledIn(_provider, _sender, _amount, _token, _orderId); } + /** @dev See {withdraw-IGateway} */ + function withdraw(bytes memory _signature, address provider, address recipient, address _token, uint256 _amount) external isValidAmount(_amount) onlyAggregator returns (bool) { + bytes32 messageHash = keccak256(abi.encodePacked(provider, recipient, _token, _amount)); + bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); + address signer = ethSignedMessageHash.recover(_signature); + require(signer == provider, 'InvalidSignature'); + + require(balance[_token][provider] >= _amount, 'InsufficientBalance'); + balance[_token][provider] -= _amount; + IERC20(_token).transfer(recipient, _amount); + emit Withdraw(provider, recipient, _token, _amount); + return true; + } /* ################################################################## VIEW CALLS diff --git a/contracts/interfaces/IGateway.sol b/contracts/interfaces/IGateway.sol index d0f4444..aa993a8 100644 --- a/contracts/interfaces/IGateway.sol +++ b/contracts/interfaces/IGateway.sol @@ -75,6 +75,14 @@ interface IGateway { * @param orderId The ID of the order. */ event OrderSettledIn(address indexed provider, address indexed senderAddress, uint256 indexed amount, address token, bytes32 orderId); + * @notice Emitted when a withdrawal is made by a provider. + * @param provider The address of the provider. + * @param sender The address of the sender. + * @param token The address of the withdrawn token. + * @param amount The amount of the withdrawal. + */ + event Withdraw(address indexed provider, address indexed sender, address indexed token, uint256 amount); + /* ################################################################## STRUCTS ################################################################## */ @@ -205,6 +213,19 @@ interface IGateway { uint256 _amount ) external; + /** + * @notice Withdraws an asset from Gateway. + * @dev Requirements: + * - The provider must have enough balance. + * @param _signature The signature of the provider. + * @param _provider The address of the provider. + * @param _recipient The address of the recipient. + * @param _token The address of the asset. + * @param _amount The amount to be withdrawn. + * @return bool The withdrawal is successful. + */ + function withdraw(bytes memory _signature, address _provider, address _recipient, address _token, uint256 _amount) external returns (bool); + /** * @notice Checks if a token is supported by Gateway. * @param _token The address of the token to check. diff --git a/test/gateway/gateway.withdraw.test.js b/test/gateway/gateway.withdraw.test.js new file mode 100644 index 0000000..c5602ea --- /dev/null +++ b/test/gateway/gateway.withdraw.test.js @@ -0,0 +1,198 @@ +const { ethers } = require("hardhat"); +const { BigNumber } = require("@ethersproject/bignumber"); + +const { gatewayFixture } = require("../fixtures/gateway.js"); + +const { + deployContract, + ZERO_AMOUNT, + Errors, + Events, + mockMintDeposit, + assertBalance, + assertDepositBalance, +} = require("../utils/utils.manager.js"); +const { expect } = require("chai"); + +describe("Gateway Provider withdraw", 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.bob, mockUSDT, this.depositAmount); + await mockMintDeposit(gateway, this.Eve, mockUSDT, this.depositAmount); + await mockMintDeposit( + gateway, + this.alice, + this.mockDAI, + this.depositAmount + ); + await mockMintDeposit(gateway, this.bob, 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, + this.depositAmount + ); + 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 aggregator = ethers.utils.formatBytes32String("aggregator"); + + await expect( + gateway + .connect(this.deployer) + .updateProtocolAddress(aggregator, this.aggregator.address) + ).to.emit(gateway, Events.Gateway.ProtocolAddressUpdated); + }); + + it("Should withdraw successfully with valid signature", async function () { + const amount = ethers.utils.parseEther("1000"); + const messageHash = ethers.utils.solidityKeccak256( + ["address", "address", "address", "uint256"], + [this.alice.address, this.bob.address, this.mockUSDT.address, amount] + ); + const signature = await this.alice.signMessage(ethers.utils.arrayify(messageHash)); + + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.alice.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 + ); + + await expect( + this.gateway + .connect(this.aggregator) + .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + ) + .to.emit(this.gateway, Events.Gateway.Withdraw) + .withArgs(this.alice.address, this.bob.address, this.mockUSDT.address, amount); + + await assertDepositBalance( + this.gateway, + this.mockUSDT.address, + this.alice.address, + this.depositAmount.sub(amount) + ); + + }); + + it("Should fail with invalid signature", async function () { + const amount = ethers.utils.parseEther("1000"); + const messageHash = ethers.utils.solidityKeccak256( + ["address", "address", "address", "uint256"], + [this.alice.address, this.bob.address, this.mockUSDT.address, amount] + ); + const ethSignedMessageHash = ethers.utils.hashMessage(messageHash); + const invalidSignature = await this.bob.signMessage(ethers.utils.arrayify(ethSignedMessageHash)); + + 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 expect( + this.gateway + .connect(this.aggregator) + .withdraw(invalidSignature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + ).to.be.revertedWith("InvalidSignature"); + }); + + it("Should fail with insufficient balance", async function () { + const amount = ethers.utils.parseEther("1000"); + const messageHash = ethers.utils.solidityKeccak256( + ["address", "address", "address", "uint256"], + [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) + .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + ).to.be.revertedWith("InsufficientBalance"); + }); + + it("Should fail when called by non-aggregator", async function () { + const amount = ethers.utils.parseEther("1000"); + const messageHash = ethers.utils.solidityKeccak256( + ["address", "address", "address", "uint256"], + [this.alice.address, this.bob.address, this.mockUSDT.address, amount] + ); + const ethSignedMessageHash = ethers.utils.hashMessage(messageHash); + const signature = await this.alice.signMessage(ethers.utils.arrayify(ethSignedMessageHash)); + + await expect( + this.gateway + .connect(this.alice) + .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + ).to.be.revertedWith("OnlyAggregator"); + }); +}); \ No newline at end of file diff --git a/test/utils/utils.manager.js b/test/utils/utils.manager.js index 9047373..84f5aaa 100644 --- a/test/utils/utils.manager.js +++ b/test/utils/utils.manager.js @@ -36,6 +36,7 @@ const Events = { ProtocolAddressUpdated: "ProtocolAddressUpdated", Deposit: "Deposit", OrderSettledIn: "OrderSettledIn", + Withdraw: "Withdraw", }, }; From c7a54e6570f91a7c564ecf3d47a6d7f9dc516ba1 Mon Sep 17 00:00:00 2001 From: Prosper Date: Tue, 1 Oct 2024 12:22:14 +0100 Subject: [PATCH 2/5] refactor: rename function withdraw to withdrawFrom in Gateway contract and IGateway interface --- contracts/Gateway.sol | 6 +++--- contracts/interfaces/IGateway.sol | 10 +++++----- test/gateway/gateway.withdraw.test.js | 22 +++++++++++----------- test/utils/utils.manager.js | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/Gateway.sol b/contracts/Gateway.sol index d80e5b9..99a8a1c 100644 --- a/contracts/Gateway.sol +++ b/contracts/Gateway.sol @@ -303,8 +303,8 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { emit OrderSettledIn(_provider, _sender, _amount, _token, _orderId); } /** @dev See {withdraw-IGateway} */ - function withdraw(bytes memory _signature, address provider, address recipient, address _token, uint256 _amount) external isValidAmount(_amount) onlyAggregator returns (bool) { - bytes32 messageHash = keccak256(abi.encodePacked(provider, recipient, _token, _amount)); + function withdrawFrom(address provider, address recipient, uint256 _amount, address _token, bytes memory _signature) external isValidAmount(_amount) onlyAggregator returns (bool) { + bytes32 messageHash = keccak256(abi.encodePacked(provider, recipient, _amount, _token)); bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); address signer = ethSignedMessageHash.recover(_signature); require(signer == provider, 'InvalidSignature'); @@ -312,7 +312,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { require(balance[_token][provider] >= _amount, 'InsufficientBalance'); balance[_token][provider] -= _amount; IERC20(_token).transfer(recipient, _amount); - emit Withdraw(provider, recipient, _token, _amount); + emit Withdrawn(provider, recipient, _amount, _token); return true; } diff --git a/contracts/interfaces/IGateway.sol b/contracts/interfaces/IGateway.sol index aa993a8..5c2af6b 100644 --- a/contracts/interfaces/IGateway.sol +++ b/contracts/interfaces/IGateway.sol @@ -78,10 +78,10 @@ interface IGateway { * @notice Emitted when a withdrawal is made by a provider. * @param provider The address of the provider. * @param sender The address of the sender. - * @param token The address of the withdrawn token. * @param amount The amount of the withdrawal. + * @param token The address of the withdrawn token. */ - event Withdraw(address indexed provider, address indexed sender, address indexed token, uint256 amount); + event Withdrawn(address indexed provider, address indexed sender, uint256 amount, address indexed token); /* ################################################################## STRUCTS @@ -217,14 +217,14 @@ interface IGateway { * @notice Withdraws an asset from Gateway. * @dev Requirements: * - The provider must have enough balance. - * @param _signature The signature of the provider. * @param _provider The address of the provider. * @param _recipient The address of the recipient. - * @param _token The address of the asset. * @param _amount The amount to be withdrawn. + * @param _token The address of the asset. + * @param _signature The signature of the provider. * @return bool The withdrawal is successful. */ - function withdraw(bytes memory _signature, address _provider, address _recipient, address _token, uint256 _amount) external returns (bool); + function withdrawFrom(address _provider, address _recipient, uint256 _amount, address _token, bytes memory _signature) external returns (bool); /** * @notice Checks if a token is supported by Gateway. diff --git a/test/gateway/gateway.withdraw.test.js b/test/gateway/gateway.withdraw.test.js index c5602ea..5810e52 100644 --- a/test/gateway/gateway.withdraw.test.js +++ b/test/gateway/gateway.withdraw.test.js @@ -89,8 +89,8 @@ describe("Gateway Provider withdraw", function () { it("Should withdraw successfully with valid signature", async function () { const amount = ethers.utils.parseEther("1000"); const messageHash = ethers.utils.solidityKeccak256( - ["address", "address", "address", "uint256"], - [this.alice.address, this.bob.address, this.mockUSDT.address, amount] + ["address", "address", "uint256", "address",], + [this.alice.address, this.bob.address, amount, this.mockUSDT.address] ); const signature = await this.alice.signMessage(ethers.utils.arrayify(messageHash)); @@ -123,10 +123,10 @@ describe("Gateway Provider withdraw", function () { await expect( this.gateway .connect(this.aggregator) - .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + .withdrawFrom(this.alice.address, this.bob.address, amount, this.mockUSDT.address, signature) ) - .to.emit(this.gateway, Events.Gateway.Withdraw) - .withArgs(this.alice.address, this.bob.address, this.mockUSDT.address, amount); + .to.emit(this.gateway, Events.Gateway.Withdrawn) + .withArgs(this.alice.address, this.bob.address, amount, this.mockUSDT.address); await assertDepositBalance( this.gateway, @@ -140,7 +140,7 @@ describe("Gateway Provider withdraw", function () { it("Should fail with invalid signature", async function () { const amount = ethers.utils.parseEther("1000"); const messageHash = ethers.utils.solidityKeccak256( - ["address", "address", "address", "uint256"], + ["address", "address", "uint256", "address"], [this.alice.address, this.bob.address, this.mockUSDT.address, amount] ); const ethSignedMessageHash = ethers.utils.hashMessage(messageHash); @@ -161,22 +161,22 @@ describe("Gateway Provider withdraw", function () { await expect( this.gateway .connect(this.aggregator) - .withdraw(invalidSignature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + .withdrawFrom( this.alice.address, this.bob.address, amount, this.mockUSDT.address, invalidSignature) ).to.be.revertedWith("InvalidSignature"); }); it("Should fail with insufficient balance", async function () { const amount = ethers.utils.parseEther("1000"); const messageHash = ethers.utils.solidityKeccak256( - ["address", "address", "address", "uint256"], - [this.alice.address, this.bob.address, this.mockUSDT.address, amount] + ["address", "address", "uint256", "address"], + [this.alice.address, this.bob.address, amount, this.mockUSDT.address] ); const signature = await this.alice.signMessage(ethers.utils.arrayify(messageHash)); await expect( this.gateway .connect(this.aggregator) - .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + .withdrawFrom(this.alice.address, this.bob.address, amount, this.mockUSDT.address, signature) ).to.be.revertedWith("InsufficientBalance"); }); @@ -192,7 +192,7 @@ describe("Gateway Provider withdraw", function () { await expect( this.gateway .connect(this.alice) - .withdraw(signature, this.alice.address, this.bob.address, this.mockUSDT.address, amount) + .withdrawFrom(this.alice.address, this.bob.address, amount, this.mockUSDT.address, signature) ).to.be.revertedWith("OnlyAggregator"); }); }); \ No newline at end of file diff --git a/test/utils/utils.manager.js b/test/utils/utils.manager.js index 84f5aaa..cafd7da 100644 --- a/test/utils/utils.manager.js +++ b/test/utils/utils.manager.js @@ -36,7 +36,7 @@ const Events = { ProtocolAddressUpdated: "ProtocolAddressUpdated", Deposit: "Deposit", OrderSettledIn: "OrderSettledIn", - Withdraw: "Withdraw", + Withdrawn: "Withdrawn", }, }; From d369397ac1c2da41347aa29772fd5aec8e894669 Mon Sep 17 00:00:00 2001 From: Prosper Date: Tue, 1 Oct 2024 19:22:11 +0100 Subject: [PATCH 3/5] refactor: rename function withdraw to withdrawFrom in Gateway contract and IGateway interface --- contracts/Gateway.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Gateway.sol b/contracts/Gateway.sol index 99a8a1c..056eb18 100644 --- a/contracts/Gateway.sol +++ b/contracts/Gateway.sol @@ -302,7 +302,8 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable { emit OrderSettledIn(_provider, _sender, _amount, _token, _orderId); } - /** @dev See {withdraw-IGateway} */ + + /** @dev See {withdrawFrom-IGateway} */ function withdrawFrom(address provider, address recipient, uint256 _amount, address _token, bytes memory _signature) external isValidAmount(_amount) onlyAggregator returns (bool) { bytes32 messageHash = keccak256(abi.encodePacked(provider, recipient, _amount, _token)); bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); From 113bf44ffd0c67c2819159ba07c7cd70394bd530 Mon Sep 17 00:00:00 2001 From: Chibuotu Amadi Date: Mon, 14 Oct 2024 15:56:05 +0100 Subject: [PATCH 4/5] fix(interfaces): IGateway rebase glitch --- contracts/interfaces/IGateway.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/interfaces/IGateway.sol b/contracts/interfaces/IGateway.sol index 5c2af6b..82caabe 100644 --- a/contracts/interfaces/IGateway.sol +++ b/contracts/interfaces/IGateway.sol @@ -75,6 +75,8 @@ interface IGateway { * @param orderId The ID of the order. */ event OrderSettledIn(address indexed provider, address indexed senderAddress, uint256 indexed amount, address token, bytes32 orderId); + + /** * @notice Emitted when a withdrawal is made by a provider. * @param provider The address of the provider. * @param sender The address of the sender. From 4f16c4d6aa9e4abc168de3a3bae2e22cbdd2d039 Mon Sep 17 00:00:00 2001 From: Prosper Date: Mon, 14 Oct 2024 16:02:52 +0100 Subject: [PATCH 5/5] refactor: update address in settleOrderOut test to emit refund address --- test/gateway/gateway.settleOrderOut.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/gateway/gateway.settleOrderOut.test.js b/test/gateway/gateway.settleOrderOut.test.js index baf1163..ff9b5b7 100644 --- a/test/gateway/gateway.settleOrderOut.test.js +++ b/test/gateway/gateway.settleOrderOut.test.js @@ -154,7 +154,7 @@ describe("Gateway offramp order", function () { ) .to.emit(gateway, Events.Gateway.OrderCreated) .withArgs( - this.sender.address, + this.alice.address, mockUSDT.address, this.orderAmount, this.protocolFee, @@ -264,7 +264,7 @@ describe("Gateway offramp order", function () { ) .to.emit(gateway, Events.Gateway.OrderCreated) .withArgs( - this.sender.address, + this.alice.address, mockUSDT.address, this.orderAmount, this.protocolFee,