Skip to content

Commit b40a832

Browse files
committed
Add DAO support
1 parent 26565b9 commit b40a832

File tree

2 files changed

+349
-0
lines changed

2 files changed

+349
-0
lines changed

src/dao.rs

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
use crate::eth::{BlockNumberOrTag, EthError, Filter as EthFilter, Provider};
2+
use alloy::rpc::types::request::{TransactionInput, TransactionRequest};
3+
use alloy_primitives::{Address, Bytes, FixedBytes, U256, B256, keccak256};
4+
use alloy_sol_macro::sol;
5+
use alloy_sol_types::{SolCall, SolEvent};
6+
7+
sol! {
8+
/// Minimal TimelockController interface.
9+
#[allow(non_camel_case_types)]
10+
contract TimelockController {
11+
function getMinDelay() external view returns (uint256);
12+
function hasRole(bytes32 role, address account) external view returns (bool);
13+
function PROPOSER_ROLE() external view returns (bytes32);
14+
function EXECUTOR_ROLE() external view returns (bytes32);
15+
function CANCELLER_ROLE() external view returns (bytes32);
16+
function schedule(
17+
address target,
18+
uint256 value,
19+
bytes data,
20+
bytes32 predecessor,
21+
bytes32 salt,
22+
uint256 delay
23+
) external;
24+
function execute(
25+
address target,
26+
uint256 value,
27+
bytes data,
28+
bytes32 predecessor,
29+
bytes32 salt
30+
) external payable;
31+
function cancel(bytes32 id) external;
32+
function hashOperation(
33+
address target,
34+
uint256 value,
35+
bytes data,
36+
bytes32 predecessor,
37+
bytes32 salt
38+
) external view returns (bytes32);
39+
}
40+
41+
/// Minimal Governor interface.
42+
#[allow(non_camel_case_types)]
43+
contract HyperwareGovernor {
44+
function propose(
45+
address[] targets,
46+
uint256[] values,
47+
bytes[] calldatas,
48+
string description
49+
) external returns (uint256);
50+
function hashProposal(
51+
address[] targets,
52+
uint256[] values,
53+
bytes[] calldatas,
54+
bytes32 descriptionHash
55+
) external view returns (uint256);
56+
function state(uint256 proposalId) external view returns (uint8);
57+
function proposalSnapshot(uint256 proposalId) external view returns (uint256);
58+
function proposalDeadline(uint256 proposalId) external view returns (uint256);
59+
function castVoteWithReason(uint256 proposalId, uint8 support, string reason) external returns (uint256);
60+
61+
/// Standard OZ ProposalCreated event layout
62+
event ProposalCreated(
63+
uint256 proposalId,
64+
address proposer,
65+
address[] targets,
66+
uint256[] values,
67+
string[] signatures,
68+
bytes[] calldatas,
69+
uint256 startBlock,
70+
uint256 endBlock,
71+
string description
72+
);
73+
}
74+
}
75+
76+
/// Convenience wrapper for Timelock/Governor interactions.
77+
#[derive(Clone, Debug)]
78+
pub struct DaoContracts {
79+
pub provider: Provider,
80+
pub timelock: Address,
81+
pub governor: Address,
82+
}
83+
84+
impl DaoContracts {
85+
pub fn new(provider: Provider, timelock: Address, governor: Address) -> Self {
86+
Self {
87+
provider,
88+
timelock,
89+
governor,
90+
}
91+
}
92+
93+
fn call_view<Call>(&self, target: Address, call: Call) -> Result<Call::Return, EthError>
94+
where
95+
Call: SolCall,
96+
{
97+
let tx_req = TransactionRequest::default()
98+
.to(target)
99+
.input(TransactionInput::new(Bytes::from(call.abi_encode())));
100+
let res_bytes = self.provider.call(tx_req, None)?;
101+
Call::abi_decode_returns(&res_bytes, false).map_err(|_| EthError::RpcMalformedResponse)
102+
}
103+
104+
/// Return the timelock's minimum delay.
105+
pub fn timelock_delay(&self) -> Result<U256, EthError> {
106+
let res = self.call_view(self.timelock, TimelockController::getMinDelayCall {})?;
107+
Ok(res._0)
108+
}
109+
110+
/// Fetch role IDs from the timelock.
111+
pub fn roles(&self) -> Result<(FixedBytes<32>, FixedBytes<32>, FixedBytes<32>), EthError> {
112+
let proposer = self.call_view(self.timelock, TimelockController::PROPOSER_ROLECall {})?._0;
113+
let executor = self.call_view(self.timelock, TimelockController::EXECUTOR_ROLECall {})?._0;
114+
let canceller = self.call_view(self.timelock, TimelockController::CANCELLER_ROLECall {})?._0;
115+
Ok((proposer, executor, canceller))
116+
}
117+
118+
/// Check if an account has a specific timelock role.
119+
pub fn has_role(&self, role: FixedBytes<32>, account: Address) -> Result<bool, EthError> {
120+
let res = self.call_view(
121+
self.timelock,
122+
TimelockController::hasRoleCall {
123+
role,
124+
account,
125+
},
126+
)?;
127+
Ok(res._0)
128+
}
129+
130+
/// Build a schedule tx for a single operation.
131+
pub fn build_schedule_tx(
132+
&self,
133+
target: Address,
134+
value: U256,
135+
data: Bytes,
136+
predecessor: FixedBytes<32>,
137+
salt: FixedBytes<32>,
138+
delay: U256,
139+
) -> TransactionRequest {
140+
let call = TimelockController::scheduleCall {
141+
target,
142+
value,
143+
data,
144+
predecessor,
145+
salt,
146+
delay,
147+
};
148+
TransactionRequest::default()
149+
.to(self.timelock)
150+
.input(TransactionInput::new(Bytes::from(call.abi_encode())))
151+
}
152+
153+
/// Build an execute tx for a scheduled operation.
154+
pub fn build_execute_tx(
155+
&self,
156+
target: Address,
157+
value: U256,
158+
data: Bytes,
159+
predecessor: FixedBytes<32>,
160+
salt: FixedBytes<32>,
161+
) -> TransactionRequest {
162+
let call = TimelockController::executeCall {
163+
target,
164+
value,
165+
data,
166+
predecessor,
167+
salt,
168+
};
169+
TransactionRequest::default()
170+
.to(self.timelock)
171+
.input(TransactionInput::new(Bytes::from(call.abi_encode())))
172+
}
173+
174+
/// Build a cancel tx for an operation id (hashOperation output).
175+
pub fn build_cancel_tx(&self, operation_id: FixedBytes<32>) -> TransactionRequest {
176+
let call = TimelockController::cancelCall { id: operation_id };
177+
TransactionRequest::default()
178+
.to(self.timelock)
179+
.input(TransactionInput::new(Bytes::from(call.abi_encode())))
180+
}
181+
182+
/// Build a propose tx on the governor.
183+
pub fn build_propose_tx(
184+
&self,
185+
targets: Vec<Address>,
186+
values: Vec<U256>,
187+
calldatas: Vec<Bytes>,
188+
description: String,
189+
) -> TransactionRequest {
190+
let call = HyperwareGovernor::proposeCall {
191+
targets,
192+
values,
193+
calldatas,
194+
description,
195+
};
196+
TransactionRequest::default()
197+
.to(self.governor)
198+
.input(TransactionInput::new(Bytes::from(call.abi_encode())))
199+
}
200+
201+
/// Compute the proposal id off-chain using the governor's hashProposal view.
202+
/// (OZ proposalId = keccak256(abi.encode(targets, values, calldatas, descriptionHash))).
203+
pub fn hash_proposal(
204+
&self,
205+
targets: Vec<Address>,
206+
values: Vec<U256>,
207+
calldatas: Vec<Bytes>,
208+
description: &str,
209+
) -> Result<U256, EthError> {
210+
let description_hash = keccak256(description.as_bytes());
211+
let res = self.call_view(
212+
self.governor,
213+
HyperwareGovernor::hashProposalCall {
214+
targets,
215+
values,
216+
calldatas,
217+
descriptionHash: description_hash,
218+
},
219+
)?;
220+
Ok(res._0)
221+
}
222+
223+
/// Build a castVoteWithReason tx (support: 0=Against,1=For,2=Abstain in OZ Governor).
224+
pub fn build_vote_tx(
225+
&self,
226+
proposal_id: U256,
227+
support: u8,
228+
reason: String,
229+
) -> TransactionRequest {
230+
let call = HyperwareGovernor::castVoteWithReasonCall {
231+
proposalId: proposal_id,
232+
support,
233+
reason,
234+
};
235+
TransactionRequest::default()
236+
.to(self.governor)
237+
.input(TransactionInput::new(Bytes::from(call.abi_encode())))
238+
}
239+
240+
/// Governor state (OZ enum: 0 Pending, 1 Active, 2 Canceled, 3 Defeated, 4 Succeeded, 5 Queued, 6 Expired, 7 Executed).
241+
pub fn proposal_state(&self, proposal_id: U256) -> Result<u8, EthError> {
242+
let res = self.call_view(
243+
self.governor,
244+
HyperwareGovernor::stateCall {
245+
proposalId: proposal_id,
246+
},
247+
)?;
248+
Ok(res._0)
249+
}
250+
251+
/// Proposal snapshot block.
252+
pub fn proposal_snapshot(&self, proposal_id: U256) -> Result<U256, EthError> {
253+
let res = self.call_view(
254+
self.governor,
255+
HyperwareGovernor::proposalSnapshotCall {
256+
proposalId: proposal_id,
257+
},
258+
)?;
259+
Ok(res._0)
260+
}
261+
262+
/// Proposal deadline block.
263+
pub fn proposal_deadline(&self, proposal_id: U256) -> Result<U256, EthError> {
264+
let res = self.call_view(
265+
self.governor,
266+
HyperwareGovernor::proposalDeadlineCall {
267+
proposalId: proposal_id,
268+
},
269+
)?;
270+
Ok(res._0)
271+
}
272+
273+
/// Fetch ProposalCreated events within a block range.
274+
pub fn fetch_proposals_created(
275+
&self,
276+
from_block: Option<BlockNumberOrTag>,
277+
to_block: Option<BlockNumberOrTag>,
278+
) -> Result<Vec<ProposalCreatedEvent>, EthError> {
279+
let topic0 = HyperwareGovernor::ProposalCreated::SIGNATURE_HASH;
280+
let mut filter = EthFilter::new()
281+
.address(self.governor)
282+
.event_signature(B256::from(topic0));
283+
if let Some(fb) = from_block {
284+
filter = filter.from_block(fb);
285+
}
286+
if let Some(tb) = to_block {
287+
filter = filter.to_block(tb);
288+
}
289+
let logs = self.provider.get_logs(&filter)?;
290+
let mut out = Vec::new();
291+
for log in logs {
292+
let prim_log = log.inner.clone();
293+
if let Ok(decoded) =
294+
HyperwareGovernor::ProposalCreated::decode_log(&prim_log, true)
295+
{
296+
out.push(ProposalCreatedEvent {
297+
proposal_id: decoded.proposalId,
298+
proposer: decoded.proposer,
299+
targets: decoded.targets.clone(),
300+
values: decoded.values.clone(),
301+
signatures: decoded.signatures.clone(),
302+
calldatas: decoded.calldatas.clone(),
303+
start_block: decoded.startBlock,
304+
end_block: decoded.endBlock,
305+
description: decoded.description.clone(),
306+
});
307+
}
308+
}
309+
Ok(out)
310+
}
311+
312+
/// Hash a timelock operation (matches timelock.hashOperation).
313+
pub fn hash_operation(
314+
&self,
315+
target: Address,
316+
value: U256,
317+
data: Bytes,
318+
predecessor: FixedBytes<32>,
319+
salt: FixedBytes<32>,
320+
) -> Result<FixedBytes<32>, EthError> {
321+
let res = self.call_view(
322+
self.timelock,
323+
TimelockController::hashOperationCall {
324+
target,
325+
value,
326+
data,
327+
predecessor,
328+
salt,
329+
},
330+
)?;
331+
Ok(res._0)
332+
}
333+
}
334+
335+
/// Parsed ProposalCreated event.
336+
#[derive(Clone, Debug)]
337+
pub struct ProposalCreatedEvent {
338+
pub proposal_id: U256,
339+
pub proposer: Address,
340+
pub targets: Vec<Address>,
341+
pub values: Vec<U256>,
342+
pub signatures: Vec<String>,
343+
pub calldatas: Vec<Bytes>,
344+
pub start_block: U256,
345+
pub end_block: U256,
346+
pub description: String,
347+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ wit_bindgen::generate!({
3232

3333
/// Interact with the tokenregistry contract data
3434
pub mod bindings;
35+
/// Interact with DAO (Timelock / Governor) contracts
36+
pub mod dao;
3537

3638
/// Interact with the eth provider module.
3739
pub mod eth;

0 commit comments

Comments
 (0)