Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts/solve): support claim to any address #2454

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading