---
# System prepended metadata

title: 'SUIFT Model:'

---

## (AirFoil Copy) Introducing SUIFT: Safe User Inception for Tickets 

Authored by [Matt Stephenson](https://twitter.com/stephensonmatt), [Xynyuan Sun](https://twitter.com/sxysun1), and [Lauren Stephanian](https://twitter.com/lstephanian)

Every few years artists go on tour and try to sell fans tickets at the price they deem fair. And each time they do this, many of those under-priced tickets are intercepted by bots and brokers, costing the artist and her fans vast amounts of money almost certainly in the billions of dollars. 

But there’s now an answer: SUIFT, the Safe User Inception For Tickets. SUIFT uses a unique auction mechanism to ensure:
a. The ticket sales process is trustworthy
b. The only purchasers of an artist’s tickets should be the true fans, not resellers.
c. Ticket buyers pay only the artist’s chosen face value of the tickets.

Sound too good to be true? Let’s dive in.

## The Ticketing Industry

Famous artists are in a repeated game with their fans. When they go on tour, they seek to choose “cooperate” by charging their fans a little less than they might be willing to pay.[^1] But this is surprisingly difficult to implement in practice, [with some experts claiming](https://www.ftc.gov/system/files/documents/public_events/1413898/slides-online-events-tickets-6-11-19.pdf) the only solution is to effectively charge the “defect” price or to try and prevent ticket resale which has [proven extremely difficult](https://www.rollingstone.com/music/music-news/the-cure-ticketmaster-fees-scalpers-1234698953/). 

The problem is that, as much as an artist might like to “cooperate”, forcing them to “defect” on their fans can be worth billions. Comparably enormous resale profits go to professional brokers who are not publicly known. But they have gamed the system so thoroughly that, according to a [government report](https://www.gao.gov/products/gao-18-347), they now “represent either the majority or overwhelming majority of ticket sales”. Artists want to sell to fans, at a cooperative price, but professional brokers intercept.

## How SUIFT Fixes This

Right now tickets are typically sold by just opening the floodgates on an ostensibly first come, first serve basis. The result, as described above, is that specialized professional resellers win the majority, if not the “overwhelming majority” of the tickets intended for fans.[^2] SUIFT can improve on this by running an effective auction using Flashbots’ SUAVE. SUAVE allows all parties to verify that the auction is being run fairly, while also protecting the valuable information that can otherwise allow auctions to be manipulated.[^3]

While running a credible auction is an improvement, we have not yet addressed the artist’s wishes that fans only pay the cooperative face value for the tickets. To accomplish this, we can do two things:

1. Refund the difference between the auction price and the “Face value price” to everyone who attends the show. This means, when a fan enters the venue the night of the show, they get refunded the difference between what they paid in auction and the intended face value. Assume the ticket’s face value is $100, but it went at auction for $300. The night of the concert, when a fan shows their ticket at the venue, they are admitted to the show AND refunded $200.[^4] 

2. Allow artists to “amplify” the bids of those they have identified as true fans. We explain this further in the appendix, but the essential upshot is that an artist can use our SUIFT contract to select some set of fans who get their bids automatically increased at no cost to them. That is, if the artist chooses double the bid of every fan, and a fan bids $100, it will be as if they had bid $200. If the winning price is $200, the fan wins the ticket but pays only $100.

The “refund at the show” approach uniquely benefits fans based on their willingness to actually attend the show. And the “amplify the bids” approach rewards fans who might be budget constrained or, offer some additional value to the show beyond what they are willing to pay. These two together segment the market such that only true fans should ever want to hold a ticket.

The price is right, but only for true fans.

# SUIFT Model:

Consider a standard setup with $n$ risk neutral bidders indexed $i = (1, 2, \dots, n)$. Each has some valuation $v_i$ which is independently and privately drawn from a common knowledge probability distribution $F_i(v_i)$ on support $([\underline{v}_i, \overline{v}_i])$. Bidder $i$ bids $b_i$ for a good, paying only if they win.

Add to this setup a “fan value”; a value some bidders are assumed to provide in addition to the amount they pay for the tickets. Artists who intentionally “underprice” their tickets may do so because there is some benefit to audience members cheering, or singing along or, as discussed earlier, engaging in some longer-term “cooperation” to which underpricing is a response. This “fan value”, assumed (initially) to accrue only to the artist, is the vector $(c_i,\dots, c_n)$.[^5]

Ticketmaster's "Verified Fan" program can be thought of as an attempt to set a threshold fan value, such that any fan with a sufficiently high $c$ gets a cheaper ticket. Here we allow the fan value to be more precise, while also being shielded from the fan so they continue to bid their true value. This shielding also frustrates hackers who might seek gain access to a fan's account in order to buy tickets for cheaper. Such a hacker would not be assured of success until they had submitted a bid.

Formally, we allow the Artist to specify augmenting bids $(d_i,\dots, d_n)$ which add differentially to each bid $b_i$. We then treat a Vickrey-style auction in which tickets are awarded to the bidder for whom $b_i+d_i>b_j+d_j$ for all $j \ne i$. If the winner pays $\text{max}_{j \ne i} (b_j+d_j)-d_i$, and the Artist pays the difference between the winner's bid and the highest bid, this auction is efficient. Truthful bidding is (weakly) dominant so each bidder $i$ bids their true valuation $b_i=v_i$, and the Artist is similarly truthful about their augmenting bid, setting the vector of bids equal to the fan value,  $(d_i,\dots, d_n)=(c_i,\dots, c_n)$.

Thus we have fans bidding their truthful valuations for tickets, with some of those fans' bids being increased by the artist themselves. Though the preceding model treats a single ticket, it can be extended to multiple tickets as in a uniform price auction. If we assume that the artist is able to identify their fans (and thus set $(d_i,\dots, d_n)=(c_i,\dots, c_n)$ we have effectively reduced the price while removing re-sellers from the market.

## Adding a Rebate

But what if an artist is unable to perfectly identify her fans?[^6] If some non-fans are able to get a "fan bonus", for instance, they can then re-sell tickets with the expectation of profit. 

Fortunately, a further mechanism can be used to weaken the bargaining power of re-sellers even further: offer the partial bid rebate upon attendance of the concert. To see this consider some fan signal $S$, that can be used by an artist to assess who is or is not a true fan. In the scenario where brokers get tickets, we have a pooling equilibrium where $S$ is insufficient to ensure brokers don't bid. 

We suggest a refund conditional on attendance may implement the desired separating equilibrium in which only fans bid. Recall that bidders bid $b_i=v_i$ but pay only $b_j<b_i$ due to the Vickrey format. The SUIFT mechanism refunds $b_i-b_j$ conditional on concert entry, which is enforceable since concert entry is tightly controlled (otherwise the tickets would have no value.) Such a refund increases the effective price for Brokers, since they by definition do not want to attend the concert. Brokers who are non-local, perhaps bidding from another country, would find it especially onerous to attend for a refund. 

On the other hand, Fans face no cost from attending the concert (they rather benefit from it of course.) The figure below depicts the seperating equilibrium, in which Fans get a higher rebate $R_f$ because they find paying the additional fan signal $\bar{S}-S*$ to be incentive compatible.

![image](https://hackmd.io/_uploads/ByiNerZL6.png)


[^1]: The artist's fans may "cooperate" by, for instance, being loyal during a difficult period, or by listening to a challenging album multiple times to see if it sticks.
[^2]: For an excellent deep-dive into this industry, see Jason Koebler's [reporting.](https://www.vice.com/en/article/m7g45a/why-ticket-brokers-can-get-taylor-swift-tickets-ticketmaster-live-nation-monopoly)
[^3]: See for example [the recent lawsuit against Google](https://techpolicy.press/how-google-manipulated-digital-ad-prices-and-hurt-publishers-per-doj/#:~:text=But%20Google%20Ads%20allegedly%20submitted,which%20it%20dubbed%20%E2%80%9CBernanke.%E2%80%9D) alleging that it did not credibly run the auctions as it claimed it did.
[^4]: Or $100 if the Swifty already had their bid amplified as in 2.
[^5]: It can be thought of as the value of participating and singing along, which makes the concert better for everyone. Or, in other settings, the value of attending a conference and asking good questions, or attending a football game and yelling to influence the refs, etc.
[^6]: Complementarity of tickets and bidder budget constraints can introduce similar problems.

---
## A Look at the Code

This contract is created by taking in a ticket address, which is the address of the contract where we've described the ERC1155 tokens as tickets, the ticket ID, which is the ticket "type" (changing this would allow us to run different auctions for e.g. different sections) and a ticket supply, indicating the max number of tickets that can be sold, and a charity address, which we will explain a little later on.

Please note that this code is untested and unaudited. As of the time of publishing, it is to be used for educational purposes only. 

```
pragma solidity ^0.8.8;

import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../suave-geth/suave/sol/libraries/Suave.sol";
import { Tickets } from './Tickets.sol';

contract Auction is ERC1155Holder, Ownable {
    address [] winners;
    address [] bidders;
    uint [] allBidValues;
    mapping(address => bool) private attendees;
    mapping(address => uint) private accountForWithdrawals;
    mapping(address => uint) private boosted;
    address public immutable TICKET_ADDRESS;
    uint public immutable TICKET_RESERVE_PRICE;
    uint public immutable TICKET_SUPPLY;
    uint public immutable AUCTION_TICKETS_TYPE; 
    address public immutable CHARITY_ADDRESS;
    uint private immutable DECRYPTION_CONDITION  = 10;
    uint private currMinBid;
    bool public auctionEnded = false;
    bool public rebatePeriodEnded = false;
    struct AuctionBid {
        address beneficiary;
        uint256 amount;
        uint256 timestamp;
    }
    AuctionBid[] private bids;
    AuctionBid[] private winningBids;

    Tickets tickets = new Tickets();

    event BitRefundReceived(address indexed beneficiary, uint256 indexed amount);
    event MinBidUpdated(uint256 indexed amount);
    event AuctionCreated(address ticketsAddress, uint auctionTicketsId, uint ticketSupply, uint ticketReservePrice, address charity);
    event AttendedEvent(address eventgoer);
    event AuctionEnded(address[] winners);

    constructor (address _ticketsAddress, uint _auctionTicketsId, uint _ticketSupply, uint _ticketReservePrice, address _charity) {
        require(_auctionTicketsId == 1 || _auctionTicketsId == 2 || _auctionTicketsId == 3, "Token does not exist");
        require(_ticketSupply > 0, 'Must provide supply');
        require(_ticketReservePrice > 0, 'Must provide reserve price');
        
        CHARITY_ADDRESS = _charity;
        AUCTION_TICKETS_TYPE = _auctionTicketsId;
        TICKET_SUPPLY = _ticketSupply;
        TICKET_RESERVE_PRICE = _ticketReservePrice;
        TICKET_ADDRESS = _ticketsAddress;

        //emit event
        emit AuctionCreated(_ticketsAddress, _auctionTicketsId, _ticketSupply, _ticketReservePrice, _charity);
    }

```
In this contract, an artist can graciously identify certain superfans to "boost" i.e. give them an additional amount of cash to boost their bid higher. This is like a fine-tunable version of Ticketmaster’s “Verified Fan” program. Rather than set a single threshold condition for which “Verified” fans get preferential access, here we can provide different “boosts” to different bidders, effectively matching their bids. The result is a more customizable was to help real fans win tickets while paying less.

```
    function setBoosted(address _bidder, uint boostAmount) public onlyOwner {
        require(boostAmount > 0, "boost amount must be greater than 0");
        boosted[_bidder] = boostAmount;
    }

    function _getBoosted(address _bidder) private onlyOwner {
        return(boosted[_bidder]);
    }
    
```
Here's where the fun begins: a bidder enters their "verbal" bid, which is an indication of what they plan to pay and at the same time sends the base value of the ticket as their "deposit." This deposit can be lost if the bidder wins a ticket in the auction and does not pay the delta between their bid and the face value of the ticket. This is to deter bidders who have no intention of paying from participating. 

The verbal bid is placed in Suave's confidential store, which uses advanced hardware-based encryption to prevent anyone from viewing or accessing the bid. This is new technology enabling privacy from all other parties in the auction, while providing verifiable guarantees that the auction process was handled fairly. This privacy is essential for ensuring the auction cannot be gamed at the expense of the bidders. 

Because we are calling an off-chain function (_sendBidToConfidentialStore(), described later below) from within the sendBid function, it will also be off-chain. This means we will need it to return a function selector and accompanying inputs to an on-chain function call to update the data. 

```
    function sendBid(uint _bidAmount) public returns(bytes) {
        require(auctionEnded == false, "The auction has ended");
        require(_bidAmount >= TICKET_RESERVE_PRICE, "The bid cannot be lower than ticket value");

        // check if bidder got a boost from the owner
        // if so, boost their bid
        if (_getBoosted(msg.sender) > 0) {
            _bidAmount += _getBoosted(msg.sender);
        }

        // creates a new auction bid, which tracks bidder, their verbal amount, and the time of bid
        AuctionBid auctionBid = new AuctionBid(msg.sender, _bidAmount, block.timestamp); 

        // send the bid to the confidential store and add bid amount to bid value array
        _sendBidToConfidentialStore(auctionBid);
        allBidValues.push(_bidAmount);

        uint latestMinBid = _getMinBid();
        if (currMinBid != latestMinBid){
            currMinBid = latestMinBid;
            emit MinBidUpdated(latestMinBid);
        }

        return abi.encodeWithSelector(this._receiveBidInfo.selector, msg.sender, msg.value);
    }

    function _receiveBidInfo(address sender, uint value) external returns(bytes) {
        //note: without this, anyone could call this and modify our storage
        require(sender == 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829, "sender should be kettle"); 

        // if bidder hasn't previously bid, they need to pay ticket reserve amount
        // add them to the bidders list
        if (bidders[sender] == false){
            require(value == TICKET_RESERVE_PRICE, "Required to send ticket reserve amount");
            bidders.push(sender);
        }    
    }
    
    function _sendBidToConfidentialStore(AuctionBid _bid) internal view {
        require(Suave.isConfidential());
        address[] memory allowedList = new address[](1);
        allowedList[0] = 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829; // wildcard address so any kettle can access

        // initialize confidential store
        // note: this is creating a new bid data for every bid, might be good to go back one day and put multiple 
        // bids in one bid data store
        Suave.Bid memory bid = Suave.newBid(DECRYPTION_CONDITION, allowedList, allowedList, "auctionBid"); 

        // save bid in confidential store
        Suave.confidentialStore(bid.id, "auctionBid", abi.encode(_bid));
    }
    
```
The owner of the contract can end the auction. Once this is done, the winners are retrieved from SUAVE's confidential store and returned. 

Because you cannot yet delete from the confidential store (or at least it's non-trivial) we will need a sorting function to sort through the winners. This leaves room for future improvement in gas efficiency.

```
    function auctionEnd() public onlyOwner {
        auctionEnded = true;

        //determine winners and emit list
        winningBids = _getWinningBids();
        emit AuctionEnded(winningBids);
    }

```
Once winners are announced, they will need to pay for their tickets. The payment due is the delta between their bid amount and the ticket purchase amount. Again, if they don't end up paying this, they will lose their initial deposit.

Again, we will be accessing an off-chain function (_checkIfWinner()), so we will need to return the function selector and accompanying inputs, keeping the off-chain actions off-chain and the on-chain actions on-chain.

```
    function payForTickets() public payable returns(bytes) {
        require(auctionEnded, "Auction still ongoing");
        require(_checkIfWinner(msg.sender), "Did not win tickets");
        //assume payment / msg.value works for now
        require(msg.value >= (_getAmountOwed(msg.sender) - TICKET_RESERVE_PRICE), "Payment amout incorrect"); 
        
        return abi.encodeWithSelector(this._onchainPayForTickets.selector, msg.sender);
    }
    
    function _onchainPayForTickets(address _sender) public  {
        require(sender == 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829, "sender should be kettle");

        // mint tickets to msg.sender
        tickets.mint(msg.sender, AUCTION_TICKETS_TYPE, 1);
    }
    
```
Post the event occurance, wow do we know who attended the concert? Easy, the owner of the contract will mark them as “attended.” Ultimately you might want to automate this step of the process, by pulling in data from a ticket scanning API. However, for ease of understanding, in this example the owner manually marks each participant as "attended."

Once a participant is marked as having attended the event they are able to collect their rebate - the delta between their bid amount and the face value of the ticket. They receive this only if they attended the concert. All auction winners who don't attend the concert - taking up the spot that otherwise could have gone to a devoted fan - will not be able to withdraw this rebate. A future design may allow this rebate address to be changed by the attendee, to prevent smart-contract enabled rebate transfer.

```
    function setAttendConcert(address _participant) public onlyOwner returns(bytes) {
        require(auctionEnded, "Auction still ongoing");
        require(_checkIfWinner(_participant), "Did not win tickets");

        return abi.encodeWithSelector(this._onChainSetAttendConcert.selector, _participant);
    }

    function _onChainSetAttendConcert(address _participant) public {
        require(sender == 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829, "sender should be kettle");
        attendees[_participant] = true;
        emit AttendedEvent(_participant);

        // allow particpant to now withdraw rebate - add rebate to their "account"
        for (uint i = 0; i < bids.length; i++) {
            if (bids[i].beneficiary == _participant) {
                accountForWithdrawals[bids[i].beneficiary] += bids[i].amount;
            }
        }
    }

    //Enable withdrawals for bids that have been overbid
    function rebateWithdraw() external returns (bool) {
        require(rebatePeriodEnded == false, "It's not rebate period");
        require(_getAttendedConcert(msg.sender), "You did not attend the event");
        require(accountForWithdrawals[msg.sender] > 0, "Nothing to withdraw");

        uint amountAvailable = accountForWithdrawals[msg.sender];

        //mark the rebate as withdrawn and send rebate
        (bool success,) = payable(msg.sender).call{ value: amountAvailable }('');
        emit BitRefundReceived(msg.sender, amountAvailable);        
        return(success);
    }
    
```
In this contract, after the rebate period (which the owner of the contract will determine), the uncollected rebates will either be burned or sent to the charity contract address. This ensures that no one maliciously prevents attendees from attending the concert in order to collect their rebates. 
``` 
    function burnOrSendRebate(address participant) public onlyOwner {
        require(rebatePeriodEnded && auctionEnded, "Rebate period is not over yet");

        uint burnAmt;

        for (uint i = 0 ; i < bids.length; i++){
            bytes32 encodedAddy = keccak256(abi.encode(bids[i].beneficiary));

            if (encodedAddy == keccak256(abi.encode(msg.sender))){
                burnAmt = accountForWithdrawals[participant];
                (bool sent,) = CHARITY_ADDRESS.call{value: burnAmt}("");
                require(sent, "Failed to send Ether");
            }
        } 
    }
    
```
Now, we get to the meat of the off-chain Suave-related functions: sendBidtoConfidentialStore() and getWinningBids(). These both rely on Suave's precompiles, which are MEVM contracts implemented in native code instead of bytecode. 

According to documentation, MEVM supports all Ethereum precompiles up to Dencun as well as four classes of additional precompiles:

* offchain computation that is too expensive in solidity
* calls to API methods to interact with the Confidential Data Store
* calls to suavex API Methods to interact with Domain-Specific Services
* calls to retrieve context for the confidential compute requests

```
    // Internal function to save order details confidentially
    function _sendBidToConfidentialStore(AuctionBid _bid) internal view {
        require(Suave.isConfidential());
        address[] memory allowedList = new address[](1);
        allowedList[0] = 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829; // wildcard address so any kettle can access

        // initialize confidential store
        // note: this is creating a new bid data for every bid, might be good to go back one day and put multiple 
        // bids in one bid data store
        Suave.Bid memory bid = Suave.newBid(DECRYPTION_CONDITION, allowedList, allowedList, "auctionBid"); 

        // save bid in confidential store
        Suave.confidentialStore(bid.id, "auctionBid", abi.encode(_bid));
    }

    function _getWinningBids() internal returns(AuctionBid []) {
        currMinBid = _getMinBid();
        
        Suave.Bid[] memory fetchedBids = bid.fetchBids(DECRYPTION_CONDITION, "auctionBid");

        // extract bids from confidential store
        for (uint i = 0; i < fetchedBids.length; i++) {
            
            bytes memory bidStruct = (Suave.confidentialRetrieve(fetchedBids[i], "auctionBid"));
            AuctionBid restoredBid  = abi.decode(data, AuctionBid);
            uint bidVal = restoredBid.amount;

            // only collecting bids that have won
            // todo: sort by timestamp and only include ticket_supply number of bids
            if (bidVal >= currMinBid) {
                bids[i] = Suave.confidentialRetrieve(bid[i], "auctionBid");
            }
            // if bids have lost, allow bidders to withdraw by adding to their withdrawal account
            if (bidVal < currMinBid) {
                accountForWithdrawals[bids[i].beneficiary] += bids[i].amount;
            }
        }
		return bids;
    }
    
```
Finally, this section simply describes some internal functions, getters and sorts, for supporting some of the important functions above.
```

    function _getAttendedConcert(address _participant) internal returns(bool) {
        return(attendees[_participant]);
    }

    function _checkIfWinner(address _bidder) internal returns (bool) {
        require(auctionEnded, "auction still ongoing");
        _getWinningBids();
        for (uint i=0; i < bids.length; i++){
            if (bids[i].beneficiary == _bidder) {
                return true;
            }
        }
        return false;
    }

    function _getAmountOwed(address _bidder) internal returns (uint) {
        require(auctionEnded, "auction still ongoing");
        _getWinningBids();
        for (uint i=0; i < bids.length; i++){
            if (bids[i].beneficiary == _bidder) {
                return bids[i].amount;
            }
        }
    }
    
    function _getMinBid() internal returns(uint) {
        (uint left, uint i) = allBidValues[0];
        (uint right, uint j) = allBidValues[allBidValues.length - 1];
        
        if (i == j) return;
        uint pivot = bids[uint(left + (right - left) / 2)];
        while (i <= j) {
            while (bids[uint(i)] > pivot) i++;
            while (pivot > bids[uint(j)]) j--;
            if (i <= j) {
                (bids[uint(i)], bids[uint(j)]) = (bids[uint(j)], bids[uint(i)]);
                i++;
                j--;
            }
        }
        if (left < j) {
            _getMinBid(bids, left, j);
        }

        if (i < right) {
            _getMinBid(bids, i, right);
        }

        return(allBidValues[TICKET_SUPPLY-1]);
    }
}
```

Note on DDOS and Gas Efficiency: one challenge faced in this auction design was the fact that it's difficult to send and store actual funds on SUAVE privately. Thus we chose to impose penalties for bidders who have bid and then removed the funds they bid before the auction is completed.

You can view the code in full on GitHub [here](https://github.com/PanteraCapital/SUIFT-Backend). 