Skip to content
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
2 changes: 1 addition & 1 deletion contracts/snapshots/BoundlessMarketBasicTest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ERC20 approve: required for depositCollateral": "45966",
"bytecode size implementation": "24312",
"bytecode size implementation": "24383",
"bytecode size proxy": "89",
"deposit: first ever deposit": "50942",
"deposit: second deposit": "33842",
Expand Down
16 changes: 13 additions & 3 deletions contracts/src/BoundlessMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,17 @@ contract BoundlessMarket is
// If the price is higher, we charge the client the difference.
// If the price is lower, we refund the client the difference.
uint96 lockPrice = lock.price;
bool partialPayment = false;
uint96 finalPrice = price;

if (price > lockPrice) {
uint96 clientOwes = price - lockPrice;
if (clientAccount.balance < clientOwes) {
return abi.encodeWithSelector(InsufficientBalance.selector, client);
// If the client does not have enough balance to cover the full amount owed,
// we will only charge them what they have available.
clientOwes = clientAccount.balance;
finalPrice = lockPrice + clientOwes;
partialPayment = true;
}
unchecked {
clientAccount.balance -= clientOwes;
Expand All @@ -577,9 +584,12 @@ contract BoundlessMarket is

requestLocks[id].setProverPaidAfterLockDeadline(assessorProver);
if (MARKET_FEE_BPS > 0) {
price = _applyMarketFee(price);
finalPrice = _applyMarketFee(finalPrice);
}
accounts[assessorProver].balance += finalPrice;
if (partialPayment) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if finalPrice != price, it must be a partial payment, so could drop this variable

return abi.encodeWithSelector(PartialPayment.selector, price, finalPrice);
}
accounts[assessorProver].balance += price;
}

/// @notice For a request that has never been locked. Marks the request as fulfilled, and transfers payment if eligible.
Expand Down
6 changes: 6 additions & 0 deletions contracts/src/IBoundlessMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ interface IBoundlessMarket {
/// @dev selector 0x897f6c58
error InsufficientBalance(address account);

/// @notice Error when a payment is partially settled due to insufficient funds.
/// @param fullAmount The full amount that was required.
/// @param paidAmount The amount that was actually paid.
/// @dev selector 0x6008fdcb
error PartialPayment(uint256 fullAmount, uint256 paidAmount);

/// @notice Error when a signature did not pass verification checks.
/// @dev selector 0x8baa579f
error InvalidSignature();
Expand Down
22 changes: 19 additions & 3 deletions contracts/test/BoundlessMarket.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3920,6 +3920,11 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest {
client.snapshotBalance();
testProver.snapshotBalance();

// Withdraw some funds so we only have funds to cover for the first offer
// and we have a deficit for the second offer to test the partial payment path
vm.prank(client.addr());
boundlessMarket.withdraw(DEFAULT_BALANCE - 2 ether);

// Lock request A
vm.prank(testProverAddress);
boundlessMarket.lockRequest(requestA, clientSignatureA);
Expand All @@ -3944,16 +3949,27 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest {
vm.expectEmit(true, true, true, true);
bytes32 imageId = bytesToBytes32(requestB.requirements.predicate.data);
emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal);
boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt);
bytes[] memory errors = boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt);
// Verify that the second request was partially payed
assertEq(errors.length, 1, "Expected one error");
assertEq(
errors[0],
abi.encodeWithSelector(IBoundlessMarket.PartialPayment.selector, 3 ether, 2 ether),
"Unexpected error"
);

// Verify only the second request's callback was called
assertEq(mockCallback.getCallCount(), 0, "First request's callback should not be called");
assertEq(mockHighGasCallback.getCallCount(), 1, "Second request's callback should be called once");

// Deposit back original funds so that the Market original balance is restored
vm.prank(client.addr());
boundlessMarket.deposit{value: DEFAULT_BALANCE - 2 ether}();

// Verify request state and balances
expectRequestFulfilled(fill.id);
client.expectBalanceChange(-3 ether);
testProver.expectBalanceChange(3 ether);
client.expectBalanceChange(-2 ether);
testProver.expectBalanceChange(2 ether);
testProver.expectCollateralBalanceChange(-1 ether); // Lost stake from lock
expectMarketBalanceUnchanged();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ interface IBoundlessMarket {
/// @dev selector 0x897f6c58
error InsufficientBalance(address account);

/// @notice Error when a payment is partially settled due to insufficient funds.
/// @param fullAmount The full amount that was required.
/// @param paidAmount The amount that was actually paid.
/// @dev selector 0x6008fdcb
error PartialPayment(uint256 fullAmount, uint256 paidAmount);

/// @notice Error when a signature did not pass verification checks.
/// @dev selector 0x8baa579f
error InvalidSignature();
Expand Down
2 changes: 1 addition & 1 deletion crates/boundless-market/src/contracts/bytecode.rs

Large diffs are not rendered by default.

Loading