Skip to content

Commit 4100b90

Browse files
authored
feat: Enable deposit and match orders in a single tx (#316)
1 parent a8d8779 commit 4100b90

File tree

9 files changed

+735
-6
lines changed

9 files changed

+735
-6
lines changed

abis/contracts/facets/IexecEscrowTokenFacet.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@
227227
},
228228
{
229229
"internalType": "bytes",
230-
"name": "",
230+
"name": "data",
231231
"type": "bytes"
232232
}
233233
],
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
"function matchOrders(tuple(address,uint256,uint256,bytes32,address,address,address,bytes32,bytes),tuple(address,uint256,uint256,bytes32,address,address,address,bytes32,bytes),tuple(address,uint256,uint256,bytes32,uint256,uint256,address,address,address,bytes32,bytes),tuple(address,uint256,address,uint256,address,uint256,address,uint256,bytes32,uint256,uint256,address,address,string,bytes32,bytes)) pure returns (bytes32)"
3+
]

contracts/facets/IexecEscrowTokenFacet.sol

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {IexecERC20Core} from "./IexecERC20Core.sol";
77
import {FacetBase} from "./FacetBase.sol";
88
import {IexecEscrowToken} from "../interfaces/IexecEscrowToken.sol";
99
import {IexecTokenSpender} from "../interfaces/IexecTokenSpender.sol";
10+
import {IexecPoco1} from "../interfaces/IexecPoco1.sol";
11+
import {IexecLibOrders_v5} from "../libs/IexecLibOrders_v5.sol";
1012
import {PocoStorageLib} from "../libs/PocoStorageLib.sol";
1113

1214
contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, FacetBase, IexecERC20Core {
@@ -64,23 +66,121 @@ contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, FacetBase
6466
return delta;
6567
}
6668

67-
// Token Spender (endpoint for approveAndCallback calls to the proxy)
69+
/***************************************************************************
70+
* Token Spender: Atomic Deposit+Match *
71+
***************************************************************************/
72+
73+
/**
74+
* @notice Receives approval, deposit and optionally matches orders in one transaction
75+
*
76+
* Usage patterns:
77+
* 1. Simple deposit: RLC.approveAndCall(escrow, amount, "")
78+
* 2. Deposit + match: RLC.approveAndCall(escrow, amount, encodedOrders)
79+
*
80+
* The `data` parameter should be ABI-encoded orders if matching is desired:
81+
* abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder)
82+
*
83+
* @dev Important notes:
84+
* - Match orders sponsoring is NOT supported. The requester (sender) always pays for the deal.
85+
* - Clients must compute the exact deal cost and deposit the right amount for the deal to be matched.
86+
* The deal cost = (appPrice + datasetPrice + workerpoolPrice) * volume.
87+
* - If insufficient funds are deposited, the match will fail.
88+
*
89+
* @param sender The address that approved tokens (must be requester if matching)
90+
* @param amount Amount of tokens approved and to be deposited
91+
* @param token Address of the token (must be RLC)
92+
* @param data Optional: ABI-encoded orders for matching
93+
* @return success True if operation succeeded
94+
*
95+
*
96+
* @custom:example
97+
* ```solidity
98+
* // Compute deal cost
99+
* uint256 dealCost = (appPrice + datasetPrice + workerpoolPrice) * volume;
100+
*
101+
* // Encode orders
102+
* bytes memory data = abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder);
103+
*
104+
* // One transaction does it all
105+
* RLC(token).approveAndCall(iexecProxy, dealCost, data);
106+
* ```
107+
*/
68108
function receiveApproval(
69109
address sender,
70110
uint256 amount,
71111
address token,
72-
bytes calldata
112+
bytes calldata data
73113
) external override returns (bool) {
74114
PocoStorageLib.PocoStorage storage $ = PocoStorageLib.getPocoStorage();
75115
require(token == address($.m_baseToken), "wrong-token");
76116
_deposit(sender, amount);
77117
_mint(sender, amount);
118+
if (data.length > 0) {
119+
_decodeDataAndMatchOrders(sender, data);
120+
}
78121
return true;
79122
}
80123

