Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
05f5632
feat: implement settleIn function for onramp order processing and upd…
onahprosper Jan 24, 2026
3d24b24
refactor: update settleIn and settleOut function documentation for cl…
onahprosper Jan 24, 2026
798b088
refactor: rename OrderSettled event to SettleOut and update order exi…
Jan 26, 2026
f282199
refactor: update Gateway contract to rename settle function to settle…
Jan 26, 2026
662ea53
chore: update package-lock and yarn.lock files to remove extraneous d…
Jan 26, 2026
122d9f2
refactor: update comments in Gateway and IGateway contracts for clari…
onahprosper Jan 26, 2026
b631eb0
Refactor code structure for improved readability and maintainability
onahprosper Jan 26, 2026
11b7047
fix: update Gateway contract to handle aggregator fees instead of pro…
onahprosper Jan 26, 2026
72204c2
refactor: update Gateway contract to improve fee handling and add agg…
chibie Jan 26, 2026
3aeaca3
Refactor fee handling in Gateway contract to calculate recipient amou…
onahprosper Jan 27, 2026
501425a
Refactor code structure for improved readability and maintainability
onahprosper Jan 28, 2026
4fe2f00
feat:
onahprosper Feb 2, 2026
cdf3ba0
chore: update hardhat configuration and dependencies
chibie Feb 2, 2026
af5d499
feat: add custom Hardhat tasks for account listing and contract flatt…
onahprosper Feb 2, 2026
eeaf0d0
chore: update Hardhat configuration and dependencies
onahprosper Mar 18, 2026
c1b88ef
refactor: enhance Gateway contract with SafeERC20 for secure token tr…
onahprosper Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 114 additions & 31 deletions contracts/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable {
AGGREGATOR FUNCTIONS
################################################################## */
/** @dev See {settle-IGateway}. */
function settle(
function settleOut(
bytes32 _splitOrderId,
bytes32 _orderId,
address _liquidityProvider,
Expand All @@ -180,25 +180,23 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable {

if (order[_orderId].senderFee != 0 && order[_orderId].protocolFee != 0) {
// fx transfer - sender keeps all fee
_handleFxTransferFeeSplitting(_orderId);
_handleFxTransferFeeSplitting(_orderId, token, order[_orderId].senderFeeRecipient, order[_orderId].senderFee);
}
}

if (order[_orderId].senderFee != 0 && order[_orderId].protocolFee == 0) {
// local transfer - split sender fee
_handleLocalTransferFeeSplitting(_orderId, _liquidityProvider, _settlePercent);
_handleLocalTransferFeeSplitting(_orderId, _liquidityProvider, order[_orderId].senderFeeRecipient, _settlePercent);
}

// transfer to liquidity provider
uint256 liquidityProviderAmount = (order[_orderId].amount * _settlePercent) /
currentOrderBPS;
uint256 liquidityProviderAmount = (order[_orderId].amount * _settlePercent) / currentOrderBPS;
order[_orderId].amount -= liquidityProviderAmount;

if (order[_orderId].protocolFee != 0) {
// FX transfer - use token-specific providerToAggregatorFx
TokenFeeSettings memory settings = _tokenFeeSettings[order[_orderId].token];
uint256 protocolFee = (liquidityProviderAmount * settings.providerToAggregatorFx) /
MAX_BPS;
uint256 protocolFee = (liquidityProviderAmount * settings.providerToAggregatorFx) / MAX_BPS;
liquidityProviderAmount -= protocolFee;

if (_rebatePercent != 0) {
Expand Down Expand Up @@ -226,6 +224,88 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable {
return true;
}

/** @dev See {processSettlement-IGateway}. */
function settleIn(
bytes32 _orderId,
address _token,
uint256 _amount,
address _senderFeeRecipient,
uint96 _senderFee,
address _recipient,
uint96 _rate,
string calldata _messageHash
) external whenNotPaused returns (bool) {
require(!order[_orderId].isFulfilled, 'OrderAlreadyFulfilled');
require(_amount > MAX_BPS, 'AmountBelowMinimum');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AmountBelowMinimum should be InvalidAmount

_handler(_token, _amount, _recipient, _senderFeeRecipient, _senderFee);

IERC20(_token).transferFrom(msg.sender, address(this), _amount);

uint256 processedAmount = _amount;
uint256 protocolFee;

// Determine if this is FX or local transfer based on rate
if (_rate == 100) {
// Local transfer (rate = 1) - no protocol fee from amount
require(_senderFee > 0, 'SenderFeeIsZero');
protocolFee = 0;
// Split sender fee for local transfers (100% settlement)
processedAmount -= _senderFee;
} else {
// FX transfer (rate != 1) - use token-specific providerToAggregatorFx
TokenFeeSettings memory settings = _tokenFeeSettings[_token];
require(settings.providerToAggregatorFx > 0, 'TokenFeeSettingsNotConfigured');

protocolFee = (_amount * settings.providerToAggregatorFx) / MAX_BPS;

if (protocolFee > 0) {
processedAmount -= protocolFee;
IERC20(_token).transfer(treasuryAddress, protocolFee);
}

if (_senderFee != 0) {
// For FX transfers, handle sender fee similar to settleOut
processedAmount -= _senderFee;
}
}

IERC20(_token).transfer(_recipient, processedAmount);

// record the order state
order[_orderId].sender = _recipient;
order[_orderId].token = _token;
order[_orderId].senderFeeRecipient = _senderFeeRecipient;
order[_orderId].senderFee = _senderFee;
order[_orderId].protocolFee = protocolFee;
order[_orderId].isFulfilled = true;
order[_orderId].amount = processedAmount;
order[_orderId].currentBPS = 0; // Fully settled

// Handle fee splitting after order state is recorded
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start lowercase

if (_senderFee != 0) {
if (protocolFee == 0) {
// Local transfer - split sender fee using the consolidated function
_handleLocalTransferFeeSplitting(_orderId, msg.sender, _senderFeeRecipient, MAX_BPS);
} else {
// FX transfer - sender keeps all fee using the consolidated function
_handleFxTransferFeeSplitting(_orderId, _token, _senderFeeRecipient, _senderFee);
}
}

// emit settlement event
emit SettleIn(
_orderId,
_amount,
_recipient,
_token,
_senderFeeRecipient,
_rate,
_messageHash
);

return true;
}

/** @dev See {refund-IGateway}. */
function refund(uint256 _fee, bytes32 _orderId) external onlyAggregator returns (bool) {
// ensure the transaction has not been fulfilled
Expand Down Expand Up @@ -275,73 +355,76 @@ contract Gateway is IGateway, GatewaySettingManager, PausableUpgradeable {
* @dev Handles fee splitting for local transfers (rate = 1).
* @param _orderId The order ID to process.
* @param _liquidityProvider The address of the liquidity provider who fulfilled the order.
* @param _settlePercent The percentage of the order being settled (10000 for 100% in settleIn).
*/
function _handleLocalTransferFeeSplitting(
bytes32 _orderId,
address _liquidityProvider,
address _senderFeeRecipient,
uint64 _settlePercent
) internal {
TokenFeeSettings memory settings = _tokenFeeSettings[order[_orderId].token];
uint256 senderFee = order[_orderId].senderFee;
address token = order[_orderId].token;

// Calculate splits based on config
uint256 providerAmount = (senderFee * settings.senderToProvider) / MAX_BPS;
uint256 currentProviderAmount = (providerAmount * _settlePercent) / MAX_BPS;
uint256 aggregatorAmount = (currentProviderAmount * settings.providerToAggregator) /
MAX_BPS;
uint256 aggregatorAmount = (currentProviderAmount * settings.providerToAggregator) / MAX_BPS;
uint256 senderAmount = senderFee - providerAmount;

// Transfer sender portion
// Transfer sender portion (only when order is fully settled)
if (senderAmount != 0 && order[_orderId].currentBPS == 0) {
IERC20(order[_orderId].token).transfer(
order[_orderId].senderFeeRecipient,
senderAmount
);
IERC20(token).transfer(_senderFeeRecipient, senderAmount);
}

// Transfer aggregator portion to treasury
if (aggregatorAmount != 0) {
IERC20(order[_orderId].token).transfer(treasuryAddress, aggregatorAmount);
IERC20(token).transfer(treasuryAddress, aggregatorAmount);
}

// Transfer provider portion to the liquidity provider who fulfilled the order
currentProviderAmount = currentProviderAmount - aggregatorAmount;
if (currentProviderAmount != 0) {
IERC20(order[_orderId].token).transfer(_liquidityProvider, currentProviderAmount);
IERC20(token).transfer(_liquidityProvider, currentProviderAmount);
}

// Emit events
emit SenderFeeTransferred(order[_orderId].senderFeeRecipient, senderAmount);
emit SenderFeeTransferred(_orderId, _senderFeeRecipient, senderAmount);
emit LocalTransferFeeSplit(_orderId, senderAmount, currentProviderAmount, aggregatorAmount);
}

/**
* @dev Handles fee splitting for FX transfers (rate != 1).
* @param _orderId The order ID to process.
* @dev Handles sender fee splitting for FX transfers.
* @param _orderId The order ID.
* @param _token The token address.
* @param _senderFeeRecipient The sender fee recipient address.
* @param _senderFee The total sender fee amount.
*/
function _handleFxTransferFeeSplitting(bytes32 _orderId) internal {
TokenFeeSettings memory settings = _tokenFeeSettings[order[_orderId].token];
uint256 senderFee = order[_orderId].senderFee;
function _handleFxTransferFeeSplitting(
bytes32 _orderId,
address _token,
address _senderFeeRecipient,
uint256 _senderFee
) internal {
TokenFeeSettings memory settings = _tokenFeeSettings[_token];

// Calculate sender portion based on senderToAggregator setting
uint256 senderAmount = (senderFee * (MAX_BPS - settings.senderToAggregator)) / MAX_BPS;
uint256 aggregatorAmount = senderFee - senderAmount;
// Calculate sender portion based on senderToAggregator setting (similar to settleOut FX)
uint256 senderAmount = (_senderFee * (MAX_BPS - settings.senderToAggregator)) / MAX_BPS;
uint256 aggregatorAmount = _senderFee - senderAmount;

// Transfer sender portion
if (senderAmount > 0) {
IERC20(order[_orderId].token).transfer(
order[_orderId].senderFeeRecipient,
senderAmount
);
IERC20(_token).transfer(_senderFeeRecipient, senderAmount);
}

// Transfer aggregator portion to treasury
if (aggregatorAmount > 0) {
IERC20(order[_orderId].token).transfer(treasuryAddress, aggregatorAmount);
IERC20(_token).transfer(treasuryAddress, aggregatorAmount);
}

// Emit events
emit SenderFeeTransferred(order[_orderId].senderFeeRecipient, senderAmount);
emit SenderFeeTransferred(_orderId, _senderFeeRecipient, senderAmount);
emit FxTransferFeeSplit(_orderId, senderAmount, aggregatorAmount);
}
}
108 changes: 108 additions & 0 deletions contracts/ProviderBatchCallAndSponsor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

interface IGateway {
function getAggregator() external view returns (address);
}
/**
* @title ProviderBatchCallAndSponsor
*
* When an EOA upgrades via EIP‑7702, it delegates to this implementation.
* Off‑chain, the account signs a message authorizing a batch of calls. The message is the hash of:
* keccak256(abi.encodePacked(nonce, calls))
* The signature must be generated with the EOA’s private key so that, once upgraded, the recovered signer equals the account’s own address (i.e. address(this)).
*
* This contract provides just one way to execute a batch:
* 1. With a signature: Any sponsor can submit the batch if it carries a valid signature.
*
* Replay protection is achieved by using a nonce that is included in the signed message.
*/
contract ProviderBatchCallAndSponsor {
using ECDSA for bytes32;

address public constant gatewayAddress = 0x30F6A8457F8E42371E204a9c103f2Bd42341dD0F;
/// @notice A nonce used for replay protection.
uint256 public nonce;

/// @notice Represents a single call within a batch.
struct Call {
address to;
uint256 value;
bytes data;
}

modifier onlyAggregator() {
require(msg.sender == IGateway(gatewayAddress).getAggregator(), 'OnlyAggregator');
_;
}

/// @notice Emitted for every individual call executed.
event CallExecuted(address indexed sender, address indexed to, uint256 value, bytes data);
/// @notice Emitted when a full batch is executed.
event BatchExecuted(uint256 indexed nonce, Call[] calls);

/**
* @notice Executes a batch of calls using an off–chain signature.
* @param calls An array of Call structs containing destination, ETH value, and calldata.
* @param signature The ECDSA signature over the current nonce and the call data.
*
* The signature must be produced off–chain by signing:
* The signing key should be the account’s key (which becomes the smart account’s own identity after upgrade).
*/
function execute(Call[] calldata calls, bytes calldata signature) external payable onlyAggregator {
// Compute the digest that the account was expected to sign.
bytes memory encodedCalls;
for (uint256 i = 0; i < calls.length; i++) {
encodedCalls = abi.encodePacked(encodedCalls, calls[i].to, calls[i].value, calls[i].data);
}
bytes32 digest = keccak256(abi.encodePacked(nonce, encodedCalls));

bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(digest);

// Recover the signer from the provided signature.
address recovered = ECDSA.recover(ethSignedMessageHash, signature);
require(recovered == address(this), "Invalid signature");

_executeBatch(calls);
}

/**
* @notice Executes a batch of calls directly.
* @dev This contract doesnt authorized self execution.
* @param calls An array of Call structs containing destination, ETH value, and calldata.
*/
function execute(Call[] calldata calls) external payable {
revert("Not implemented"); // we don't expect this to be called directly
}

/**
* @dev Internal function that handles batch execution and nonce incrementation.
* @param calls An array of Call structs.
*/
function _executeBatch(Call[] calldata calls) internal {
uint256 currentNonce = nonce;
nonce++; // Increment nonce to protect against replay attacks

for (uint256 i = 0; i < calls.length; i++) {
_executeCall(calls[i]);
}

emit BatchExecuted(currentNonce, calls);
}

/**
* @dev Internal function to execute a single call.
* @param callItem The Call struct containing destination, value, and calldata.
*/
function _executeCall(Call calldata callItem) internal {
(bool success,) = callItem.to.call{value: callItem.value}(callItem.data);
require(success, "Call reverted");
emit CallExecuted(msg.sender, callItem.to, callItem.value, callItem.data);
}

// Allow the contract to receive ETH (e.g. from DEX swaps or other transfers).
fallback() external payable {}
receive() external payable {}
}
Loading