Skip to content

Commit

Permalink
Test handleWrapper revert reasons
Browse files Browse the repository at this point in the history
  • Loading branch information
zajck committed Jan 9, 2024
1 parent 547e2a0 commit 7f84db8
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 0 deletions.
239 changes: 239 additions & 0 deletions contracts/mock/MockWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.9;

import { IBosonOfferHandler } from "../interfaces/handlers/IBosonOfferHandler.sol";
import { IBosonExchangeHandler } from "../interfaces/handlers/IBosonExchangeHandler.sol";
import { BosonTypes } from "../domain/BosonTypes.sol";
import { ERC721 } from "../example/support/ERC721.sol";
import { IERC721Metadata } from "../example/support/IERC721Metadata.sol";
import { IERC721 } from "../interfaces/IERC721.sol";
import { IERC165 } from "../interfaces/IERC165.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721Receiver } from "../interfaces/IERC721Receiver.sol";

/**
* @title MockWrapper
* @notice Wrapper contract used in tests
*
*/
contract MockWrapper is BosonTypes, ERC721, IERC721Receiver {
// Add safeTransferFrom to IERC20
using SafeERC20 for IERC20;

// Contract addresses
address private immutable voucherAddress;
address private immutable mockAuctionAddress;
address private immutable protocolAddress;
address private immutable wethAddress;

// Token ID for which the price is not yet known
uint256 private pendingTokenId;

// Mapping from token ID to price. If pendingTokenId == tokenId, this is not the final price.
mapping(uint256 => uint256) private price;

// Mapping to cache exchange token address, so costly call to the protocol is not needed every time.
mapping(uint256 => address) private cachedExchangeToken;

mapping(uint256 => address) private wrapper;

/**
* @notice Constructor
*
* @param _voucherAddress The address of the voucher that are wrapped by this contract.
* @param _mockAuctionAddress The address of Mock Auction.
*/
constructor(
address _voucherAddress,
address _mockAuctionAddress,
address _protocolAddress,
address _wethAddress
) ERC721(getVoucherName(_voucherAddress), getVoucherSymbol(_voucherAddress)) {
voucherAddress = _voucherAddress;
mockAuctionAddress = _mockAuctionAddress;
protocolAddress = _protocolAddress;
wethAddress = _wethAddress;

// Approve Mock Auction to transfer wrapped vouchers
_setApprovalForAll(address(this), _mockAuctionAddress, true);
}

/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*/
function supportsInterface(bytes4 _interfaceId) public view virtual override(ERC721) returns (bool) {
return (_interfaceId == type(IERC721).interfaceId || _interfaceId == type(IERC165).interfaceId);
}

/**
* @notice Wraps the voucher, transfer true voucher to itself and approves the contract owner to operate on it.
*
* Reverts if:
* - caller is not the contract owner
*
* @param _tokenId The token id.
*/
function wrap(uint256 _tokenId) external {
// Transfer voucher to this contract
// Instead of msg.sender it could be voucherAddress, if vouchers were preminted to contract itself
// Not using safeTransferFrom since this contract is the recipient and we are sure it can handle the vouchers
IERC721(voucherAddress).transferFrom(msg.sender, address(this), _tokenId);

// Mint to itself, so it can be used with Mock Auction
_mint(address(this), _tokenId); // why not sender instead of address(this)?

// Approves original token owner to operate on wrapped token
_approve(msg.sender, _tokenId);

wrapper[_tokenId] = msg.sender;
}

/**
* @notice Unwraps the voucher, transfer true voucher to owner and funds to the protocol.
*
* Reverts if:
* - caller is neither protocol nor voucher owner
*
* @param _tokenId The token id.
*/
function unwrap(uint256 _tokenId) external {
address wrappedVoucherOwner = ownerOf(_tokenId);
if (wrappedVoucherOwner == address(this)) wrappedVoucherOwner = wrapper[_tokenId];

// Either contract owner or protocol can unwrap
// If contract owner is unwrapping, this is equivalent to canceled auction
require(
msg.sender == protocolAddress || wrappedVoucherOwner == msg.sender,
"MockWrapper: Only owner or protocol can unwrap"
);

// If some token price is not know yet, update it now
if (pendingTokenId != 0) updatePendingTokenPrice();

uint256 priceToPay = price[_tokenId];

// Delete price and pendingTokenId to prevent reentrancy
delete price[_tokenId];
delete pendingTokenId;

// transfer voucher to voucher owner
IERC721(voucherAddress).safeTransferFrom(address(this), wrappedVoucherOwner, _tokenId);

// Transfer token to protocol
if (priceToPay > 0) {
IERC20(cachedExchangeToken[_tokenId]).safeTransfer(protocolAddress, priceToPay);
}

delete cachedExchangeToken[_tokenId]; // gas refund
delete wrapper[_tokenId];

// Burn wrapped voucher
_burn(_tokenId);

// Send funds to protocol (testing purposes)
payable(msg.sender).transfer(address(this).balance);
}

/**
* @notice Handle transfers out of Mock Auction.
*
* @param _from The address of the sender.
* @param _to The address of the recipient.
* @param _tokenId The token id.
*/
function _beforeTokenTransfer(address _from, address _to, uint256 _tokenId) internal virtual override(ERC721) {
if (_from == mockAuctionAddress && _to != address(this)) {
// Auction is over, and wrapped voucher is being transferred to voucher owner
// If recipient is address(this), it means the auction was canceled and price updating can be skipped

// If some token price is not know yet, update it now
if (pendingTokenId != 0) updatePendingTokenPrice();

// Store current balance and set the pending token id
price[_tokenId] = getCurrentBalance(_tokenId);
pendingTokenId = _tokenId;
}

super._beforeTokenTransfer(_from, _to, _tokenId);
}

function updatePendingTokenPrice() internal {
uint256 tokenId = pendingTokenId;
price[tokenId] = getCurrentBalance(tokenId) - price[tokenId];
}

/**
* @notice Gets own token balance for the exchange token, associated with the token ID.
*
* @dev If the exchange token is not known, it is fetched from the protocol and cached for future use.
*
* @param _tokenId The token id.
*/
function getCurrentBalance(uint256 _tokenId) internal returns (uint256) {
address exchangeToken = cachedExchangeToken[_tokenId];

// If exchange token is not known, get it from the protocol.
if (exchangeToken == address(0)) {
uint256 offerId = _tokenId >> 128; // OfferId is the first 128 bits of the token ID.

if (offerId == 0) {
// pre v2.2.0. Token does not have offerId, so we need to get it from the protocol.
// Get Boson exchange. Don't explicitly check if the exchange exists, since existance of the token implies it does.
uint256 exchangeId = _tokenId & type(uint128).max; // ExchangeId is the last 128 bits of the token ID.
(, BosonTypes.Exchange memory exchange, ) = IBosonExchangeHandler(protocolAddress).getExchange(
exchangeId
);
offerId = exchange.offerId;
}

// Get Boson offer. Don't explicitly check if the offer exists, since existance of the token implies it does.
(, BosonTypes.Offer memory offer, , , , ) = IBosonOfferHandler(protocolAddress).getOffer(offerId);
exchangeToken = offer.exchangeToken;

// If exchange token is 0, it means native token is used. In that case, use WETH.
if (exchangeToken == address(0)) exchangeToken = wethAddress;
cachedExchangeToken[_tokenId] = exchangeToken;
}

return IERC20(exchangeToken).balanceOf(address(this));
}

/**
* @notice Gets the Boson Voucher token name and adds "Wrapped" prefix.
*
* @dev Used only in the constructor.
*
* @param _voucherAddress Boson Voucher address
*/
function getVoucherName(address _voucherAddress) internal view returns (string memory) {
string memory name = IERC721Metadata(_voucherAddress).name();
return string.concat("Wrapped ", name);
}

/**
* @notice Gets the the Boson Voucher symbol and adds "W" prefix.
*
* @dev Used only in the constructor.
*
* @param _voucherAddress Boson Voucher address
*/
function getVoucherSymbol(address _voucherAddress) internal view returns (string memory) {
string memory symbol = IERC721Metadata(_voucherAddress).symbol();
return string.concat("W", symbol);
}

/**
* @dev See {IERC721Receiver-onERC721Received}.
*
* Always returns `IERC721Receiver.onERC721Received.selector`.
*/
function onERC721Received(address, address, uint256, bytes calldata) public virtual override returns (bytes4) {
return this.onERC721Received.selector;
}

function topUp() external payable {}
}
2 changes: 2 additions & 0 deletions scripts/config/revert-reasons.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,6 @@ exports.RevertReasons = {
FEE_AMOUNT_TOO_HIGH: "FeeAmountTooHigh",
INVALID_PRICE_DISCOVERY: "InvalidPriceDiscovery",
INVALID_CONDUIT_ADDRESS: "InvalidConduitAddress",
TOKEN_ID_MANDATORY: "TokenIdMandatory",
NEGATIVE_PRICE_NOT_ALLOWED: "NegativePriceNotAllowed",
};
121 changes: 121 additions & 0 deletions test/protocol/PriceDiscoveryHandlerFacet.js
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,127 @@ describe("IPriceDiscoveryHandlerFacet", function () {
expect(voucher.committedDate).to.equal(timestamp);
});
});