124+
/******************************************************************************
125+
* Token Spender: Atomic Deposit+Match if used with RLC.approveAndCall *
126+
*****************************************************************************/
127+
128+
/**
129+
* @dev Internal function to match orders after deposit
130+
* @param sender The user who deposited (must be the requester)
131+
* @param data ABI-encoded orders
132+
*/
133+
function _decodeDataAndMatchOrders(address sender, bytes calldata data) internal {
134+
// Decode the orders from calldata
135+
(
136+
IexecLibOrders_v5.AppOrder memory apporder,
137+
IexecLibOrders_v5.DatasetOrder memory datasetorder,
138+
IexecLibOrders_v5.WorkerpoolOrder memory workerpoolorder,
139+
IexecLibOrders_v5.RequestOrder memory requestorder
140+
) = abi.decode(
141+
data,
142+
(
143+
IexecLibOrders_v5.AppOrder,
144+
IexecLibOrders_v5.DatasetOrder,
145+
IexecLibOrders_v5.WorkerpoolOrder,
146+
IexecLibOrders_v5.RequestOrder
147+
)
148+
);
149+
150+
// Validate that sender is the requester
151+
if (requestorder.requester != sender) revert("caller-must-be-requester");
152+
153+
// Call matchOrders on the IexecPoco1 facet through the diamond
154+
// Using delegatecall for safety: preserves msg.sender context (RLC address in this case)
155+
// Note: matchOrders doesn't use msg.sender, but delegatecall is safer
156+
// in case the implementation changes in the future
157+
(bool success, bytes memory result) = address(this).delegatecall(
158+
abi.encodeWithSelector(
159+
IexecPoco1.matchOrders.selector,
160+
apporder,
161+
datasetorder,
162+
workerpoolorder,
163+
requestorder
164+
)
165+
);
166+
167+
// Handle failure and bubble up revert reason
168+
if (!success) {
169+
if (result.length > 0) {
170+
// Decode and revert with the original error
171+
assembly {
172+
let returndata_size := mload(result)
173+
revert(add(result, 32), returndata_size)
174+
}
175+
} else {
176+
revert("receive-approval-failed");
177+
}
178+
}
179+
}
180+
81181
function _deposit(address from, uint256 amount) internal {
82182
PocoStorageLib.PocoStorage storage $ = PocoStorageLib.getPocoStorage();
83-
require($.m_baseToken.transferFrom(from, address(this), amount), "failled-transferFrom");
183+
require($.m_baseToken.transferFrom(from, address(this), amount), "failed-transferFrom");
84184
}
85185

