use k256::ecdsa::SigningKey;
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedProposal {
pub guild_id: u64,
pub proposer: String,
pub to: String,
pub amount: u128,
pub deadline: u64,
pub status: u8,
pub for_shares: u128,
pub against_shares: u128,
pub snapshot_total_shares: u128,
}
impl WeightedProposal {
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 WeightedTally {
pub for_shares: u128,
pub against_shares: u128,
pub quorum_shares: u128,
pub cast_shares: u128,
pub passing: bool,
}
pub(crate) fn encode_set_shares(guild_id: u64, member: &[u8; 20], shares: u128) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 3 * 32);
out.extend_from_slice(&selector("setShares(uint256,address,uint256)"));
out.extend_from_slice(&u256_be(guild_id as u128)); out.extend_from_slice(&addr_word(member)); out.extend_from_slice(&u256_be(shares)); out
}
pub(crate) fn encode_propose_weighted(
guild_id: u64,
to: &[u8; 20],
amount_wei: u128,
period_secs: u64,
memo: &[u8],
) -> 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("proposeWeighted(uint256,address,uint256,uint256,string)"));
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(period_secs as u128)); out.extend_from_slice(&u256_be(5 * 32)); 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_weighted(proposal_id: u64, support: bool) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 64);
out.extend_from_slice(&selector("voteWeighted(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_weighted_calldata(proposal_id: u64, support: bool) -> Vec<u8> {
encode_vote_weighted(proposal_id, support)
}
pub async fn set_shares_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
member_hex: &str,
shares: u128,
fee_token: &str,
) -> Result<String, String> {
let member = parse_eth_address(member_hex)?;
sponsored_diamond_call(
sender,
fee_payer,
encode_set_shares(guild_id, &member, shares),
fee_token,
800_000,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn propose_weighted_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
guild_id: u64,
to_hex: &str,
amount_wei: u128,
period_secs: u64,
memo: &[u8],
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_weighted(guild_id, &to, amount_wei, period_secs, memo),
fee_token,
gas,
)
.await
}
pub async fn vote_weighted_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_weighted(proposal_id, support),
fee_token,
800_000,
)
.await
}
pub async fn execute_weighted_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("executeWeighted(uint256)", proposal_id),
fee_token,
3_000_000,
)
.await
}
pub async fn shares_of(guild_id: u64, member_hex: &str) -> Result<u128, String> {
let member = parse_eth_address(member_hex)?;
let result = read_view(
selector("sharesOf(uint256,address)"),
&[u256_be(guild_id as u128), addr_word(&member)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 32 {
return Err(format!("sharesOf: short response {} bytes", bytes.len()));
}
Ok(u128_low(&bytes[0..32]))
}
pub async fn total_shares_of(guild_id: u64) -> Result<u128, String> {
let result = read_view(selector("totalSharesOf(uint256)"), &[u256_be(guild_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 32 {
return Err(format!("totalSharesOf: short response {} bytes", bytes.len()));
}
Ok(u128_low(&bytes[0..32]))
}
pub async fn get_weighted_proposal(proposal_id: u64) -> Result<WeightedProposal, String> {
let result = read_view(
selector("weightedProposal(uint256)"),
&[u256_be(proposal_id as u128)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 9 * 32 {
return Err(format!("weightedProposal: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
Ok(WeightedProposal {
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_shares: u128_low(word(6)),
against_shares: u128_low(word(7)),
snapshot_total_shares: u128_low(word(8)),
})
}
pub async fn weighted_tally_of(proposal_id: u64) -> Result<WeightedTally, String> {
let result = read_view(selector("weightedTallyOf(uint256)"), &[u256_be(proposal_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 5 * 32 {
return Err(format!("weightedTallyOf: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
Ok(WeightedTally {
for_shares: u128_low(word(0)),
against_shares: u128_low(word(1)),
quorum_shares: u128_low(word(2)),
cast_shares: u128_low(word(3)),
passing: bytes[4 * 32 + 31] != 0, })
}
pub async fn has_voted_weighted(proposal_id: u64, voter_hex: &str) -> Result<bool, String> {
let voter = parse_eth_address(voter_hex)?;
let result = read_view(
selector("hasVotedWeighted(uint256,address)"),
&[u256_be(proposal_id as u128), addr_word(&voter)],
)
.await?;
decode_u256_as_u64(&result).map(|v| v != 0)
}
pub async fn weighted_proposals_of(guild_id: u64, start_after: u64, limit: u64) -> Result<Vec<u64>, String> {
let result = read_view(
selector("weightedProposalsOf(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 weighted_proposal_memo_of(proposal_id: u64) -> Result<String, String> {
decode_bytes_string_call("weightedProposalMemoOf(uint256)", proposal_id, "weightedProposalMemoOf").await
}
pub async fn weighted_proposal_count() -> Result<u64, String> {
let result = read_view(selector("weightedProposalCount()"), &[]).await?;
decode_u256_as_u64(&result)
}
#[cfg(test)]
mod weighted_voting_tests {
use super::*;
#[test]
fn set_shares_calldata_layout() {
let member = [0xABu8; 20];
let cd = encode_set_shares(0x10, &member, 60);
assert_eq!(&cd[0..4], &selector("setShares(uint256,address,uint256)"));
assert_eq!(cd.len(), 4 + 3 * 32);
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], &member);
assert_eq!(u128::from_be_bytes(cd[68 + 16..68 + 32].try_into().unwrap()), 60);
}
#[test]
fn propose_weighted_calldata_layout() {
let to = [0xCDu8; 20];
let amount = 2_000_000_000_000_000_000u128; let period = 86_400u64; let memo = b"fund q3 audit"; let cd = encode_propose_weighted(0x10, &to, amount, period, memo);
assert_eq!(&cd[0..4], &selector("proposeWeighted(uint256,address,uint256,uint256,string)"));
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()), period);
assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), 0xA0);
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_weighted_empty_memo() {
let to = [0x01u8; 20];
let cd = encode_propose_weighted(1, &to, 1, 3600, b"");
assert_eq!(u64::from_be_bytes(cd[100 + 24..100 + 32].try_into().unwrap()), 3600); assert_eq!(u64::from_be_bytes(cd[132 + 24..132 + 32].try_into().unwrap()), 0xA0); 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_weighted_pads_long_memo() {
let to = [0x02u8; 20];
let memo = b"a-treasury-measure-memo-well-over-32-bytes!!"; let cd = encode_propose_weighted(3, &to, 5, 7200, memo);
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_weighted_calldata_bool_encoding() {
let yes = encode_vote_weighted(7, true);
assert_eq!(&yes[0..4], &selector("voteWeighted(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_weighted(7, false);
assert!(no[36..36 + 32].iter().all(|&b| b == 0)); assert_eq!(encode_vote_weighted_calldata(7, true), yes);
}
#[test]
fn execute_weighted_calldata_layout() {
let cd = call_uint_bytes("executeWeighted(uint256)", 11);
assert_eq!(&cd[0..4], &selector("executeWeighted(uint256)"));
assert_eq!(cd.len(), 36);
assert_eq!(u64::from_be_bytes(cd[28..36].try_into().unwrap()), 11);
}
#[test]
fn weighted_proposal_status_label_maps_enum() {
let mut p = WeightedProposal {
guild_id: 0,
proposer: "0x00".into(),
to: "0x00".into(),
amount: 0,
deadline: 0,
status: 0,
for_shares: 0,
against_shares: 0,
snapshot_total_shares: 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 weighted_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 weighted_tally_decode_fields() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(60)); bytes.extend_from_slice(&u256_be(10)); bytes.extend_from_slice(&u256_be(51)); bytes.extend_from_slice(&u256_be(70)); bytes.extend_from_slice(&u256_be(1)); let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
let t = WeightedTally {
for_shares: u128_low(word(0)),
against_shares: u128_low(word(1)),
quorum_shares: u128_low(word(2)),
cast_shares: u128_low(word(3)),
passing: bytes[4 * 32 + 31] != 0,
};
assert_eq!(
t,
WeightedTally { for_shares: 60, against_shares: 10, quorum_shares: 51, cast_shares: 70, passing: true }
);
}
#[test]
fn weighted_proposal_decode_has_snapshot_word() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u256_be(5)); let mut proposer = [0u8; 32];
proposer[12..].copy_from_slice(&[0x11u8; 20]);
bytes.extend_from_slice(&proposer); let mut to = [0u8; 32];
to[12..].copy_from_slice(&[0x22u8; 20]);
bytes.extend_from_slice(&to); bytes.extend_from_slice(&u256_be(2_000_000_000_000_000_000)); bytes.extend_from_slice(&u256_be(1_700_000_000)); bytes.extend_from_slice(&u256_be(3)); bytes.extend_from_slice(&u256_be(60)); bytes.extend_from_slice(&u256_be(10)); bytes.extend_from_slice(&u256_be(100)); let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
let p = WeightedProposal {
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_shares: u128_low(word(6)),
against_shares: u128_low(word(7)),
snapshot_total_shares: u128_low(word(8)),
};
assert_eq!(p.guild_id, 5);
assert_eq!(p.proposer, format!("0x{}", "11".repeat(20)));
assert_eq!(p.to, format!("0x{}", "22".repeat(20)));
assert_eq!(p.amount, 2_000_000_000_000_000_000);
assert_eq!(p.status, 3);
assert_eq!(p.for_shares, 60);
assert_eq!(p.against_shares, 10);
assert_eq!(p.snapshot_total_shares, 100);
assert_eq!(p.status_label(), "executed");
}
}