Exit and Claim Tokens
Exit outbid positions and claim purchased tokens in a Uniswap Continuous Clearing Auction lifecycle.
This section will walk you through exiting a bid and claiming purchased tokens on a CCAÂ auction.
Prerequisites
This guide continues from the previous section. Basic knowledge of the CCA auction mechanism and Solidity is assumed.
Step 1: Review the Current Auction State
Currently we have a CCA contract deployed which we have submitted a bid to. We're the only bidder in the auction and the clearing price of the auction is at our max price. Let's modify the script to add another bid which will outbid our initial one, and show how both bids can be exited.
Step 2: Exit an Outbid Position
From the previous section we have the following script (copy and pasted for convenience):
contract ExampleCCABidScript is Script {
function setUp() public {}
function run() public {
vm.startBroadcast();
ContinuousClearingAuction auction = ContinuousClearingAuction(vm.envAddress("AUCTION_ADDRESS"));
uint256 maxPrice = auction.floorPrice() + auction.tickSpacing(); // Bid at the next possible price
uint256 amountRequired = (maxPrice * uint256(auction.totalSupply())) >> 96;
uint128 amount = uint128(amountRequired);
address owner = vm.envAddress("DEPLOYER"); // The deployer is the owner of the bid by default
uint256 bidId = auction.submitBid{value: amount}(maxPrice, amount, owner, bytes(""));
console2.log("Bid submitted with ID:", bidId);
vm.roll(block.number + 1);
auction.checkpoint();
console2.log("checkpoint clearingPrice:", auction.clearingPrice());
vm.stopBroadcast();
}
}Let's add another bid to the auction after our initial one. We'll bid at a price higher than the first bid but unlike the first bid, we won't deposit enough ETH to move the clearing price of the auction up to this new price. The second bid will be large enough to move the clearing price of the auction up, outbidding the first bid, but not large enough to move the clearing price of the auction up to the second bid's max price.
function run() public {
vm.startBroadcast();
ContinuousClearingAuction auction = ContinuousClearingAuction(vm.envAddress("AUCTION_ADDRESS"));
uint256 maxPrice = auction.floorPrice() + auction.tickSpacing(); // Bid at the next possible price
uint256 amountRequired = (maxPrice * uint256(auction.totalSupply())) >> 96;
uint128 amount = uint128(amountRequired);
address owner = vm.envAddress("DEPLOYER"); // The deployer is the owner of the bid by default
uint256 bidId = auction.submitBid{value: amount}(maxPrice, amount, owner, bytes(""));
console2.log("First bid submitted with ID:", bidId);
vm.roll(block.number + 1);
auction.checkpoint();
console2.log("checkpoint clearingPrice after first bid:", auction.clearingPrice());
maxPrice = auction.floorPrice() + 2 * auction.tickSpacing(); // Bid at a higher price than the first one
amountRequired = (maxPrice * uint256(auction.totalSupply())) >> 96;
// Deposit ~90% the amount of ETH required so the clearing price ends up somewhere between the first and second bid's max prices.
amount = uint128(amountRequired * 9 / 10);
bidId = auction.submitBid{value: amount}(maxPrice, amount, owner, bytes(""));
console2.log("Second bid submitted with ID:", bidId);
vm.roll(block.number + 1);
auction.checkpoint();
console2.log("checkpoint clearingPrice after second bid:", auction.clearingPrice());
vm.stopBroadcast();
}
}You can run the script with the following command:
AUCTION_ADDRESS=<auction address> forge script scripts/ExampleCCABidScript.s.sol:ExampleCCABidScript \
--rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast -vvvv --slowYou'll see the following logs in the console:
== Logs ==
First bid submitted with ID: 0
checkpoint clearingPrice after first bid: 158456325028528668016640
Second bid submitted with ID: 1
checkpoint clearingPrice after second bid: 215207282483414186944057Great! We've successfully submitted two bids to the auction and the clearing price of the auction is now at 215207282483414186944057.
Partially vs. fully filled bids
Bids in the auction can have periods where they are partially and fully filled.
At a high level, a bid is considered "partially filled" whenever the clearingPrice is equal to the bid's max price. When the clearing price is strictly lower than the bid's max price, the bid is considered "fully filled". Once the clearingPrice moves above the bid's max price, the bid is outbid and stops purchasing tokens.
Let's take a look at the state of the two bids in the auction at this point in time:
| Checkpoint | Bid 0 (max price: 1:1e6) | Bid 1 (max price: 1:2e6) |
|---|---|---|
| Checkpoint 0 (79228162514264334008320) | Not placed | Not placed |
| Checkpoint 1 (158456325028528668016640) | Partially filled | Not placed |
| Checkpoint 2 (215207282483414186944057) | Outbid | Fully filled |
The first bid is partially filled because the clearing price of the auction is equal to the bid's max price at checkpoint 1. The second bid is fully filled because the clearing price of the auction is strictly lower than the bid's max price at checkpoint 2 (215207282483414186944057 < 237684487542793002024960) or equivalently (2.7e-06 <Â 3e-06).
As of checkpoint 2 the first bid is considered outbid and it can be exited from the auction. It only participated for one block and the remainder of its ETH can be refunded back to the owner.
Let's append the following code to the end of the script to exit the first bid.
function run() public {
// All the code from the previous script...
uint64 lastCheckpointedBlock = auction.lastCheckpointedBlock();
uint256 ownerBalanceBefore = address(owner).balance;
auction.exitPartiallyFilledBid(0, uint64(firstCheckpointBlock), lastCheckpointedBlock);
console2.log("Bid 0 exited");
console2.log("Refunded ETH:", address(owner).balance - ownerBalanceBefore);
Bid memory bid = auction.bids(0);
console2.log("Bid 0 tokensFilled:", bid.tokensFilled);
console2.log("Bid 0 exitedBlock:", bid.exitedBlock);
}
}Running this script gives the following output:
== Logs ==
First bid submitted with ID: 0
checkpoint clearingPrice after first bid: 158456325028528668016640
Second bid submitted with ID: 1
checkpoint clearingPrice after second bid: 215641168133582360708057
Bid 0 exited
Refunded ETH: 1995999999999999909677
Bid 0 tokensFilled: 2000000000000000000000000
Bid 0 exitedBlock: 5The first bid is exited successfully and the remainder of its ETH can be refunded back to the owner. It purchased 2000000000000000000000000 wei of tokens (2,000,000) and was exited at block 5.
As the auction has not completed yet, the second bid is not exitable yet since its maxPrice is higher than the clearing price of the auction.
At the end of the auction and after claimBlock both bids can claim their purchased tokens.
Step 3: Claim Purchased Tokens
A bid must be exited before claiming tokens. Let's fast forward to the end of the auction so we can exit the second bid and claim tokens for both bids.
Append the following code to the end of the script:
function run() public {
// All the code from the previous script...
vm.roll(auction.endBlock());
ownerBalanceBefore = address(owner).balance;
auction.exitBid(1); // Exit the second bid
console2.log("Bid 1 exited");
console2.log("Refunded ETH:", address(owner).balance - ownerBalanceBefore);
bid = auction.bids(1);
console2.log("Bid 1 exitedBlock:", bid.exitedBlock);
vm.roll(auction.claimBlock());
uint256 ownerTokenBalanceBefore = auction.token().balanceOf(owner);
auction.claimTokens(0);
uint256 ownerTokenBalanceAfter = auction.token().balanceOf(owner);
console2.log("Bid 0 tokens claimed:", ownerTokenBalanceAfter - ownerTokenBalanceBefore, (ownerTokenBalanceAfter - ownerTokenBalanceBefore) / 1e18);
ownerTokenBalanceBefore = auction.token().balanceOf(owner);
auction.claimTokens(1);
ownerTokenBalanceAfter = auction.token().balanceOf(owner);
console2.log("Bid 1 tokens claimed:", ownerTokenBalanceAfter - ownerTokenBalanceBefore, (ownerTokenBalanceAfter - ownerTokenBalanceBefore) / 1e18);
vm.stopBroadcast();
}
}Running this script gives the following output:
Bid 0 exited
Refunded ETH: 1995999999999999909677
Bid 0 exitedBlock: 4
Bid 1 exited
Refunded ETH: 0
Bid 1 exitedBlock: 100
Bid 0 tokens claimed: 2000000000000000000000000 2000000
Bid 1 tokens claimed: 993999999999999999999999459 993999999The second bid refunds 0 ETH because it remained fully filled for the auction duration.
The first and second bids claim 2,000,000 and 993,999,999 tokens respectively. Any unsold remainder can be swept to tokensRecipient after auction end.
Sweep unsold tokens
At the end of the auction any unsold tokens can be swept back to the preconfigured tokensRecipient by calling sweepUnsoldTokens. This function is permissionless and can be called by anyone.
function run() public {
// All the code from the previous script...
uint256 tokensRecipientBalanceBefore = auction.token().balanceOf(auction.tokensRecipient());
auction.sweepUnsoldTokens();
uint256 tokensRecipientBalanceAfter = auction.token().balanceOf(auction.tokensRecipient());
console2.log("Unsold tokens swept:", tokensRecipientBalanceAfter - tokensRecipientBalanceBefore, (tokensRecipientBalanceAfter - tokensRecipientBalanceBefore) / 1e18);
vm.stopBroadcast();
}Running this script gives the following output:
Unsold tokens swept: 4000000000000000000000540 4000000The output shows 4,000,000 unsold tokens swept to tokensRecipient.
Sweep raised currency
At the end of the auction any raised currency can be swept back to the preconfigured currencyRecipient by calling sweepCurrency. This function is also permissionless and can be called by anyone.
function run() public {
// All the code from the previous script...
uint256 fundsRecipientBalanceBefore = address(auction.fundsRecipient()).balance;
auction.sweepCurrency();
uint256 fundsRecipientBalanceAfter = address(auction.fundsRecipient()).balance;
console2.log("Currency swept:", fundsRecipientBalanceAfter - fundsRecipientBalanceBefore, (fundsRecipientBalanceAfter - fundsRecipientBalanceBefore) / 1e18);
}Running this script gives the following output:
Currency swept: 2703999999999999877637 2703The output shows raised currency swept to fundsRecipient.
Verification
Confirm each lifecycle action succeeds in order: exitPartiallyFilledBid, exitBid, claimTokens, sweepUnsoldTokens, and sweepCurrency.
Next Steps
This concludes the getting started guide for the CCA auction mechanism. We covered:
- How to setup a local development environment
- How to configure a CCAÂ auction
- Submitting a bid and understanding price discovery
- Exiting a bid and claiming tokens
- Sweeping unsold tokens and the total raised currency
For more details please refer to the CCA auction mechanism.
Additionally, the contracts are open sourced and MIT licensed. You can find the source code for the contracts in the GitHub repository.