Skip to content

Commit

Permalink
feat(contracts/solve): support claim to any address (#2454)
Browse files Browse the repository at this point in the history
Support claiming to any address.

issue: #2355
  • Loading branch information
kevinhalliday authored Nov 11, 2024
1 parent d943b40 commit 8555b45
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 62 deletions.
71 changes: 45 additions & 26 deletions contracts/bindings/solveinbox.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/bindings/solveoutbox.go

Large diffs are not rendered by default.

56 changes: 30 additions & 26 deletions contracts/solve/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
SolveInbox_accept_Test:test_accept_one_request() (gas: 395190)
SolveInbox_accept_Test:test_accept_reverts() (gas: 1046338)
SolveInbox_accept_Test:test_accept_skip_first() (gas: 703125)
SolveInbox_accept_Test:test_accept_two_requests() (gas: 727140)
SolveInbox_cancel_Test:test_cancel_multiToken() (gas: 548524)
SolveInbox_cancel_Test:test_cancel_nativeMultiToken() (gas: 637564)
SolveInbox_cancel_Test:test_cancel_oldest_request() (gas: 693985)
SolveInbox_cancel_Test:test_cancel_one_request() (gas: 396185)
SolveInbox_cancel_Test:test_cancel_rejected_nativeMultiToken_request() (gas: 647092)
SolveInbox_cancel_Test:test_cancel_rejected_nativeToken_request() (gas: 405226)
SolveInbox_cancel_Test:test_cancel_reverts() (gas: 1068205)
SolveInbox_cancel_Test:test_cancel_singleToken() (gas: 427729)
SolveInbox_cancel_Test:test_cancel_two_requests() (gas: 705750)
SolveInbox_markFulfilled_Test:test_markFulfilled_reverts() (gas: 506851)
SolveInbox_markFulfilled_Test:test_markFulfilled_success() (gas: 417191)
SolveInbox_reject_Test:test_reject_nativeMultiToken() (gas: 601388)
SolveInbox_reject_Test:test_reject_oldest_request() (gas: 666425)
SolveInbox_reject_Test:test_reject_one_request() (gas: 368118)
SolveInbox_reject_Test:test_reject_reverts() (gas: 749070)
SolveInbox_reject_Test:test_reject_two_requests() (gas: 670785)
SolveInbox_request_Test:test_request_multiToken() (gas: 551295)
SolveInbox_request_Test:test_request_nativeMultiToken() (gas: 609496)
SolveInbox_request_Test:test_request_reverts() (gas: 930379)
SolveInbox_request_Test:test_request_singleNative() (gas: 370023)
SolveInbox_request_Test:test_request_singleToken() (gas: 431537)
SolveInbox_request_Test:test_request_two() (gas: 678981)
SolveInbox_accept_Test:test_accept_one_request() (gas: 395222)
SolveInbox_accept_Test:test_accept_reverts() (gas: 1046469)
SolveInbox_accept_Test:test_accept_skip_first() (gas: 703189)
SolveInbox_accept_Test:test_accept_two_requests() (gas: 727204)
SolveInbox_cancel_Test:test_cancel_multiToken() (gas: 548591)
SolveInbox_cancel_Test:test_cancel_nativeMultiToken() (gas: 637631)
SolveInbox_cancel_Test:test_cancel_oldest_request() (gas: 694084)
SolveInbox_cancel_Test:test_cancel_one_request() (gas: 396252)
SolveInbox_cancel_Test:test_cancel_rejected_nativeMultiToken_request() (gas: 647159)
SolveInbox_cancel_Test:test_cancel_rejected_nativeToken_request() (gas: 405293)
SolveInbox_cancel_Test:test_cancel_reverts() (gas: 1068371)
SolveInbox_cancel_Test:test_cancel_singleToken() (gas: 427796)
SolveInbox_cancel_Test:test_cancel_two_requests() (gas: 705884)
SolveInbox_claim_Test:test_claim_multiDeposit() (gas: 719086)
SolveInbox_claim_Test:test_claim_reverts() (gas: 448914)
SolveInbox_claim_Test:test_claim_singleNative() (gas: 471302)
SolveInbox_claim_Test:test_claim_singleToken() (gas: 502224)
SolveInbox_markFulfilled_Test:test_markFulfilled_reverts() (gas: 506883)
SolveInbox_markFulfilled_Test:test_markFulfilled_success() (gas: 417223)
SolveInbox_reject_Test:test_reject_nativeMultiToken() (gas: 601420)
SolveInbox_reject_Test:test_reject_oldest_request() (gas: 666489)
SolveInbox_reject_Test:test_reject_one_request() (gas: 368150)
SolveInbox_reject_Test:test_reject_reverts() (gas: 749169)
SolveInbox_reject_Test:test_reject_two_requests() (gas: 670849)
SolveInbox_request_Test:test_request_multiToken() (gas: 551327)
SolveInbox_request_Test:test_request_nativeMultiToken() (gas: 609528)
SolveInbox_request_Test:test_request_reverts() (gas: 930411)
SolveInbox_request_Test:test_request_singleNative() (gas: 370055)
SolveInbox_request_Test:test_request_singleToken() (gas: 431569)
SolveInbox_request_Test:test_request_two() (gas: 679045)
13 changes: 9 additions & 4 deletions contracts/solve/src/SolveInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ contract SolveInbox is OwnableRoles, ReentrancyGuard, Initializable, XAppBase, I
error WrongCallHash();
error WrongSourceChain();

// General errors
// Transfer errors
error TransferFailed();
error InvalidRecipient();

/**
* @notice Role for solvers.
Expand Down Expand Up @@ -176,17 +177,19 @@ contract SolveInbox is OwnableRoles, ReentrancyGuard, Initializable, XAppBase, I
/**
* @notice Claim a fulfilled request.
* @param id ID of the request.
* @param to Address to send deposits to.
*/
function claim(bytes32 id) external nonReentrant {
function claim(bytes32 id, address to) external nonReentrant {
Solve.Request storage req = _requests[id];
if (req.status != Solve.Status.Fulfilled) revert NotFulfilled();
if (req.acceptedBy != msg.sender) revert Unauthorized();

req.updatedAt = uint40(block.timestamp);
req.status = Solve.Status.Claimed;

_transferDeposits(req.acceptedBy, req.deposits);
_transferDeposits(to, req.deposits);

emit Claimed(id);
emit Claimed(id, msg.sender, to, req.deposits);
}

/**
Expand Down Expand Up @@ -215,6 +218,8 @@ contract SolveInbox is OwnableRoles, ReentrancyGuard, Initializable, XAppBase, I
* @dev Transfer deposits to recipient. Used regardless of refund or claim.
*/
function _transferDeposits(address recipient, Solve.Deposit[] memory deposits) internal {
if (recipient == address(0)) revert InvalidRecipient();

for (uint256 i; i < deposits.length; ++i) {
if (deposits[i].isNative) {
(bool success,) = payable(recipient).call{ value: deposits[i].amount }("");
Expand Down
10 changes: 7 additions & 3 deletions contracts/solve/src/interfaces/ISolveInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ interface ISolveInbox {

/**
* @notice Emitted when a request is claimed.
* @param id ID of the request.
* @param id ID of the request.
* @param by The solver address that claimed the request.
* @param to The recipient of claimed deposits.
* @param deposits Array of deposits claimed
*/
event Claimed(bytes32 indexed id);
event Claimed(bytes32 indexed id, address indexed by, address indexed to, Solve.Deposit[] deposits);

/**
* /**
Expand Down Expand Up @@ -107,6 +110,7 @@ interface ISolveInbox {
/**
* @notice Claim a fulfilled request.
* @param id ID of the request.
* @param to Address to send deposits to.
*/
function claim(bytes32 id) external;
function claim(bytes32 id, address to) external;
}
142 changes: 142 additions & 0 deletions contracts/solve/test/Inbox_claim.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity =0.8.24;

import { Ownable } from "solady/src/auth/Ownable.sol";
import { SolveInbox, ISolveInbox } from "src/SolveInbox.sol";
import { Solve } from "src/Solve.sol";
import { InboxBase } from "./InboxBase.sol";

/**
* @title SolveInbox_claim_Test
* @notice Test suite for SolveInbox.claim(...)
*/
contract SolveInbox_claim_Test is InboxBase {
address claimTo = makeAddr("claim-to");

function test_claim_reverts() public {
// no request
vm.expectRevert(SolveInbox.NotFulfilled.selector);
inbox.claim(bytes32(0), claimTo);

// open request
vm.deal(user, 1 ether);
Solve.Call memory call = randCall();
Solve.TokenDeposit[] memory deposits = new Solve.TokenDeposit[](0);
vm.prank(user);
bytes32 id = inbox.request{ value: 1 ether }(call, deposits);

// pending (not fulfilled)
vm.expectRevert(SolveInbox.NotFulfilled.selector);
inbox.claim(id, claimTo);

// accept
vm.prank(solver);
inbox.accept(id);

// accepted (not fulfilled)
vm.expectRevert(SolveInbox.NotFulfilled.selector);
inbox.claim(id, claimTo);

// mark fulfilled
portal.mockXCall({
sourceChainId: call.destChainId,
sender: address(outbox),
data: abi.encodeCall(inbox.markFulfilled, (id, callHash(id, call))),
to: address(inbox)
});

// not acceptedBy
vm.expectRevert(Ownable.Unauthorized.selector);
vm.prank(makeAddr("not-solver"));
inbox.claim(id, claimTo);

// no claimTo zero
vm.expectRevert(SolveInbox.InvalidRecipient.selector);
vm.prank(solver);
inbox.claim(id, address(0));
}

function test_claim_singleNative() public {
// open, accept, fulfill
Solve.Call memory call = randCall();
Solve.TokenDeposit[] memory deposits = new Solve.TokenDeposit[](0);
bytes32 id = openAcceptFulfill(call, deposits, 1 ether);

// claim
vm.expectEmit(address(inbox));
emit ISolveInbox.Claimed(id, solver, claimTo, inbox.getRequest(id).deposits);
vm.prank(solver);
inbox.claim(id, claimTo);

// assert claimed
Solve.Request memory req = inbox.getRequest(id);
assertEq(uint8(req.status), uint8(Solve.Status.Claimed), "req.status");
assertEq(claimTo.balance, 1 ether, "claimTo.balance");
}

function test_claim_singleToken() public {
// open, accept, fulfill
Solve.Call memory call = randCall();
Solve.TokenDeposit[] memory deposits = new Solve.TokenDeposit[](1);
deposits[0] = Solve.TokenDeposit({ token: address(token1), amount: 1 ether });
bytes32 id = openAcceptFulfill(call, deposits, 0);

// claim
vm.expectEmit(address(inbox));
emit ISolveInbox.Claimed(id, solver, claimTo, inbox.getRequest(id).deposits);
vm.prank(solver);
inbox.claim(id, claimTo);

// assert claimed
Solve.Request memory req = inbox.getRequest(id);
assertEq(uint8(req.status), uint8(Solve.Status.Claimed), "req.status");
assertEq(token1.balanceOf(claimTo), 1 ether, "token.balanceOf(claimTo)");
}

function test_claim_multiDeposit() public {
// open, accept, fulfill
Solve.Call memory call = randCall();
Solve.TokenDeposit[] memory deposits = new Solve.TokenDeposit[](2);
deposits[0] = Solve.TokenDeposit({ token: address(token1), amount: 1 ether });
deposits[1] = Solve.TokenDeposit({ token: address(token2), amount: 2 ether });
bytes32 id = openAcceptFulfill(call, deposits, 3 ether);

// claim
vm.expectEmit(address(inbox));
emit ISolveInbox.Claimed(id, solver, claimTo, inbox.getRequest(id).deposits);
vm.prank(solver);
inbox.claim(id, claimTo);

// assert claimed
Solve.Request memory req = inbox.getRequest(id);
assertEq(uint8(req.status), uint8(Solve.Status.Claimed), "req.status");
assertEq(claimTo.balance, 3 ether, "claimTo.balance");
assertEq(token1.balanceOf(claimTo), 1 ether, "token1.balanceOf(claimTo)");
assertEq(token2.balanceOf(claimTo), 2 ether, "token2.balanceOf(claimTo)");
}

/// @dev Open a request, accept it, mark it as fulfilled, and return the request ID.
function openAcceptFulfill(Solve.Call memory call, Solve.TokenDeposit[] memory tokenDeposits, uint256 nativeDeposit)
internal
returns (bytes32 id)
{
// open request
vm.deal(user, nativeDeposit);
vm.startPrank(user);
mintAndApprove(tokenDeposits);
id = inbox.request{ value: nativeDeposit }(call, tokenDeposits);
vm.stopPrank();

// accept
vm.prank(solver);
inbox.accept(id);

// mark fulfilled
portal.mockXCall({
sourceChainId: call.destChainId,
sender: address(outbox),
data: abi.encodeCall(inbox.markFulfilled, (id, callHash(id, call))),
to: address(inbox)
});
}
}
6 changes: 4 additions & 2 deletions solver/app/deps.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//nolint:dupl,unused // It's okay to have similar code for different events
//nolint:unused // Some functions are unused but are kept for future use
package app

import (
Expand Down Expand Up @@ -48,7 +48,9 @@ func newClaimer(
return err
}

tx, err := inbox.Claim(txOpts, req.Id)
// Claim to solver address for now
// TODO: consider claiming to hot / cold funding wallet
tx, err := inbox.Claim(txOpts, req.Id, solverAddr)
if err != nil {
return errors.Wrap(err, "claim request")
} else if _, err := backend.WaitMined(ctx, tx); err != nil {
Expand Down

0 comments on commit 8555b45

Please sign in to comment.