Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auction scripts #345

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
59d7f79
Add auction scripts: fetch-users.sh to retrieve unique user addresses…
ylv-io Feb 18, 2025
ec1459e
Add BigInt support for 6-decimal USDC calculations in auction script,…
ylv-io Feb 18, 2025
ccfb188
Updated auction allocation logic and refactored clear-auction.js for …
ylv-io Feb 18, 2025
f8b2711
Update allocations and clear-auction.js: adjust token allocations, mo…
ylv-io Feb 18, 2025
299027f
Add new Merkle tree JSON and script for building Merkle proofs, along…
ylv-io Feb 18, 2025
d56c3aa
Add build-merkle-proof.mjs script to auction utilities in scripts.md …
ylv-io Feb 18, 2025
c0f11da
Add set-auction-root script for updating Merkle root and rename set-r…
ylv-io Feb 19, 2025
9eb48cd
Updated auction allocations and clear-auction.js to implement a unifo…
ylv-io Feb 19, 2025
46a1bc6
auction script
ylv-io Feb 19, 2025
a880641
Add script to sum amounts within a specified price range from a file …
ylv-io Feb 19, 2025
0c24922
Engen
ylv-io Feb 20, 2025
6f068a3
Add support for emissaries priority
ylv-io Feb 20, 2025
e88e3bf
Add scripts to fetch user balances from KINTO_RPC_URL and sort them b…
ylv-io Feb 20, 2025
1d10eb1
Remove sort-by-amount.sh script from utils/auction as it is no longer…
ylv-io Feb 20, 2025
8387816
Save final bids
ylv-io Feb 20, 2025
93bafb6
merkle-tree fix
ylv-io Feb 20, 2025
34f6567
Update data
ylv-io Feb 20, 2025
040279a
Update data
ylv-io Feb 21, 2025
2896777
claim JSON
ylv-io Feb 21, 2025
b387c4f
Update data
ylv-io Feb 21, 2025
5bc0e19
Update data
ylv-io Feb 21, 2025
acd5bd5
Fix bug, update data
ylv-io Feb 21, 2025
a7028d1
Update data
ylv-io Feb 21, 2025
574397c
Add allocations.csv output to build-merkle-proof.mjs for storing addr…
ylv-io Feb 21, 2025
030ce1c
Add new JSON files for auction root setup with transaction and receip…
ylv-io Feb 21, 2025
c141d86
Upgrade Kinto Token to version V6, update contract addresses, remove …
ylv-io Feb 21, 2025
aa7f584
Update transaction data and investor details in migration script for …
ylv-io Feb 21, 2025
2472c2c
Update SealedBidTokenSale contract to allow partial USDC withdrawals,…
ylv-io Feb 21, 2025
390507d
Add new migration script for 161-withdraw_sale.s.sol and correspondin…
ylv-io Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions script/data/auction/allocations.txt

Large diffs are not rendered by default.

700 changes: 700 additions & 0 deletions script/data/auction/bids.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions script/data/auction/merkle-tree.json

Large diffs are not rendered by default.

700 changes: 700 additions & 0 deletions script/data/auction/users.txt

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions utils/auction/build-merkle-proof.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";

// ESM-friendly __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Relative path to your allocations file.
// Adjust if needed, or make it an absolute path.
const ALLOCATIONS_FILE = path.join(__dirname, "../../script/data/auction/allocations.txt");

// JSON file to dump the entire Merkle tree.
const OUTPUT_JSON = path.join(__dirname, "../../script/data/auction/merkle-tree.json");

async function main() {
// 1) Read lines from allocations.txt
const content = fs.readFileSync(ALLOCATIONS_FILE, "utf-8");

// Split into non-empty lines
const lines = content.split(/\r?\n/).filter(line => line.trim().length > 0);

// 2) Build the "values" array for Merkle tree
// Each element is [ userAddress, saleTokenAllocation, usdcAllocation ]
// all as strings, matching ["address","uint256","uint256"] in the tree schema.
const values = [];
for (const line of lines) {
const [address, saleAlloc, usdcAlloc] = line.trim().split(/\s+/);
if (!address || !saleAlloc || !usdcAlloc) {
console.warn(`Skipping malformed line: "${line}"`);
continue;
}

// Push as an array: [ string, string, string ]
values.push([address, saleAlloc, usdcAlloc]);
}

if (values.length === 0) {
console.error("No allocations found. Exiting.");
process.exit(1);
}

// 3) Create the Merkle tree
// The schema must match the types: ["address", "uint256", "uint256"]
const tree = StandardMerkleTree.of(values, ["address", "uint256", "uint256"]);

// 4) Print the Merkle root (paste this into your contract)
console.log("Merkle Root:", tree.root);

// 5) Write the entire tree to a JSON file (for off-chain reference)
fs.writeFileSync(OUTPUT_JSON, JSON.stringify(tree.dump()), "utf-8");
console.log(`Merkle tree dumped to ${OUTPUT_JSON}`);
}

