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*110yes, 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*11040% 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 = YESamount = 1_000_000 * 10**9
send(erc20.functions.approve(vote_address, amount))
send(vote.functions.vote(amount, True)) # True = YESYour 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.
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
| Revert | Cause |
|---|---|
Already voted | Use add_votes() instead |
Must vote first | add_votes() without an existing position |
Vote period expired | Past vote_end |
Voting ended | Already closed |
Transfer failed | Insufficient allowance or balance |
Withdraw next block | The flash-loan guard — wait one block |
Too early to end early | Less than 3,600 s (1 hour) since the vote opened |