Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
394908b
feat: Update Solidity version to ^0.8.0 and refactor contracts for im…
gfournierPro Oct 29, 2025
68c1e89
feat: Enhance IexecEscrowTokenFacet with approval event and order mat…
gfournierPro Oct 29, 2025
23a067f
feat: Add error and event definitions for approval handling in IexecE…
gfournierPro Oct 29, 2025
fc6926b
feat: Add ApprovalReceivedAndMatched event and refactor error handlin…
gfournierPro Oct 30, 2025
0ae822a
Merge branch 'main' into feat/deposit-matchOrder-via-receiveApproval
gfournierPro Nov 4, 2025
fb844af
feat: update ABI definitions and remove deprecated events
gfournierPro Nov 4, 2025
7f08cf3
feat: implement encodeOrders function for ABI encoding in receiveAppr…
gfournierPro Nov 6, 2025
3517bf0
refactor: reorganize order handling functions in receiveApproval tests
gfournierPro Nov 6, 2025
47ecf74
fix: clarify return value description in _matchOrdersAfterDeposit fun…
gfournierPro Nov 6, 2025
0b8ed3e
Merge branch 'main' into feat/deposit-matchOrder-via-receiveApproval
gfournierPro Nov 6, 2025
bd2adf1
Merge branch 'chore/solidity-v8' into feat/deposit-matchOrder-via-rec…
gfournierPro Nov 6, 2025
0e57654
chore: remove deprecated ABI files from the project
gfournierPro Nov 6, 2025
fda1c38
style: standardize code formatting and improve readability across mul…
gfournierPro Nov 6, 2025
6ca7c61
fix: solidity docs
gfournierPro Nov 6, 2025
921df75
Merge branch 'fix/format-sc' into feat/deposit-matchOrder-via-receive…
gfournierPro Nov 6, 2025
0c661b3
refactor: remove duplicate import statements in IexecEscrowTokenFacet…
gfournierPro Nov 6, 2025
d9b56ba
docs: enhance receiveApproval function documentation with usage patte…
gfournierPro Nov 6, 2025
b51807f
test: remove describe.only to enable all tests in IexecEscrowToken.re…
gfournierPro Nov 6, 2025
ad0130f
feat: add human-readable ABIs for ERC20, ERC721, and utility contracts
gfournierPro Nov 6, 2025
689e1c3
Merge branch 'fix/format-sc' into feat/deposit-matchOrder-via-receive…
gfournierPro Nov 6, 2025
ec05830
fix: correct typo in error message for transferFrom in _deposit function
gfournierPro Nov 6, 2025
22e3b3e
docs: update receiveApproval function documentation for clarity and a…
gfournierPro Nov 6, 2025
be65435
refactor: remove ApprovalReceivedAndMatched event and related references
gfournierPro Nov 6, 2025
648078e
refactor: move IexecInterfaceToken documentation to the correct section
gfournierPro Nov 6, 2025
f8681d3
refactor: rename _matchOrdersAfterDeposit to _decodeDataAndMatchOrder…
gfournierPro Nov 6, 2025
b229dc5
refactor: remove human-readable ABIs for unused contracts
gfournierPro Nov 6, 2025
0e656ee
Merge branch 'fix/format-sc' into feat/deposit-matchOrder-via-receive…
gfournierPro Nov 6, 2025
137dd04
refactor: update delegatecall usage for safety and clarify function d…
gfournierPro Nov 6, 2025
c49a93e
Merge branch 'chore/solidity-v8' into fix/format-sc
gfournierPro Nov 7, 2025
69ebbcd
fix: docs
gfournierPro Nov 7, 2025
f21473b
Merge branch 'fix/format-sc' into feat/deposit-matchOrder-via-receive…
gfournierPro Nov 7, 2025
024675e
feat: enhance receiveApproval function with order matching capabiliti…
gfournierPro Nov 7, 2025
b06e552
docs: enhance receiveApproval function documentation with important n…
gfournierPro Nov 7, 2025
d554755
feat: update receiveApproval function to execute operations with func…
gfournierPro Nov 7, 2025
ff00267
Merge branch 'chore/solidity-v8' into feat/receiveApproval-generic
gfournierPro Nov 12, 2025
204f2f3
feat: update receiveApproval to support atomic deposit and match orders
gfournierPro Nov 12, 2025
2806a5d
feat: enhance receiveApproval to support matchOrders operation with s…
gfournierPro Nov 12, 2025
81c7557
feat: update receiveApproval to support generalized operation executi…
gfournierPro Nov 12, 2025
0a9144e
feat: simplify matchOrders selector retrieval in encodeOrders function
gfournierPro Nov 12, 2025
fe6c25c
feat: enhance receiveApproval to support deposit and operation execut…
gfournierPro Nov 12, 2025
8123e0f
Merge branch 'chore/solidity-v8' into feat/receiveApproval-generic
gfournierPro Nov 12, 2025
8f85fc3
Merge branch 'chore/solidity-v8' into feat/receiveApproval-generic
gfournierPro Nov 12, 2025
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
126 changes: 71 additions & 55 deletions contracts/facets/IexecEscrowTokenFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,33 @@ contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, IexecERC2
***************************************************************************/

