Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c341cdb
feat: verify multiple proofs of read
kss-t1 Sep 25, 2025
9fb4334
fix: now it compiles, which is a plus
kss-t1 Sep 26, 2025
0f5731e
feat: use batching in T1ERC7683.sol
kss-t1 Sep 26, 2025
88a0f10
chore: better interface
kss-t1 Sep 26, 2025
d7aabe7
fix: me silly
kss-t1 Sep 26, 2025
45bd568
chore: fmt
kss-t1 Sep 26, 2025
c953631
Merge remote-tracking branch 'origin/canary' into feat/7683/batch-fil…
kss-t1 Sep 26, 2025
f068373
feat: minimalize number of ERC20.transfer calls when processing batch…
kss-t1 Sep 28, 2025
22607f2
chore: fmt
kss-t1 Sep 28, 2025
a2494e4
chore: housekeeping
kss-t1 Sep 28, 2025
3bf978d
feat: only call _transferTokenOut if order was settled for extra secu…
kss-t1 Sep 28, 2025
7af4fbc
chore: style, comments, simplifications
kss-t1 Sep 28, 2025
42e8a7d
chore: private method to increase readability
kss-t1 Sep 28, 2025
c551644
chore: more flexible intent opening in tests
kss-t1 Oct 3, 2025
93f4c84
test: unit test for batch solver repayment using murky for Merkle Tre…
kss-t1 Oct 6, 2025
47b20da
feat: introduced murky (with adapted hashing) for merkle tree operati…
kss-t1 Oct 6, 2025
54993e9
fix: batch test fixes
kss-t1 Oct 7, 2025
7a002d7
refactor: allowed batching test for N intents
kss-t1 Oct 7, 2025
a2e26a0
chore: shared intentCount
kss-t1 Oct 7, 2025
5f45d8b
fix: only calling ERC20.transfer when necessary
kss-t1 Oct 7, 2025
8e68431
test: testing case for two solvers and calculating gas usage
kss-t1 Oct 7, 2025
44cd920
chore: simplification
kss-t1 Oct 7, 2025
2e6e1ed
chore: dropped Murky dependency by moving needed code inside the repo
kss-t1 Oct 7, 2025
783ffa3
chore: renames
kss-t1 Oct 7, 2025
666dbf8
chore: fmt
kss-t1 Oct 7, 2025
8ebe098
chore: lint
kss-t1 Oct 7, 2025
07fe3d8
Merge remote-tracking branch 'origin/canary' into feat/7683/batch-fil…
kss-t1 Oct 7, 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
1 change: 1 addition & 0 deletions contracts/.solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MurkyMerkleBase.sol
107 changes: 99 additions & 8 deletions contracts/src/7683/T1ERC7683.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {
import { T1Permit2 } from "./T1Permit2.sol";
import { IT1XChainReader } from "../libraries/xChain/IT1XChainReader.sol";

struct UserTokens {
address receiver;
address token;
}

/// @title T1ERC7683
/// @author t1 Labs
contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 {
Expand Down Expand Up @@ -437,15 +442,69 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 {
function handleReadResultWithProof(bytes calldata encodedProofOfRead) external whenSettleNotPaused {
(bytes32 requestId, bytes memory result) = xChainRead.verifyProofOfRead(encodedProofOfRead);

bytes32 orderId = settlementReadRequestToOrderId[requestId];
(bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount) =
_decodeOrdersToSettle(requestId, result);

if (isSettled) {
_transferTokenOut(inputToken, settlementReceiver, amount);
}

emit SettlementVerified(orderId, isSettled);
}

/// @notice Use result of proof of read to handle batch of orders depending on the result
/// Also enforce auction winner bid if the orderId has closed auction.
/// @param encodedProofsOfRead The encoded proofs of read which are formatted as following:
/// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof)
function handleBatchOfReadResultsWithProofs(bytes[] calldata encodedProofsOfRead) external whenSettleNotPaused {
(bytes32[] memory requestIds, bytes[] memory results) = xChainRead.verifyProofsOfRead(encodedProofsOfRead);

bytes32[] memory orderIds = new bytes32[](encodedProofsOfRead.length);
bool[] memory areSettled = new bool[](encodedProofsOfRead.length);

UserTokens[] memory userTokenKeys = new UserTokens[](encodedProofsOfRead.length);
uint256[] memory amounts = new uint256[](encodedProofsOfRead.length);
uint256 uniqueUserTokenCount = 0;

for (uint256 i = 0; i < encodedProofsOfRead.length; i++) {
(bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount) =
_decodeOrdersToSettle(requestIds[i], results[i]);

orderIds[i] = orderId;
areSettled[i] = isSettled;

if (isSettled) {
uniqueUserTokenCount = _updateOrInsertUserToken(
userTokenKeys, amounts, uniqueUserTokenCount, settlementReceiver, inputToken, amount
);
}
}

for (uint256 i = 0; i < userTokenKeys.length; i++) {
if (userTokenKeys[i].receiver != address(0)) {
_transferTokenOut(userTokenKeys[i].token, userTokenKeys[i].receiver, amounts[i]);
}
}

emit SettlementBatchVerified(orderIds, areSettled);
}

function _decodeOrdersToSettle(
bytes32 requestId,
bytes memory result
)
internal
returns (bytes32 orderId, bool isSettled, address inputToken, address settlementReceiver, uint256 amount)
{
orderId = settlementReadRequestToOrderId[requestId];

// Ensure we have a valid order
if (orderId == bytes32(0)) revert InvalidOrder();

delete settlementReadRequestToOrderId[requestId];

// Check if the order is FILLED based on result length
bool isSettled = (result.length != 0);
isSettled = (result.length != 0);

// process the settlement if verified
Status status = orderStatus[orderId];
Expand All @@ -461,15 +520,47 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 {

for (uint256 i = 0; i < _orderIds.length; i++) {
if (_settled) {
(, address settlementReceiver) = abi.decode(_ordersFillerData[i], (uint256, address));
_handleSettleOrder(
(, settlementReceiver) = abi.decode(_ordersFillerData[i], (uint256, address));
(inputToken, amount) = _handleSettleOrder(
orderData.destinationDomain, orderData.destinationSettler, _orderIds[i], settlementReceiver
);
}
}
}
}

/// @dev Tries to increment amount to be sent if given (settlementReceiver,token) pair exists. Creates a new pair
/// otherwise
/// @param userTokenKeys Existing (settlementReceiver,token) pairs
/// @param amountsToBeSent Existing amounts to be sent for these (settlementReceiver,token) pairs
/// @param uniqueUserTokenCount Number of existing (settlementReceiver,token) pairs so far
/// @param settlementReceiver The currently processed receiver address
/// @param inputToken The currently processed token address (could be 0, which is native token)
/// @param amount The currently processed token amount
function _updateOrInsertUserToken(
UserTokens[] memory userTokenKeys,
uint256[] memory amountsToBeSent,
uint256 uniqueUserTokenCount,
address settlementReceiver,
address inputToken,
uint256 amount
)
internal
pure
returns (uint256)
{
// try to find existing (receiver,token) pair and increment amount
for (uint256 j = 0; j < uniqueUserTokenCount; j++) {
if (userTokenKeys[j].receiver == settlementReceiver && userTokenKeys[j].token == inputToken) {
amountsToBeSent[j] += amount;
return uniqueUserTokenCount;
}
}

emit SettlementVerified(orderId, isSettled);
// create a new (receiver,token) pair otherwise
userTokenKeys[uniqueUserTokenCount] = UserTokens({ receiver: settlementReceiver, token: inputToken });
amountsToBeSent[uniqueUserTokenCount] = amount;
return uniqueUserTokenCount + 1;
}

/// @dev Handles settling an individual order, should be called by the inheriting contract when receiving a setting
Expand All @@ -486,16 +577,16 @@ contract T1ERC7683 is IT1ERC7683, T1Permit2, AccessControlUpgradeable, EIP712 {
)
internal
virtual
returns (address inputToken, uint256 amount)
{
(bool isEligible, OrderData memory orderData) = _checkOrderEligibility(_messageOrigin, _messageSender, _orderId);

if (!isEligible) revert NotEligible();

orderStatus[_orderId] = Status.SETTLED;

address inputToken = TypeCasts.bytes32ToAddress(orderData.inputToken);

_transferTokenOut(inputToken, settlementReceiver, orderData.amountIn);
inputToken = TypeCasts.bytes32ToAddress(orderData.inputToken);
amount = orderData.amountIn;

emit Settled(_orderId, settlementReceiver);
}
Expand Down
12 changes: 12 additions & 0 deletions contracts/src/interfaces/IT1ERC7683.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ interface IT1ERC7683 is IOriginSettler, IDestinationSettler {
* @param isSettled Whether the order is settled
*/
event SettlementVerified(bytes32 indexed orderId, bool isSettled);
/**
* @notice Emitted when a batch of order settlements is verified
* @param orderIds The IDs of all verified orders
* @param areSettled Whether given orders are settled
*/
event SettlementBatchVerified(bytes32[] indexed orderIds, bool[] areSettled);
/**
* @notice Emitted when an order is settled.
* @param orderId The ID of the settled order.
Expand Down Expand Up @@ -142,6 +148,12 @@ interface IT1ERC7683 is IOriginSettler, IDestinationSettler {
/// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof)
function handleReadResultWithProof(bytes calldata encodedProofOfRead) external;

/// @notice Use result of proof of read to handle batch of orders depending on the result
/// Also enforce auction winner bid if the orderId has closed auction.
/// @param encodedProofsOfRead The encoded proofs of read which are formatted as following:
/// abi.encode(uint256 batchIndex, bytes32 requestId, uint256 position, bytes result, bytes proof)
function handleBatchOfReadResultsWithProofs(bytes[] calldata encodedProofsOfRead) external;

/// @notice Refunds a batch of expired GaslessCrossChainOrders on the chain where the orders were opened.
/// This process needs a proof of read triggered by `verifyRefund` that proves the intent has not be filled.
/// @param _orders An array of GaslessCrossChainOrders to refund.
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/libraries/xChain/IT1XChainReader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface IT1XChainReader {
function requestRead(ReadRequest calldata request) external payable returns (bytes32 requestId);
function commitProofOfReadRoot(uint256 batchIndex, bytes32 newRoot) external;
function verifyProofOfRead(bytes calldata encodedProofOfRead) external view returns (bytes32, bytes memory);
function verifyProofsOfRead(bytes[] calldata encodedProofOfRead)
external
view
returns (bytes32[] memory requestIds, bytes[] memory results);
function verifyProofOfReadWithResult(
bytes calldata encodedProofOfRead,
bytes calldata result
Expand Down
27 changes: 27 additions & 0 deletions contracts/src/libraries/xChain/T1XChainReader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,33 @@ contract T1XChainReader is IT1XChainReader, OwnableUpgradeable, ReentrancyGuardU
return requestId;
}

/**
* @notice Verifies a batch of many proofs of read and returns the raw function results for all of them
* @param encodedProofsOfRead Array of encoded proofs of read
* @return requestIds The IDs of all read requests, in the same order
* @return results The raw ABI-encoded return values from the target function for all read requests, in the same
* order
*/
function verifyProofsOfRead(bytes[] calldata encodedProofsOfRead)
external
view
override
returns (bytes32[] memory requestIds, bytes[] memory results)
{
requestIds = new bytes32[](encodedProofsOfRead.length);
results = new bytes[](encodedProofsOfRead.length);

for (uint256 i = 0; i < encodedProofsOfRead.length; i++) {
(uint256 batchIndex, bytes32 requestId, uint256 position, bytes memory result, bytes memory proof) =
abi.decode(encodedProofsOfRead[i], (uint256, bytes32, uint256, bytes, bytes));

_verifyProofOfRead(batchIndex, requestId, position, result, proof);

requestIds[i] = requestId;
results[i] = result;
}
}

function _verifyProofOfRead(
uint256 batchIndex,
bytes32 requestId,
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/test/7683/ClosedAuctionTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ contract ClosedAuctionTest is T1XChainReaderBaseTestSetup {
}

function _openOrder(bool closedAuction) internal returns (OrderData memory orderData, bytes32 orderId) {
orderData = _prepareOrderData();
orderData = _prepareOrderData(amount);
orderData.closedAuction = closedAuction;
OnchainCrossChainOrder memory order =
_prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType());
Expand Down
14 changes: 7 additions & 7 deletions contracts/src/test/7683/PausableTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
}

function test_canOpenWhenNotPaused() public {
OrderData memory orderData = _prepareOrderData();
OrderData memory orderData = _prepareOrderData(amount);
OnchainCrossChainOrder memory order =
_prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType());

Expand All @@ -135,7 +135,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
inputToken.approve(permit2, type(uint256).max);

uint32 openDeadline = uint32(block.timestamp + 100);
OrderData memory orderData = _prepareOrderData();
OrderData memory orderData = _prepareOrderData(amount);
GaslessCrossChainOrder memory order = _prepareGaslessOrder(
address(l1T1ERC7683),
kakaroto,
Expand Down Expand Up @@ -167,7 +167,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
}

function test_canSettleWhenNotPaused() public {
(, bytes32 orderId, bytes32 requestId) = _openAndFillOrder();
(, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount);
bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId));
(bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position);
originReader.commitProofOfReadRoot(batchIndex, root);
Expand All @@ -183,7 +183,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
function test_cannotOpenWhenPaused() public {
l1T1ERC7683.pauseOpen();

OrderData memory orderData = _prepareOrderData();
OrderData memory orderData = _prepareOrderData(amount);
OnchainCrossChainOrder memory order =
_prepareOnchainOrder(OrderEncoder.encode(orderData), orderData.fillDeadline, OrderEncoder.orderDataType());

Expand All @@ -197,7 +197,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
function test_cannotOpenForWhenPaused() public {
l1T1ERC7683.pauseOpen();

OrderData memory orderData = _prepareOrderData();
OrderData memory orderData = _prepareOrderData(amount);
GaslessCrossChainOrder memory order = _prepareGaslessOrder(
address(l1T1ERC7683),
kakaroto,
Expand All @@ -217,7 +217,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
}

function test_cannotSettleWhenPaused() public {
(, bytes32 orderId, bytes32 requestId) = _openAndFillOrder();
(, bytes32 orderId, bytes32 requestId) = _openAndFillOrder(kakaroto, vegeta, amount);
bytes memory result = abi.encode(l2T1ERC7683.getFilledOrderStatus(orderId));
(bytes32 root, bytes memory proof) = _generateMerkleTree(requestId, result, position);
originReader.commitProofOfReadRoot(batchIndex, root);
Expand Down Expand Up @@ -288,7 +288,7 @@ contract PausableTest is T1XChainReaderBaseTestSetup {
function test_cannotRefundForWhenPaused() public {
vm.warp(2000); // Set block.timestamp to 2000
uint32 deadline = 1000; // Past deadline for testing refund
OrderData memory defaultOrderData = _prepareOrderData();
OrderData memory defaultOrderData = _prepareOrderData(amount);
defaultOrderData.fillDeadline = deadline;
bytes memory orderData = OrderEncoder.encode(defaultOrderData);

Expand Down
Loading