From e2567f4157f5683955aec614e6f0e219aa855b55 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 12 Sep 2024 14:00:16 +0300 Subject: [PATCH 01/37] feat: develop for native currencies --- .../contracts/EthereumSingleRequestProxy.sol | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol new file mode 100644 index 0000000000..83bdbda4bf --- /dev/null +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import './interfaces/EthereumFeeProxy.sol'; + +/** + * @title EthereumSingleRequestProxy + * @notice This contract is used to send a single request to a payee with a fee sent to a third address + */ +contract EthereumSingleRequestProxy { + address public payee; + bytes public paymentReference; + address public feeAddress; + uint256 public feeAmount; + IEthereumFeeProxy public ethereumFeeProxy; + + constructor( + address _payee, + bytes memory _paymentReference, + address _ethereumFeeProxy, + address _feeAddress, + uint256 _feeAmount + ) { + payee = _payee; + paymentReference = _paymentReference; + feeAddress = _feeAddress; + feeAmount = _feeAmount; + ethereumFeeProxy = IEthereumFeeProxy(_ethereumFeeProxy); + } + + receive() external payable { + ethereumFeeProxy.transferWithReferenceAndFee{value: msg.value}( + payable(payee), + paymentReference, + feeAmount, + payable(feeAddress) + ); + } +} From c62447402563cc0f8c4b47acbfe6a1ac29bebc46 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 12 Sep 2024 14:00:36 +0300 Subject: [PATCH 02/37] feat: develop for ERC20 currencies --- .../src/contracts/ERC20SingleRequestProxy.sol | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol new file mode 100644 index 0000000000..b320190ab6 --- /dev/null +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './interfaces/ERC20FeeProxy.sol'; + +/** + * @title ERC20SingleRequestProxy + * @notice This contract is used to send a single request to a payee with a fee sent to a third address for ERC20 + */ + +contract ERC20SingleRequestProxy { + address public payee; + address public tokenAddress; + address public feeAddress; + uint256 public feeAmount; + bytes public paymentReference; + IERC20FeeProxy public erc20FeeProxy; + + constructor( + address _payee, + address _tokenAddress, + address _feeAddress, + uint256 _feeAmount, + bytes memory _paymentReference, + address _erc20FeeProxy + ) { + payee = _payee; + tokenAddress = _tokenAddress; + feeAddress = _feeAddress; + feeAmount = _feeAmount; + paymentReference = _paymentReference; + erc20FeeProxy = IERC20FeeProxy(_erc20FeeProxy); + } + + receive() external payable { + require(msg.value == 0, 'This function is only for triggering the transfer'); + IERC20 token = IERC20(tokenAddress); + uint256 balance = token.balanceOf(address(this)); + + token.approve(address(erc20FeeProxy), balance); + + erc20FeeProxy.transferFromWithReferenceAndFee( + tokenAddress, + payee, + transferAmount, + paymentReference, + feeAmount, + feeAddress + ); + } +} From 917d472544274babc1185c263fbbc3821309eb6b Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 12 Sep 2024 15:24:52 +0300 Subject: [PATCH 03/37] feat: create `EthereumSingleRequestProxy` factory contract --- .../EthereumSingleRequestProxyFactory.sol | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol new file mode 100644 index 0000000000..dad47ee3dc --- /dev/null +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import '@openzeppelin/contracts/access/Ownable.sol'; +import './EthereumSingleRequestProxy.sol'; + +/** + * @title EthereumSingleRequestProxyFactory + * @notice This contract is used to create EthereumSingleRequestProxy instances + */ +contract EthereumSingleRequestProxyFactory is Ownable { + address public ethereumFeeProxy; + + event EtheruemSingleRequestProxyCreated( + address indexed proxyAddress, + address indexed payee, + bytes indexed paymentReference + ); + + constructor(address _ethereumFeeProxy) { + ethereumFeeProxy = _ethereumFeeProxy; + } + + /** + * @notice Creates a new EthereumSingleRequestProxy instance + * @param _payee The address of the payee + * @param _paymentReference The payment reference + * @param _feeAddress The address of the fee recipient + * @param _feeAmount The fee amount + * @return The address of the newly created proxy + */ + function createEthereumSingleRequestProxy( + address _payee, + bytes memory _paymentReference, + address _feeAddress, + uint256 _feeAmount + ) external returns (address) { + EthereumSingleRequestProxy proxy = new EthereumSingleRequestProxy( + _payee, + _paymentReference, + ethereumFeeProxy, + _feeAddress, + _feeAmount + ); + emit EtheruemSingleRequestProxyCreated(address(proxy), _payee, _paymentReference); + return address(proxy); + } + + /** + * @notice Updates the EthereumFeeProxy address + * @param _newEthereumFeeProxy The new EthereumFeeProxy address + */ + function updateEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { + ethereumFeeProxy = _newEthereumFeeProxy; + } +} From d5e47284768b41fe39ea2850f4a3d2e370bc54f8 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 12 Sep 2024 15:32:59 +0300 Subject: [PATCH 04/37] feat: create `ERC20SingleRequestProxy` factory contract --- .../ERC20SingleRequestProxyFactory.sol | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol new file mode 100644 index 0000000000..fb3e2170f7 --- /dev/null +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import '@openzeppelin/contracts/access/Ownable.sol'; +import './ERC20SingleRequestProxy.sol'; + +/** + * @title ERC20SingleRequestProxyFactory + * @notice This contract is used to create ERC20SingleRequestProxy instances + */ +contract ERC20SingleRequestProxyFactory is Ownable { + address public erc20FeeProxy; + + event ERC20SingleRequestProxyCreated( + address indexed proxyAddress, + address indexed payee, + bytes indexed paymentReference + ); + + constructor(address _erc20FeeProxy) { + erc20FeeProxy = _erc20FeeProxy; + } + + /** + * @notice Creates a new ERC20SingleRequestProxy instance + * @param _payee The address of the payee + * @param _paymentReference The payment reference + * @param _feeAddress The address of the fee recipient + * @param _feeAmount The fee amount + * @return The address of the newly created proxy + */ + function createERC20SingleRequestProxy( + address _payee, + address _tokenAddress, + bytes memory _paymentReference, + address _feeAddress, + uint256 _feeAmount + ) external returns (address) { + ERC20SingleRequestProxy proxy = new ERC20SingleRequestProxy( + _payee, + _tokenAddress, + _feeAddress, + _feeAmount, + _paymentReference, + erc20FeeProxy + ); + + emit ERC20SingleRequestProxyCreated(address(proxy), _payee, _paymentReference); + return address(proxy); + } + + /** + * @notice Updates the ERC20FeeProxy address + * @param _newERC20FeeProxy The new ERC20FeeProxy address + */ + function setERC20FeeProxy(address _newERC20FeeProxy) external onlyOwner { + erc20FeeProxy = _newERC20FeeProxy; + } +} From 05430928a52840d99d4fc6ec5ffc756fea4e5a11 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Fri, 13 Sep 2024 14:44:44 +0300 Subject: [PATCH 05/37] refactor: merge the single request proxy factories into one factory --- .../ERC20SingleRequestProxyFactory.sol | 59 ---------- .../EthereumSingleRequestProxyFactory.sol | 56 ---------- .../contracts/SingleRequestProxyFactory.sol | 103 ++++++++++++++++++ 3 files changed, 103 insertions(+), 115 deletions(-) delete mode 100644 packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol delete mode 100644 packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol create mode 100644 packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol deleted file mode 100644 index fb3e2170f7..0000000000 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxyFactory.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import '@openzeppelin/contracts/access/Ownable.sol'; -import './ERC20SingleRequestProxy.sol'; - -/** - * @title ERC20SingleRequestProxyFactory - * @notice This contract is used to create ERC20SingleRequestProxy instances - */ -contract ERC20SingleRequestProxyFactory is Ownable { - address public erc20FeeProxy; - - event ERC20SingleRequestProxyCreated( - address indexed proxyAddress, - address indexed payee, - bytes indexed paymentReference - ); - - constructor(address _erc20FeeProxy) { - erc20FeeProxy = _erc20FeeProxy; - } - - /** - * @notice Creates a new ERC20SingleRequestProxy instance - * @param _payee The address of the payee - * @param _paymentReference The payment reference - * @param _feeAddress The address of the fee recipient - * @param _feeAmount The fee amount - * @return The address of the newly created proxy - */ - function createERC20SingleRequestProxy( - address _payee, - address _tokenAddress, - bytes memory _paymentReference, - address _feeAddress, - uint256 _feeAmount - ) external returns (address) { - ERC20SingleRequestProxy proxy = new ERC20SingleRequestProxy( - _payee, - _tokenAddress, - _feeAddress, - _feeAmount, - _paymentReference, - erc20FeeProxy - ); - - emit ERC20SingleRequestProxyCreated(address(proxy), _payee, _paymentReference); - return address(proxy); - } - - /** - * @notice Updates the ERC20FeeProxy address - * @param _newERC20FeeProxy The new ERC20FeeProxy address - */ - function setERC20FeeProxy(address _newERC20FeeProxy) external onlyOwner { - erc20FeeProxy = _newERC20FeeProxy; - } -} diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol deleted file mode 100644 index dad47ee3dc..0000000000 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxyFactory.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import '@openzeppelin/contracts/access/Ownable.sol'; -import './EthereumSingleRequestProxy.sol'; - -/** - * @title EthereumSingleRequestProxyFactory - * @notice This contract is used to create EthereumSingleRequestProxy instances - */ -contract EthereumSingleRequestProxyFactory is Ownable { - address public ethereumFeeProxy; - - event EtheruemSingleRequestProxyCreated( - address indexed proxyAddress, - address indexed payee, - bytes indexed paymentReference - ); - - constructor(address _ethereumFeeProxy) { - ethereumFeeProxy = _ethereumFeeProxy; - } - - /** - * @notice Creates a new EthereumSingleRequestProxy instance - * @param _payee The address of the payee - * @param _paymentReference The payment reference - * @param _feeAddress The address of the fee recipient - * @param _feeAmount The fee amount - * @return The address of the newly created proxy - */ - function createEthereumSingleRequestProxy( - address _payee, - bytes memory _paymentReference, - address _feeAddress, - uint256 _feeAmount - ) external returns (address) { - EthereumSingleRequestProxy proxy = new EthereumSingleRequestProxy( - _payee, - _paymentReference, - ethereumFeeProxy, - _feeAddress, - _feeAmount - ); - emit EtheruemSingleRequestProxyCreated(address(proxy), _payee, _paymentReference); - return address(proxy); - } - - /** - * @notice Updates the EthereumFeeProxy address - * @param _newEthereumFeeProxy The new EthereumFeeProxy address - */ - function updateEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { - ethereumFeeProxy = _newEthereumFeeProxy; - } -} diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol new file mode 100644 index 0000000000..e59f2ba741 --- /dev/null +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import '@openzeppelin/contracts/access/Ownable.sol'; +import './ERC20SingleRequestProxy.sol'; +import './EthereumSingleRequestProxy.sol'; + +/** + * @title SingleRequestProxyFactory + * @notice This contract is used to create SingleRequestProxy instances + */ +contract SingleRequestProxyFactory is Ownable { + address public ethereumFeeProxy; + address public erc20FeeProxy; + + event EtheruemSingleRequestProxyCreated( + address indexed proxyAddress, + address indexed payee, + bytes indexed paymentReference + ); + + event ERC20SingleRequestProxyCreated( + address indexed proxyAddress, + address indexed payee, + address tokenAddress, + bytes indexed paymentReference + ); + + constructor(address _ethereumFeeProxy, address _erc20FeeProxy) { + ethereumFeeProxy = _ethereumFeeProxy; + erc20FeeProxy = _erc20FeeProxy; + } + + /** + * @notice Creates a new EthereumSingleRequestProxy instance + * @param _payee The address of the payee + * @param _paymentReference The payment reference + * @param _feeAddress The address of the fee recipient + * @param _feeAmount The fee amount + * @return The address of the newly created proxy + */ + function createEthereumSingleRequestProxy( + address _payee, + bytes memory _paymentReference, + address _feeAddress, + uint256 _feeAmount + ) external returns (address) { + EthereumSingleRequestProxy proxy = new EthereumSingleRequestProxy( + _payee, + _paymentReference, + ethereumFeeProxy, + _feeAddress, + _feeAmount + ); + emit EtheruemSingleRequestProxyCreated(address(proxy), _payee, _paymentReference); + return address(proxy); + } + + /** + * @notice Creates a new ERC20SingleRequestProxy instance + * @param _payee The address of the payee + * @param _tokenAddress The address of the token + * @param _paymentReference The payment reference + * @param _feeAddress The address of the fee recipient + * @param _feeAmount The fee amount + * @return The address of the newly created proxy + */ + function createERC20SingleRequestProxy( + address _payee, + address _tokenAddress, + bytes memory _paymentReference, + address _feeAddress, + uint256 _feeAmount + ) external returns (address) { + ERC20SingleRequestProxy proxy = new ERC20SingleRequestProxy( + _payee, + _tokenAddress, + _feeAddress, + _feeAmount, + _paymentReference, + erc20FeeProxy + ); + + emit ERC20SingleRequestProxyCreated(address(proxy), _payee, _tokenAddress, _paymentReference); + return address(proxy); + } + + /** + * @notice Updates the ERC20FeeProxy address + * @param _newERC20FeeProxy The new ERC20FeeProxy address + */ + function setERC20FeeProxy(address _newERC20FeeProxy) external onlyOwner { + erc20FeeProxy = _newERC20FeeProxy; + } + + /** + * @notice Updates the EthereumFeeProxy address + * @param _newEthereumFeeProxy The new EthereumFeeProxy address + */ + function updateEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { + ethereumFeeProxy = _newEthereumFeeProxy; + } +} From 2bd3495cb0c73c3e27ef6ae06e15e8908a942387 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Fri, 13 Sep 2024 16:56:15 +0300 Subject: [PATCH 06/37] fix: infinite loop between `EthSingleRequestProxy` and `EthFeeProxy` --- .../contracts/EthereumSingleRequestProxy.sol | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index 83bdbda4bf..ba72dfda06 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -14,6 +14,9 @@ contract EthereumSingleRequestProxy { uint256 public feeAmount; IEthereumFeeProxy public ethereumFeeProxy; + address private originalSender; + bool private locked; + constructor( address _payee, bytes memory _paymentReference, @@ -28,12 +31,44 @@ contract EthereumSingleRequestProxy { ethereumFeeProxy = IEthereumFeeProxy(_ethereumFeeProxy); } - receive() external payable { - ethereumFeeProxy.transferWithReferenceAndFee{value: msg.value}( - payable(payee), - paymentReference, - feeAmount, - payable(feeAddress) - ); + modifier noReentrant() { + if (msg.sender != address(ethereumFeeProxy)) { + require(!locked, 'Reentrant call detected'); + locked = true; + _; + locked = false; + } else { + // Allow the call if it's from Contract B + _; + } + } + + receive() external payable noReentrant { + if (msg.sender == address(ethereumFeeProxy)) { + // Funds are being sent back from EthereumFeeProxy + require(originalSender != address(0), 'No original sender stored'); + + // Forward the funds to the original sender + (bool forwardSuccess, ) = payable(originalSender).call{value: msg.value}(''); + require(forwardSuccess, 'Forwarding to original sender failed'); + + // Clear the stored original sender + originalSender = address(0); + } else { + require(originalSender == address(0), 'Another request is in progress'); + + originalSender = msg.sender; + + bytes memory data = abi.encodeWithSignature( + 'transferWithReferenceAndFee(address,bytes,uint256,address)', + payable(payee), + paymentReference, + feeAmount, + payable(feeAddress) + ); + + (bool callSuccess, ) = address(ethereumFeeProxy).call{value: msg.value}(data); + require(callSuccess, 'Call to EthereumFeeProxy failed'); + } } } From 19b5e4047088856dfac56c5eef520965aca7b7c3 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 16 Sep 2024 14:38:38 +0300 Subject: [PATCH 07/37] refactor: refactor `nonReentrant` to match openzeppelin implementation --- .../contracts/EthereumSingleRequestProxy.sol | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index ba72dfda06..aa3d1c0b9a 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -15,7 +15,11 @@ contract EthereumSingleRequestProxy { IEthereumFeeProxy public ethereumFeeProxy; address private originalSender; - bool private locked; + + // Reentrancy guard + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + uint256 private _status; constructor( address _payee, @@ -29,21 +33,24 @@ contract EthereumSingleRequestProxy { feeAddress = _feeAddress; feeAmount = _feeAmount; ethereumFeeProxy = IEthereumFeeProxy(_ethereumFeeProxy); + _status = _NOT_ENTERED; } - modifier noReentrant() { + modifier nonReentrant() { if (msg.sender != address(ethereumFeeProxy)) { - require(!locked, 'Reentrant call detected'); - locked = true; - _; - locked = false; - } else { - // Allow the call if it's from Contract B - _; + // On the first call to nonReentrant, _status will be _NOT_ENTERED + require(_status != _ENTERED, 'ReentrancyGuard: reentrant call'); + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + } + _; + if (msg.sender != address(ethereumFeeProxy)) { + // By storing the original value once again, a refund is triggered + _status = _NOT_ENTERED; } } - receive() external payable noReentrant { + receive() external payable nonReentrant { if (msg.sender == address(ethereumFeeProxy)) { // Funds are being sent back from EthereumFeeProxy require(originalSender != address(0), 'No original sender stored'); From c1d3a77bd34ad580a61e81355d1ce4ed52cc4758 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 16 Sep 2024 14:39:16 +0300 Subject: [PATCH 08/37] fix: update `ERC20SingleRequestProxy` to use `balance` --- .../smart-contracts/src/contracts/ERC20SingleRequestProxy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index b320190ab6..d641984cc4 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -43,7 +43,7 @@ contract ERC20SingleRequestProxy { erc20FeeProxy.transferFromWithReferenceAndFee( tokenAddress, payee, - transferAmount, + balance, paymentReference, feeAmount, feeAddress From 1376582f99b4a8a2c96596396c00d02f9f897ec8 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 16 Sep 2024 14:39:48 +0300 Subject: [PATCH 09/37] fix: update `SingleRequestProxyFactory` to call Ownable constructor --- .../smart-contracts/src/contracts/SingleRequestProxyFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index e59f2ba741..1ce1e93881 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -26,7 +26,7 @@ contract SingleRequestProxyFactory is Ownable { bytes indexed paymentReference ); - constructor(address _ethereumFeeProxy, address _erc20FeeProxy) { + constructor(address _ethereumFeeProxy, address _erc20FeeProxy) Ownable(msg.sender) { ethereumFeeProxy = _ethereumFeeProxy; erc20FeeProxy = _erc20FeeProxy; } From c2f55ee6bad72f930ca86e7f0fbe08f19425d8df Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Tue, 17 Sep 2024 22:00:05 +0300 Subject: [PATCH 10/37] chore: update contracts to use `0.8.9` solidity version --- .../smart-contracts/src/contracts/ERC20SingleRequestProxy.sol | 2 +- .../src/contracts/EthereumSingleRequestProxy.sol | 2 +- .../src/contracts/SingleRequestProxyFactory.sol | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index d641984cc4..f2043985b3 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity 0.8.9; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import './interfaces/ERC20FeeProxy.sol'; diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index aa3d1c0b9a..bcc6555d62 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity 0.8.9; import './interfaces/EthereumFeeProxy.sol'; diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index 1ce1e93881..f79dfc918e 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity 0.8.9; import '@openzeppelin/contracts/access/Ownable.sol'; import './ERC20SingleRequestProxy.sol'; @@ -26,7 +26,7 @@ contract SingleRequestProxyFactory is Ownable { bytes indexed paymentReference ); - constructor(address _ethereumFeeProxy, address _erc20FeeProxy) Ownable(msg.sender) { + constructor(address _ethereumFeeProxy, address _erc20FeeProxy) Ownable() { ethereumFeeProxy = _ethereumFeeProxy; erc20FeeProxy = _erc20FeeProxy; } From bc922e8430726a8105b6f69396d682702a6cac6e Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 18 Sep 2024 14:51:24 +0300 Subject: [PATCH 11/37] test: add test for `EthereumSingleRequestProxy` --- .../contracts/test/EthereumFeeProxyMock.sol | 20 ++++ .../EthereumSingleRequestProxy.test.ts | 111 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/test/EthereumFeeProxyMock.sol create mode 100644 packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts diff --git a/packages/smart-contracts/src/contracts/test/EthereumFeeProxyMock.sol b/packages/smart-contracts/src/contracts/test/EthereumFeeProxyMock.sol new file mode 100644 index 0000000000..46d8064fc0 --- /dev/null +++ b/packages/smart-contracts/src/contracts/test/EthereumFeeProxyMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MockEthereumFeeProxy { + function transferWithReferenceAndFee( + address payable _to, + bytes calldata _paymentReference, + uint256 _feeAmount, + address payable _feeAddress + ) external payable { + // Do nothing, just accept the funds + } + + function sendFundsBack(address payable _to, uint256 _amount) external { + (bool success, ) = _to.call{value: _amount}(''); + require(success, 'Failed to send funds back'); + } + + receive() external payable {} +} diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts new file mode 100644 index 0000000000..3e777e9bec --- /dev/null +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -0,0 +1,111 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { BigNumber, Signer } from 'ethers'; +import { EthereumSingleRequestProxy, EthereumFeeProxy } from '../../src/types'; + +describe('contract : EthereumSingleRequestProxy', () => { + let ethereumSingleRequestProxy: EthereumSingleRequestProxy; + let ethereumFeeProxy: EthereumFeeProxy; + let owner: Signer; + let payee: Signer; + let feeRecipient: Signer; + let payeeAddress: string; + let feeRecipientAddress: string; + + const paymentReference: string = ethers.utils.formatBytes32String('payment_reference'); + const feeAmount: string = ethers.utils.parseEther('0.1').toString(); + + beforeEach(async () => { + [owner, payee, feeRecipient] = await ethers.getSigners(); + payeeAddress = await payee.getAddress(); + feeRecipientAddress = await feeRecipient.getAddress(); + + const ethereumFeeProxyFactory = await ethers.getContractFactory('EthereumFeeProxy'); + ethereumFeeProxy = await ethereumFeeProxyFactory.deploy(); + await ethereumFeeProxy.deployed(); + + const ethereumSingleRequestProxyFactory = await ethers.getContractFactory( + 'EthereumSingleRequestProxy', + ); + ethereumSingleRequestProxy = await ethereumSingleRequestProxyFactory.deploy( + payeeAddress, + paymentReference, + ethereumFeeProxy.address, + feeRecipientAddress, + feeAmount, + ); + await ethereumSingleRequestProxy.deployed(); + }); + + it('should be deployed', async () => { + expect(ethereumSingleRequestProxy.address).to.not.equal(ethers.constants.AddressZero); + }); + + it('should set the correct initial values', async () => { + expect(await ethereumSingleRequestProxy.payee()).to.equal(payeeAddress); + expect(await ethereumSingleRequestProxy.paymentReference()).to.equal(paymentReference); + expect(await ethereumSingleRequestProxy.ethereumFeeProxy()).to.equal(ethereumFeeProxy.address); + expect(await ethereumSingleRequestProxy.feeAddress()).to.equal(feeRecipientAddress); + expect(await ethereumSingleRequestProxy.feeAmount()).to.equal(feeAmount); + }); + + it('should process a payment correctly and emit event', async () => { + const paymentAmount = ethers.utils.parseEther('1'); + const totalAmount = paymentAmount.add(feeAmount); + + await expect( + owner.sendTransaction({ + to: ethereumSingleRequestProxy.address, + value: totalAmount, + }), + ) + .to.changeEtherBalances( + [owner, payee, feeRecipient], + [totalAmount.mul(-1), paymentAmount, feeAmount], + ) + .and.to.emit(ethereumFeeProxy, 'TransferWithReferenceAndFee') + .withArgs(payeeAddress, paymentAmount, paymentReference, feeAmount, feeRecipientAddress); + + expect(await ethers.provider.getBalance(ethereumSingleRequestProxy.address)).to.equal(0); + expect(await ethers.provider.getBalance(ethereumFeeProxy.address)).to.equal(0); + }); + + it('should handle funds sent back from EthereumFeeProxy', async () => { + const MockEthereumFeeProxyFactory = await ethers.getContractFactory('MockEthereumFeeProxy'); + const mockEthereumFeeProxy = await MockEthereumFeeProxyFactory.deploy(); + await mockEthereumFeeProxy.deployed(); + + const newEthereumSingleRequestProxyFactory = await ethers.getContractFactory( + 'EthereumSingleRequestProxy', + ); + const newEthereumSingleRequestProxy = await newEthereumSingleRequestProxyFactory.deploy( + payeeAddress, + paymentReference, + mockEthereumFeeProxy.address, + feeRecipientAddress, + feeAmount, + ); + await newEthereumSingleRequestProxy.deployed(); + + const paymentAmount = ethers.utils.parseEther('1'); + const totalAmount = paymentAmount.add(feeAmount); + + await owner.sendTransaction({ + to: newEthereumSingleRequestProxy.address, + value: totalAmount, + }); + + expect(await ethers.provider.getBalance(newEthereumSingleRequestProxy.address)).to.equal(0); + expect(await ethers.provider.getBalance(mockEthereumFeeProxy.address)).to.equal(totalAmount); + + await expect(() => + mockEthereumFeeProxy.sendFundsBack(newEthereumSingleRequestProxy.address, totalAmount), + ).to.changeEtherBalances( + [owner, newEthereumSingleRequestProxy, mockEthereumFeeProxy], + [totalAmount, 0, totalAmount.mul(-1)], + ); + + expect(await ethers.provider.getBalance(newEthereumSingleRequestProxy.address)).to.equal(0); + expect(await ethers.provider.getBalance(mockEthereumFeeProxy.address)).to.equal(0); + }); +}); From 8aef234c06d77b1a8e66a0543b5709e9cd80afe9 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 18 Sep 2024 22:03:58 +0300 Subject: [PATCH 12/37] fix: `ERC20SingleRequestProxy` to factor in fee amount --- .../src/contracts/ERC20SingleRequestProxy.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index f2043985b3..1ab17d0535 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -37,13 +37,18 @@ contract ERC20SingleRequestProxy { require(msg.value == 0, 'This function is only for triggering the transfer'); IERC20 token = IERC20(tokenAddress); uint256 balance = token.balanceOf(address(this)); + uint256 paymentAmount = balance; + if (feeAmount > 0 && feeAddress != address(0)) { + require(balance > feeAmount, 'Insufficient balance to cover fee'); + paymentAmount = balance - feeAmount; + } token.approve(address(erc20FeeProxy), balance); erc20FeeProxy.transferFromWithReferenceAndFee( tokenAddress, payee, - balance, + paymentAmount, paymentReference, feeAmount, feeAddress From 0769aa712048ed3d3bcdc9d4eabba47f320a4e2a Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 18 Sep 2024 22:44:05 +0300 Subject: [PATCH 13/37] test: `ERC20SingleRequestProxy` functionality --- .../src/contracts/test/TestToken.sol | 15 ++ .../contracts/ERC20SingleRequestProxy.test.ts | 152 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 packages/smart-contracts/src/contracts/test/TestToken.sol create mode 100644 packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts diff --git a/packages/smart-contracts/src/contracts/test/TestToken.sol b/packages/smart-contracts/src/contracts/test/TestToken.sol new file mode 100644 index 0000000000..f971756647 --- /dev/null +++ b/packages/smart-contracts/src/contracts/test/TestToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.9; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol'; + +contract TestToken is ERC20, Ownable, ERC20Permit { + constructor(address initialOwner) ERC20('TestToken', 'TTK') Ownable() ERC20Permit('TestToken') {} + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts new file mode 100644 index 0000000000..8cf3373f0b --- /dev/null +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -0,0 +1,152 @@ +import '@nomiclabs/hardhat-ethers'; +import { BytesLike, Signer } from 'ethers'; +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { + TestToken__factory, + TestToken, + ERC20SingleRequestProxy__factory, + ERC20SingleRequestProxy, + ERC20FeeProxy, + ERC20FeeProxy__factory, +} from '../../src/types'; +import { BigNumber as BN } from 'ethers'; + +const BASE_DECIMAL = BN.from(10).pow(BN.from(18)); + +describe('contract: ERC20SingleRequestProxy', () => { + let deployer: Signer; + let user1: Signer, user1Addr: string; + let user2: Signer, user2Addr: string; + let feeRecipient: Signer, feeRecipientAddr: string; + + let testToken: TestToken, + erc20SingleRequestProxy: ERC20SingleRequestProxy, + erc20FeeProxy: ERC20FeeProxy; + + const paymentReference: BytesLike = '0xd0bc835c22f49e7e'; + const feeAmount: BN = BN.from(10).mul(BASE_DECIMAL); + + before(async function () { + [deployer, user1, user2, feeRecipient] = await ethers.getSigners(); + user1Addr = await user1.getAddress(); + user2Addr = await user2.getAddress(); + feeRecipientAddr = await feeRecipient.getAddress(); + }); + + beforeEach(async function () { + const deployerAddr = await deployer.getAddress(); + testToken = await new TestToken__factory(deployer).deploy(deployerAddr); + await testToken.mint(deployerAddr, BN.from(1000000).mul(BASE_DECIMAL)); + + erc20FeeProxy = await new ERC20FeeProxy__factory(deployer).deploy(); + erc20SingleRequestProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( + user2Addr, + testToken.address, + feeRecipientAddr, + feeAmount, + paymentReference, + erc20FeeProxy.address, + ); + + await testToken.transfer(user1Addr, BN.from(10000).mul(BASE_DECIMAL)); + await testToken + .connect(user1) + .approve(erc20SingleRequestProxy.address, ethers.constants.MaxUint256); + }); + + it('should be deployed', async () => { + expect(erc20SingleRequestProxy.address).to.not.equal(ethers.constants.AddressZero); + }); + + it('should set the correct initial values', async () => { + expect(await erc20SingleRequestProxy.payee()).to.equal(user2Addr); + expect(await erc20SingleRequestProxy.tokenAddress()).to.equal(testToken.address); + expect(await erc20SingleRequestProxy.feeAddress()).to.equal(feeRecipientAddr); + expect(await erc20SingleRequestProxy.feeAmount()).to.equal(feeAmount); + expect(await erc20SingleRequestProxy.paymentReference()).to.equal(paymentReference); + expect(await erc20SingleRequestProxy.erc20FeeProxy()).to.equal(erc20FeeProxy.address); + }); + + it('should process a payment correctly', async () => { + const paymentAmount = BN.from(100).mul(BASE_DECIMAL); + const totalAmount = paymentAmount.add(feeAmount); + + await testToken.connect(user1).transfer(erc20SingleRequestProxy.address, totalAmount); + + const erc20SingleRequestProxyBalanceBefore = await testToken.balanceOf( + erc20SingleRequestProxy.address, + ); + expect(erc20SingleRequestProxyBalanceBefore).to.equal(totalAmount); + + await user1.sendTransaction({ + to: erc20SingleRequestProxy.address, + value: 0, + }); + + const erc20SingleRequestProxyBalanceAfter = await testToken.balanceOf( + erc20SingleRequestProxy.address, + ); + const user2BalanceAfter = await testToken.balanceOf(user2Addr); + const feeRecipientBalanceAfter = await testToken.balanceOf(feeRecipientAddr); + + expect(erc20SingleRequestProxyBalanceAfter).to.equal(0); + expect(user2BalanceAfter).to.equal(paymentAmount); + expect(feeRecipientBalanceAfter).to.equal(feeAmount); + }); + + it('should revert if called with non-zero value', async () => { + await expect( + user1.sendTransaction({ + to: erc20SingleRequestProxy.address, + value: 1, + }), + ).to.be.revertedWith('This function is only for triggering the transfer'); + }); + + it('should handle zero fee amount correctly', async () => { + const zeroFeeProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( + user2Addr, + testToken.address, + feeRecipientAddr, + 0, + paymentReference, + erc20FeeProxy.address, + ); + + const paymentAmount = BN.from(100).mul(BASE_DECIMAL); + await testToken.connect(user1).transfer(zeroFeeProxy.address, paymentAmount); + + await expect( + user1.sendTransaction({ + to: zeroFeeProxy.address, + value: 0, + }), + ) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user2Addr, + paymentAmount, + ethers.utils.keccak256(paymentReference), + 0, + feeRecipientAddr, + ); + + expect(await testToken.balanceOf(zeroFeeProxy.address)).to.equal(0); + expect(await testToken.balanceOf(user2Addr)).to.equal(paymentAmount); + expect(await testToken.balanceOf(feeRecipientAddr)).to.equal(0); + }); + + it('should revert if there are not enough tokens', async () => { + const insufficientAmount = BN.from(1).mul(BASE_DECIMAL); + await testToken.connect(user1).transfer(erc20SingleRequestProxy.address, insufficientAmount); + + await expect( + user1.sendTransaction({ + to: erc20SingleRequestProxy.address, + value: 0, + }), + ).to.be.reverted; + }); +}); From 3bd9e38febac9fa36c2c17fc56ec8f09479b52d9 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 18 Sep 2024 23:07:19 +0300 Subject: [PATCH 14/37] test: `SingleRequestProxyFactory` functionality --- .../SingleRequestProxyFactory.test.ts | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts diff --git a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts new file mode 100644 index 0000000000..3ee472935a --- /dev/null +++ b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts @@ -0,0 +1,194 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { Signer } from 'ethers'; +import { + SingleRequestProxyFactory, + EthereumFeeProxy, + ERC20FeeProxy, + EthereumSingleRequestProxy, + ERC20SingleRequestProxy, + TestToken, +} from '../../src/types'; + +describe('contract: SingleRequestProxyFactory', () => { + let singleRequestProxyFactory: SingleRequestProxyFactory; + let ethereumFeeProxy: EthereumFeeProxy; + let erc20FeeProxy: ERC20FeeProxy; + let testToken: TestToken; + let owner: Signer; + let user: Signer; + let payee: Signer; + let feeRecipient: Signer; + let ownerAddress: string; + let userAddress: string; + let payeeAddress: string; + let feeRecipientAddress: string; + + const paymentReference: string = ethers.utils.formatBytes32String('payment_reference'); + const feeAmount: string = ethers.utils.parseEther('0.1').toString(); + + beforeEach(async () => { + [owner, user, payee, feeRecipient] = await ethers.getSigners(); + ownerAddress = await owner.getAddress(); + userAddress = await user.getAddress(); + payeeAddress = await payee.getAddress(); + feeRecipientAddress = await feeRecipient.getAddress(); + + const EthereumFeeProxyFactory = await ethers.getContractFactory('EthereumFeeProxy'); + ethereumFeeProxy = await EthereumFeeProxyFactory.deploy(); + await ethereumFeeProxy.deployed(); + + const ERC20FeeProxyFactory = await ethers.getContractFactory('ERC20FeeProxy'); + erc20FeeProxy = await ERC20FeeProxyFactory.deploy(); + await erc20FeeProxy.deployed(); + + const SingleRequestProxyFactoryFactory = await ethers.getContractFactory( + 'SingleRequestProxyFactory', + ); + singleRequestProxyFactory = await SingleRequestProxyFactoryFactory.deploy( + ethereumFeeProxy.address, + erc20FeeProxy.address, + ); + await singleRequestProxyFactory.deployed(); + + const TestTokenFactory = await ethers.getContractFactory('TestToken'); + testToken = await TestTokenFactory.deploy(ownerAddress); + await testToken.deployed(); + }); + + it('should be deployed with correct initial values', async () => { + expect(singleRequestProxyFactory.address).to.not.equal(ethers.constants.AddressZero); + expect(await singleRequestProxyFactory.ethereumFeeProxy()).to.equal(ethereumFeeProxy.address); + expect(await singleRequestProxyFactory.erc20FeeProxy()).to.equal(erc20FeeProxy.address); + expect(await singleRequestProxyFactory.owner()).to.equal(ownerAddress); + }); + + it('should create a new EthereumSingleRequestProxy and emit event', async () => { + const tx = await singleRequestProxyFactory.createEthereumSingleRequestProxy( + payeeAddress, + paymentReference, + feeRecipientAddress, + feeAmount, + ); + + const receipt = await tx.wait(); + + expect(receipt.events).to.exist; + expect(receipt.events).to.have.length(1); + expect(receipt.events?.[0]?.event).to.equal('EtheruemSingleRequestProxyCreated'); + + const proxyAddress = receipt.events?.[0]?.args?.[0]; + + expect(proxyAddress).to.not.equal(ethers.constants.AddressZero); + expect(proxyAddress).to.be.properAddress; + + // Check if the event was emitted with correct parameters + await expect(tx) + .to.emit(singleRequestProxyFactory, 'EtheruemSingleRequestProxyCreated') + .withArgs(proxyAddress, payeeAddress, paymentReference); + + const proxy = (await ethers.getContractAt( + 'EthereumSingleRequestProxy', + proxyAddress, + )) as EthereumSingleRequestProxy; + expect(await proxy.payee()).to.equal(payeeAddress); + expect(await proxy.paymentReference()).to.equal(paymentReference); + expect(await proxy.ethereumFeeProxy()).to.equal(ethereumFeeProxy.address); + expect(await proxy.feeAddress()).to.equal(feeRecipientAddress); + expect(await proxy.feeAmount()).to.equal(feeAmount); + }); + + it('should create a new ERC20SingleRequestProxy and emit event', async () => { + const tx = await singleRequestProxyFactory.createERC20SingleRequestProxy( + payeeAddress, + testToken.address, + paymentReference, + feeRecipientAddress, + feeAmount, + ); + + const receipt = await tx.wait(); + + expect(receipt.events).to.exist; + expect(receipt.events).to.have.length(1); + expect(receipt.events?.[0]?.event).to.equal('ERC20SingleRequestProxyCreated'); + + const proxyAddress = receipt.events?.[0]?.args?.[0]; + + expect(proxyAddress).to.not.equal(ethers.constants.AddressZero); + expect(proxyAddress).to.be.properAddress; + + // Check if the event was emitted with correct parameters + await expect(tx) + .to.emit(singleRequestProxyFactory, 'ERC20SingleRequestProxyCreated') + .withArgs(proxyAddress, payeeAddress, testToken.address, paymentReference); + + const proxy = (await ethers.getContractAt( + 'ERC20SingleRequestProxy', + proxyAddress, + )) as ERC20SingleRequestProxy; + expect(await proxy.payee()).to.equal(payeeAddress); + expect(await proxy.tokenAddress()).to.equal(testToken.address); + expect(await proxy.paymentReference()).to.equal(paymentReference); + expect(await proxy.erc20FeeProxy()).to.equal(erc20FeeProxy.address); + expect(await proxy.feeAddress()).to.equal(feeRecipientAddress); + expect(await proxy.feeAmount()).to.equal(feeAmount); + }); + + it('should update ERC20FeeProxy address when called by owner', async () => { + const newERC20FeeProxy = await (await ethers.getContractFactory('ERC20FeeProxy')).deploy(); + await newERC20FeeProxy.deployed(); + + await singleRequestProxyFactory.setERC20FeeProxy(newERC20FeeProxy.address); + expect(await singleRequestProxyFactory.erc20FeeProxy()).to.equal(newERC20FeeProxy.address); + }); + + it('should revert when called by non-owner', async () => { + const newERC20FeeProxy = await (await ethers.getContractFactory('ERC20FeeProxy')).deploy(); + await newERC20FeeProxy.deployed(); + + await expect( + singleRequestProxyFactory.connect(user).setERC20FeeProxy(newERC20FeeProxy.address), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('should update EthereumFeeProxy address when called by owner', async () => { + const newEthereumFeeProxy = await ( + await ethers.getContractFactory('EthereumFeeProxy') + ).deploy(); + await newEthereumFeeProxy.deployed(); + + await singleRequestProxyFactory.updateEthereumFeeProxy(newEthereumFeeProxy.address); + expect(await singleRequestProxyFactory.ethereumFeeProxy()).to.equal( + newEthereumFeeProxy.address, + ); + + it('should revert when called by non-owner', async () => { + const newEthereumFeeProxy = await ( + await ethers.getContractFactory('EthereumFeeProxy') + ).deploy(); + await newEthereumFeeProxy.deployed(); + + await expect( + singleRequestProxyFactory.connect(user).updateEthereumFeeProxy(newEthereumFeeProxy.address), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + it('should allow owner to transfer ownership', async () => { + await singleRequestProxyFactory.transferOwnership(userAddress); + expect(await singleRequestProxyFactory.owner()).to.equal(userAddress); + }); + + it('should allow new owner to renounce ownership', async () => { + await singleRequestProxyFactory.transferOwnership(userAddress); + await singleRequestProxyFactory.connect(user).renounceOwnership(); + expect(await singleRequestProxyFactory.owner()).to.equal(ethers.constants.AddressZero); + }); + + it('should revert when non-owner tries to transfer ownership', async () => { + await expect( + singleRequestProxyFactory.connect(user).transferOwnership(userAddress), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); +}); From ed429ae39e2b0d5aa81d3f6395d669bec38ecd06 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 18 Sep 2024 23:24:03 +0300 Subject: [PATCH 15/37] chore: update `EthereumSingleRequestProxy` tests --- .../EthereumSingleRequestProxy.test.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts index 3e777e9bec..38a8865cff 100644 --- a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -51,7 +51,17 @@ describe('contract : EthereumSingleRequestProxy', () => { it('should process a payment correctly and emit event', async () => { const paymentAmount = ethers.utils.parseEther('1'); - const totalAmount = paymentAmount.add(feeAmount); + const totalAmount = BigNumber.from(paymentAmount).add(BigNumber.from(feeAmount)); + + await expect( + await owner.sendTransaction({ + to: ethereumSingleRequestProxy.address, + value: totalAmount, + }), + ).to.changeEtherBalances( + [owner, payee, feeRecipient], + [totalAmount.mul(-1), paymentAmount, feeAmount], + ); await expect( owner.sendTransaction({ @@ -59,11 +69,7 @@ describe('contract : EthereumSingleRequestProxy', () => { value: totalAmount, }), ) - .to.changeEtherBalances( - [owner, payee, feeRecipient], - [totalAmount.mul(-1), paymentAmount, feeAmount], - ) - .and.to.emit(ethereumFeeProxy, 'TransferWithReferenceAndFee') + .to.emit(ethereumFeeProxy, 'TransferWithReferenceAndFee') .withArgs(payeeAddress, paymentAmount, paymentReference, feeAmount, feeRecipientAddress); expect(await ethers.provider.getBalance(ethereumSingleRequestProxy.address)).to.equal(0); From a341ce1a92e06a693a32ef89646b280e46f2c10d Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 23 Sep 2024 13:40:24 +0300 Subject: [PATCH 16/37] feat: write deployment script for `SingleRequestProxyFactory` --- .../deploy-single-request-proxy-factory.ts | 33 +++++++++ .../scripts/test-deploy-all.ts | 2 + packages/smart-contracts/scripts/utils.ts | 74 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts diff --git a/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts b/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts new file mode 100644 index 0000000000..2cdcb0d8f8 --- /dev/null +++ b/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts @@ -0,0 +1,33 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; +import { feeProxyAddresses } from './utils'; + +export async function deploySingleRequestProxyFactory( + args: any, + hre: HardhatRuntimeEnvironment, +): Promise { + const [deployer] = await hre.ethers.getSigners(); + const networkName = hre.network.name; + + console.log( + `Deploying with the account: ${deployer.address} on the network ${networkName} (${hre.network.config.chainId})`, + ); + + const config = feeProxyAddresses[networkName]; + if (!config) { + throw new Error(`No configuration found for network: ${networkName}`); + } + + const { ethereumFeeProxyAddress, erc20FeeProxyAddress } = config; + + const { address: SingleRequestProxyFactoryAddress } = await deployOne( + args, + hre, + 'SingleRequestProxyFactory', + { + constructorArguments: [ethereumFeeProxyAddress, erc20FeeProxyAddress], + }, + ); + + console.log('SingleRequestProxyFactory Contract deployed: ' + SingleRequestProxyFactoryAddress); +} diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index 567b0ccc2c..4276f83ecb 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -7,6 +7,7 @@ import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; import { deployERC20TransferableReceivable } from './test-deploy-erc20-transferable-receivable'; +import { deploySingleRequestProxyFactory } from './deploy-single-request-proxy-factory'; // Deploys, set up the contracts export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -18,4 +19,5 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); await deployERC20TransferableReceivable(_args, hre, mainPaymentAddresses); + await deploySingleRequestProxyFactory(_args, hre); } diff --git a/packages/smart-contracts/scripts/utils.ts b/packages/smart-contracts/scripts/utils.ts index 419d224a27..c5b5b2dbea 100644 --- a/packages/smart-contracts/scripts/utils.ts +++ b/packages/smart-contracts/scripts/utils.ts @@ -33,6 +33,80 @@ export const uniswapV2RouterAddresses: Record = { core: '0x0000000000000000000000000000000000000000', }; +export const feeProxyAddresses: Record< + string, + { ethereumFeeProxyAddress: string; erc20FeeProxyAddress: string } +> = { + mainnet: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C', + }, + sepolia: { + ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + }, + optimism: { + ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + }, + 'arbitrum-one': { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + base: { + ethereumFeeProxyAddress: '0xd9C3889eB8DA6ce449bfFE3cd194d08A436e96f2', + erc20FeeProxyAddress: '0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814', + }, + 'zksync-era': { + ethereumFeeProxyAddress: '0xE9A708db0D30409e39810C44cA240fd15cdA9b1a', + erc20FeeProxyAddress: '0x6e28Cc56C2E64c9250f39Cb134686C87dB196532', + }, + gnosis: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + polygon: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + bsc: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + celo: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C', + }, + fantom: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + core: { + ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + }, + avalanche: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', + }, + fuse: { + ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', + erc20FeeProxyAddress: '0xee07ef5B414955188d2A9fF50bdCE784A49031Fc', + }, + moonbeam: { + ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + }, + ronin: { + ethereumFeeProxyAddress: '0xe9cbD1Aa5496628F4302426693Ad63006C56959F', + erc20FeeProxyAddress: '0xAe23992483FeDA6E718a808Ce824f6864F13B64B', + }, + mantle: { + ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', + erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', + }, +}; + /** * Executes as many empty transactions as needed for the nonce goes up to a certain target. * Assuming that the deployer is the first signer. From 2cc82a02c1eb343219eafc4a83867658ab033420 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 23 Sep 2024 13:59:12 +0300 Subject: [PATCH 17/37] refactor: rewrite deployment to use `CREATE2` schema --- .../scripts-create2/compute-one-address.ts | 3 +- .../scripts-create2/constructor-args.ts | 11 +++ .../deploy-single-request-proxy-factory.ts | 33 --------- .../scripts/test-deploy-all.ts | 2 - packages/smart-contracts/scripts/utils.ts | 74 ------------------- 5 files changed, 13 insertions(+), 110 deletions(-) delete mode 100644 packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index 2996f1ca96..84eca479b2 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -64,7 +64,8 @@ export const computeCreate2DeploymentAddressesFromList = async ( case 'BatchConversionPayments': case 'ERC20SwapToPay': case 'ERC20SwapToConversion': - case 'ERC20TransferableReceivable': { + case 'ERC20TransferableReceivable': + case 'SingleRequestProxyFactory': { try { const constructorArgs = getConstructorArgs(contract, chain); address = await computeCreate2DeploymentAddress({ contract, constructorArgs }, hre); diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 042da3569c..808b10df2e 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -67,6 +67,17 @@ export const getConstructorArgs = ( const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); return ['Request Network Transferable Receivable', 'tREC', erc20FeeProxyAddress]; } + case 'SingleRequestProxyFactory': { + if (!network) { + throw new Error('SingleRequestProxyFactory requires network parameter'); + } + const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; + const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); + const ethereumFeeProxy = artifacts.ethereumFeeProxyArtifact; + const ethereumFeeProxyAddress = ethereumFeeProxy.getAddress(network); + + return [ethereumFeeProxyAddress, erc20FeeProxyAddress]; + } default: return []; } diff --git a/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts b/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts deleted file mode 100644 index 2cdcb0d8f8..0000000000 --- a/packages/smart-contracts/scripts/deploy-single-request-proxy-factory.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { deployOne } from './deploy-one'; -import { feeProxyAddresses } from './utils'; - -export async function deploySingleRequestProxyFactory( - args: any, - hre: HardhatRuntimeEnvironment, -): Promise { - const [deployer] = await hre.ethers.getSigners(); - const networkName = hre.network.name; - - console.log( - `Deploying with the account: ${deployer.address} on the network ${networkName} (${hre.network.config.chainId})`, - ); - - const config = feeProxyAddresses[networkName]; - if (!config) { - throw new Error(`No configuration found for network: ${networkName}`); - } - - const { ethereumFeeProxyAddress, erc20FeeProxyAddress } = config; - - const { address: SingleRequestProxyFactoryAddress } = await deployOne( - args, - hre, - 'SingleRequestProxyFactory', - { - constructorArguments: [ethereumFeeProxyAddress, erc20FeeProxyAddress], - }, - ); - - console.log('SingleRequestProxyFactory Contract deployed: ' + SingleRequestProxyFactoryAddress); -} diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index 4276f83ecb..567b0ccc2c 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -7,7 +7,6 @@ import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; import { deployERC20TransferableReceivable } from './test-deploy-erc20-transferable-receivable'; -import { deploySingleRequestProxyFactory } from './deploy-single-request-proxy-factory'; // Deploys, set up the contracts export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -19,5 +18,4 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); await deployERC20TransferableReceivable(_args, hre, mainPaymentAddresses); - await deploySingleRequestProxyFactory(_args, hre); } diff --git a/packages/smart-contracts/scripts/utils.ts b/packages/smart-contracts/scripts/utils.ts index c5b5b2dbea..419d224a27 100644 --- a/packages/smart-contracts/scripts/utils.ts +++ b/packages/smart-contracts/scripts/utils.ts @@ -33,80 +33,6 @@ export const uniswapV2RouterAddresses: Record = { core: '0x0000000000000000000000000000000000000000', }; -export const feeProxyAddresses: Record< - string, - { ethereumFeeProxyAddress: string; erc20FeeProxyAddress: string } -> = { - mainnet: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C', - }, - sepolia: { - ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', - erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', - }, - optimism: { - ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', - erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', - }, - 'arbitrum-one': { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - base: { - ethereumFeeProxyAddress: '0xd9C3889eB8DA6ce449bfFE3cd194d08A436e96f2', - erc20FeeProxyAddress: '0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814', - }, - 'zksync-era': { - ethereumFeeProxyAddress: '0xE9A708db0D30409e39810C44cA240fd15cdA9b1a', - erc20FeeProxyAddress: '0x6e28Cc56C2E64c9250f39Cb134686C87dB196532', - }, - gnosis: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - polygon: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - bsc: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - celo: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x2171a0dc12a9E5b1659feF2BB20E54c84Fa7dB0C', - }, - fantom: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - core: { - ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', - erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', - }, - avalanche: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9', - }, - fuse: { - ethereumFeeProxyAddress: '0xfCFBcfc4f5A421089e3Df45455F7f4985FE2D6a8', - erc20FeeProxyAddress: '0xee07ef5B414955188d2A9fF50bdCE784A49031Fc', - }, - moonbeam: { - ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', - erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', - }, - ronin: { - ethereumFeeProxyAddress: '0xe9cbD1Aa5496628F4302426693Ad63006C56959F', - erc20FeeProxyAddress: '0xAe23992483FeDA6E718a808Ce824f6864F13B64B', - }, - mantle: { - ethereumFeeProxyAddress: '0xe11BF2fDA23bF0A98365e1A4c04A87C9339e8687', - erc20FeeProxyAddress: '0x399F5EE127ce7432E4921a61b8CF52b0af52cbfE', - }, -}; - /** * Executes as many empty transactions as needed for the nonce goes up to a certain target. * Assuming that the deployer is the first signer. From bee1f58c93d94074c7bf2bd86a69144c192d7ec4 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 23 Sep 2024 14:01:09 +0300 Subject: [PATCH 18/37] fix: add SingleRequestProxyFactory `create2ContractDeploymentList` --- packages/smart-contracts/scripts-create2/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index df387afe71..90db473d07 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -20,6 +20,7 @@ export const create2ContractDeploymentList = [ 'BatchConversionPayments', 'ERC20EscrowToPay', 'ERC20TransferableReceivable', + 'SingleRequestProxyFactory', ]; /** From aab67110665b1cf8f9d6957e589a4fd4ba3de8b0 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 23 Sep 2024 14:09:41 +0300 Subject: [PATCH 19/37] chore: include SingleRequestProxyFactory in transfer ownership cases in create2 script --- packages/smart-contracts/scripts-create2/transfer-ownership.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/scripts-create2/transfer-ownership.ts b/packages/smart-contracts/scripts-create2/transfer-ownership.ts index 0824c6bc88..52880e6947 100644 --- a/packages/smart-contracts/scripts-create2/transfer-ownership.ts +++ b/packages/smart-contracts/scripts-create2/transfer-ownership.ts @@ -12,7 +12,8 @@ export const transferOwnership = async ( case 'Erc20ConversionProxy': case 'BatchConversionPayments': case 'ERC20SwapToPay': - case 'ERC20SwapToConversion': { + case 'ERC20SwapToConversion': + case 'SingleRequestProxyFactory': { await updateOwner({ contract, hre, signWithEoa }); break; } From 0c68eba142b16a4cbd1c2167cffcff771082c024 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 23 Sep 2024 14:14:13 +0300 Subject: [PATCH 20/37] refactor: rename updateEthereumFeeProxy to setEthereumFeeProxy for clarity in SingleRequestProxyFactory contract --- .../src/contracts/SingleRequestProxyFactory.sol | 2 +- .../test/contracts/SingleRequestProxyFactory.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index f79dfc918e..e6345e818b 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -97,7 +97,7 @@ contract SingleRequestProxyFactory is Ownable { * @notice Updates the EthereumFeeProxy address * @param _newEthereumFeeProxy The new EthereumFeeProxy address */ - function updateEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { + function setEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { ethereumFeeProxy = _newEthereumFeeProxy; } } diff --git a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts index 3ee472935a..b5752489a0 100644 --- a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts +++ b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts @@ -158,7 +158,7 @@ describe('contract: SingleRequestProxyFactory', () => { ).deploy(); await newEthereumFeeProxy.deployed(); - await singleRequestProxyFactory.updateEthereumFeeProxy(newEthereumFeeProxy.address); + await singleRequestProxyFactory.setEthereumFeeProxy(newEthereumFeeProxy.address); expect(await singleRequestProxyFactory.ethereumFeeProxy()).to.equal( newEthereumFeeProxy.address, ); @@ -170,7 +170,7 @@ describe('contract: SingleRequestProxyFactory', () => { await newEthereumFeeProxy.deployed(); await expect( - singleRequestProxyFactory.connect(user).updateEthereumFeeProxy(newEthereumFeeProxy.address), + singleRequestProxyFactory.connect(user).setEthereumFeeProxy(newEthereumFeeProxy.address), ).to.be.revertedWith('Ownable: caller is not the owner'); }); }); From 0482f6ddb14ac2fe693b18d1ba808c7a88cef127 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 26 Sep 2024 11:16:51 +0300 Subject: [PATCH 21/37] fix: type and add new events --- .../contracts/SingleRequestProxyFactory.sol | 13 ++++++++-- .../SingleRequestProxyFactory.test.ts | 24 +++++++++---------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index e6345e818b..87386a5e04 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -13,7 +13,7 @@ contract SingleRequestProxyFactory is Ownable { address public ethereumFeeProxy; address public erc20FeeProxy; - event EtheruemSingleRequestProxyCreated( + event EthereumSingleRequestProxyCreated( address indexed proxyAddress, address indexed payee, bytes indexed paymentReference @@ -26,7 +26,12 @@ contract SingleRequestProxyFactory is Ownable { bytes indexed paymentReference ); + event ERC20FeeProxyUpdated(address indexed newERC20FeeProxy); + event EthereumFeeProxyUpdated(address indexed newEthereumFeeProxy); + constructor(address _ethereumFeeProxy, address _erc20FeeProxy) Ownable() { + require(_ethereumFeeProxy != address(0), 'EthereumFeeProxy address cannot be zero'); + require(_erc20FeeProxy != address(0), 'ERC20FeeProxy address cannot be zero'); ethereumFeeProxy = _ethereumFeeProxy; erc20FeeProxy = _erc20FeeProxy; } @@ -52,7 +57,7 @@ contract SingleRequestProxyFactory is Ownable { _feeAddress, _feeAmount ); - emit EtheruemSingleRequestProxyCreated(address(proxy), _payee, _paymentReference); + emit EthereumSingleRequestProxyCreated(address(proxy), _payee, _paymentReference); return address(proxy); } @@ -90,7 +95,9 @@ contract SingleRequestProxyFactory is Ownable { * @param _newERC20FeeProxy The new ERC20FeeProxy address */ function setERC20FeeProxy(address _newERC20FeeProxy) external onlyOwner { + require(_newERC20FeeProxy != address(0), 'ERC20FeeProxy address cannot be zero'); erc20FeeProxy = _newERC20FeeProxy; + emit ERC20FeeProxyUpdated(_newERC20FeeProxy); } /** @@ -98,6 +105,8 @@ contract SingleRequestProxyFactory is Ownable { * @param _newEthereumFeeProxy The new EthereumFeeProxy address */ function setEthereumFeeProxy(address _newEthereumFeeProxy) external onlyOwner { + require(_newEthereumFeeProxy != address(0), 'EthereumFeeProxy address cannot be zero'); ethereumFeeProxy = _newEthereumFeeProxy; + emit EthereumFeeProxyUpdated(_newEthereumFeeProxy); } } diff --git a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts index b5752489a0..1d39668d3c 100644 --- a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts +++ b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts @@ -75,7 +75,7 @@ describe('contract: SingleRequestProxyFactory', () => { expect(receipt.events).to.exist; expect(receipt.events).to.have.length(1); - expect(receipt.events?.[0]?.event).to.equal('EtheruemSingleRequestProxyCreated'); + expect(receipt.events?.[0]?.event).to.equal('EthereumSingleRequestProxyCreated'); const proxyAddress = receipt.events?.[0]?.args?.[0]; @@ -84,7 +84,7 @@ describe('contract: SingleRequestProxyFactory', () => { // Check if the event was emitted with correct parameters await expect(tx) - .to.emit(singleRequestProxyFactory, 'EtheruemSingleRequestProxyCreated') + .to.emit(singleRequestProxyFactory, 'EthereumSingleRequestProxyCreated') .withArgs(proxyAddress, payeeAddress, paymentReference); const proxy = (await ethers.getContractAt( @@ -143,7 +143,7 @@ describe('contract: SingleRequestProxyFactory', () => { expect(await singleRequestProxyFactory.erc20FeeProxy()).to.equal(newERC20FeeProxy.address); }); - it('should revert when called by non-owner', async () => { + it('should revert when non-owner tries to set ERC20FeeProxy address', async () => { const newERC20FeeProxy = await (await ethers.getContractFactory('ERC20FeeProxy')).deploy(); await newERC20FeeProxy.deployed(); @@ -162,17 +162,17 @@ describe('contract: SingleRequestProxyFactory', () => { expect(await singleRequestProxyFactory.ethereumFeeProxy()).to.equal( newEthereumFeeProxy.address, ); + }); - it('should revert when called by non-owner', async () => { - const newEthereumFeeProxy = await ( - await ethers.getContractFactory('EthereumFeeProxy') - ).deploy(); - await newEthereumFeeProxy.deployed(); + it('should revert when non-owner tries to set EthereumFeeProxy address', async () => { + const newEthereumFeeProxy = await ( + await ethers.getContractFactory('EthereumFeeProxy') + ).deploy(); + await newEthereumFeeProxy.deployed(); - await expect( - singleRequestProxyFactory.connect(user).setEthereumFeeProxy(newEthereumFeeProxy.address), - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); + await expect( + singleRequestProxyFactory.connect(user).setEthereumFeeProxy(newEthereumFeeProxy.address), + ).to.be.revertedWith('Ownable: caller is not the owner'); }); it('should allow owner to transfer ownership', async () => { From 90330661b039417530abf23c42249faf2892c5ef Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 16 Oct 2024 16:02:54 +0300 Subject: [PATCH 22/37] docs: add more documentation to nonReentrant modifier --- .../src/contracts/EthereumSingleRequestProxy.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index bcc6555d62..fddd802399 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -16,7 +16,11 @@ contract EthereumSingleRequestProxy { address private originalSender; - // Reentrancy guard + /** + * @dev Custom reentrancy guard. + * Similar to OpenZeppelin's ReentrancyGuard, but allows reentrancy from ethereumFeeProxy. + * This enables controlled callbacks from ethereumFeeProxy while protecting against other reentrancy attacks. + */ uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _status; @@ -36,16 +40,17 @@ contract EthereumSingleRequestProxy { _status = _NOT_ENTERED; } + /** + * @dev Modified nonReentrant guard. + * Prevents reentrancy except for calls from ethereumFeeProxy. + */ modifier nonReentrant() { if (msg.sender != address(ethereumFeeProxy)) { - // On the first call to nonReentrant, _status will be _NOT_ENTERED require(_status != _ENTERED, 'ReentrancyGuard: reentrant call'); - // Any calls to nonReentrant after this point will fail _status = _ENTERED; } _; if (msg.sender != address(ethereumFeeProxy)) { - // By storing the original value once again, a refund is triggered _status = _NOT_ENTERED; } } From cd91ec2457be0277bf5803a99eb986bb16804941 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 16 Oct 2024 16:04:22 +0300 Subject: [PATCH 23/37] feat: add `rescueFunds` to `EthereumSingleRequestProxy` --- .../src/contracts/EthereumSingleRequestProxy.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index fddd802399..104d3d5dc8 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -83,4 +83,16 @@ contract EthereumSingleRequestProxy { require(callSuccess, 'Call to EthereumFeeProxy failed'); } } + + /** + * @notice Rescues any trapped funds by sending them to the payee + * @dev Can be called by anyone, but funds are always sent to the payee + */ + function rescueFunds() external nonReentrant { + uint256 balance = address(this).balance; + require(balance > 0, 'No funds to rescue'); + + (bool success, ) = payable(payee).call{value: balance}(''); + require(success, 'Rescue failed'); + } } From f94419901d6c26e95d547bf48631690a6ce56541 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 16 Oct 2024 16:23:33 +0300 Subject: [PATCH 24/37] docs: add more documentation to `SingleRequestProxyFactory` --- .../src/contracts/SingleRequestProxyFactory.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index 87386a5e04..606ff85443 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -10,7 +10,12 @@ import './EthereumSingleRequestProxy.sol'; * @notice This contract is used to create SingleRequestProxy instances */ contract SingleRequestProxyFactory is Ownable { + /// @notice The address of the EthereumFeeProxy contract + /// @dev This proxy is used for handling Ethereum-based fee transactions address public ethereumFeeProxy; + + /// @notice The address of the ERC20FeeProxy contract + /// @dev This proxy is used for handling ERC20-based fee transactions address public erc20FeeProxy; event EthereumSingleRequestProxyCreated( From c77c1eb49e09b501dfd71099b2daa42e5ce32256 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 16 Oct 2024 16:28:20 +0300 Subject: [PATCH 25/37] fix: use `safeApprove` instead of `approve` --- .../smart-contracts/src/contracts/ERC20SingleRequestProxy.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index 1ab17d0535..a458033d1b 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.9; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import './interfaces/ERC20FeeProxy.sol'; +import './lib/SafeERC20.sol'; /** * @title ERC20SingleRequestProxy @@ -43,7 +44,7 @@ contract ERC20SingleRequestProxy { paymentAmount = balance - feeAmount; } - token.approve(address(erc20FeeProxy), balance); + require(SafeERC20.safeApprove(token, address(erc20FeeProxy), balance), 'Approval failed'); erc20FeeProxy.transferFromWithReferenceAndFee( tokenAddress, From 41e43b5dbc5a2412d4b85f570b4ea44dd6cdcdc3 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 12:01:21 +0300 Subject: [PATCH 26/37] feat: add rescue funds method --- .../src/contracts/ERC20SingleRequestProxy.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index a458033d1b..96befa0096 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -55,4 +55,16 @@ contract ERC20SingleRequestProxy { feeAddress ); } + + /** + * @notice Rescues any trapped funds by sending them to the payee + * @dev Can be called by anyone, but funds are always sent to the payee + */ + function rescueFunds() external { + IERC20 token = IERC20(tokenAddress); + uint256 balance = token.balanceOf(address(this)); + require(balance > 0, 'No funds to rescue'); + bool success = SafeERC20.safeTransfer(token, payee, balance); + require(success, 'ERC20 rescue failed'); + } } From 01c28d2e31619c10b97c31726427a5d913fd7d3a Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 12:18:27 +0300 Subject: [PATCH 27/37] test: add more ownership tests --- .../src/contracts/SingleRequestProxyFactory.sol | 2 +- .../contracts/SingleRequestProxyFactory.test.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index 606ff85443..9d621ab375 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -34,7 +34,7 @@ contract SingleRequestProxyFactory is Ownable { event ERC20FeeProxyUpdated(address indexed newERC20FeeProxy); event EthereumFeeProxyUpdated(address indexed newEthereumFeeProxy); - constructor(address _ethereumFeeProxy, address _erc20FeeProxy) Ownable() { + constructor(address _ethereumFeeProxy, address _erc20FeeProxy) { require(_ethereumFeeProxy != address(0), 'EthereumFeeProxy address cannot be zero'); require(_erc20FeeProxy != address(0), 'ERC20FeeProxy address cannot be zero'); ethereumFeeProxy = _ethereumFeeProxy; diff --git a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts index 1d39668d3c..cf3630e461 100644 --- a/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts +++ b/packages/smart-contracts/test/contracts/SingleRequestProxyFactory.test.ts @@ -181,8 +181,12 @@ describe('contract: SingleRequestProxyFactory', () => { }); it('should allow new owner to renounce ownership', async () => { - await singleRequestProxyFactory.transferOwnership(userAddress); - await singleRequestProxyFactory.connect(user).renounceOwnership(); + await expect(singleRequestProxyFactory.transferOwnership(userAddress)) + .to.emit(singleRequestProxyFactory, 'OwnershipTransferred') + .withArgs(ownerAddress, userAddress); + await expect(singleRequestProxyFactory.connect(user).renounceOwnership()) + .to.emit(singleRequestProxyFactory, 'OwnershipTransferred') + .withArgs(userAddress, ethers.constants.AddressZero); expect(await singleRequestProxyFactory.owner()).to.equal(ethers.constants.AddressZero); }); @@ -191,4 +195,10 @@ describe('contract: SingleRequestProxyFactory', () => { singleRequestProxyFactory.connect(user).transferOwnership(userAddress), ).to.be.revertedWith('Ownable: caller is not the owner'); }); + + it('should revert when non-owner tries to renounce ownership', async () => { + await expect(singleRequestProxyFactory.connect(user).renounceOwnership()).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); }); From cfba1b9af1d9dc2413155f3b0da33e2e1cdeb616 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 13:55:50 +0300 Subject: [PATCH 28/37] test: add partial payment and non-standard ERC20 tests --- .../src/contracts/test/UsdtFake.sol | 59 +++++++ .../contracts/ERC20SingleRequestProxy.test.ts | 154 +++++++++++++++++- 2 files changed, 206 insertions(+), 7 deletions(-) diff --git a/packages/smart-contracts/src/contracts/test/UsdtFake.sol b/packages/smart-contracts/src/contracts/test/UsdtFake.sol index 169952ceaf..8f53885094 100644 --- a/packages/smart-contracts/src/contracts/test/UsdtFake.sol +++ b/packages/smart-contracts/src/contracts/test/UsdtFake.sol @@ -2,7 +2,66 @@ pragma solidity ^0.8.0; contract UsdtFake { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + uint256 private _totalSupply; + function decimals() external pure returns (uint8) { return 6; } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + // Non-standard: no return value + function transfer(address recipient, uint256 amount) external { + _transfer(msg.sender, recipient, amount); + } + + function allowance(address owner, address spender) external view returns (uint256) { + return _allowances[owner][spender]; + } + + // Non-standard: no return value + function approve(address spender, uint256 amount) external { + _approve(msg.sender, spender, amount); + } + + // Non-standard: no return value + function transferFrom(address sender, address recipient, uint256 amount) external { + _transfer(sender, recipient, amount); + uint256 currentAllowance = _allowances[sender][msg.sender]; + require(currentAllowance >= amount, 'ERC20: transfer amount exceeds allowance'); + unchecked { + _approve(sender, msg.sender, currentAllowance - amount); + } + } + + function _transfer(address sender, address recipient, uint256 amount) internal { + require(sender != address(0), 'ERC20: transfer from the zero address'); + require(recipient != address(0), 'ERC20: transfer to the zero address'); + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, 'ERC20: transfer amount exceeds balance'); + unchecked { + _balances[sender] = senderBalance - amount; + } + _balances[recipient] += amount; + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), 'ERC20: approve from the zero address'); + require(spender != address(0), 'ERC20: approve to the zero address'); + _allowances[owner][spender] = amount; + } + + // For testing purposes + function mint(address account, uint256 amount) external { + _totalSupply += amount; + _balances[account] += amount; + } } diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 8cf3373f0b..8982dff241 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -9,10 +9,13 @@ import { ERC20SingleRequestProxy, ERC20FeeProxy, ERC20FeeProxy__factory, + UsdtFake, + UsdtFake__factory, } from '../../src/types'; import { BigNumber as BN } from 'ethers'; const BASE_DECIMAL = BN.from(10).pow(BN.from(18)); +const USDT_DECIMAL = BN.from(10).pow(BN.from(6)); describe('contract: ERC20SingleRequestProxy', () => { let deployer: Signer; @@ -20,9 +23,10 @@ describe('contract: ERC20SingleRequestProxy', () => { let user2: Signer, user2Addr: string; let feeRecipient: Signer, feeRecipientAddr: string; - let testToken: TestToken, - erc20SingleRequestProxy: ERC20SingleRequestProxy, - erc20FeeProxy: ERC20FeeProxy; + let testToken: TestToken; + let erc20SingleRequestProxy: ERC20SingleRequestProxy; + let erc20FeeProxy: ERC20FeeProxy; + let usdtFake: UsdtFake; const paymentReference: BytesLike = '0xd0bc835c22f49e7e'; const feeAmount: BN = BN.from(10).mul(BASE_DECIMAL); @@ -53,6 +57,10 @@ describe('contract: ERC20SingleRequestProxy', () => { await testToken .connect(user1) .approve(erc20SingleRequestProxy.address, ethers.constants.MaxUint256); + + // Deploy UsdtFake + usdtFake = await new UsdtFake__factory(deployer).deploy(); + await usdtFake.mint(deployerAddr, BN.from(1000000).mul(USDT_DECIMAL)); }); it('should be deployed', async () => { @@ -79,10 +87,21 @@ describe('contract: ERC20SingleRequestProxy', () => { ); expect(erc20SingleRequestProxyBalanceBefore).to.equal(totalAmount); - await user1.sendTransaction({ - to: erc20SingleRequestProxy.address, - value: 0, - }); + await expect( + user1.sendTransaction({ + to: erc20SingleRequestProxy.address, + value: 0, + }), + ) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user2Addr, + paymentAmount, + paymentReference, + feeAmount, + feeRecipientAddr, + ); const erc20SingleRequestProxyBalanceAfter = await testToken.balanceOf( erc20SingleRequestProxy.address, @@ -95,6 +114,91 @@ describe('contract: ERC20SingleRequestProxy', () => { expect(feeRecipientBalanceAfter).to.equal(feeAmount); }); + it('should process a partial payment correctly', async () => { + // Pay 10 tokens instead of 100 + const paymentAmount = BN.from(10).mul(BASE_DECIMAL); + const totalAmount = paymentAmount.add(feeAmount); + + await testToken.connect(user1).transfer(erc20SingleRequestProxy.address, totalAmount); + + const erc20SingleRequestProxyBalanceBefore = await testToken.balanceOf( + erc20SingleRequestProxy.address, + ); + expect(erc20SingleRequestProxyBalanceBefore).to.equal(totalAmount); + + await expect( + user1.sendTransaction({ + to: erc20SingleRequestProxy.address, + value: 0, + }), + ) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user2Addr, + paymentAmount, + paymentReference, + feeAmount, + feeRecipientAddr, + ); + + const erc20SingleRequestProxyBalanceAfter = await testToken.balanceOf( + erc20SingleRequestProxy.address, + ); + const user2BalanceAfter = await testToken.balanceOf(user2Addr); + const feeRecipientBalanceAfter = await testToken.balanceOf(feeRecipientAddr); + + expect(erc20SingleRequestProxyBalanceAfter).to.equal(0); + expect(user2BalanceAfter).to.equal(paymentAmount); + expect(feeRecipientBalanceAfter).to.equal(feeAmount); + }); + + it('should process a payment with a non-standard ERC20', async () => { + const usdtFeeAmount = BN.from(10).mul(USDT_DECIMAL); + const usdtProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( + user2Addr, + usdtFake.address, + feeRecipientAddr, + usdtFeeAmount, + paymentReference, + erc20FeeProxy.address, + ); + + const paymentAmount = BN.from(50).mul(USDT_DECIMAL); + const totalAmount = paymentAmount.add(usdtFeeAmount); + + await usdtFake.mint(user1Addr, BN.from(1000).mul(USDT_DECIMAL)); + + await usdtFake.connect(user1).transfer(usdtProxy.address, totalAmount); + + const usdtProxyBalanceBefore = await usdtFake.balanceOf(usdtProxy.address); + expect(usdtProxyBalanceBefore).to.equal(totalAmount); + + await expect( + user1.sendTransaction({ + to: usdtProxy.address, + value: 0, + }), + ) + .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + .withArgs( + usdtFake.address, + user2Addr, + paymentAmount, + paymentReference, + usdtFeeAmount, + feeRecipientAddr, + ); + + const usdtProxyBalanceAfter = await usdtFake.balanceOf(usdtProxy.address); + const user2BalanceAfter = await usdtFake.balanceOf(user2Addr); + const feeRecipientBalanceAfter = await usdtFake.balanceOf(feeRecipientAddr); + + expect(usdtProxyBalanceAfter).to.equal(0); + expect(user2BalanceAfter).to.equal(paymentAmount); + expect(feeRecipientBalanceAfter).to.equal(usdtFeeAmount); + }); + it('should revert if called with non-zero value', async () => { await expect( user1.sendTransaction({ @@ -149,4 +253,40 @@ describe('contract: ERC20SingleRequestProxy', () => { }), ).to.be.reverted; }); + + // it('should work with USDT-like non-standard ERC20 tokens', async () => { + // const usdtProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( + // user2Addr, + // usdtFake.address, + // feeRecipientAddr, + // feeAmount, + // paymentReference, + // erc20FeeProxy.address, + // ); + + // const paymentAmount = BN.from(100).mul(USDT_DECIMAL); + // const totalAmount = paymentAmount.add(feeAmount); + + // await usdtFake.transfer(usdtProxy.address, totalAmount); + + // const usdtProxyBalanceBefore = await usdtFake.balanceOf(usdtProxy.address); + // expect(usdtProxyBalanceBefore).to.equal(totalAmount); + + // await expect( + // user1.sendTransaction({ + // to: usdtProxy.address, + // value: 0, + // }), + // ) + // .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') + // .withArgs(usdtFake.address, user2Addr, paymentAmount, paymentReference, feeAmount, feeRecipientAddr); + + // const usdtProxyBalanceAfter = await usdtFake.balanceOf(usdtProxy.address); + // const user2BalanceAfter = await usdtFake.balanceOf(user2Addr); + // const feeRecipientBalanceAfter = await usdtFake.balanceOf(feeRecipientAddr); + + // expect(usdtProxyBalanceAfter).to.equal(0); + // expect(user2BalanceAfter).to.equal(paymentAmount); + // expect(feeRecipientBalanceAfter).to.equal(feeAmount); + // }); }); From 7241e0ca993657a4a3d6dfc646e64ddd3b47d5bc Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 14:05:04 +0300 Subject: [PATCH 29/37] test: add rescue-funds tests --- .../src/contracts/test/ForceSend.sol | 8 +++ .../contracts/ERC20SingleRequestProxy.test.ts | 54 +++++++------------ .../EthereumSingleRequestProxy.test.ts | 26 +++++++++ 3 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 packages/smart-contracts/src/contracts/test/ForceSend.sol diff --git a/packages/smart-contracts/src/contracts/test/ForceSend.sol b/packages/smart-contracts/src/contracts/test/ForceSend.sol new file mode 100644 index 0000000000..5d552c7338 --- /dev/null +++ b/packages/smart-contracts/src/contracts/test/ForceSend.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ForceSend { + function forceSend(address payable recipient) public payable { + selfdestruct(recipient); + } +} diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 8982dff241..5e6172455d 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -254,39 +254,23 @@ describe('contract: ERC20SingleRequestProxy', () => { ).to.be.reverted; }); - // it('should work with USDT-like non-standard ERC20 tokens', async () => { - // const usdtProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( - // user2Addr, - // usdtFake.address, - // feeRecipientAddr, - // feeAmount, - // paymentReference, - // erc20FeeProxy.address, - // ); - - // const paymentAmount = BN.from(100).mul(USDT_DECIMAL); - // const totalAmount = paymentAmount.add(feeAmount); - - // await usdtFake.transfer(usdtProxy.address, totalAmount); - - // const usdtProxyBalanceBefore = await usdtFake.balanceOf(usdtProxy.address); - // expect(usdtProxyBalanceBefore).to.equal(totalAmount); - - // await expect( - // user1.sendTransaction({ - // to: usdtProxy.address, - // value: 0, - // }), - // ) - // .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') - // .withArgs(usdtFake.address, user2Addr, paymentAmount, paymentReference, feeAmount, feeRecipientAddr); - - // const usdtProxyBalanceAfter = await usdtFake.balanceOf(usdtProxy.address); - // const user2BalanceAfter = await usdtFake.balanceOf(user2Addr); - // const feeRecipientBalanceAfter = await usdtFake.balanceOf(feeRecipientAddr); - - // expect(usdtProxyBalanceAfter).to.equal(0); - // expect(user2BalanceAfter).to.equal(paymentAmount); - // expect(feeRecipientBalanceAfter).to.equal(feeAmount); - // }); + it('should rescue ERC20 tokens', async () => { + const rescueAmount = BN.from(100).mul(BASE_DECIMAL); + + // Transfer tokens directly to the contract + await testToken.transfer(erc20SingleRequestProxy.address, rescueAmount); + + const contractBalanceBefore = await testToken.balanceOf(erc20SingleRequestProxy.address); + expect(contractBalanceBefore).to.equal(rescueAmount); + + const payeeBalanceBefore = await testToken.balanceOf(user2Addr); + + await erc20SingleRequestProxy.rescueFunds(); + + const contractBalanceAfter = await testToken.balanceOf(erc20SingleRequestProxy.address); + expect(contractBalanceAfter).to.equal(0); + + const payeeBalanceAfter = await testToken.balanceOf(user2Addr); + expect(payeeBalanceAfter.sub(payeeBalanceBefore)).to.equal(rescueAmount); + }); }); diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts index 38a8865cff..fc9454aafe 100644 --- a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -114,4 +114,30 @@ describe('contract : EthereumSingleRequestProxy', () => { expect(await ethers.provider.getBalance(newEthereumSingleRequestProxy.address)).to.equal(0); expect(await ethers.provider.getBalance(mockEthereumFeeProxy.address)).to.equal(0); }); + + it('should rescue funds', async () => { + const paymentAmount = ethers.utils.parseEther('1'); + const totalAmount = paymentAmount.add(feeAmount); + + const ForceSendFactory = await ethers.getContractFactory('ForceSend'); + const forceSend = await ForceSendFactory.deploy(); + await forceSend.deployed(); + + await forceSend.forceSend(ethereumSingleRequestProxy.address, { value: totalAmount }); + + const balanceAfterForceSend = await ethers.provider.getBalance( + ethereumSingleRequestProxy.address, + ); + expect(balanceAfterForceSend).to.be.gt(0); + expect(balanceAfterForceSend).to.equal(totalAmount); + + const initialPayeeBalance = await ethers.provider.getBalance(payeeAddress); + + await ethereumSingleRequestProxy.rescueFunds(); + + expect(await ethers.provider.getBalance(ethereumSingleRequestProxy.address)).to.equal(0); + + const finalPayeeBalance = await ethers.provider.getBalance(payeeAddress); + expect(finalPayeeBalance.sub(initialPayeeBalance)).to.equal(balanceAfterForceSend); + }); }); From 4abe9e96bcea7564aef7485bdb3c5d7363fd3926 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 14:17:27 +0300 Subject: [PATCH 30/37] test: refactor tests --- .../test/contracts/EthereumSingleRequestProxy.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts index fc9454aafe..3f94699b54 100644 --- a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -2,7 +2,7 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { BigNumber, Signer } from 'ethers'; import { EthereumSingleRequestProxy, EthereumFeeProxy } from '../../src/types'; - +import { BigNumber as BN } from 'ethers'; describe('contract : EthereumSingleRequestProxy', () => { let ethereumSingleRequestProxy: EthereumSingleRequestProxy; let ethereumFeeProxy: EthereumFeeProxy; @@ -13,7 +13,7 @@ describe('contract : EthereumSingleRequestProxy', () => { let feeRecipientAddress: string; const paymentReference: string = ethers.utils.formatBytes32String('payment_reference'); - const feeAmount: string = ethers.utils.parseEther('0.1').toString(); + const feeAmount: BN = ethers.utils.parseEther('0.1'); beforeEach(async () => { [owner, payee, feeRecipient] = await ethers.getSigners(); @@ -54,7 +54,7 @@ describe('contract : EthereumSingleRequestProxy', () => { const totalAmount = BigNumber.from(paymentAmount).add(BigNumber.from(feeAmount)); await expect( - await owner.sendTransaction({ + owner.sendTransaction({ to: ethereumSingleRequestProxy.address, value: totalAmount, }), From 90b413f102e74758a7d02b7a74280544447ad257 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 14:59:39 +0300 Subject: [PATCH 31/37] test: add await back --- .../test/contracts/EthereumSingleRequestProxy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts index 3f94699b54..52e4bea635 100644 --- a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -54,7 +54,7 @@ describe('contract : EthereumSingleRequestProxy', () => { const totalAmount = BigNumber.from(paymentAmount).add(BigNumber.from(feeAmount)); await expect( - owner.sendTransaction({ + await owner.sendTransaction({ to: ethereumSingleRequestProxy.address, value: totalAmount, }), From c4937584d7836acf49f323e9c587d8d10319b765 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 18:31:18 +0300 Subject: [PATCH 32/37] feat: add triggerERC20Payment --- .../src/contracts/ERC20SingleRequestProxy.sol | 12 +++++++++-- .../contracts/ERC20SingleRequestProxy.test.ts | 20 +++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index 96befa0096..44017b9095 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -36,6 +36,14 @@ contract ERC20SingleRequestProxy { receive() external payable { require(msg.value == 0, 'This function is only for triggering the transfer'); + _processPayment(); + } + + function triggerERC20Payment() external { + _processPayment(); + } + + function _processPayment() internal { IERC20 token = IERC20(tokenAddress); uint256 balance = token.balanceOf(address(this)); uint256 paymentAmount = balance; @@ -60,8 +68,8 @@ contract ERC20SingleRequestProxy { * @notice Rescues any trapped funds by sending them to the payee * @dev Can be called by anyone, but funds are always sent to the payee */ - function rescueFunds() external { - IERC20 token = IERC20(tokenAddress); + function rescueFunds(address _tokenAddress) external { + IERC20 token = IERC20(_tokenAddress); uint256 balance = token.balanceOf(address(this)); require(balance > 0, 'No funds to rescue'); bool success = SafeERC20.safeTransfer(token, payee, balance); diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 5e6172455d..1dbd7ad948 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -76,7 +76,7 @@ describe('contract: ERC20SingleRequestProxy', () => { expect(await erc20SingleRequestProxy.erc20FeeProxy()).to.equal(erc20FeeProxy.address); }); - it('should process a payment correctly', async () => { + it('should process a payment correctly via recieve', async () => { const paymentAmount = BN.from(100).mul(BASE_DECIMAL); const totalAmount = paymentAmount.add(feeAmount); @@ -114,9 +114,8 @@ describe('contract: ERC20SingleRequestProxy', () => { expect(feeRecipientBalanceAfter).to.equal(feeAmount); }); - it('should process a partial payment correctly', async () => { - // Pay 10 tokens instead of 100 - const paymentAmount = BN.from(10).mul(BASE_DECIMAL); + it('should process a payment correctly via triggerERC20Payment', async () => { + const paymentAmount = BN.from(100).mul(BASE_DECIMAL); const totalAmount = paymentAmount.add(feeAmount); await testToken.connect(user1).transfer(erc20SingleRequestProxy.address, totalAmount); @@ -126,12 +125,7 @@ describe('contract: ERC20SingleRequestProxy', () => { ); expect(erc20SingleRequestProxyBalanceBefore).to.equal(totalAmount); - await expect( - user1.sendTransaction({ - to: erc20SingleRequestProxy.address, - value: 0, - }), - ) + await expect(erc20SingleRequestProxy.triggerERC20Payment()) .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') .withArgs( testToken.address, @@ -153,6 +147,10 @@ describe('contract: ERC20SingleRequestProxy', () => { expect(feeRecipientBalanceAfter).to.equal(feeAmount); }); + it.skip('should process a partial payment correctly', async () => { + // Smart contract does not keep track of the payment amount, it accepts any amount of tokens + }); + it('should process a payment with a non-standard ERC20', async () => { const usdtFeeAmount = BN.from(10).mul(USDT_DECIMAL); const usdtProxy = await new ERC20SingleRequestProxy__factory(deployer).deploy( @@ -265,7 +263,7 @@ describe('contract: ERC20SingleRequestProxy', () => { const payeeBalanceBefore = await testToken.balanceOf(user2Addr); - await erc20SingleRequestProxy.rescueFunds(); + await erc20SingleRequestProxy.rescueFunds(testToken.address); const contractBalanceAfter = await testToken.balanceOf(erc20SingleRequestProxy.address); expect(contractBalanceAfter).to.equal(0); From 06e015a5e2d6e9762dd4a4bea86bfae186c8f010 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 18:33:15 +0300 Subject: [PATCH 33/37] chore: add require for zero address --- .../smart-contracts/src/contracts/ERC20SingleRequestProxy.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index 44017b9095..a73a2157c0 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -65,10 +65,11 @@ contract ERC20SingleRequestProxy { } /** - * @notice Rescues any trapped funds by sending them to the payee + * @notice Rescues any trapped funds by sending them to the payee * @dev Can be called by anyone, but funds are always sent to the payee */ function rescueFunds(address _tokenAddress) external { + require(_tokenAddress != address(0), 'Invalid token address'); IERC20 token = IERC20(_tokenAddress); uint256 balance = token.balanceOf(address(this)); require(balance > 0, 'No funds to rescue'); From c778b17f849f61a447e3c81990fd055c7e122221 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 18:41:31 +0300 Subject: [PATCH 34/37] feat: add rescue methods for ERC20 and native tokens --- .../src/contracts/ERC20SingleRequestProxy.sol | 12 ++++++++++++ .../contracts/EthereumSingleRequestProxy.sol | 18 +++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index a73a2157c0..c7b93636f4 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -76,4 +76,16 @@ contract ERC20SingleRequestProxy { bool success = SafeERC20.safeTransfer(token, payee, balance); require(success, 'ERC20 rescue failed'); } + + /** + * @notice Rescues any trapped funds by sending them to the payee + * @dev Can be called by anyone, but funds are always sent to the payee + */ + function rescueNativeFunds() external { + uint256 balance = address(this).balance; + require(balance > 0, 'No funds to rescue'); + + (bool success, ) = payable(payee).call{value: balance}(''); + require(success, 'Rescue failed'); + } } diff --git a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol index 104d3d5dc8..03d8752da6 100644 --- a/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumSingleRequestProxy.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.9; import './interfaces/EthereumFeeProxy.sol'; +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './lib/SafeERC20.sol'; /** * @title EthereumSingleRequestProxy @@ -88,11 +90,25 @@ contract EthereumSingleRequestProxy { * @notice Rescues any trapped funds by sending them to the payee * @dev Can be called by anyone, but funds are always sent to the payee */ - function rescueFunds() external nonReentrant { + function rescueNativeFunds() external nonReentrant { uint256 balance = address(this).balance; require(balance > 0, 'No funds to rescue'); (bool success, ) = payable(payee).call{value: balance}(''); require(success, 'Rescue failed'); } + + /** + * @notice Rescues any trapped ERC20 funds by sending them to the payee + * @dev Can be called by anyone, but funds are always sent to the payee + * @param _tokenAddress The address of the ERC20 token to rescue + */ + function rescueERC20Funds(address _tokenAddress) external nonReentrant { + require(_tokenAddress != address(0), 'Invalid token address'); + IERC20 token = IERC20(_tokenAddress); + uint256 balance = token.balanceOf(address(this)); + require(balance > 0, 'No funds to rescue'); + bool success = SafeERC20.safeTransfer(token, payee, balance); + require(success, 'Rescue failed'); + } } From b45607ad568db447856880057295712e8ccbd490 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 18:50:43 +0300 Subject: [PATCH 35/37] test: add tests for both rescue methods --- .../contracts/ERC20SingleRequestProxy.test.ts | 19 ++++++++++++ .../EthereumSingleRequestProxy.test.ts | 31 +++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 1dbd7ad948..3ee69bca9b 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -271,4 +271,23 @@ describe('contract: ERC20SingleRequestProxy', () => { const payeeBalanceAfter = await testToken.balanceOf(user2Addr); expect(payeeBalanceAfter.sub(payeeBalanceBefore)).to.equal(rescueAmount); }); + + it('should rescue native funds', async () => { + const paymentAmount = ethers.utils.parseEther('1'); + const totalAmount = paymentAmount.add(feeAmount); + + const ForceSendFactory = await ethers.getContractFactory('ForceSend'); + const forceSend = await ForceSendFactory.deploy(); + await forceSend.deployed(); + + await forceSend.forceSend(erc20SingleRequestProxy.address, { value: totalAmount }); + + const contractBalanceBefore = await ethers.provider.getBalance(erc20SingleRequestProxy.address); + expect(contractBalanceBefore).to.gt(0); + + await erc20SingleRequestProxy.rescueNativeFunds(); + + const contractBalanceAfter = await ethers.provider.getBalance(erc20SingleRequestProxy.address); + expect(contractBalanceAfter).to.equal(0); + }); }); diff --git a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts index 52e4bea635..7cb7982e59 100644 --- a/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumSingleRequestProxy.test.ts @@ -1,7 +1,7 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { BigNumber, Signer } from 'ethers'; -import { EthereumSingleRequestProxy, EthereumFeeProxy } from '../../src/types'; +import { EthereumSingleRequestProxy, EthereumFeeProxy, TestToken__factory } from '../../src/types'; import { BigNumber as BN } from 'ethers'; describe('contract : EthereumSingleRequestProxy', () => { let ethereumSingleRequestProxy: EthereumSingleRequestProxy; @@ -115,7 +115,7 @@ describe('contract : EthereumSingleRequestProxy', () => { expect(await ethers.provider.getBalance(mockEthereumFeeProxy.address)).to.equal(0); }); - it('should rescue funds', async () => { + it('should rescue native funds', async () => { const paymentAmount = ethers.utils.parseEther('1'); const totalAmount = paymentAmount.add(feeAmount); @@ -133,11 +133,36 @@ describe('contract : EthereumSingleRequestProxy', () => { const initialPayeeBalance = await ethers.provider.getBalance(payeeAddress); - await ethereumSingleRequestProxy.rescueFunds(); + await ethereumSingleRequestProxy.rescueNativeFunds(); expect(await ethers.provider.getBalance(ethereumSingleRequestProxy.address)).to.equal(0); const finalPayeeBalance = await ethers.provider.getBalance(payeeAddress); expect(finalPayeeBalance.sub(initialPayeeBalance)).to.equal(balanceAfterForceSend); }); + + it('should rescue ERC20 funds', async () => { + const rescueAmount = BN.from(100).mul(18); + const [deployer] = await ethers.getSigners(); + + const deployerAddr = await deployer.getAddress(); + const testToken = await new TestToken__factory(deployer).deploy(deployerAddr); + await testToken.mint(deployerAddr, BN.from(1000000).mul(18)); + + await testToken.transfer(ethereumSingleRequestProxy.address, rescueAmount); + + const contractBalanceBefore = await testToken.balanceOf(ethereumSingleRequestProxy.address); + + const initialPayeeBalance = await testToken.balanceOf(payeeAddress); + expect(initialPayeeBalance).to.equal(0); + expect(contractBalanceBefore).to.equal(rescueAmount); + + await ethereumSingleRequestProxy.rescueERC20Funds(testToken.address); + + const contractBalanceAfter = await testToken.balanceOf(ethereumSingleRequestProxy.address); + ethereumSingleRequestProxy.address, expect(contractBalanceAfter).to.equal(0); + + const finalPayeeBalance = await testToken.balanceOf(payeeAddress); + expect(finalPayeeBalance.sub(initialPayeeBalance)).to.equal(rescueAmount); + }); }); From c58dfc074f8dff7bd71a541aad22c3f57383c321 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 17 Oct 2024 18:52:29 +0300 Subject: [PATCH 36/37] fix: rename rescueFunds to rescueERC20Funds --- .../smart-contracts/src/contracts/ERC20SingleRequestProxy.sol | 2 +- .../test/contracts/ERC20SingleRequestProxy.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol index c7b93636f4..c2cf1b534c 100644 --- a/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol +++ b/packages/smart-contracts/src/contracts/ERC20SingleRequestProxy.sol @@ -68,7 +68,7 @@ contract ERC20SingleRequestProxy { * @notice Rescues any trapped funds by sending them to the payee * @dev Can be called by anyone, but funds are always sent to the payee */ - function rescueFunds(address _tokenAddress) external { + function rescueERC20Funds(address _tokenAddress) external { require(_tokenAddress != address(0), 'Invalid token address'); IERC20 token = IERC20(_tokenAddress); uint256 balance = token.balanceOf(address(this)); diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 3ee69bca9b..7b2c5f5997 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -263,7 +263,7 @@ describe('contract: ERC20SingleRequestProxy', () => { const payeeBalanceBefore = await testToken.balanceOf(user2Addr); - await erc20SingleRequestProxy.rescueFunds(testToken.address); + await erc20SingleRequestProxy.rescueERC20Funds(testToken.address); const contractBalanceAfter = await testToken.balanceOf(erc20SingleRequestProxy.address); expect(contractBalanceAfter).to.equal(0); From 9ee661a6d1b1c9c93594fab0d97b0b1d316aed48 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Fri, 18 Oct 2024 12:39:01 +0300 Subject: [PATCH 37/37] fix: typo in "receive" --- .../test/contracts/ERC20SingleRequestProxy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts index 7b2c5f5997..8384641791 100644 --- a/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20SingleRequestProxy.test.ts @@ -76,7 +76,7 @@ describe('contract: ERC20SingleRequestProxy', () => { expect(await erc20SingleRequestProxy.erc20FeeProxy()).to.equal(erc20FeeProxy.address); }); - it('should process a payment correctly via recieve', async () => { + it('should process a payment correctly via receive', async () => { const paymentAmount = BN.from(100).mul(BASE_DECIMAL); const totalAmount = paymentAmount.add(feeAmount);