main().catch(err => {
console.error(err);
process.exit(1);
});
189 changes: 189 additions & 0 deletions utils/auction/clear-auction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
const fs = require('fs');
const path = require('path');

const USDC_SCALE = 1_000_000n; // 1 USDC = 1e6

/**
* Runs a uniform-price auction using BigInt with:
* - USDC in 6 decimals (1 USDC = 1e6)
* - Tokens in 18 decimals (1 TOKEN = 1e18)
*
* @param {Object[]} bids
* @param {string} bids[].address - Ethereum address.
* @param {bigint} bids[].usdcAmount - USDC in 1e6 units.
* @param {bigint} bids[].maxPrice - Max price in USDC’s 1e6 format.
* @param {bigint} bids[].priority - Priority for tie-breaks.
* @param {bigint} totalTokens - Total tokens to sell in 1e18 units.
*
* @returns {{
* finalPrice: bigint, // Price per token in 1e6 USDC units (6 decimals)
* allocations: {
* [address: string]: {
* tokens: bigint, // Tokens allocated (18 decimals)
* usedUSDC: bigint, // USDC used (6 decimals)
* refundedUSDC: bigint // USDC refunded (6 decimals)
* }
* }
* }}
*/
function runAuction(bids, totalTokens) {
// Constants for scaling
const TOKEN_SCALE = 1_000_000_000_000_000_000n; // 1 TOKEN = 1e18

// 1) Sort bids by maxPrice (desc), then priority (desc)
bids.sort((a, b) => {
if (b.maxPrice === a.maxPrice) {
return Number(b.priority - a.priority);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are you calculating priority? Take into account that within engen, the engen balance matters and then emissaries and then people sorted by their K balance

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, calculating priority would be another script, as we have to fetch data for Engen and Balances.

}
return Number(b.maxPrice - a.maxPrice);
});

// 2) Determine the clearing price
let tokensAccumulated = 0n;
let finalPrice = 0n;

for (const bid of bids) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this logic is correct.

You are assuming people are getting the tokens at their bid price which is incorrect.

For example I bid 30, you bid 30 and the third person bids 10.

I should be getting my tokens at 10 not at 30, so to calculate tokens accumulated you need to use 10 not 30

Copy link
Contributor

@rrecuero rrecuero Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise we are not grabbing the real last bid, we are finishing up earlier than we should

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't assume people are getting the tokens at their bid. The first look is only to calculate how many tokens people are willing to buy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but youa re using this to calculate the finalPrice

// tokensDemanded = (usdcAmount * TOKEN_SCALE) / maxPrice
const tokensDemanded = (bid.usdcAmount * TOKEN_SCALE) / bid.maxPrice;

tokensAccumulated += tokensDemanded;
if (tokensAccumulated >= totalTokens) {
finalPrice = bid.maxPrice;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what I think is incorrect

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is like the worst case. Because if I bid $300 for $30, you are only accumulating 10 tokens but in reality maybe you should have accumulated 30

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you would run out of tokens earlier

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I think the loop needs to be over each bid price first, and then over bids. It needs to be a double loop

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I agree. thanks.

break;
}
}

// If finalPrice is 0, not enough demand to sell all tokens => "not successful"
if (finalPrice === 0n) {
const allocations = {};
for (const bid of bids) {
allocations[bid.address] = {
tokens: 0n,
usedUSDC: 0n,
refundedUSDC: bid.usdcAmount
};
}
return { finalPrice, allocations };
}

// 3) Allocate tokens
let tokensLeft = totalTokens;
const allocations = {};

for (const bid of bids) {
// If user bid below finalPrice => no tokens
if (bid.maxPrice < finalPrice) {
allocations[bid.address] = {
tokens: 0n,
usedUSDC: 0n,
refundedUSDC: bid.usdcAmount
};
continue;
}

// tokensWanted = (usdcAmount * TOKEN_SCALE) / finalPrice
let tokensWanted = (bid.usdcAmount * TOKEN_SCALE) / finalPrice;
if (tokensWanted > tokensLeft) {
tokensWanted = tokensLeft;
}

// usedUSDC = (tokensAllocated * finalPrice) / TOKEN_SCALE
let usedUSDC = (tokensWanted * finalPrice) / TOKEN_SCALE;

// Refund leftover USDC. Clamp to 0 if negative or 1.
let refundedUSDC = bid.usdcAmount - usedUSDC;
if (refundedUSDC < 2n) {
refundedUSDC = 0n;
}

allocations[bid.address] = {
tokens: tokensWanted,
usedUSDC,
refundedUSDC
};

tokensLeft -= tokensWanted;
if (tokensLeft <= 0n) {
break;
}
}

// If the loop broke early, ensure leftover bidders get 0 tokens
if (tokensLeft > 0n) {
for (const bid of bids) {
if (!allocations[bid.address]) {
allocations[bid.address] = {
tokens: 0n,
usedUSDC: 0n,
refundedUSDC: bid.usdcAmount
};
}
}
}

return { finalPrice, allocations };
}