/**
* @notice Receives approval, deposit and optionally matches orders in one transaction
* @notice Receives approval, deposit and optionally executes an operation in one transaction
*
* Usage patterns:
* 1. Simple deposit: RLC.approveAndCall(escrow, amount, "")
* 2. Deposit + match: RLC.approveAndCall(escrow, amount, encodedOrders)
* 2. Deposit + operation: RLC.approveAndCall(escrow, amount, encodedOperation)
*
* The `data` parameter should be ABI-encoded orders if matching is desired:
* abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder)
* The `data` parameter should include a function selector (first 4 bytes) to identify
* the operation, followed by ABI-encoded parameters. Supported operations:
* - matchOrders: Validates sender is requester, then matches orders
*
* @dev Important notes:
* - Match orders sponsoring is NOT supported. The requester (sender) always pays for the deal.
* - Clients must compute the exact deal cost and deposit the right amount for the deal to be matched.
* @dev Implementation details:
* - Deposits tokens first, then executes the operation if data is provided
* - Extracts function selector from data to determine which operation
* - Each operation has a validator (_validateMatchOrders, etc.) for preconditions
* - After validation, _executeOperation performs the delegatecall
* - Error handling is generalized: bubbles up revert reasons or returns 'operation-failed'
* - Future operations can be added by implementing a validator and adding a selector case
*
* @dev matchOrders specific notes:
* - Sponsoring is NOT supported. The requester (sender) always pays for the deal.
* - Clients must compute the exact deal cost and deposit the right amount.
* The deal cost = (appPrice + datasetPrice + workerpoolPrice) * volume.
* - If insufficient funds are deposited, the match will fail.
*
* @param sender The address that approved tokens (must be requester if matching)
* @param sender The address that approved tokens
* @param amount Amount of tokens approved and to be deposited
* @param token Address of the token (must be RLC)
* @param data Optional: ABI-encoded orders for matching
* @param data Optional: Function selector + ABI-encoded parameters for operation
* @return success True if operation succeeded
*
*
Expand All @@ -97,10 +105,16 @@ contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, IexecERC2
* // Compute deal cost
* uint256 dealCost = (appPrice + datasetPrice + workerpoolPrice) * volume;
*
* // Encode orders
* bytes memory data = abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder);
* // Encode matchOrders operation with selector
* bytes memory data = abi.encodeWithSelector(
* IexecPoco1.matchOrders.selector,
* appOrder,
* datasetOrder,
* workerpoolOrder,
* requestOrder
* );
*
* // One transaction does it all
* // One transaction does it all: approve, deposit, and match
* RLC(token).approveAndCall(iexecProxy, dealCost, data);
* ```
*/
Expand All @@ -114,54 +128,28 @@ contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, IexecERC2
require(token == address($.m_baseToken), "wrong-token");
_deposit(sender, amount);
_mint(sender, amount);

