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

[PDB-01C] Inexistent Error Message #886

Merged
merged 3 commits into from
Jan 9, 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
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: 1 addition & 1 deletion contracts/protocol/bases/PriceDiscoveryBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ contract PriceDiscoveryBase is ProtocolBase {

// Check the native balance and revert if there is a surplus
uint256 protocolNativeBalanceAfter = getBalance(address(0), address(this));
require(protocolNativeBalanceAfter == protocolNativeBalanceBefore);
if (protocolNativeBalanceAfter != protocolNativeBalanceBefore) revert NativeNotAllowed();

// Check balance after the price discovery call
uint256 protocolBalanceAfter = getBalance(_exchangeToken, address(this));
Expand Down
2 changes: 2 additions & 0 deletions scripts/config/revert-reasons.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,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
Loading