// ----------------------------------------------------------------------
// Read input file (address amount maxPrice priority) => BigInt
// ----------------------------------------------------------------------
function readBidsFromFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split(/\r?\n/);

const bids = [];
for (const line of lines) {
if (!line.trim()) continue; // skip empty lines

const [address, amountStr, maxPriceStr, priorityStr] = line.trim().split(/\s+/);
if (!address || !amountStr || !maxPriceStr || !priorityStr) {
continue; // skip malformed lines
}

// Convert to BigInt (these are already in 1e6 for USDC or just integer priority)
const usdcAmount = BigInt(amountStr);
const maxPrice = BigInt(maxPriceStr);
const priority = BigInt(priorityStr);

bids.push({ address, usdcAmount, maxPrice, priority });
}
return bids;
}

// ----------------------------------------------------------------------
// Write output file
// ----------------------------------------------------------------------
function writeOutputToFile(filePath, finalPrice, allocations) {
let output = '';
for (const address in allocations) {
const { tokens, usedUSDC, refundedUSDC } = allocations[address];
output += `${address} ${tokens.toString()} ${usedUSDC.toString()} ${refundedUSDC.toString()}\n`;
}

fs.writeFileSync(filePath, output, 'utf-8');
}

// ----------------------------------------------------------------------
// Main
// ----------------------------------------------------------------------
function main() {
const inputFilePath = path.join(__dirname, '../../script/data/auction/bids.txt');
const outputFilePath = path.join(__dirname, '../../script/data/auction/allocations.txt');

const bids = readBidsFromFile(inputFilePath);

// If we want to sell 1,000 tokens, in 18-decimal:
// 1000 tokens * 1e18 = 1e21
const totalTokens = 40_000n * 1_000_000_000_000_000_000n; // 1000 * 1e18 = 1e21

const { finalPrice, allocations } = runAuction(bids, totalTokens);

writeOutputToFile(outputFilePath, finalPrice, allocations);

console.log(`Auction complete. Final Price: $${(finalPrice/USDC_SCALE).toString()}.00. Results written to ${outputFilePath}`);
}

// Execute if called directly (e.g. `node auction-bigint.js`)
if (require.main === module) {
main();
}
50 changes: 50 additions & 0 deletions utils/auction/fetch-bids.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

# Ensure KINTO_RPC_URL is set
if [ -z "$KINTO_RPC_URL" ]; then
echo "Error: KINTO_RPC_URL is not set."
exit 1
fi

# Output file
output="./script/data/auction/bids.txt"
rm -f "$output" # Remove previous output file if it exists
touch "$output"

# Maximum number of parallel jobs
max_jobs=8

# Function to process a single user
process_user() {
local user=$1
local deposit
local maxPrice

deposit=$(cast call --rpc-url "$KINTO_RPC_URL" \
0x5a1E00884e35bF2dC39Af51712D08bEF24b1817f \
"deposits(address)" "$user" | cast to-dec)

maxPrice=$(cast call --rpc-url "$KINTO_RPC_URL" \
0x5a1E00884e35bF2dC39Af51712D08bEF24b1817f \
"maxPrices(address)" "$user" | cast to-dec)

# Append the result to the output file.
# Using a newline ensures each record is separate.
echo "$user $deposit $maxPrice 0" >> "$output"
}

# Read users from file and process in parallel
while IFS= read -r user || [ -n "$user" ]; do
# Launch the process in the background
process_user "$user" &

# Limit the number of parallel jobs
while [ "$(jobs -r | wc -l)" -ge "$max_jobs" ]; do
sleep 0.1
done
done < ./script/data/auction/users.txt

# Wait for any remaining background processes to finish
wait

echo "Data fetched and stored in $output"
21 changes: 21 additions & 0 deletions utils/auction/fetch-users.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail

# Ensure KINTO_RPC_URL is set
if [ -z "${KINTO_RPC_URL:-}" ]; then
echo "Error: KINTO_RPC_URL is not set."
exit 1
fi

# Fetch logs and process them:
# 1. `cast logs` fetches the logs for the Deposited event.
# 2. `awk` selects the appropriate lines (skip two lines after matching "topics: [").
# 3. `xargs` runs `cast parse-bytes32-address` for each address.
# 4. `sort -u` ensures only unique addresses are written to the file.
cast logs --rpc-url "$KINTO_RPC_URL" --from-block 731179 --to-block latest \
'Deposited(address indexed user, uint256 amount)' \
--address 0x5a1E00884e35bF2dC39Af51712D08bEF24b1817f | \
awk '/topics: \[/{getline; getline; print}' | \
xargs -n1 cast parse-bytes32-address | sort -u > ./script/data/auction/users.txt

echo "Unique user addresses saved to 'users'"
7 changes: 7 additions & 0 deletions utils/auction/scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
./utils/auction/fetch-users.sh

./utils/auction/fetch-bids.sh

node utils/auction/clear-auction.js

node utils/auction/build-merkle-proof.mjs