Initiate a CTO Vote
Starting a Community Takeover. Requires a 5% stake and the chain's fee, both paid up front.
What it costs
| Requirement | Amount |
|---|---|
| Token stake (approved, then transferred into the vote) | 60,000,000 × 10⁹ (60M, 5% of supply) |
| Native fee (non-refundable, goes to the protocol) | Base 0.01 ETH, BSC 0.03 BNB, Polygon 240 POL, Robinhood 0.01 ETH, Gnosis 24 xDAI |
The stake is returned when you withdraw after voting closes, win or lose. The fee is not.
Preflight
const HOOK_ABI = [
"function initiate_cto_vote(address token) payable returns (address)",
"function canInitiateCTO(address token) view returns (bool)",
"function active_cto_votes(address token) view returns (address)",
"function cancel_failed_cto(address token)",
"function getTokenInfo(address token) view returns (address deployer, uint256 positionId)"
];
const hook = new ethers.Contract(HOOK_ADDRESS, HOOK_ABI, signer);
const me = await signer.getAddress();
const STAKE = 60_000_000_000_000_000n; // 60M tokens, 9 decimals
const [ok, balance] = await Promise.all([
hook.canInitiateCTO(token),
erc20.balanceOf(me)
]);
if (!ok) throw new Error("not eligible — check for an active vote");
if (balance < STAKE) throw new Error("need 5% of supply");HOOK_ABI = json.load(open("PrimaryHook.abi.json"))
hook = w3.eth.contract(address=HOOK_ADDRESS, abi=HOOK_ABI)
me = acct.address
STAKE = 60_000_000_000_000_000 # 60M tokens, 9 decimals
ok = hook.functions.canInitiateCTO(token).call()
balance = erc20.functions.balanceOf(me).call()
if not ok:
raise RuntimeError("not eligible — check for an active vote")
if balance < STAKE:
raise RuntimeError("need 5% of supply")Initiating
// 1. Approve the stake to the hook
await (await erc20.approve(HOOK_ADDRESS, STAKE)).wait();
// 2. Send the exact fee — no overage refund
const receipt = await (
await hook.initiate_cto_vote(token, { value: CTO_VOTE_FEE })
).wait();
const ev = receipt.logs
.map(l => { try { return hook.interface.parseLog(l); } catch { return null; } })
.find(e => e?.name === "CTOVoteInitiated");
const voteContract = ev.args.vote_contract;# 1. Approve the stake to the hook
send(erc20.functions.approve(HOOK_ADDRESS, STAKE))
# 2. Send the exact fee — no overage refund
tx = hook.functions.initiate_cto_vote(token).build_transaction({
"from": acct.address,
"value": CTO_VOTE_FEE,
"nonce": w3.eth.get_transaction_count(acct.address),
})
signed = acct.sign_transaction(tx)
receipt = w3.eth.wait_for_transaction_receipt(
w3.eth.send_raw_transaction(signed.raw_transaction)
)
ev = hook.events.CTOVoteInitiated().process_receipt(receipt)[0]
vote_contract = ev["args"]["vote_contract"]Overpaying reverts with Send exact CTO fee rather than refunding. This is deliberate — refunding to a contract wallet that reverts on receive would strand funds or fail the call. Read the constant for your chain and send precisely that.
You start with one vote
Initialization reads the stake already transferred in and registers it as your YES vote. You are voter number one and your stake is already counted — do not call vote() afterwards; it will revert with Already voted. Use add_votes() if you want to add more.
What has to happen to win
| Condition | Threshold |
|---|---|
| YES as a share of circulating supply | 40% YES of circulating |
| Distinct voters | at least 10 — including you |
| Margin over NO | YES ≥ 1.1 × NO |
| Time | 259,200 s (3 days), or early once the above hold |
The current deployer's stake counts 2× in the tally, as an incumbency defence. Their real deposited balance is unaffected for withdrawal purposes — only the tally is weighted. Budget for it when estimating what you need.
Closing and finalizing
const VOTE_ABI = [
"function end_vote()",
"function end_vote_early()",
"function finalize()",
"function withdraw_tokens()",
"function is_takeover_successful() view returns (bool)",
"function voting_ended() view returns (bool)",
"function vote_passed() view returns (bool)",
"function time_remaining() view returns (uint256)"
];
const vote = new ethers.Contract(voteContract, VOTE_ABI, signer);
// Close early once quorum and majority already hold
if (await vote.is_takeover_successful()) {
await (await vote.end_vote_early()).wait();
} else if ((await vote.time_remaining()) === 0n) {
await (await vote.end_vote()).wait();
}
// Then, in a later transaction
if (await vote.vote_passed()) {
await (await vote.finalize()).wait();
}VOTE_ABI = json.load(open("CTOVoteTemplate.abi.json"))
vote = w3.eth.contract(address=vote_contract, abi=VOTE_ABI)
# Close early once quorum and majority already hold
if vote.functions.is_takeover_successful().call():
send(vote.functions.end_vote_early())
elif vote.functions.time_remaining().call() == 0:
send(vote.functions.end_vote())
# Then, in a later transaction
if vote.functions.vote_passed().call():
send(vote.functions.finalize())Finalization transfers the deployer role to you, moves the token in the factory's deployer registry, pays out the outgoing deployer's accrued balance, and transfers socials ownership. Nothing a third party controls can block it — every external call there is best-effort.
If it fails
A failed vote leaves active_cto_votes[token] set, which blocks a new attempt. Anyone can clear it:
await (await hook.cancel_failed_cto(token)).wait();send(hook.functions.cancel_failed_cto(token))Then withdraw your stake with withdraw_tokens(). There is no cap on attempts, but each one costs another non-refundable fee.
What you get
The deployer fee share and metadata ownership. Nothing else. You cannot mint, move liquidity, change any constant, or alter the token contract — those capabilities do not exist for anyone. $LP24 cannot be taken over at all.