The easiest way to collectively buy, own, and govern the NFTs you already know and love.
Platform: Code4rena
Start Date: 16/12/2022
End Date: 19/12/2022
Period: 3 days
Status: Completed
Pot Size: $32,070 USDC
Participants: 5
Reporter: itsmetechjay
Judge: hickuphh3
Id: 195
League: ETH
The C4audit output for the contest can be found [here](add link to report) within an hour of contest opening.
Note for C4 wardens: Anything included in the C4udit output is considered a publicly known issue and is ineligible for awards.
Tessera is a decentralized protocol that allows for shared ownership and governance of NFTs. When an NFT is vaulted, the newly minted tokens function as normal ERC-1155 tokens which govern the non-custodial Vault containing the NFT(s).
The Tessera Protocol is designed around the concept of Hyperstructures, which are crypto protocols that can run for free and forever, without maintenance, interruption or intermediaries.
The home of all items on Tessera
Vaults are a slightly modified implementation of PRBProxy which is a basic and non-upgradeable proxy contract. Think of a Vault as a smart wallet that enables the execution of arbitrary smart contract calls in a single transaction. Generally, the calls a Vault can make are disabled and must be explicitly allowed through permissions. The vault is intentionally left unopinionated and extremely flexible for experimentation in the future.
Authorize transactions performed on a vault
A permission is a combination of a Module contract, Target contract, and specific function Selector in a target contract that can be executed by the vault. A group of permissions creates the set of functions that are callable by the Vault in order to carry out its specific use case. Each permission is then hashed and used as a leaf node to generate a merkle tree, where the merkle root of the tree is stored on the vault itself. Whenever a transaction is executed on a vault, a merkle proof is used to verify legitimacy of the leaf node (Permission) and the merkle root.
Make vaults do cool stuff
Modules are the bread and butter of what makes Tessera unique. At Vault creation, modules are added to permissions for the vault. Each module should have specific goals it plans to accomplish. Some general examples are Buyouts, Inflation, and Renting. If a vault wants to update the set of modules enabled, then it must have the migration module enabled and go through the migration process. In general, it is highly recommended for vaults to have a module that enables items to be removed from vaults. Without this, all items inside of a vault will be stuck forever.
Execute vault transactions
Targets are stateless script-like contracts for executing transactions by the Vault on-behalf of a user. Only functions of a target contract that are initially set as enabled permissions for a vault can be executed by a vault.
Unlike Optimistic Buyouts, Optimist Listings do not use ETH (except for gas) in proposals. A user must be a current Rae holder in order to propose a listing. They must select what marketplace the listing will be done on, what price the NFT(s) should be listed for, how long the listing should last (if applicable), and how many Raes they are willing to use to sponsor the listing.
If there is a current listing in the review period, another use can list lower which would restart the review period at the same price. There can only be one active listing for a vault at a time, even if its partial.
A proposal will include all NFTs within the vault.
After a user creates a listing proposal, there will be a 4 day rejection window. All vault holders will be able to reject a proposal by purchasing Raes within the proposal pool. These Raes will be priced according to the price inputed in step 1. Immediately when all the Raes within the proposal period are purchased, the proposal will end.
If at the end of the review period there are any Raes remaining within the pool, the NFT(s) will then be listed on the marketplace for the proposed time/price.
Once the NFT(s) are listed on a marketplace, anybody can purchase the NFT normally. The Raes that were contributed to the pool during the review period are then stored away.
Delisting
Users that initiated the proposal can claim the Raes from the lock up at any time for free (besides gas).
If other users want to cancel the listing, they can purchase the Raes for the implied valuation, similar to the review period.
If there are no more Raes left in the sponsor storage, the NFT will be immediately delisted from the marketplace.
Another Listing Proposal
There can only be one active listing per marketplace. To prevent a user from holding a vault hostage and never letting the piece be reasonably bought, cheaper counter-listings can be created.
If a user wants to make a listing at a lower price at the same valuation, they can go through steps 1 and 2. The proposed listing price must be at least 5% cheaper than the active proposal price.
If there are still Raes remaining in the new optimistic listing by the end of the new proposals listing period, the old listing is immediately terminated. The Raes from the old listing will be returned to the proposal creator, and the NFT(s) will then be listed for cheaper.
Listing Expiration
Listings can never expire, they can only be canceled via Optimistic Listings.
Once the listing is purchased, the vault will act similarly to an Optimistic Buyout. The proposer will have their Raes returned to them, and all users can burn their Raes for their portion of ETH gained from the sale.
A user will first create a Group Buy Pool, by selecting what NFT collection they would like to start the pool for. Users will only be able to select NFT collections that are currently supported on Tessera with Protoforms (front end only).
The user will then specify which token IDs are valid to be purchased (which specific NFTs). They will also select the total supply of Raes, and the initial price that they think the NFT will sell for. This initial price will then determine the minimum bid price anyone can make for the pool. The final step will be for the creator to contribute to the pool (at least the minimum price).
To contribute to the pool, the user will specify how many tokens they want, and the price they want to pay per token (has to be greater than minimum price). The total amount that they will pay is price per Rae * amount of tokens.
If the quantity of Raes has been filled (filled quantities = total supply) then users contributing to the pool must at least meet the current minimum reserve price per Rae to have their bid included.
As other users deposit funds into the pool, the pool will begin to increase. Once the total supply of Raes have been sold (filled quantities = total supply), the pool will start filtering. During the filtering process, the pool will start to remove the quantity of Raes from lower bids in order to fill the orders with a higher price per-Rae.
If two users place bids at the same price but with different quantities, the queue will pull from the bid with a higher quantity first. If the users have the same quantity as well, the bid that was placed later will have Raes removed.
If a users bid becomes less than the current minimum reserve price, the user’s bid will not be included in the pool. They will be able to withdrawal their refund at any time.
Users cannot withdraw funds from a pool unless their bid gets removed from the pool.
Once the pool has raised enough ETH to purchase a specified NFT that is listed on a marketplace, any user is able to execute the purchase order once the minBid * # Raes is enough to successfully purchase an NFT Id from the initial list used to create the pool. (arbitrary amount of data that is based on the marketplace the NFT is listed on) on behalf of the pool.
Once the purchase is executed, a new vault gets deployed with the proper permissions, the NFT then gets transferred to the vault, and ownership of the NFT by the vault is verified.
After the purchase is made, users can then claim their portion of the Raes (based on amount contributed).
If there is excess ETH in the pool after the purchase is made, when users claim their Raes they also will receive a refund for their portion of excess ETH.
Contract | SLOC | Purpose | Libraries used |
---|---|---|---|
src/seaport/modules/OptimisticListingSeaport.sol | 318 | Module contract for listing vault assets through the Seaport protocol | Seaport |
src/seaport/targets/SeaportLister.sol | 35 | Target contract for listing orders on Seaport | Seaport |
src/modules/GroupBuy.sol | 290 | Module contract for pooling group funds to purchase and vault NFTs | OpenZeppelin |
src/lib/MinPriorityQueue.sol | 103 | Queue used for smart batch auction in GroupBuy | |
src/punks/protoforms/PunksMarketBuyer.sol | 49 | Protoform contract for executing CryptoPunk purchase orders and deploying vaults |
Contract |
---|
src/constants/Memory.sol |
src/constants/Permit.sol |
src/constants/Supply.sol |
src/constants/Transfer.sol |
src/interfaces/IBaseVault.sol |
src/interfaces/IERC20.sol |
src/interfaces/IERC721.sol |
src/interfaces/IERC1155.sol |
src/interfaces/IGroupBuy.sol |
src/interfaces/IIssuerFactory.sol |
src/interfaces/IMarketBuyer.sol |
src/interfaces/IMetadataDelegate.sol |
src/interfaces/IMinter.sol |
src/interfaces/IModule.sol |
src/interfaces/INFTReceiver.sol |
src/interfaces/IProtofrom.sol |
src/interfaces/IRae.sol |
src/interfaces/ISupply.sol |
src/interfaces/ITransfer.sol |
src/interfaces/IVault.sol |
src/interfaces/IVaultFactory.sol |
src/interfaces/IVaultRegistry.sol |
src/mocks/MockERC20.sol |
src/mocks/MockERC721.sol |
src/mocks/MockERC1155.sol |
src/mocks/MockEthReceiver.sol |
src/mocks/MockModule.sol |
src/mocks/MockPermitter.sol |
src/mocks/MockSender.sol |
src/modules/Minter.sol |
src/modules/Module.sol |
src/protoforms/BaseVault.sol |
src/protoforms/IssuerFactory.sol |
src/protoforms/Protoform.sol |
src/punks/interfaces/ICryptoPunk.sol |
src/punks/interfaces/IOptimisticListing.sol |
src/punks/interfaces/IPunksMarketAdapter.sol |
src/punks/interfaces/IMunksMarketBuyer.sol |
src/punks/interfaces/IPunksMarketLister.sol |
src/punks/interfaces/IPunksProtofrom.sol |
src/punks/interfaces/IWrappedPunks.sol |
src/punks/modules/OptimisticListingPunks.sol |
src/punks/protoforms/PunksProtoform.sol |
src/punks/targets/PunksMarketLister.sol |
src/punks/utils/CryptoPunksMarket.sol |
src/punks/utils/PunksMarketAdapter.sol |
src/punks/utils/WrappedPunk.sol |
src/references/SupplyReference.sol |
src/references/TransferReference.sol |
src/seaport/interfaces/IOptimisticListingSeaport.sol |
src/seaport/interfaces/ISeaportLister.sol |
src/targets/Supply.sol |
src/targets/Transfer.sol |
src/utils/MerkleBase.sol |
src/utils/MetadataDelegate.sol |
src/utils/Multicall.sol |
src/utils/NFTReceiver.sol |
src/utils/PermitBase.sol |
src/utils/SafeSend.sol |
src/utils/SelfPermit.sol |
src/Rae.sol |
src/Vault.sol |
src/VaultFactory.sol |
src/VaultRegistry.sol |
Required > node 12
On windows use WSL or the Docker image
curl -L https://foundry.paradigm.xyz | bash
docker pull ghcr.io/foundry-rs/foundry:latest
Our tests that interact with Seaport require forking of Goerli currently. There is an Alchemy RPC_URL with an API key that will remain active during the contest
Setup a .env with the following variables:
GOERLI_RPC_URL=
You copy .env.example to .env which has defined a temporary RPC_URL that will remain active for the duration of the contest
Please be respectful of the key in the .example.env and replace it with your personal RPC_URL if you have one.
NOTE: For fastest performance in forked mode use an Infura Goerli RPC URL. Alchemy in my experience is between 2-5x slower than Infura for forked tests
Below we have broken up test commands to run with and without forking where the tests without forking don't run any tests that interact with Seaport, so please keep that in mind as your explore the tests / make edits to them
npm ci
git submodule update --init --recursive
forge build
Run only GroupBuy
make test-nofork
Run GroupBuy and Seaport Optimistic Listing
make test-fork
forge test --gas-report
npm run lint
Required: Python 3.10
slither .