use k256::ecdsa::SigningKey;
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Proposal {
pub guild_id: u64,
pub proposer: String,
pub to: String,
pub amount: u128,
pub deadline: u64,
pub status: u8,
pub for_votes: u128,
pub against_votes: u128,
}
impl Proposal {
pub fn status_label(&self) -> &'static str {
match self.status {
0 => "active",
1 => "passed",
2 => "failed",
3 => "executed",
4 => "expired",
_ => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tally {
pub for_votes: u128,
pub against_votes: u128,
pub quorum: u128,
pub votes_cast: u128,
pub passing: bool,
}
pub(crate) fn encode_propose(guild_id: u64, to: &[u8; 20], amount_wei: u128, memo: &[u8], voting_period_secs: u64) -> Vec<u8> {
let padded_len = memo.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 5 * 32 + 32 + padded_len);
out.extend_from_slice(&selector("propose(uint256,address,uint256,bytes,uint64)"));
out.extend_from_slice(&u256_be(guild_id as u128)); out.extend_from_slice(&addr_word(to)); out.extend_from_slice(&u256_be(amount_wei)); out.extend_from_slice(&u256_be(5 * 32)); out.extend_from_slice(&u256_be(voting_period_secs as u128)); out.extend_from_slice(&u256_be(memo.len() as u128)); out.extend_from_slice(memo); out.resize(out.len() + (padded_len - memo.len()), 0); out
}
pub(crate) fn encode_vote(proposal_id: u64, support: bool) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("vote(uint256,bool)"));
out.extend_from_slice(&u256_be(proposal_id as u128));
out.extend_from_slice(&u256_be(if support { 1 } else { 0 }));
out
}
pub fn encode_vote_calldata(proposal_id: u64, support: bool) -> Vec<u8> {
encode_vote(proposal_id, support)
}
#[allow(clippy::too_many_arguments)]
pub async fn propose_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
to_hex: &str,
amount_wei: u128,
memo: &[u8],
voting_period_secs: u64,
fee_token: &str,
) -> Result<String, String> {
let to = parse_eth_address(to_hex)?;
let gas = 3_000_000 + (memo.len() as u128) * 9_000;
sponsored_diamond_call(
sender,
fee_payer,
encode_propose(guild_id, &to, amount_wei, memo, voting_period_secs),
fee_token,
gas,
)
.await
}
pub async fn vote_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
proposal_id: u64,
support: bool,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
encode_vote(proposal_id, support),
fee_token,
800_000,
)
.await
}
pub async fn execute_proposal_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
proposal_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
call_uint_bytes("execute(uint256)", proposal_id),
fee_token,
3_000_000,
)
.await
}
pub async fn get_proposal(proposal_id: u64) -> Result<Proposal, String> {
let result = read_view(
selector("getProposal(uint256)"),
&[u256_be(proposal_id as u128)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 8 * 32 {
return Err(format!("getProposal: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
Ok(Proposal {
guild_id: u64_low(word(0)),
proposer: format!("0x{}", bytes_to_hex(&word(1)[12..32])), to: format!("0x{}", bytes_to_hex(&word(2)[12..32])),
amount: u128_low(word(3)),
deadline: u64_low(word(4)),
status: bytes[5 * 32 + 31], for_votes: u128_low(word(6)),
against_votes: u128_low(word(7)),
})
}
pub async fn tally_of(proposal_id: u64) -> Result<Tally, String> {
let result = read_view(selector("tallyOf(uint256)"), &[u256_be(proposal_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 5 * 32 {
return Err(format!("tallyOf: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
Ok(Tally {
for_votes: u128_low(word(0)),
against_votes: u128_low(word(1)),
quorum: u128_low(word(2)),
votes_cast: u128_low(word(3)),
passing: bytes[4 * 32 + 31] != 0, })
}
pub async fn has_voted(proposal_id: u64, voter_hex: &str) -> Result<bool, String> {
let voter = parse_eth_address(voter_hex)?;
let result = read_view(
selector("hasVoted(uint256,address)"),
&[u256_be(proposal_id as u128), addr_word(&voter)],
)
.await?;
decode_u256_as_u64(&result).map(|v| v != 0)
}
pub async fn proposals_of(guild_id: u64, start_after: u64, limit: u64) -> Result<Vec<u64>, String> {
let result = read_view(
selector("proposalsOf(uint256,uint256,uint256)"),
&[
u256_be(guild_id as u128),
u256_be(start_after as u128),
u256_be(limit as u128),
],
)
.await?;
let bytes = hex_to_bytes(&result)?;
decode_uint_array_with_cursor(&bytes)
}
pub async fn proposal_memo_of(proposal_id: u64) -> Result<String, String> {
decode_bytes_string_call("proposalMemoOf(uint256)", proposal_id, "proposalMemoOf").await
}
pub async fn proposal_count() -> Result<u64, String> {
let result = read_view(selector("proposalCount()"), &[]).await?;
decode_u256_as_u64(&result)
}
#[cfg(test)]
mod voting_tests {
use super::*;
#[test]
fn propose_calldata_layout() {
let to = [0xCDu8; 20];
let amount = 2_000_000_000_000_000_000u128; let memo = b"fund q3 audit"; let period = 86_400u64; let cd = encode_propose(0x10, &to, amount, memo, period);
assert_eq!(&cd[0..4], &selector("propose(uint256,address,uint256,bytes,uint64)"));
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 0x10);
assert!(cd[36..36 + 12].iter().all(|&b| b == 0));
assert_eq!(&cd[36 + 12..36 + 32], &to);
assert_eq!(u128::from_be_bytes(cd[68 + 16..68 + 32].try_into().unwrap()), amount);
assert_eq!(u64::from_be_bytes(cd[100 + 24..100 + 32].try_into().unwrap()), 0xA0);
assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), period);
assert_eq!(u64::from_be_bytes(cd[164 + 24..164 + 32].try_into().unwrap()), memo.len() as u64);
assert_eq!(&cd[196..196 + memo.len()], memo);
assert_eq!(cd.len(), 4 + 5 * 32 + 32 + 32); }
#[test]
fn propose_empty_memo() {
let to = [0x01u8; 20];
let cd = encode_propose(1, &to, 1, b"", 3600);
assert_eq!(u64::from_be_bytes(cd[100 + 24..100 + 32].try_into().unwrap()), 0xA0); assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), 3600); assert_eq!(u64::from_be_bytes(cd[164 + 24..164 + 32].try_into().unwrap()), 0); assert_eq!(cd.len(), 4 + 5 * 32 + 32); }
#[test]
fn propose_pads_long_memo() {
let to = [0x02u8; 20];
let memo = b"a-treasury-measure-memo-well-over-32-bytes!!"; let cd = encode_propose(3, &to, 5, memo, 7200);
assert_eq!(u64::from_be_bytes(cd[164 + 24..164 + 32].try_into().unwrap()), memo.len() as u64);
assert_eq!(&cd[196..196 + memo.len()], memo.as_slice());
assert_eq!(cd.len(), 4 + 5 * 32 + 32 + 64);
assert!(cd[196 + memo.len()..].iter().all(|&b| b == 0)); }
#[test]
fn vote_calldata_bool_encoding() {
let yes = encode_vote(7, true);
assert_eq!(&yes[0..4], &selector("vote(uint256,bool)"));
assert_eq!(yes.len(), 4 + 64);
assert_eq!(u64::from_be_bytes(yes[4 + 24..4 + 32].try_into().unwrap()), 7); assert!(yes[36..36 + 31].iter().all(|&b| b == 0));
assert_eq!(yes[36 + 31], 1);
let no = encode_vote(7, false);
assert!(no[36..36 + 32].iter().all(|&b| b == 0));
}
#[test]
fn execute_calldata_layout() {
let cd = call_uint_bytes("execute(uint256)", 11);
assert_eq!(&cd[0..4], &selector("execute(uint256)"));
assert_eq!(cd.len(), 36);
assert_eq!(u64::from_be_bytes(cd[28..36].try_into().unwrap()), 11);
}
#[test]
fn proposal_status_label_maps_enum() {
let mut p = Proposal {
guild_id: 0,
proposer: "0x00".into(),
to: "0x00".into(),
amount: 0,
deadline: 0,
status: 0,
for_votes: 0,
against_votes: 0,
};
for (s, label) in [
(0u8, "active"),
(1, "passed"),
(2, "failed"),
(3, "executed"),
(4, "expired"),
(9, "unknown"),
] {
p.status = s;
assert_eq!(p.status_label(), label);
}
}
#[test]
fn proposals_of_cursor_decode() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(64)); bytes.extend_from_slice(&u256_be(42)); bytes.extend_from_slice(&u256_be(2)); bytes.extend_from_slice(&u256_be(11));
bytes.extend_from_slice(&u256_be(17));
assert_eq!(decode_uint_array_with_cursor(&bytes).unwrap(), vec![11, 17]);
}
#[test]
fn tally_decode_fields() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(3)); bytes.extend_from_slice(&u256_be(1)); bytes.extend_from_slice(&u256_be(2)); bytes.extend_from_slice(&u256_be(4)); bytes.extend_from_slice(&u256_be(1)); let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
let u128_low = |w: &[u8]| {
let mut b = [0u8; 16];
b.copy_from_slice(&w[16..32]);
u128::from_be_bytes(b)
};
let t = Tally {
for_votes: u128_low(word(0)),
against_votes: u128_low(word(1)),
quorum: u128_low(word(2)),
votes_cast: u128_low(word(3)),
passing: bytes[4 * 32 + 31] != 0,
};
assert_eq!(t, Tally { for_votes: 3, against_votes: 1, quorum: 2, votes_cast: 4, passing: true });
}
}