Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Script.sol";

import "../../../src/v2/auctionhouse/RareBatchAuctionHouse.sol";

/// @title RareBatchAuctionHouseUpgrade
/// @notice Deploys a new RareBatchAuctionHouse implementation and upgrades the
/// existing UUPS proxy to point at it. Must be broadcast by the proxy
/// owner (see _authorizeUpgrade -> onlyOwner).
/// @dev Required env vars:
/// PRIVATE_KEY - deployer/owner key
/// RARE_BATCH_AUCTIONHOUSE - address of the deployed proxy to upgrade
contract RareBatchAuctionHouseUpgrade is Script {
function run() external {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address proxy = vm.envAddress("RARE_BATCH_AUCTIONHOUSE");

vm.startBroadcast(privateKey);

// Deploy the new implementation.
RareBatchAuctionHouse newImplementation = new RareBatchAuctionHouse();

// Point the proxy at the new implementation (no re-initialization needed).
RareBatchAuctionHouse(payable(proxy)).upgradeTo(address(newImplementation));

console.log("RareBatchAuctionHouse proxy:", proxy);
console.log("New RareBatchAuctionHouse implementation:", address(newImplementation));

vm.stopBroadcast();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Script.sol";

import "../../../src/v2/marketplace/RareBatchListingMarketplace.sol";

/// @title RareBatchListingMarketplaceUpgrade
/// @notice Deploys a new RareBatchListingMarketplace implementation and upgrades
/// the existing UUPS proxy to point at it. Must be broadcast by the proxy
/// owner (see _authorizeUpgrade -> onlyOwner).
/// @dev Required env vars:
/// PRIVATE_KEY - deployer/owner key
/// RARE_BATCH_LISTING_MARKETPLACE - address of the deployed proxy to upgrade
contract RareBatchListingMarketplaceUpgrade is Script {
function run() external {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address proxy = vm.envAddress("RARE_BATCH_LISTING_MARKETPLACE");

vm.startBroadcast(privateKey);

// Deploy the new implementation.
RareBatchListingMarketplace newImplementation = new RareBatchListingMarketplace();

// Point the proxy at the new implementation (no re-initialization needed).
RareBatchListingMarketplace(address(proxy)).upgradeTo(address(newImplementation));

console.log("RareBatchListingMarketplace proxy:", proxy);
console.log("New RareBatchListingMarketplace implementation:", address(newImplementation));

vm.stopBroadcast();
}
}
88 changes: 88 additions & 0 deletions src/test/v2/utils/MarketUtilsV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,94 @@ contract MarketUtilsV2Test is Test {
assertEq(bobBalanceAfter, bobExpectedBalance, "incorrect second split receiver balance after payout");
}

function test_payout_multipleSplits_indivisibleAmount() public {
// Regression test for the split-payout rounding bug.
//
// payoutWithMarketplaceFee floors each recipient's cut
// (splitAmounts[i] = remainingAmount * ratio_i / 100) but passes the
// un-floored remainingAmount as the total to performPayouts. When the
// floors don't sum back to remainingAmount (the common case for non-round
// amounts), Payments.payout reverts with "payout::not enough sent",
// permanently bricking batch auction settlement / merkle-proof buys.
address originContract = address(0xaaaa);
uint256 tokenId = 1;
address currencyAddress = address(0);

// Odd-wei amount: a 50/50 split floors to (amount-1)/2 each, leaving the
// sum 1 wei short of remainingAmount.
uint256 amount = 1 ether + 1;

address payable[] memory splitAddrs = new address payable[](2);
uint8[] memory splitRatios = new uint8[](2);
splitAddrs[0] = payable(charlie);
splitAddrs[1] = payable(bob);
splitRatios[0] = 50;
splitRatios[1] = 50;

// setup getRewardAccumulatorAddressForUser call
vm.mockCall(
stakingRegistry,
abi.encodeWithSelector(IRareStakingRegistry.getRewardAccumulatorAddressForUser.selector, charlie),
abi.encode(address(0))
);

// setup calculateStakingFee call
vm.mockCall(
stakingSettings,
abi.encodeWithSelector(IStakingSettings.calculateStakingFee.selector, amount),
abi.encode(0)
);

// setup getMarketplaceFeePercentage call
vm.mockCall(
marketplaceSettings,
abi.encodeWithSelector(IMarketplaceSettings.getMarketplaceFeePercentage.selector),
abi.encode(3)
);

// secondary sale so remainingAmount stays == amount (no primary fee)
vm.mockCall(
marketplaceSettings,
abi.encodeWithSelector(IMarketplaceSettings.hasERC721TokenSold.selector, originContract, 1),
abi.encode(true)
);

// no royalties -> remainingAmount is the full amount
vm.mockCall(
royaltyEngine,
abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector, originContract, tokenId, amount),
abi.encode(new address payable[](0), new uint256[](0))
);

uint256 charlieBalanceBefore = charlie.balance;
uint256 bobBalanceBefore = bob.balance;

uint256 marketplaceFee = (amount * 3) / 100;

// Must not revert: the full remaining amount has to be distributable.
vm.prank(deployer);
tc.payout{value: amount + marketplaceFee}(
originContract,
tokenId,
currencyAddress,
amount,
charlie,
splitAddrs,
splitRatios
);

// First recipient gets its floored share; the last recipient absorbs the
// rounding remainder so the entire remaining amount is paid out.
uint256 charlieShare = (amount * 50) / 100;
uint256 bobShare = amount - charlieShare;

assertEq(charlie.balance, charlieBalanceBefore + charlieShare, "first split incorrect");
assertEq(bob.balance, bobBalanceBefore + bobShare, "last split should absorb remainder");
assertEq(charlieShare + bobShare, amount, "splits must sum to the full remaining amount");
// Nothing should be stranded in the market contract.
assertEq(address(tc).balance, 0, "no funds should remain in market contract");
}

function test_payout_tooManyRoyaltyRecipients() public {
address originContract = address(0xaaaa);
uint256 tokenId = 1;
Expand Down
11 changes: 11 additions & 0 deletions src/v2/utils/MarketUtilsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,19 @@ library MarketUtilsV2 {

// Calculate and pay out splits
uint256[] memory splitAmounts = new uint256[](_splitRatios.length);
uint256 distributed = 0;
for (uint256 i = 0; i < _splitRatios.length; i++) {
splitAmounts[i] = (remainingAmount * _splitRatios[i]) / 100;
distributed += splitAmounts[i];
}

// Flooring each share can leave a rounding remainder so the per-recipient
// amounts sum to less than remainingAmount. performPayouts forwards
// remainingAmount as the total, and Payments.payout requires the total to
// equal the sum of the amounts -- so assign the remainder to the last
// recipient to keep them equal and distribute the full amount.
if (_splitRatios.length > 0) {
splitAmounts[_splitRatios.length - 1] += remainingAmount - distributed;
}

performPayouts(_config, _currencyAddress, remainingAmount, _splitAddrs, splitAmounts);
Expand Down