context("💔 Revert Reasons", async function () {
let price;
let wrappedBosonVoucher;
beforeEach(async function () {
// Deploy wrapped voucher contract
const wrappedBosonVoucherFactory = await ethers.getContractFactory("MockWrapper");
wrappedBosonVoucher = await wrappedBosonVoucherFactory
.connect(assistant)
.deploy(
await bosonVoucher.getAddress(),
await mockAuction.getAddress(),
await exchangeHandler.getAddress(),
await weth.getAddress()
);

// Price discovery data
price = 10n;
const calldata = wrappedBosonVoucher.interface.encodeFunctionData("unwrap", [tokenId]);
priceDiscovery = new PriceDiscovery(
price,
Side.Wrapper,
await wrappedBosonVoucher.getAddress(),
await wrappedBosonVoucher.getAddress(),
calldata
);
});

it("Committing with offer id", async function () {
const tokenIdOrOfferId = offer.id;

// Attempt to commit, expecting revert
await expect(
priceDiscoveryHandler
.connect(assistant)
.commitToPriceDiscoveryOffer(buyer.address, tokenIdOrOfferId, priceDiscovery)
).to.revertedWithCustomError(bosonErrors, RevertReasons.TOKEN_ID_MANDATORY);
});

it("Price discovery is not the owner", async function () {
// Attempt to commit, expecting revert
await expect(
priceDiscoveryHandler
.connect(assistant)
.commitToPriceDiscoveryOffer(buyer.address, tokenId, priceDiscovery)
).to.revertedWithCustomError(bosonErrors, RevertReasons.NOT_VOUCHER_HOLDER);
});

context("Malfunctioning wrapper", async function () {
beforeEach(async function () {
// 3. Wrap voucher
await bosonVoucher.connect(assistant).setApprovalForAll(await wrappedBosonVoucher.getAddress(), true);
await wrappedBosonVoucher.connect(assistant).wrap(tokenId);

// 4. Create an auction
const tokenContract = await wrappedBosonVoucher.getAddress();
const curator = assistant.address;
const auctionCurrency = offer.exchangeToken;

await mockAuction.connect(assistant).createAuction(tokenId, tokenContract, auctionCurrency, curator);

auctionId = 0;

await mockAuction.connect(buyer).createBid(auctionId, price, { value: price });

// 6. End auction
await getCurrentBlockAndSetTimeForward(oneWeek);
await mockAuction.connect(assistant).endAuction(auctionId);

expect(await wrappedBosonVoucher.ownerOf(tokenId)).to.equal(buyer.address);
expect(await weth.balanceOf(await wrappedBosonVoucher.getAddress())).to.equal(price);
});

it("Wrapper sends some ether", async function () {
// send some ether to wrapper
await wrappedBosonVoucher.topUp({ value: parseUnits("1", "ether") });

// Attempt to commit, expecting revert
await expect(
priceDiscoveryHandler
.connect(assistant)
.commitToPriceDiscoveryOffer(buyer.address, tokenId, priceDiscovery)
).to.revertedWithCustomError(bosonErrors, RevertReasons.NATIVE_NOT_ALLOWED);
});

it("Price mismatch", async function () {
priceDiscovery.price += 10n;

// Attempt to commit, expecting revert
await expect(
priceDiscoveryHandler
.connect(assistant)
.commitToPriceDiscoveryOffer(buyer.address, tokenId, priceDiscovery)
).to.revertedWithCustomError(bosonErrors, RevertReasons.PRICE_TOO_LOW);
});

it("Negative price", async function () {
// Deposit some weth to the protocol
const wethAddress = await weth.getAddress();
await weth.connect(assistant).deposit({ value: parseUnits("1", "ether") });
await weth.connect(assistant).approve(await fundsHandler.getAddress(), parseUnits("1", "ether"));
await fundsHandler.connect(assistant).depositFunds(seller.id, wethAddress, parseUnits("1", "ether"));

const calldata = weth.interface.encodeFunctionData("transfer", [
rando.address,
parseUnits("1", "ether"),
]);
priceDiscovery = new PriceDiscovery(price, Side.Wrapper, wethAddress, wethAddress, calldata);

// Transfer the voucher to weth to pass the "is owner" check
await bosonVoucher.connect(assistant).transferFrom(assistant.address, wethAddress, tokenId);

// Attempt to commit, expecting revert
await expect(
priceDiscoveryHandler
.connect(assistant)
.commitToPriceDiscoveryOffer(buyer.address, tokenId, priceDiscovery)
).to.revertedWithCustomError(bosonErrors, RevertReasons.NEGATIVE_PRICE_NOT_ALLOWED);
});
});
});
});
});
});
Expand Down

0 comments on commit 7f84db8

Please sign in to comment.