-
Notifications
You must be signed in to change notification settings - Fork 84
feat: Develop SingleRequestProxy
Smart Contracts
#1453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2567f4
c624474
917d472
d5e4728
0543092
2bd3495
19b5e40
c1d3a77
1376582
c2f55ee
bc922e8
8aef234
0769aa7
3bd9e38
ed429ae
d9a4484
a341ce1
10974b5
2cc82a0
bee1f58
aab6711
0c68eba
796f2b9
0482f6d
b8183f3
85fdc13
9033066
cd91ec2
f944199
c77c1eb
41e43b5
01c28d2
cfba1b9
7241e0c
4abe9e9
90b413f
c493758
06e015a
c778b17
b45607a
c58dfc0
9ee661a
0141d22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.9; | ||
|
||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | ||
import './interfaces/ERC20FeeProxy.sol'; | ||
import './lib/SafeERC20.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 | ||
*/ | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
contract ERC20SingleRequestProxy { | ||
address public payee; | ||
address public tokenAddress; | ||
address public feeAddress; | ||
uint256 public feeAmount; | ||
leoslr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bytes public paymentReference; | ||
IERC20FeeProxy public erc20FeeProxy; | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
} | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
receive() external payable { | ||
require(msg.value == 0, 'This function is only for triggering the transfer'); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_processPayment(); | ||
} | ||
|
||
function triggerERC20Payment() external { | ||
_processPayment(); | ||
} | ||
|
||
function _processPayment() internal { | ||
IERC20 token = IERC20(tokenAddress); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
require(SafeERC20.safeApprove(token, address(erc20FeeProxy), balance), 'Approval failed'); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
erc20FeeProxy.transferFromWithReferenceAndFee( | ||
tokenAddress, | ||
payee, | ||
paymentAmount, | ||
paymentReference, | ||
feeAmount, | ||
feeAddress | ||
); | ||
} | ||
Comment on lines
+46
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add Reentrancy Guard to The Consider implementing a reentrancy guard to secure this function. You can use a custom reentrancy guard similar to the one used in the Apply this diff to add a reentrancy guard: + bool private locked;
function _processPayment() internal {
+ require(!locked, "Reentrant call detected");
+ locked = true;
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;
}
SafeERC20.safeApprove(token, address(erc20FeeProxy), balance);
erc20FeeProxy.transferFromWithReferenceAndFee(
tokenAddress,
payee,
paymentAmount,
paymentReference,
feeAmount,
feeAddress
);
+ locked = false;
}
|
||
|
||
/** | ||
* @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 rescueERC20Funds(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'); | ||
bool success = SafeERC20.safeTransfer(token, payee, balance); | ||
require(success, 'ERC20 rescue failed'); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
Comment on lines
+71
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add Reentrancy Guard to The Implement a reentrancy guard to secure this function. You can reuse the same guard applied in Apply this diff to add the guard: function rescueERC20Funds(address _tokenAddress) external {
+ require(!locked, "Reentrant call detected");
+ locked = true;
require(_tokenAddress != address(0), 'Invalid token address');
IERC20 token = IERC20(_tokenAddress);
uint256 balance = token.balanceOf(address(this));
require(balance > 0, 'No funds to rescue');
SafeERC20.safeTransfer(token, payee, balance);
+ locked = false;
}
|
||
/** | ||
* @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'); | ||
} | ||
Comment on lines
+84
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add Reentrancy Guard to The Add a reentrancy guard to this function to enhance security. Apply this diff to add the guard: function rescueNativeFunds() external {
+ require(!locked, "Reentrant call detected");
+ locked = true;
uint256 balance = address(this).balance;
require(balance > 0, 'No funds to rescue');
(bool success, ) = payable(payee).call{value: balance}('');
require(success, 'Rescue failed');
+ locked = false;
}
|
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,114 @@ | ||||||||||||||||||||||||||||||||
// SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||||||
pragma solidity 0.8.9; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import './interfaces/EthereumFeeProxy.sol'; | ||||||||||||||||||||||||||||||||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | ||||||||||||||||||||||||||||||||
import './lib/SafeERC20.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; | ||||||||||||||||||||||||||||||||
leoslr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
IEthereumFeeProxy public ethereumFeeProxy; | ||||||||||||||||||||||||||||||||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
address private originalSender; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||
* @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; | ||||||||||||||||||||||||||||||||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
constructor( | ||||||||||||||||||||||||||||||||
address _payee, | ||||||||||||||||||||||||||||||||
bytes memory _paymentReference, | ||||||||||||||||||||||||||||||||
address _ethereumFeeProxy, | ||||||||||||||||||||||||||||||||
address _feeAddress, | ||||||||||||||||||||||||||||||||
uint256 _feeAmount | ||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||
payee = _payee; | ||||||||||||||||||||||||||||||||
paymentReference = _paymentReference; | ||||||||||||||||||||||||||||||||
feeAddress = _feeAddress; | ||||||||||||||||||||||||||||||||
feeAmount = _feeAmount; | ||||||||||||||||||||||||||||||||
ethereumFeeProxy = IEthereumFeeProxy(_ethereumFeeProxy); | ||||||||||||||||||||||||||||||||
_status = _NOT_ENTERED; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||
* @dev Modified nonReentrant guard. | ||||||||||||||||||||||||||||||||
* Prevents reentrancy except for calls from ethereumFeeProxy. | ||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||
modifier nonReentrant() { | ||||||||||||||||||||||||||||||||
if (msg.sender != address(ethereumFeeProxy)) { | ||||||||||||||||||||||||||||||||
require(_status != _ENTERED, 'ReentrancyGuard: reentrant call'); | ||||||||||||||||||||||||||||||||
_status = _ENTERED; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
_; | ||||||||||||||||||||||||||||||||
if (msg.sender != address(ethereumFeeProxy)) { | ||||||||||||||||||||||||||||||||
_status = _NOT_ENTERED; | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
receive() external payable nonReentrant { | ||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
payable(feeAddress) | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
(bool callSuccess, ) = address(ethereumFeeProxy).call{value: msg.value}(data); | ||||||||||||||||||||||||||||||||
require(callSuccess, 'Call to EthereumFeeProxy failed'); | ||||||||||||||||||||||||||||||||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||
* @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 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'); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+106
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect usage of The Apply this diff to fix the code: - bool success = SafeERC20.safeTransfer(token, payee, balance);
- require(success, 'Rescue failed');
+ SafeERC20.safeTransfer(token, payee, balance); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.9; | ||
|
||
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 { | ||
/// @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( | ||
address indexed proxyAddress, | ||
address indexed payee, | ||
bytes indexed paymentReference | ||
); | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
event ERC20SingleRequestProxyCreated( | ||
address indexed proxyAddress, | ||
address indexed payee, | ||
address tokenAddress, | ||
bytes indexed paymentReference | ||
); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
event ERC20FeeProxyUpdated(address indexed newERC20FeeProxy); | ||
event EthereumFeeProxyUpdated(address indexed newEthereumFeeProxy); | ||
|
||
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; | ||
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 EthereumSingleRequestProxyCreated(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); | ||
} | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @notice Updates the ERC20FeeProxy address | ||
* @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); | ||
} | ||
|
||
/** | ||
* @notice Updates the EthereumFeeProxy address | ||
* @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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function sendFundsBack(address payable _to, uint256 _amount) external { | ||
(bool success, ) = _to.call{value: _amount}(''); | ||
require(success, 'Failed to send funds back'); | ||
} | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
receive() external payable {} | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.