if (data.length > 0) {
_decodeDataAndMatchOrders(sender, data);
_executeOperation(sender, data);
}
return true;
}

/******************************************************************************
* Token Spender: Atomic Deposit+Match if used with RLC.approveAndCall *
*****************************************************************************/

/**
* @dev Internal function to match orders after deposit
* @param sender The user who deposited (must be the requester)
* @param data ABI-encoded orders
*/
function _decodeDataAndMatchOrders(address sender, bytes calldata data) internal {
// Decode the orders from calldata
(
IexecLibOrders_v5.AppOrder memory apporder,
IexecLibOrders_v5.DatasetOrder memory datasetorder,
IexecLibOrders_v5.WorkerpoolOrder memory workerpoolorder,
IexecLibOrders_v5.RequestOrder memory requestorder
) = abi.decode(
data,
(
IexecLibOrders_v5.AppOrder,
IexecLibOrders_v5.DatasetOrder,
IexecLibOrders_v5.WorkerpoolOrder,
IexecLibOrders_v5.RequestOrder
)
);
function _executeOperation(address sender, bytes calldata data) internal {
// Extract the function selector (first 4 bytes)
bytes4 selector = bytes4(data[:4]);

// Validate that sender is the requester
if (requestorder.requester != sender) revert("caller-must-be-requester");
// Validate operation-specific preconditions before execution
if (selector == IexecPoco1.matchOrders.selector) {
_validateMatchOrders(sender, data);
} else {
revert("unsupported-operation");
}

// Call matchOrders on the IexecPoco1 facet through the diamond
// Using delegatecall for safety: preserves msg.sender context (RLC address in this case)
// Note: matchOrders doesn't use msg.sender, but delegatecall is safer
// in case the implementation changes in the future
(bool success, bytes memory result) = address(this).delegatecall(
abi.encodeWithSelector(
IexecPoco1.matchOrders.selector,
apporder,
datasetorder,
workerpoolorder,
requestorder
)
);
// Execute the operation via delegatecall
// This preserves msg.sender context and allows the operation to access
// the diamond's storage and functions
(bool success, bytes memory result) = address(this).delegatecall(data);

// Handle failure and bubble up revert reason
if (!success) {
Expand All @@ -172,11 +160,39 @@ contract IexecEscrowTokenFacet is IexecEscrowToken, IexecTokenSpender, IexecERC2
revert(add(result, 32), returndata_size)
}
} else {
revert("receive-approval-failed");
revert("operation-failed");
}
}
}

/******************************************************************************
* Token Spender: Atomic Deposit+Match if used with RLC.approveAndCall *
*****************************************************************************/

