Pre-launch. The protocol is not publicly live yet. This documentation describes the contracts as implemented; deployed addresses are published at launch.

Vote in a CTO

Casting a vote in an active takeover, and getting your tokens back afterwards.

Finding the vote

const voteAddress = await hook.active_cto_votes(token);
if (voteAddress === ethers.ZeroAddress) throw new Error("no active vote");

const VOTE_ABI = [
  "function vote(uint256 amount, bool support)",
  "function add_votes(uint256 amount)",
  "function withdraw_tokens()",
  "function get_vote_stats() view returns (uint256 yes, uint256 no, uint256 total, uint256 circulating, bool passing)",
  "function get_voter_info(address) view returns (uint256 balance, bool choice, bool hasVoted)",
  "function get_yes_quorum_percentage() view returns (uint256)",
  "function time_remaining() view returns (uint256)",
  "function voter_count() view returns (uint256)",
  "function voting_ended() view returns (bool)",
  "function voting_ended_block() view returns (uint256)"
];

const vote = new ethers.Contract(voteAddress, VOTE_ABI, signer);
vote_address = hook.functions.active_cto_votes(token).call()
if int(vote_address, 16) == 0:
    raise RuntimeError("no active vote")

VOTE_ABI = json.load(open("CTOVoteTemplate.abi.json"))

vote = w3.eth.contract(address=vote_address, abi=VOTE_ABI)

Reading the standing

const [yes, no, total, circulating, passing] = await vote.get_vote_stats();
const quorumPct = await vote.get_yes_quorum_percentage();  // YES / circulating
const voters    = await vote.voter_count();
const remaining = await vote.time_remaining();

// Passing requires ALL of:
//   quorumPct >= 40, voters >= 10, yes*100 >= no*110
yes, no, total, circulating, passing = vote.functions.get_vote_stats().call()
quorum_pct = vote.functions.get_yes_quorum_percentage().call()  # YES / circulating
voters     = vote.functions.voter_count().call()
remaining  = vote.functions.time_remaining().call()

# Passing requires ALL of:
#   quorum_pct >= 40, voters >= 10, yes*100 >= no*110
Quorum is measured on YES, not turnout

40% YES of circulating must vote yes. A vote with heavy participation but only 30% support fails, however lopsided the yes/no ratio looks. Watch get_yes_quorum_percentage(), not get_yes_percentage().

Casting a vote

const amount = ethers.parseUnits("1000000", 9);

await (await erc20.approve(voteAddress, amount)).wait();
await (await vote.vote(amount, true)).wait();   // true = YES
amount = 1_000_000 * 10**9

send(erc20.functions.approve(vote_address, amount))
send(vote.functions.vote(amount, True))   # True = YES

Your tokens are held by the vote contract until voting closes. Voting weight equals the amount deposited, and you cannot vote on both sides.

Adding to a position

const [balance, choice, hasVoted] = await vote.get_voter_info(me);

if (hasVoted) {
    await (await erc20.approve(voteAddress, more)).wait();
    await (await vote.add_votes(more)).wait();  // same direction as before
} else {
    await (await vote.vote(amount, support)).wait();
}
balance, choice, has_voted = vote.functions.get_voter_info(me).call()

if has_voted:
    send(erc20.functions.approve(vote_address, more))
    send(vote.functions.add_votes(more))  # same direction as before
else:
    send(vote.functions.vote(amount, support))

add_votes always follows your original direction — there is no way to switch sides, and it does not increase the distinct-voter count.

Buying tokens to vote

This is allowed and expected. The circulating-supply denominator is read live while voting is open, so acquiring tokens to influence the outcome is legitimate — and for the token itself, buying pressure is not a bad thing. What is blocked is doing it atomically: withdrawals are locked until a block strictly after voting closes, so a flash-loaned buy → vote → end → withdraw → repay cannot complete in one transaction.

Withdrawing

const ended = await vote.voting_ended();
const endedBlock = await vote.voting_ended_block();
const current = await provider.getBlockNumber();

if (ended && BigInt(current) > endedBlock) {
    await (await vote.withdraw_tokens()).wait();
}
ended = vote.functions.voting_ended().call()
ended_block = vote.functions.voting_ended_block().call()
current = w3.eth.block_number

if ended and current > ended_block:
    send(vote.functions.withdraw_tokens())

Everyone gets their tokens back regardless of outcome. There is no slashing, no lockup beyond the one-block delay, and no deadline — you can withdraw whenever.

The result freezes when voting closes

While voting is open, is_takeover_successful() reflects live standing and can move as balances change. Once end_vote() runs, the outcome is computed once and stored, and finalization reads the stored value. Selling your tokens after the close does not unwind a decided vote.

Common reverts

RevertCause
Already votedUse add_votes() instead
Must vote firstadd_votes() without an existing position
Vote period expiredPast vote_end
Voting endedAlready closed
Transfer failedInsufficient allowance or balance
Withdraw next blockThe flash-loan guard — wait one block
Too early to end earlyLess than 3,600 s (1 hour) since the vote opened