Skip to main content

Overview

In order to verify that a wallet is eligible to participate in a sale, the Sonar backend issues signed purchase permits that tie the wallet to an entity on Sonar and prove that they passed the necessary checks configured for the sale. These purchase permits are intended to be passed to your sale contract with each purchase, and validated there using common ECDSA libraries. Next to information about the purchasing Sonar entity, the purchase permit can also contain extra information such as purchase limits, price limits, etc.

Purchase Permit

Sonar generates permits with this structure (see also the sonar contract repo):
struct PurchasePermitV2 {
    bytes16 entityID;       // Unique entity identifier from Sonar
    bytes16 saleUUID;       // Your sale UUID
    address wallet;         // Authorized wallet address
    uint64 expiresAt;       // Unix timestamp expiration
    uint256 minAmount;      // Minimum purchase amount
    uint256 maxAmount;      // Maximum purchase amount
    uint64 minPrice;        // Minimum price that the asset can be purchased for
    uint64 maxPrice;        // Maximum price that the asset can be purchased for
    bytes payload;          // Generic extra data field for future use
}

Validation

To ensure the validity of a purchase permit, you need to verify the following aspects (see also the example implementation below). Please make sure to implement all of these checks to ensure correct enforcement of the requirements of your sale.
  1. The ECDSA signature was issued by the sonar signer
  2. The permit’s sale UUID matches yours
  3. The permit is not expired
  4. The sender matches the wallet address in the permit
  5. The purchase amount is within the minimum and maximum amount limits
  6. The purchase price is within the minimum and maximum price limits

Example Validation

These snippets are based on the example sale contract.

1. Signature Verification

You must verify that permits are signed by Sonar’s authorized signer:
// Grant this role to the signer address provided by Sonar
bytes32 public constant PURCHASE_PERMIT_SIGNER_ROLE = keccak256("PURCHASE_PERMIT_SIGNER_ROLE");

// Verify signature in your purchase function
address recoveredSigner = PurchasePermitV2Lib.recoverSigner(permit, signature);
if (!hasRole(PURCHASE_PERMIT_SIGNER_ROLE, recoveredSigner)) {
    revert PurchasePermitUnauthorizedSigner(recoveredSigner);
}

2. Sale UUID Validation

Prevent permit reuse across different sales:
if (permit.saleUUID != saleUUID) {
    revert PurchasePermitSaleUUIDMismatch(permit.saleUUID, saleUUID);
}

3. Expiration Checks

Permits have short expiration times:
if (permit.expiresAt <= block.timestamp) {
    revert PurchasePermitExpired();
}

4. Wallet Authorization

Ensure the permit is used by the authorized wallet:
if (permit.wallet != msg.sender) {
    revert PurchasePermitSenderMismatch(msg.sender, permit.wallet);
}

Integration Patterns

Wallet-Level Tracking (Basic)

The example contract tracks purchases by wallet address:
// Track amount purchased by each wallet
mapping(address => uint256) public amountByAddress;

function purchase(uint256 amount, /* permit params */) external {
    _validatePurchasePermit(purchasePermit, purchasePermitSignature);
    
    uint256 newTotalAmount = amountByAddress[msg.sender] + amount;
    
    // Validate purchase limits
    if (newTotalAmount < purchasePermit.minAmount) {
        revert AmountBelowMinimum(newTotalAmount, purchasePermit.minAmount);
    }
    if (newTotalAmount > purchasePermit.maxAmount) {
        revert AmountExceedsMaximum(newTotalAmount, purchasePermit.maxAmount);
    }
    
    amountByAddress[msg.sender] = newTotalAmount;
}

Entity-Level Tracking

One might want to additionally enforce limits by entity, across all wallets they can use:
// Track amount purchased by each entity
mapping(bytes16 => uint256) public amountByEntity;

function purchase(uint256 amount, /* permit params */) external {
    _validatePurchasePermit(purchasePermit, purchasePermitSignature);
    
    bytes16 entityID = purchasePermit.permit.entityID;
    uint256 newTotalAmount = amountByEntity[entityID] + amount;
    
    if (newTotalAmount > purchasePermit.maxAmount) {
        revert AmountExceedsMaximum(newTotalAmount, purchasePermit.maxAmount);
    }
    
    amountByEntity[entityID] = newTotalAmount;
}

Auction bid price validation

If you are running a sale with a dynamic pricing model where the user can commit with a requested price (e.g. auctions), you should validate the bid price is within the minimum and maximum price limits.
struct Bid {
    uint64 price;
    uint256 amount;
}

mapping(address => Bid) internal bidByAddress;

function processBid(uint64 price, uint256 amount, /* permit params */) external {
    _validatePurchasePermit(purchasePermit, purchasePermitSignature);

    if (price > purchasePermit.maxPrice) {
        revert BidPriceExceedsMaxPrice(price, purchasePermit.maxPrice);
    }

    if (price < purchasePermit.minPrice) {
        revert BidPriceBelowMinPrice(price, purchasePermit.minPrice);
    }

    if (amount < purchasePermit.minAmount) {
        revert BidBelowMinAmount(amount, purchasePermit.minAmount);
    }

    if (amount > purchasePermit.maxAmount) {
        revert BidExceedsMaxAmount(amount, purchasePermit.maxAmount);
    }
    
    // Replace the existing bid
    bidByAddress[msg.sender] = Bid(price, amount);
}

Getting the Signer Address

The signer address for permits is provided when your sale is approved. You can find it by contacting [email protected].

Next Steps