86186
function _withdraw(address to, uint256 amount) internal {

contracts/facets/IexecPoco1Facet.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ contract IexecPoco1Facet is
140140
/**
141141
* Match orders. The requester gets debited.
142142
*
143+
* @notice This function does not use `msg.sender` to determine who pays for the deal.
144+
* The sponsor is always set to `_requestorder.requester`, regardless of who calls this function.
145+
* This design allows the function to be safely called via delegatecall from other facets
146+
* (e.g., IexecEscrowTokenFacet.receiveApproval) without security concerns.
147+
*
143148
* @param _apporder The app order.
144149
* @param _datasetorder The dataset order.
145150
* @param _workerpoolorder The workerpool order.
@@ -161,6 +166,7 @@ contract IexecPoco1Facet is
161166
);
162167
}
163168

169+
// TODO: check if we want to modify sponsor origin to be a variable instead of msg.sender
164170
/**
165171
* Sponsor match orders for a requester.
166172
* Unlike the standard `matchOrders(..)` hook where the requester pays for

contracts/interfaces/IexecEscrowToken.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pragma solidity ^0.8.0;
66
interface IexecEscrowToken {
77
receive() external payable;
88
fallback() external payable;
9-
109
function deposit(uint256) external returns (bool);
1110
function depositFor(uint256, address) external returns (bool);
1211
function depositForArray(uint256[] calldata, address[] calldata) external returns (bool);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <[email protected]>
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pragma solidity ^0.8.0;
5+
6+
import {IexecLibOrders_v5} from "../../libs/IexecLibOrders_v5.sol";
7+
8+
/**
9+
* @title ReceiveApprovalTestHelper
10+
* @notice Helper contract to test edge cases in receiveApproval function
11+
* @dev This contract simulates a facet that fails silently (no error data)
12+
*/
13+
contract ReceiveApprovalTestHelper {
14+
/**
15+
* @notice Mock matchOrders function that fails without returning error data
16+
* @dev Uses assembly to revert without data, simulating the edge case where
17+
* delegatecall fails and result.length == 0
18+
*/
19+
function matchOrders(
20+
IexecLibOrders_v5.AppOrder calldata,
21+
IexecLibOrders_v5.DatasetOrder calldata,
22+
IexecLibOrders_v5.WorkerpoolOrder calldata,
23+
IexecLibOrders_v5.RequestOrder calldata
24+
) external pure returns (bytes32) {
25+
// Revert without any error data
26+
// This simulates: delegatecall fails with success=false and result.length=0
27+
assembly {
28+
revert(0, 0)
29+
}
30+
}
31+
}

docs/solidity/index.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,39 @@ function recover() external returns (uint256)
247247
### receiveApproval
248248

249249
```solidity
250-
function receiveApproval(address sender, uint256 amount, address token, bytes) external returns (bool)
250+
function receiveApproval(address sender, uint256 amount, address token, bytes data) external returns (bool)
251251
```
252252

253+
Receives approval and optionally matches orders in one transaction
254+
255+
Usage patterns:
256+
1. Simple deposit: RLC.approveAndCall(escrow, amount, "")
257+
2. Deposit + match: RLC.approveAndCall(escrow, amount, encodedOrders)
258+
259+
The `data` parameter should be ABI-encoded orders if matching is desired:
260+
abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder)
261+
262+
_Important notes:
263+
- Match orders sponsoring is NOT supported. The requester (sender) always pays for the deal.
264+
- Clients must compute the exact deal cost and deposit the right amount for the deal to be matched.
265+
The deal cost = (appPrice + datasetPrice + workerpoolPrice) * volume.
266+
- If insufficient funds are deposited, the match will fail._
267+
268+
#### Parameters
269+
270+
| Name | Type | Description |
271+
| ---- | ---- | ----------- |
272+
| sender | address | The address that approved tokens (must be requester if matching) |
273+
| amount | uint256 | Amount of tokens approved and to be deposited |
274+
| token | address | Address of the token (must be RLC) |
275+
| data | bytes | Optional: ABI-encoded orders for matching |
276+
277+
#### Return Values
278+
279+
| Name | Type | Description |
280+
| ---- | ---- | ----------- |
281+
| [0] | bool | success True if operation succeeded @custom:example ```solidity // Compute deal cost uint256 dealCost = (appPrice + datasetPrice + workerpoolPrice) * volume; // Encode orders bytes memory data = abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder); // One transaction does it all RLC(token).approveAndCall(iexecProxy, dealCost, data); ``` |
282+
253283
## IexecOrderManagementFacet
254284

255285
### manageAppOrder
@@ -343,6 +373,11 @@ function matchOrders(struct IexecLibOrders_v5.AppOrder _apporder, struct IexecLi
343373

344374
Match orders. The requester gets debited.
345375

376+
This function does not use `msg.sender` to determine who pays for the deal.
377+
The sponsor is always set to `_requestorder.requester`, regardless of who calls this function.
378+
This design allows the function to be safely called via delegatecall from other facets
379+
(e.g., IexecEscrowTokenFacet.receiveApproval) without security concerns.
380+
346381
#### Parameters
347382

348383
| Name | Type | Description |

0 commit comments

Comments
 (0)