零基礎 DAO 治理實驗:從合約到測試的完整演練
发布于 2025-07-26 14:23:22(微信公众号导出记录)。
本文来自公众号后台的“导出文章内容”功能。博客正文由导出长图进行本地 OCR 转写,并保留原始排版图用于逐段核对。
原文链接:查看原文
OCR 转写有效文字约 10868 字;代码、流程图和版式以文末原始排版图为准。
正文(本地 OCR 转写)
零基DA0治理實验:合約到测試的完整演
original wex7ce EI V1ajero 2825年7月26日 14:24 日本 實融目標:創建雨智能合的。合的A(SimpleStorage)用於储存一四数字。合B (SimpLeDAO)作為一去中心化白治组感,將有唯一且排他的權力束修改合約A中的数字,而 道種修改必须經通DAD成員的投票沃策。
0995 DAO控制置板
B887:306
B86F:3
第一步:编寫智能合約
我仍需要雨個合的:一個是被管理的對象,另一個是管理的主糖。
1.1 目合的 SimpleStorage.sol
這借合约的核心是,它有一他“排有者(owner):的概念,业且只有摊有者才能修改数字。我們科 後會将這個“携有害:身份轉交给DA0合的。 操作:在Remix IDE 中創建Sinple5torage.sol 權案,贴入以下完整程式碼。 5olidity se-Identifier: HIT pragna solidity ^0.8.0; /1这留合的是货DA0所管理的财象 contract Simplestorage { uint publlc nyNunber; address public ovner;// 合约的频有密地址 11事件:當所有根通移诗網 event OwnershipTransferred(address indexed previousowner, address //修款符: 留程式码棕查工具,确保只有owner才能熟行某嘿函数 require(nsg.sender -- ouner, "Caller ls not the owner*); 11建横子:合前部署時谨行,投定初始数字和腋有者 constructor(uint _initialNumber){ owner = nsg.sender;// 部署合的 的增址成為初始摊有者 才能股定新数字 function setNunber(uint _newNunber] public onlyowner myNumber = _newNumber; } //任何人都可以馈取数字 function getNunber[) pubLic view returns (uint) { return nyNunber; //韩移所有權的函数,可楼被onLyOwner保膜 function transferDwmership(address newowner) public onlyOuner { require(newowner I= address(e), "New ouner cannot bc thc zcro emit OunershipTransterred(owner, newouner); owner = nevovner:
1.2 治理合的 SimpleDA0.sol
这個合的是我們的DA0,它负责處理提案和投票。當提案通過時,它官以「癌有害:的身份去调用 S1mpleStorage 的 setNumber 函敷. 操作:在Remix IDE 中创建 SimpleDAO.sol 穆案,贴入以下完整程式碍。 Salidity // SPDX-LIcense-IdentIfIer: NIT pragna solidity ^θ.B.D; |1定菱一倡介面(Interface),告斯本合约目標合约有感些功能可以期用。 //道就像一份功能的“就明書”,孩仍的 DA0 知道如何典 SinpleStorage互数 interface Isinplestorage { function setNunber(uint _newNunber] external; function transfer0wnership(address newowner) external; } 11速是DAO治理合豹 contract SimpleDAO { //指向我得想要控制的 5inpleStorage 合的真例 I5implestorage publlc sinplestorage; atdress public owner; napping(addrcss => bool) public isMcmber; uint public nextProposalId; 11提东的数球结摄 |1受案的数球经梢 struct Proposal { uint id; address proposer; string description; uint neNNunberToSet; uint forVotes; uint againstvotes; bool executed; mapping(address => boot ) hasvoted; napping(uint => Proposal) pubLlc proposals; //事件日味 event Proposalcreated{uint id, address proposer, string descriptio event Voted(uint proposalId, address voter, bool supported); event PraposalExecuted{uint proposalId): //部置 DAo 時,必须偶入它要管理的 5inpicStoragc 合约的地址 simpleStorage = ISimpleStorage( sinpleStorageAddress): owner = nsg.sender; } //添加DAO成具 function addMenber (address _newMember] pubLic { require(nsg.sender == ouner, "0nly DAo ouner can add menbers") isNenber[_newMember] = true; //别建提察 function createProposal(string memory _description, uint _newNunbe require(isMenber[msg.sender], "only nenbers can create proposa Proposal storage newProposal = proposals[nextProposalId]; newProposal.proposer - nsg sender; newProposal.description = _description; newProposal newlumberTo5et = _newunber; emit ProposalCreated(nextProposalId, nsg.sender, _description) nextProposalId++; } //报票 function vote(uint _proposalId, bool _support) public { require(isMenber[msg.sender], "only nenbers can vote*); Proposal storage p = proposals[_proposalId]; require(!p.hasvoted[msg-sender] . "You have already voted*): p.hasvoted[nsg sender] = true; 1f (_support) ( p. forvotes++; }else { p.againstvotes++; emit Voted(_proposalId, msg.sender, _support); //块行提案:掌提廉通通,此函数游以DA0的身份颁用SinpleStorage 的函数 function executeProposal(uint _proposalId) public { Proposal storage p = proposals[_proposalId]: require(Ip.executed, *Proposal already executed"); require(p.forvotes > p.againstvotes, *Proposal did not pass"); p.executed = true; // DAo 合的娱用 SinpleStorage 的 setMumber 通数 simpLeStorage. setNumber (p.newlurberToSet) : emit ProposalExecuted(_proposalId);
第二步:在Remix中部署與配置權限
道一步至關重要,我们路刻建雨回合的的置例,亚將它们的耀力保建立起来。 操作:暂格按照以下顺序软行。
1.编合的:在Remix的SalidityCompiler页,分别中
SinpleStorage.sol和 SimpleDA0,sol,贴 Compile 按钮,致保雨惬合的都成功 崩露且没有错换。
2.部sinpleStorage:
前往 Deploy& Run Transactions 标页。
-
在CONTRACT 下拉菜单中,理 SinpleStorage.
-
在 Dcploy 按超旁的_initialNumbcr 截位输入一图初始数字,如 166。
o黏聖DepLoy。在 MetaMask 弹际中確想交易(插使用您的户1)。 部署成功德,在下方的 DepLoyed Contracts 列表中奢出现一個 SimpleStorage 實 例。贴旁遗的要固示,表其合的地址,我們碼此地址为STORAGE_ADDRES5.
1.部 SinpleDAO:
- 回到顶部的CONTRACT 下拉菜单,遗次通握 SimpleDA0。
在DepLoy按翻旁的_simpleStorageAddress榴位,贴上删才植的 STORAGE_ADDRESS, 點聚Deploy.在MetaMask 中两次殖交易(分使用户1)。
- 部暑成功後,列表中需出现一困新的SimplcDAO實例。同搓地,其合的地址。我們
稀此地址卷DAO_ADDRESS.
1.執行键的權限交接:
在 Deployed Contracts 列表中,找到黏阳 SinpleStorage 胃例的介面。 o找到橙色的transferowncrship 函数。 在旁遗的newOwner 入框中,贴上剧才的 DAO_ADDRESS。 贴型transact 按辑。在MetaMask 中確超交易(必须由概户 1敬起,因幕它此刻仍是 Sinplestorage 的觅有者)。
1.融盟限交接:
o權限交接的交易成功,回到simple5toragc 置例的介面。 黏型暂色的owner 被钮(此接辑只取数,不清耗Gas)。 。下方返回的地址必须是您製的DAO_ADDRESs.遗證明SimpleStorage 的所有榴已成 功辑移绘 SimpleDAO 合的。
1.為DAO添加测腻成员:
- 黏開 SinpleDA0 實例的介面。
找到adHember 函数。 在_newMcnber 输入框中,貼人您MctaMask 中的第二圆幅户地址。 o黏量transact 按钮,亚用户1在 MetaMask 中殖交易。 至此,所有的智能合的配置工作已全部完成。
第三步:编寫運行前端交互界面
现在我們需要一個细页来與DAO進行互助。 操作:
1.在您的電胸上創建一個名為 dao.htnl的案。
2.等以下完整的 HTML 程式碼贴人旅案。
3.最重要的一步:格改程式弧開顾虚的雨個地址,將其替换為您在上一步中崔震的
STORAGE_ADDRESS 和 DAO_ADDRESS, TWLH
<ID0CTYPE htnl>
<,tueH-4z=buel (414
cheadx
cneta charset=UTF-8"$
ctitle>DA0 治理實验s/title>
cstyles
body font-family: -apple-systen, BlinkMacSystemFont, Segce
.container { text-align: center; padding: 3Bpx; hackground-co
hl, hz, h3 {color: W627EEA;}
button ( background-color: t627EEA; color: white; border: nen
hutton:hover:not(:disabled) ( background-color: #4a67de; }
button:disabled { opacity: e.6; cursor: not-allowed; }
input f padding: 10px; border: 1px solid #ecc; border-radlus:
.info-section, .contract-section { margin-top: 25px; text-ali.
.proposal-card { border: 1px solid #627EEA: border-radius: 8p
.message { margin-top: 15px; font-weight: bold; }
.error { color: dc3545; }
success {color:#28a745:}
.warning-box background-color: fff3cd; border: lpx solid #
≤/stylea
c/head>
cbody>
<div class=container'>
<h1>DA0 治睡實睡c/h1x
<div class="warning-box"s
trong重要:</strang行前,读殖保已在 HTML 權案的<scrif
c/divs
<button 1c=connectButton>谨继到 MetaNask</button>
sdiv class="infa-section'>
<p id-"statusclass="ncssage">状燃:未速结</px
cp 1d=account'>幅户:未理</p>
s/div>
xdiv id--sinplestoragecontrol" class-"contract-section style
<h2>S1nplestorage 联感</h2>
spsstrong>被控合的地址:</stronga span id="storageContract!
<button 1d-"retreshstorageNumber>剧新数字</button>
</div>
xdiv id--daocontrol class="contract-section" style-*display:
<h2>DA0控制面板</h2>
sp>strong>0Ao 合的地址:</strong> <span id=*dapContractAddr
c.pues-lesodoud,=ssels Aips
ch3>剧键新提累</h3>
sinput type=*text" id="praposalDescription' placeheld
cbutton 1d-createProposaLButton>創健提案</button>
sp i="proposalstatus" class=*nessage"s</p>
</divs
<div 1c-"proposalsList" style-"nargin-top: 20px;"s
<h3>现有提案</h3>
c/divs
52
</divs
cscript src=-https://unpkg-con/ethersg5-6-o/dist/ethers-und.min.j
<scripts
!!!替换為震原部署的合的地址!!!
Const SIMPLE_STORAGE_CONTRACT_ADDRESS = *YOUR_SINPLE_STORAGE_
ConSt DAO_CONTRACT_ADDRESS - 'YOUR_DAO_CONTRACT_ADDRESS';
const SIMPLE STORAGE ABI = ["function getNumber[) vien return
const DAo_ABI = [ "constructor(address _simple5toragcAddress]
let provider, s1gner, sinpleStoragecontract, daocontract;
Const connectButton = document .getElenentById['connectButten'
const sinplestorageControl = document -getElementById(*simples
Const storageAddressDisplay - docunent-getElementById( storag
Const daoAddressDisplay = document -getElenentById( *daoContrat
const currentContractNunber = docunent-getElementById(*curren
Const createProposaLButton = document -getElementById(*createP
const proposalsListoiv - document.getElenentById('proposalsL1
connectButton.addEventListener( 'click, connectwallet);
async function connecthallet() {
1f (typeof window.ethereum ---“undefined’){return;}
try {
await window.ethereum.request(( nethod: 'eth_requestA
provider - new ethers.providers.Meb3Provider(window.e
signer = provider.get5igner():
const account = wwait signer-getAddress[);
statusEl.textContent-‘糖:已建结';
accountEl.textContent =*栋户: s{account}:
connectButton.disabled = true;
awalt initializeDApp(1;
}catch [error){ console.error(涯接失:,error]:}
async function initializeDApp(l {
sinpleStorageContract = new ethers.Contract(SIMPLE_STORAC
daocontrol. style.display = block ;
daoAddressDisplay.textContent - DAO_CONTRACT_ADDRESS;
aMalt updatestorageNumber(l:
aMait loadPraposals();
refreshStorageNumberBtn addEventListener('click', updateStora
async function updateStorageNumher() {
try(
currentContractNumber.textcontent =A中...';
10 1
const numher = await simpleStorageContract.getNumber(
currentcontractNunber,textContent = number.toString()
1.03
}catch [error){console.error(更薪数字失:,error]:} createProposalButton.addEventListener( click', createProposal async functlon createProposalll { const nunber = docunent.gctElenentById('proposalNunber'), If(!cescription 11 Inumber)<return; } 110
- -。=uauxsesesoo
try( const tx = await daoContract.createProposal(descript1 113 await tx.wait(); awalt loadProposals(): 116 }catch[errar){consple.errar(创建提案失败:,error):} async function loadProposals(1 119 proposalsListDiv.innerHTML =*<h3>现有提案</h3><p>载A中.. .< try { TZT const proposalcount = await daocontract.nextProposalI
1.22
. 2 Z if(proposalCount.isZeru())< proposalsListDiv.inerH proposalsListDiv.1nnerHTNL =<h3>现有提案</h3>; for (let 1 = 0; 1 < proposalcount.toNumber(); 1++) { 125 const p = await daocontract.proposals(i); const card - docunent.createElenent(*div');
1.27
card.classNane = 'proposal-card'; pies-gesodoud. = suenssels*pues 128 card.innerHTML = `ch4>提案 #$[p.id} S{p.executed 3 proposalsListoiv.appendchild (card): 130 document.querySelectorAll('.vote-btn').forEach(btn 132 documcnt.queryselectorAll('-cxecute-btn').forEach(btn 133 }catch (error){consale.error(加敏提系失:,error);} 135 async function handlevote(event) { const 1d = event.target.dataset.1d; 138 const statusEl = document-getElenentById( vote-status-${i 6ET 140 try { 142 const tx = awalt daocontract.vote(id, support); await tx.wait(); statusEl.textContent =‘授票成功!; 145 awalt loadProposals(); }catch (error){consale-error(*投票失致:,error); status
1. 465
148 async functian handleExecute(event) { const 1d = event.target.dataset.1d; 150 const statusEl = docunent-gctElencntById('vote-status-$(i 151 statusEl.textContent=“正在轨行提察...: 153 try{ Const tx = await daoContract.executeProposal(id); 155 await tx.wait(); x await loadProposals(); awalt updatestorageNunber(); }catch(error){consale.errar[=软行失:",error); statust 159 161 if (window.ethereun)( 163 window.ethereum.on(*chainchanged', () => Window. location. 165 </seripts x/bodys 166 </htmlx 15 7
第四步:完整交互测試流程
1.敏勤本地伺服器:在dao.html 橙案所在的目中打開终端,轨行python-m
http.server 8000.
2.防間DApp:用测宽器打http://localhast:8eee/dao.htnl.
建接包(幅户1):贴壁「还结到MctaHask:,便用幅户1道行还接。页面感成功加 鼓业示初始数字100 逾建提案:在「创建新提案:盈域,输入描述(例如将數字更新為777)和口标敏字 4. 777。贴「创建提案按翘在MetaMask中确蕊交易。
5.测试權限:回到Remix中的 SimpleStorage 合約實例,警试直接使用帐户1调用
setNunber 函数。此交易预期會失败,亚提示“Caller is not the owner",橙明檬 限已成功移交给DA0合約,外部帐户無法再直接修改。
6.投票(帐户1):在前莲页面,找到则創建的提案,贴型「投簧成票:亚用帐户1在
MetaMask 中确交易。
7.切换包蓝投票(帐户2):
c打 MetaMask,将需前幅户切换到帐户 2 0细页會白動剧新,再次點壁按訊重新速接罐包。 在同一個提案卡片上。再次贴聚投赞成票;,用幅户2在MetaMask中確記交易。 交易成功後,提案的票数愿更新為「2优成/日反到。
1.软行提案:
現在提案已满足“觉成票大於反對票:的德件。 贴擎「敦行提案按钮(使用概户1或2均可),亚在MetaMask中硬部交易。
1.额察最接结果:
交易成功後,提案卡片會示為(已救行)。 最間键的化是,頂部「simplestorage 状:区域的“目前存的款字」患自劲更新為 100, 交县调求 无史 DAD 控制血板 I&ATEAB交发
a 0 交易调求 88 n1 tzea H 88 2 888中 100
E 交县调求 8 5T6318000 I&ATEAB交置 36: 03001(
Ma:R28T8100
至此。已成功完成了一具借安全權限設置的DA0微提案、投票到最终键上敦行的完整治理流程。
原始排版图