/**
* @dev Validates matchOrders preconditions
* @param sender The user who deposited (must be the requester)
* @param data ABI-encoded matchOrders call with orders
*/
function _validateMatchOrders(address sender, bytes calldata data) internal pure {
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The function is marked as pure but it performs abi.decode on calldata. While this is technically allowed in newer Solidity versions, the function should be marked as view for clarity and consistency, as it reads from calldata which is external data.

Suggested change
function _validateMatchOrders(address sender, bytes calldata data) internal pure {
function _validateMatchOrders(address sender, bytes calldata data) internal view {

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Function state mutability can be restricted to puresolidity(2018)

// Decode only the request order to validate the requester
// Full decoding: (AppOrder, DatasetOrder, WorkerpoolOrder, RequestOrder)
// We only need to check requestorder.requester
(, , , IexecLibOrders_v5.RequestOrder memory requestorder) = abi.decode(
data[4:],
Comment on lines +181 to +182
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The comment on line 179-181 states 'We only need to check requestorder.requester', but this approach still decodes all four order structs (consuming gas for all of them). Consider adding a note about the gas trade-off or exploring if there's a more efficient way to decode only the request order if optimization is important.

Copilot uses AI. Check for mistakes.
(
IexecLibOrders_v5.AppOrder,
IexecLibOrders_v5.DatasetOrder,
IexecLibOrders_v5.WorkerpoolOrder,
IexecLibOrders_v5.RequestOrder
)
);

// Validate that sender is the requester
// This ensures the caller is authorized to create this deal
if (requestorder.requester != sender) revert("caller-must-be-requester");
}

function _deposit(address from, uint256 amount) internal {
PocoStorageLib.PocoStorage storage $ = PocoStorageLib.getPocoStorage();
require($.m_baseToken.transferFrom(from, address(this), amount), "failed-transferFrom");
Expand Down
32 changes: 20 additions & 12 deletions docs/solidity/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,35 +252,43 @@ function recover() external returns (uint256)
function receiveApproval(address sender, uint256 amount, address token, bytes data) external returns (bool)
```

Receives approval, deposit and optionally matches orders in one transaction
Receives approval, deposit and optionally executes an operation in one transaction

Usage patterns:
1. Simple deposit: RLC.approveAndCall(escrow, amount, "")
2. Deposit + match: RLC.approveAndCall(escrow, amount, encodedOrders)
2. Deposit + operation: RLC.approveAndCall(escrow, amount, encodedOperation)

The `data` parameter should be ABI-encoded orders if matching is desired:
abi.encode(appOrder, datasetOrder, workerpoolOrder, requestOrder)
The `data` parameter should include a function selector (first 4 bytes) to identify
the operation, followed by ABI-encoded parameters. Supported operations:
- matchOrders: Validates sender is requester, then matches orders

_Important notes:
- Match orders sponsoring is NOT supported. The requester (sender) always pays for the deal.
- Clients must compute the exact deal cost and deposit the right amount for the deal to be matched.
The deal cost = (appPrice + datasetPrice + workerpoolPrice) * volume.
- If insufficient funds are deposited, the match will fail._
_Implementation details:
- Deposits tokens first, then executes the operation if data is provided
- Extracts function selector from data to determine which operation
- Each operation has a validator (_validateMatchOrders, etc.) for preconditions
- After validation, _executeOperation performs the delegatecall
- Error handling is generalized: bubbles up revert reasons or returns 'operation-failed'
- Future operations can be added by implementing a validator and adding a selector case

matchOrders specific notes:
- Sponsoring is NOT supported. The requester (sender) always pays for the deal.
- Clients must compute the exact deal cost and deposit the right amount.
The deal cost = (appPrice + datasetPrice + workerpoolPrice) * volume._

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| sender | address | The address that approved tokens (must be requester if matching) |
| sender | address | The address that approved tokens |
| amount | uint256 | Amount of tokens approved and to be deposited |
| token | address | Address of the token (must be RLC) |
| data | bytes | Optional: ABI-encoded orders for matching |
| data | bytes | Optional: Function selector + ABI-encoded parameters for operation |

#### Return Values

| Name | Type | Description |
| ---- | ---- | ----------- |
| [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); ``` |
| [0] | bool | success True if operation succeeded @custom:example ```solidity // Compute deal cost uint256 dealCost = (appPrice + datasetPrice + workerpoolPrice) * volume; // Encode matchOrders operation with selector bytes memory data = abi.encodeWithSelector( IexecPoco1.matchOrders.selector, appOrder, datasetOrder, workerpoolOrder, requestOrder ); // One transaction does it all: approve, deposit, and match RLC(token).approveAndCall(iexecProxy, dealCost, data); ``` |

## IexecOrderManagementFacet

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ describe('IexecEscrowToken-receiveApproval', () => {
});
});

describe('receiveApproval with Order Matching', () => {
it('Should approve, deposit and match orders with all assets', async () => {
describe('receiveApproval with generalized operation execution', () => {
it('Should approve, deposit and execute matchOrders operation with all assets', async () => {
const orders = buildOrders({
assets: ordersAssets,
prices: ordersPrices,
Expand All @@ -167,6 +167,7 @@ describe('IexecEscrowToken-receiveApproval', () => {
const initialBalance = await iexecPoco.balanceOf(requester.address);
const initialTotalSupply = await iexecPoco.totalSupply();

// Encode the matchOrders operation with its selector
const encodedOrders = encodeOrdersForCallback(orders);

const tx = await rlcInstanceAsRequester.approveAndCall(
Expand Down Expand Up @@ -344,6 +345,19 @@ describe('IexecEscrowToken-receiveApproval', () => {
).to.be.revertedWith('IexecEscrow: Transfer amount exceeds balance');
});

it('Should revert with unsupported-operation for unknown function selector', async () => {
const dealCost = 1000n;
// Create calldata with an unsupported function selector (not matchOrders)
// Using a random selector that doesn't exist
const unsupportedSelector = '0x12345678';
const dummyData = ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [42]);
const invalidData = unsupportedSelector + dummyData.slice(2);

await expect(
rlcInstanceAsRequester.approveAndCall(proxyAddress, dealCost, invalidData),
).to.be.revertedWith('unsupported-operation');
});

it('Should not match orders with invalid calldata', async () => {
const dealCost = (appPrice + datasetPrice + workerpoolPrice) * volume;
const invalidData = '0x1234'; // Too short to be valid
Expand Down Expand Up @@ -446,8 +460,10 @@ describe('IexecEscrowToken-receiveApproval', () => {
expect(await iexecPoco.frozenOf(requester.address)).to.equal(0n);
});

it('Should revert with receive-approval-failed when delegatecall fails silently', async () => {
it('Should revert with operation-failed when delegatecall fails silently', async () => {
// Deploy the mock helper contract that fails silently
// This tests that the generalized _executeOperation properly handles
// delegatecall failures and bubbles up errors
const mockFacet = await new ReceiveApprovalTestHelper__factory()
.connect(iexecAdmin)
.deploy()
Expand Down Expand Up @@ -495,7 +511,7 @@ describe('IexecEscrowToken-receiveApproval', () => {
depositAmount,
encodedOrders,
);
await expect(tx).to.be.revertedWith('receive-approval-failed');
await expect(tx).to.be.revertedWith('operation-failed');

// Restore original facet
await diamondCut.diamondCut(
Expand Down
19 changes: 15 additions & 4 deletions utils/odb-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { TypedDataDomain, TypedDataEncoder, TypedDataField, ethers } from 'ethers';
import hre from 'hardhat';
import { IexecPoco1Facet__factory } from '../typechain';

interface WalletInfo {
privateKey?: string;
Expand Down Expand Up @@ -169,13 +170,18 @@ export function hashStruct(
}

/**
* Encode orders for callback data in receiveApproval.
* Uses typechain-generated struct definitions to ensure type consistency.
* Encode orders with matchOrders selector for receiveApproval callback.
*
* The encoded data includes the function selector as the first 4 bytes, which allows
* the generalized receiveApproval implementation to:
* 1. Extract the selector to identify the operation (matchOrders in this case)
* 2. Call the appropriate validator (_validateMatchOrders for permission checks)
*
* @param appOrder App order struct
* @param datasetOrder Dataset order struct
* @param workerpoolOrder Workerpool order struct
* @param requestOrder Request order struct
* @returns ABI-encoded orders
* @returns ABI-encoded calldata with matchOrders selector + encoded order structs
*/
export function encodeOrders(
appOrder: Record<string, any>,
Expand All @@ -195,8 +201,13 @@ export function encodeOrders(
const requestOrderType =
'tuple(address app, uint256 appmaxprice, address dataset, uint256 datasetmaxprice, address workerpool, uint256 workerpoolmaxprice, address requester, uint256 volume, bytes32 tag, uint256 category, uint256 trust, address beneficiary, address callback, string params, bytes32 salt, bytes sign)';

return ethers.AbiCoder.defaultAbiCoder().encode(
// Encode the function parameters (without selector)
const encodedParams = ethers.AbiCoder.defaultAbiCoder().encode(
[appOrderType, datasetOrderType, workerpoolOrderType, requestOrderType],
[appOrder, datasetOrder, workerpoolOrder, requestOrder],
);
const matchOrdersSelector =
IexecPoco1Facet__factory.createInterface().getFunction('matchOrders')!.selector;

return matchOrdersSelector + encodedParams.slice(2);
}