Configuring a CCA auction
This section will walk you through each parameter of a CCA auction and an example configuration. For more details please refer to the technical reference.
Prerequisites
Basic understanding of the CCA auction mechanism and Solidity is assumed. This guide continues from the previous section.
Auction Parameters
The AuctionParameters struct parameterizes a new CCA auction. It is encoded and passed to the ContinuousClearingAuctionFactory contract when deploying a new auction. The struct definition is as follows:
/// from: https://github.com/Uniswap/continuous-clearing-auction/blob/main/src/interfaces/IContinuousClearingAuction.sol
struct AuctionParameters {
address currency; // token to raise funds in. Use address(0) for ETH
address tokensRecipient; // address to receive leftover tokens
address fundsRecipient; // address to receive all raised funds
uint64 startBlock; // Block which the first step starts
uint64 endBlock; // When the auction finishes
uint64 claimBlock; // Block when the auction can claimed
uint256 tickSpacing; // Fixed granularity for prices
address validationHook; // Optional hook called before a bid
uint256 floorPrice; // Starting floor price for the auction
uint128 requiredCurrencyRaised; // Amount of currency required to be raised for the auction to graduate
bytes auctionStepsData; // Packed bytes describing token issuance schedule
}
We'll cover each parameter in detail below.
currency
The currency parameter is the address of the token that will be used to raise funds for the auction. This can be any ERC20 token or the native token of the chain (address(0)).
tokensRecipient
The tokensRecipient parameter is the address that will receive the leftover tokens after the auction is complete. Depending on the implementation, this may be a trusted EOA address or a strategy contract address if the auction is being used to bootstrap a liquidity pool.
fundsRecipient
The fundsRecipient parameter is the address that will receive the funds raised during the auction. Again, depending on the implementation, this may be a trusted EOA address or a strategy contract address if the auction is being used to bootstrap a liquidity pool.
startBlock
The startBlock parameter is the block number at which the auction will start. Once the auction starts, the supply schedule will begin and bids will be accepted. Note that startBlock is inclusive so the auction will start exactly on the block specified.
endBlock
The endBlock parameter is the block number at which the auction will end. Note that endBlock is exclusive so the auction will end on the block specified. No more bids are accepted at and after the end block.
claimBlock
The claimBlock parameter is the block number at which purchased tokens can be claimed. It must be at or after the endBlock.
tickSpacing
The tickSpacing parameter denotes the minimum price increment for bids. It is used to prevent users from being outbid by others by infinitesimally small amounts and for gas efficiency in finding new clearing prices. Generally integrators should choose a tick spacing of AT LEAST 1 basis point of the floor price. 1% or 10% is also reasonable.
Bids can only be placed at increments of tickSpacing, but the auction may clear at any price.
validationHook
The validationHook parameter is an optional contract that can be used to validate bids before they are accepted. It is called before a bid is accepted and can be used to reject bids that do not meet certain criteria. It must implement the IValidationHook interface. Use address(0) to opt-out of validation.
floorPrice
The floorPrice parameter is the starting floor price for the auction. It is the minimum price at which bids will be accepted.
All prices in the auction are represented as the ratio of currency to token. For example, a floor price with an integer component of 1000 means that 1000 currency is required to purchase 1 token. Additionally, the price is represented as a Q96 fixed-point number to allow for fractional prices. For more details about the Q-number format please refer to this wikipedia article.
Using the above example, a floor price of 1000 would be represented as 1000 << 96 or 1000 * 2^96.
requiredCurrencyRaised
The requiredCurrencyRaised parameter is the amount of currency required to be raised for the auction to graduate. If the auction does not raise this amount, the auction will not graduate and all bidders will be able to withdraw their initial bid amounts. No tokens will be sold and the totalSupply will be swept back to the tokensRecipient.
auctionStepsData
The auctionStepsData parameter is a packed bytes array that describes the token issuance schedule. It is used to determine the amount of tokens that will be sold in each block. It is a series of uint64 values that represent the per-block issuance rate in MPS (milli-bips), and the number of blocks to sell over.
For more details about the auction steps please refer to the technical reference.
Example configuration
Let's create an example configuration for a CCA auction. We'll use the script we started in the previous section.
Here's the script copy and pasted for convenience:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import {ContinuousClearingAuctionFactory} from "../src/ContinuousClearingAuctionFactory.sol";
import {IDistributionContract} from "../src/interfaces/external/IDistributionContract.sol";
import {console2} from "forge-std/console2.sol";
contract ExampleCCADeploymentScript is Script {
function setUp() public {}
function run() public {
vm.startBroadcast();
ContinuousClearingAuctionFactory factory = new ContinuousClearingAuctionFactory();
console2.log("Factory deployed to:", address(factory));
// TODO: configure the auction and deploy it via `initializeDistribution()`
vm.stopBroadcast();
}
}
First, make sure to import the AuctionParameters struct from the IContinuousClearingAuction interface:
import {AuctionParameters} from "../src/interfaces/IContinuousClearingAuction.sol";
Then, we can configure the auction parameters:
address deployer = vm.envAddress("DEPLOYER");
AuctionParameters memory parameters = AuctionParameters({
currency: address(0), // We'll use the native token for this example
tokensRecipient: deployer,
fundsRecipient: deployer,
startBlock: uint64(block.number), // Start the auction on the current block
endBlock: uint64(block.number + 100), // End the auction after 100 blocks
claimBlock: uint64(block.number + 100), // Allow claims at the end of the auction
tickSpacing: 79228162514264334008320, // Use a tick spacing equal to the floor price
validationHook: address(0), // Use no validation hook
floorPrice: 79228162514264334008320, // Use a floor price representing a ratio of 1:1,000,000 (1 ETH for 1 million tokens)
requiredCurrencyRaised: 0, // No graduation threshold
auctionStepsData: bytes("") // Leave this blank for now
});
Let's build the auction steps data. For simplicity, we'll sell tokens following a monotonically increasing schedule. We'll sell 10% over 50 blocks, 49% over 49 blocks, and the final 41% in the last block. See the note about auction steps for more details about the rationale behind this example schedule.
To derive the steps:
- First tranche: 10% over 50 blocks Express 10% in MPS as 1e6 (1,000,000). Over 50 blocks this is 1e6 / 50 = 20,000 MPS per block. Pack this into a bytes8 value:
bytes8 firstTranche = uint64(20_000) | (uint64(50) << 24);
Repeat this for the rest of the tranches to get the final auction steps data:
bytes8 secondTranche = uint64(100_000) | (uint64(49) << 24); // 49e6 / 49 = 100_000 MPS per block
bytes8 thirdTranche = uint64(4_100_000) | (uint64(1) << 24); // 41e6 / 1 = 4_100_000 MPS per block
Finally, pack the auction steps data into a bytes array:
bytes memory auctionStepsData = abi.encodePacked(firstTranche, secondTranche, thirdTranche);
// Set the auction steps data
parameters.auctionStepsData = auctionStepsData;
You can leverage the AuctionStepsBuilder helper library to build the auction steps data.
Before we can finish the script we need to deploy a MockERC20 token to use in the auction. You can use the ERC20Mock contract in @openzeppelin/contracts/mocks/token/ERC20Mock.sol, or any other MockERC20 token you prefer.
import {ERC20Mock} from '@openzeppelin/contracts/mocks/token/ERC20Mock.sol';
Now let's finish the script:
using AuctionStepsBuilder for bytes;
function run() public {
address deployer = vm.envAddress("DEPLOYER");
vm.startBroadcast();
ContinuousClearingAuctionFactory factory = new ContinuousClearingAuctionFactory();
console2.log("Factory deployed to:", address(factory));
ERC20Mock token = new ERC20Mock();
uint256 totalSupply = 1_000_000_000e18; // 1 billion tokens
bytes memory auctionStepsData = AuctionStepsBuilder.init().addStep(20_000, 50).addStep(100_000, 49).addStep(4_100_000, 1);
AuctionParameters memory parameters = AuctionParameters({
currency: address(0),
tokensRecipient: deployer,
fundsRecipient: deployer,
startBlock: uint64(block.number),
endBlock: uint64(block.number + 100),
claimBlock: uint64(block.number + 100),
tickSpacing: 79228162514264334008320,
validationHook: address(0),
floorPrice: 79228162514264334008320,
requiredCurrencyRaised: 0,
auctionStepsData: auctionStepsData
});
IDistributionContract auction = IDistributionContract(factory.initializeDistribution(address(token), totalSupply, abi.encode(parameters), bytes32(0)));
token.mint(address(auction), totalSupply);
console2.log("Auction deployed to:", address(auction));
vm.stopBroadcast();
}
This will deploy the mock token, deploy the auction contract, and mint the total supply (1 billion tokens) to the auction contract.
The final step in the script is to ensure that we call onTokensReceived() on the auction contract to register the receipt of the tokens.
token.mint(address(auction), totalSupply);
auction.onTokensReceived();
The complete script should look like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import {ContinuousClearingAuctionFactory} from "../src/ContinuousClearingAuctionFactory.sol";
import {AuctionParameters} from "../src/interfaces/IContinuousClearingAuction.sol";
import {IDistributionContract} from "../src/interfaces/external/IDistributionContract.sol";
import {AuctionStepsBuilder} from "../test/utils/AuctionStepsBuilder.sol";
import {ERC20Mock} from '@openzeppelin/contracts/mocks/token/ERC20Mock.sol';
import {console2} from "forge-std/console2.sol";
contract ExampleCCADeploymentScript is Script {
using AuctionStepsBuilder for bytes;
function setUp() public {}
function run() public {
address deployer = vm.envAddress("DEPLOYER");
vm.startBroadcast();
ContinuousClearingAuctionFactory factory = new ContinuousClearingAuctionFactory();
console2.log("Factory deployed to:", address(factory));
ERC20Mock token = new ERC20Mock();
uint256 totalSupply = 1_000_000_000e18; // 1 billion tokens
bytes memory auctionStepsData = AuctionStepsBuilder.init().addStep(20_000, 50).addStep(100_000, 49).addStep(4_100_000, 1);
AuctionParameters memory parameters = AuctionParameters({
currency: address(0),
tokensRecipient: deployer,
fundsRecipient: deployer,
startBlock: uint64(block.number),
endBlock: uint64(block.number + 100),
claimBlock: uint64(block.number + 100),
tickSpacing: 79228162514264334008320,
validationHook: address(0),
floorPrice: 79228162514264334008320,
requiredCurrencyRaised: 0,
auctionStepsData: auctionStepsData
});
IDistributionContract auction = IDistributionContract(factory.initializeDistribution(address(token), totalSupply, abi.encode(parameters), bytes32(0)));
token.mint(address(auction), totalSupply);
auction.onTokensReceived();
console2.log("Auction deployed to:", address(auction));
vm.stopBroadcast();
}
}
Let's run the script:
forge script scripts/ExampleCCADeploymentScript.s.sol:ExampleCCADeploymentScript \
--rpc-url http://localhost:8545 --private-key <your-private-key> --broadcast
The deployment should be successful and you should see the factory and auction contract addresses logged to the console.
Next steps
In the next section we'll write some scripts to interact with the deployed auction contract.