Ver3.0 ๐ณ๏ธ ๋ผ๋ผ๋ฒจ๋ก DAO ํฌํ ์์คํ ๊ตฌํํ๊ธฐ

๐ณ๏ธ ๋ผ๋ผ๋ฒจ๋ก DAO ํฌํ ์์คํ ๊ตฌํํ๊ธฐ
๐ฏ DAO ํฌํ ์์คํ , ์ ์ง๊ธ ์ฃผ๋ชฉ๋ฐ์๊น?
์์ฆ ๋ธ๋ก์ฒด์ธ ์ํ๊ณ์์ ๊ฐ์ฅ ํซํ ํค์๋ ์ค ํ๋๊ฐ ๋ฐ๋ก DAO(Decentralized Autonomous Organization)์ผ. ํ์ค์ํ ์์จ ์กฐ์ง์ด๋ผ๋ ๊ฑฐ์ฐฝํ ์ด๋ฆ์ด์ง๋ง, ์ฌ์ค ํต์ฌ์ ๊ฐ๋จํด. "์ค์์ ๊ถ๋ ฅ ์์ด๋ ๊ตฌ์ฑ์๋ค์ด ํฌ๋ช
ํ๊ฒ ์์ฌ๊ฒฐ์ ์ ํ ์ ์๋ ์์คํ
"์ด๋ผ๋ ๊ฑฐ์ง! ๐ช
์ ํต์ ์ธ ์กฐ์ง์์๋ CEO๋ ์ด์ฌํ๊ฐ ๋ชจ๋ ๊ฒฐ์ ์ ๋ด๋ฆฌ์์? ๊ทผ๋ฐ DAO๋ ๋ฌ๋ผ. ํ ํฐ์ ๋ณด์ ํ ๋ชจ๋ ๊ตฌ์ฑ์์ด ํฌํ๊ถ์ ๊ฐ์ง๊ณ , ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์๋์ผ๋ก ์คํํด. ์์ ๋ฏผ์ฃผ์ ์ด๋ฉด์๋ ํจ์จ์ ์ด์ง! ๐ช
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ํ๋ ์์ด. ์์ํ๊ฒ ๋ธ๋ก์ฒด์ธ๋ง์ผ๋ก DAO๋ฅผ ๊ตฌํํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋ณ๋ก์ผ. ๊ฐ์ค๋น๋ ๋น์ธ๊ณ , ์๋๋ ๋๋ฆฌ๊ณ , ์ผ๋ฐ ์ฌ์ฉ์๋ค์ด ์ ๊ทผํ๊ธฐ ์ด๋ ต๊ฑฐ๋ . ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ํ์ด๋ธ๋ฆฌ๋ ์ํคํ
์ฒ์ผ! ๋ธ๋ก์ฒด์ธ์ ํฌ๋ช
์ฑ๊ณผ ๋ณด์์ฑ์ ์ ์งํ๋ฉด์, ์น ์ ํ๋ฆฌ์ผ์ด์
์ ํธ๋ฆฌํจ์ ๋ํ๋ ๊ฑฐ์ง. ๐
โข MakerDAO: ์คํ ์ด๋ธ์ฝ์ธ DAI์ ๊ธ๋ฆฌ์ ๋ด๋ณด ๋น์จ์ ํฌํ๋ก ๊ฒฐ์
โข Uniswap: ํ๋กํ ์ฝ ์ ๊ทธ๋ ์ด๋์ ์์๋ฃ ์ ์ฑ ์ ์ปค๋ฎค๋ํฐ๊ฐ ๊ฒฐ์
โข Decentraland: ๊ฐ์์ธ๊ณ์ ์ ์ฑ ๊ณผ ๊ฐ๋ฐ ๋ฐฉํฅ์ ํ ํฐ ํ๋๋ค์ด ํฌํ
โข ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ๋ ํฅํ ๊ฑฐ๋ฒ๋์ค ํ ํฐ์ ๋์ ํ๋ฉด ์ฌ์ฉ์๋ค์ด ์ง์ ํ๋ซํผ ์ ์ฑ ์ ๊ฒฐ์ ํ ์ ์๊ฒ ์ง? ๐จ
๐๏ธ ์์คํ ์ํคํ ์ฒ ์ค๊ณํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ๋ผ๋ผ๋ฒจ๋ก DAO ํฌํ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๋จผ์ ์ ์ฒด ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋ ๊ฒ ์ค์ํด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์ ํฌ๊ฒ ์ธ ๊ฐ์ง ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋ผ. ๐ฏ
๐ 3-Tier ์ํคํ ์ฒ
์ฌ์ฉ์๊ฐ ์ง์ ์ํธ์์ฉํ๋ ๋ถ๋ถ์ด์ผ. ์ ์์๋ฅผ ๋ณด๊ณ , ํฌํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๋ UI๋ฅผ ์ ๊ณตํด. Web3.js๋ฅผ ํตํด ๋ฉํ๋ง์คํฌ ๊ฐ์ ์ง๊ฐ๊ณผ ์ฐ๊ฒฐ๋์ง. ๋ผ๋ผ๋ฒจ์ Blade ํ ํ๋ฆฟ๊ณผ Vue.js๋ฅผ ์กฐํฉํ๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค ์ ์์ด! ๐จ
๋น์ฆ๋์ค ๋ก์ง์ ํต์ฌ์ด์ผ. ์ฌ์ฉ์ ์ธ์ฆ, ์ ์์ ๊ด๋ฆฌ, ํฌํ ๋ฐ์ดํฐ ์บ์ฑ, ์๋ฆผ ๋ฐ์ก ๋ฑ์ ๋ด๋นํด. ๋ชจ๋ ํฌํ๋ฅผ ๋ธ๋ก์ฒด์ธ์ ์ง์ ๊ธฐ๋กํ๋ฉด ๋น์ฉ์ด ๋๋ฌด ๋น์ธ๋๊น, ์ค์ํ ๋ฐ์ดํฐ๋ง ์จ์ฒด์ธ์ ์ฌ๋ฆฌ๊ณ ๋๋จธ์ง๋ ์คํ์ฒด์ธ์์ ์ฒ๋ฆฌํ๋ ๊ฑฐ์ง. ๋ผ๋ผ๋ฒจ์ Eloquent ORM๊ณผ Queue ์์คํ ์ด ์ฌ๊ธฐ์ ๋น์ ๋ฐํด! โจ
ํฌ๋ช ์ฑ๊ณผ ๋ถ๋ณ์ฑ์ ๋ณด์ฅํ๋ ์ง์ค์ ์์ฒ์ด์ผ. ์ต์ข ํฌํ ๊ฒฐ๊ณผ, ์ ์์ ํด์, ์ค์ํ ๊ฑฐ๋ฒ๋์ค ๊ฒฐ์ ๋ค์ด ์ฌ๊ธฐ ๊ธฐ๋ก๋ผ. Solidity๋ก ์์ฑ๋ ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ํฌํ ๊ท์น์ ๊ฐ์ ํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ ์๋์ผ๋ก ์คํํด. ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์ ๋น์ธ๋๊น Polygon ๊ฐ์ L2 ์๋ฃจ์ ์ ์ฐ๋ ๊ฒ ํ๋ช ํด! ๐
๐ ๋ฐ์ดํฐ ํ๋ฆ ์ดํดํ๊ธฐ
ํฌํ ํ๋ก์ธ์ค๊ฐ ์ด๋ป๊ฒ ์งํ๋๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์:
1๋จ๊ณ: ์ ์์ ์์ฑ ๐
์ฌ์ฉ์๊ฐ ์ ์์๋ฅผ ์์ฑํ๋ฉด, ๋ผ๋ผ๋ฒจ ๋ฐฑ์๋๊ฐ MySQL์ ์ ์ฅํด. ๋์์ ์ ์์ ๋ด์ฉ์ ํด์๊ฐ์ ์์ฑํด์ IPFS์ ์
๋ก๋ํ๊ณ , ๊ทธ CID(Content Identifier)๋ฅผ ์ค๋งํธ ์ปจํธ๋ํธ์ ๊ธฐ๋กํด. ์ด๋ ๊ฒ ํ๋ฉด ์ ์์๊ฐ ์๋ณ์กฐ๋์ง ์์๋ค๋ ๊ฑธ ์ฆ๋ช
ํ ์ ์์ด! ๐
2๋จ๊ณ: ํฌํ ์งํ ๐ณ๏ธ
์ฌ์ฉ์๊ฐ ํฌํํ๋ฉด, ๋จผ์ ๋ผ๋ผ๋ฒจ์ด ํฌํ ์๊ฒฉ์ ๊ฒ์ฆํด. ํ ํฐ ๋ณด์ ๋, ์ด์ ํฌํ ์ฌ๋ถ, ์ ์์ ๊ธฐ๊ฐ ๋ฑ์ ์ฒดํฌํ๋ ๊ฑฐ์ง. ๊ฒ์ฆ์ด ์๋ฃ๋๋ฉด Web3.js๋ฅผ ํตํด ๋ฉํ๋ง์คํฌ๊ฐ ์ด๋ฆฌ๊ณ , ์ฌ์ฉ์๊ฐ ํธ๋์ญ์
์ ์๋ช
ํด. ์ด ํธ๋์ญ์
์ด ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋๋ฉด ํฌํ๊ฐ ์๋ฃ๋ผ! โ
3๋จ๊ณ: ๊ฒฐ๊ณผ ์ง๊ณ ๐
ํฌํ ๊ธฐ๊ฐ์ด ๋๋๋ฉด, ๋ผ๋ผ๋ฒจ์ ์ค์ผ์ค๋ฌ๊ฐ ์๋์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ์ง๊ณํด. ๋ธ๋ก์ฒด์ธ์์ ๋ชจ๋ ํฌํ ์ด๋ฒคํธ๋ฅผ ์ฝ์ด์์ MySQL์ ์บ์ฑํ๊ณ , ์ต์ข
๊ฒฐ๊ณผ๋ฅผ ์ค๋งํธ ์ปจํธ๋ํธ์ ๊ธฐ๋กํด. ์ด๋ Merkle Tree๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ค๋น๋ฅผ ํฌ๊ฒ ์ ์ฝํ ์ ์์ด! ๐ณ
4๋จ๊ณ: ์๋ ์คํ โก
์ ์์ด ํต๊ณผ๋๋ฉด, ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ๋ฏธ๋ฆฌ ์ ์๋ ์ก์
์ ์๋์ผ๋ก ์คํํด. ์๋ฅผ ๋ค์ด "ํ๋กํ ์ฝ ์์๋ฃ๋ฅผ 2%์์ 1.5%๋ก ๋ณ๊ฒฝ"์ด๋ผ๋ ์ ์์ด ํต๊ณผ๋๋ฉด, ์ปจํธ๋ํธ๊ฐ ์ฆ์ ์์๋ฃ ๋ณ์๋ฅผ ์
๋ฐ์ดํธํ๋ ๊ฑฐ์ง. ์ฌ๋์ ๊ฐ์
์ด ์ ํ ํ์ ์์ด! ๐ค
โ๏ธ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์ธํ ํ๊ธฐ
์, ์ด์ ์ค์ ๋ก ์ฝ๋๋ฅผ ์์ฑํด๋ณผ ์๊ฐ์ด์ผ! ๋จผ์ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ๋ฅผ ์ธํ ํ๊ณ ํ์ํ ํจํค์ง๋ค์ ์ค์นํ์. ๐ ๏ธ
๐ ํ๋ก์ ํธ ์ด๊ธฐํ
// Composer๋ก ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ ์์ฑ
composer create-project laravel/laravel dao-voting-system
cd dao-voting-system
// ํ์ ํจํค์ง ์ค์น
composer require web3p/web3.php
composer require kornrunner/keccak
composer require simplito/elliptic-php
composer require phpseclib/phpseclib
// ํ๋ก ํธ์๋ ์์กด์ฑ
npm install web3 ethers @metamask/detect-provider
npm install vue@next @vitejs/plugin-vue
๐ง ํ๊ฒฝ ์ค์
.env ํ์ผ์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํ์:
// .env ํ์ผ
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dao_voting
DB_USERNAME=root
DB_PASSWORD=
// ๋ธ๋ก์ฒด์ธ ์ค์
BLOCKCHAIN_NETWORK=polygon_mumbai
BLOCKCHAIN_RPC_URL=https://rpc-mumbai.maticvigil.com
BLOCKCHAIN_CHAIN_ID=80001
// ์ค๋งํธ ์ปจํธ๋ํธ ์ฃผ์
VOTING_CONTRACT_ADDRESS=0x...
GOVERNANCE_TOKEN_ADDRESS=0x...
// IPFS ์ค์
IPFS_GATEWAY=https://ipfs.io/ipfs/
IPFS_API_URL=https://api.pinata.cloud
// ๊ด๋ฆฌ์ ์ง๊ฐ (์๋ฒ ์ฌ์ด๋ ํธ๋์ญ์
์ฉ)
ADMIN_WALLET_ADDRESS=0x...
ADMIN_WALLET_PRIVATE_KEY=...
๐ฆ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
ํฌํ ์์คํ ์ ํ์ํ ํ ์ด๋ธ๋ค์ ๋ง๋ค์ด๋ณด์. ๋ผ๋ผ๋ฒจ์ ๋ง์ด๊ทธ๋ ์ด์ ์์คํ ์ ํ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ๋ฒ์ ๊ด๋ฆฌํ ์ ์์ด์ ์ ๋ง ํธํด! ๐๏ธ
// ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ
php artisan make:migration create_proposals_table
php artisan make:migration create_votes_table
php artisan make:migration create_vote_snapshots_table
๊ฐ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์์ฑํด๋ณด์:
// database/migrations/xxxx_create_proposals_table.php
public function up()
{
Schema::create('proposals', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->string('ipfs_hash')->nullable(); // IPFS CID
$table->string('blockchain_tx_hash')->nullable();
$table->unsignedBigInteger('proposal_id_onchain')->nullable();
$table->string('proposer_address', 42);
$table->enum('status', ['pending', 'active', 'passed', 'rejected', 'executed'])
->default('pending');
$table->timestamp('voting_starts_at');
$table->timestamp('voting_ends_at');
$table->decimal('quorum_required', 5, 2)->default(10.00); // 10%
$table->decimal('approval_threshold', 5, 2)->default(50.00); // 50%
$table->bigInteger('votes_for')->default(0);
$table->bigInteger('votes_against')->default(0);
$table->bigInteger('votes_abstain')->default(0);
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index('proposer_address');
$table->index('status');
$table->index(['voting_starts_at', 'voting_ends_at']);
});
}
// database/migrations/xxxx_create_votes_table.php
public function up()
{
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained()->onDelete('cascade');
$table->string('voter_address', 42);
$table->enum('vote_choice', ['for', 'against', 'abstain']);
$table->bigInteger('voting_power'); // ํ ํฐ ๋ณด์ ๋
$table->string('blockchain_tx_hash')->unique();
$table->unsignedBigInteger('block_number');
$table->timestamp('voted_at');
$table->string('signature')->nullable(); // ์คํ์ฒด์ธ ์๋ช
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['proposal_id', 'voter_address']);
$table->index('voter_address');
$table->index('blockchain_tx_hash');
});
}
// database/migrations/xxxx_create_vote_snapshots_table.php
public function up()
{
Schema::create('vote_snapshots', function (Blueprint $table) {
$table->id();
$table->foreignId('proposal_id')->constrained()->onDelete('cascade');
$table->string('voter_address', 42);
$table->bigInteger('token_balance');
$table->unsignedBigInteger('snapshot_block');
$table->timestamps();
$table->unique(['proposal_id', 'voter_address']);
$table->index('snapshot_block');
});
}
๋ง์ด๊ทธ๋ ์ด์ ์ ์คํํ์:
php artisan migrate
โข proposals ํ ์ด๋ธ: ์ ์์์ ๋ชจ๋ ์ ๋ณด๋ฅผ ์ ์ฅ. IPFS ํด์์ ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ํด์๋ฅผ ํจ๊ป ์ ์ฅํด์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํด.
โข votes ํ ์ด๋ธ: ๊ฐ๋ณ ํฌํ ๊ธฐ๋ก. ํ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์ ์์ ์ค๋ณต ํฌํํ์ง ๋ชปํ๋๋ก unique ์ ์ฝ์กฐ๊ฑด์ ๊ฑธ์์ด.
โข vote_snapshots ํ ์ด๋ธ: ํฌํ ์์ ์ ํ ํฐ ๋ณด์ ๋์ ๊ธฐ๋ก. ์ด๊ฒ ์์ผ๋ฉด ํฌํ ์ค์ ํ ํฐ์ ์ฌ๊ณ ํ์์ ํฌํ๊ถ์ ์กฐ์ํ ์ ์๊ฑฐ๋ ! ๐ธ
๐จ Eloquent ๋ชจ๋ธ ๋ง๋ค๊ธฐ
๋ผ๋ผ๋ฒจ์ Eloquent ORM์ ์ ๋ง ๊ฐ๋ ฅํด! ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ๊ฐ์ฒด์งํฅ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๊ฑฐ๋ . ์ฐ๋ฆฌ ํฌํ ์์คํ ์ ํ์ํ ๋ชจ๋ธ๋ค์ ๋ง๋ค์ด๋ณด์. ๐๏ธ
// ๋ชจ๋ธ ์์ฑ
php artisan make:model Proposal
php artisan make:model Vote
php artisan make:model VoteSnapshot
๐ Proposal ๋ชจ๋ธ
// app/Models/Proposal.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Carbon\Carbon;
class Proposal extends Model
{
use SoftDeletes;
protected $fillable = [
'title',
'description',
'ipfs_hash',
'blockchain_tx_hash',
'proposal_id_onchain',
'proposer_address',
'status',
'voting_starts_at',
'voting_ends_at',
'quorum_required',
'approval_threshold',
'votes_for',
'votes_against',
'votes_abstain',
'metadata'
];
protected $casts = [
'voting_starts_at' => 'datetime',
'voting_ends_at' => 'datetime',
'metadata' => 'array',
'quorum_required' => 'decimal:2',
'approval_threshold' => 'decimal:2'
];
// ๊ด๊ณ ์ ์
public function votes()
{
return $this->hasMany(Vote::class);
}
public function snapshots()
{
return $this->hasMany(VoteSnapshot::class);
}
// ํฌํ ๊ฐ๋ฅ ์ฌ๋ถ ์ฒดํฌ
public function isVotingActive(): bool
{
$now = Carbon::now();
return $this->status === 'active'
&& $now->between($this->voting_starts_at, $this->voting_ends_at);
}
// ์ ์กฑ์ ๋ฌ์ฑ ์ฌ๋ถ
public function hasReachedQuorum(): bool
{
$totalVotes = $this->votes_for + $this->votes_against + $this->votes_abstain;
$totalSupply = $this->getTotalTokenSupply();
if ($totalSupply == 0) return false;
$participationRate = ($totalVotes / $totalSupply) * 100;
return $participationRate >= $this->quorum_required;
}
// ์น์ธ ์ฌ๋ถ
public function isApproved(): bool
{
$totalVotes = $this->votes_for + $this->votes_against;
if ($totalVotes == 0) return false;
$approvalRate = ($this->votes_for / $totalVotes) * 100;
return $approvalRate >= $this->approval_threshold;
}
// ์ต์ข
๊ฒฐ๊ณผ ๊ณ์ฐ
public function calculateResult(): string
{
if (!$this->hasReachedQuorum()) {
return 'rejected'; // ์ ์กฑ์ ๋ฏธ๋ฌ
}
return $this->isApproved() ? 'passed' : 'rejected';
}
// ์ด ํ ํฐ ๊ณต๊ธ๋ ๊ฐ์ ธ์ค๊ธฐ (๋ธ๋ก์ฒด์ธ์์)
protected function getTotalTokenSupply(): int
{
// ์ด ๋ถ๋ถ์ ๋์ค์ Web3 ์๋น์ค๋ก ๊ตฌํ
return cache()->remember(
'total_token_supply',
3600,
fn() => app('App\Services\Web3Service')->getTotalSupply()
);
}
// Accessor: ์งํ๋ฅ
public function getProgressPercentageAttribute(): float
{
$now = Carbon::now();
if ($now->lt($this->voting_starts_at)) {
return 0;
}
if ($now->gt($this->voting_ends_at)) {
return 100;
}
$total = $this->voting_ends_at->diffInSeconds($this->voting_starts_at);
$elapsed = $now->diffInSeconds($this->voting_starts_at);
return ($elapsed / $total) * 100;
}
// Scope: ํ์ฑ ์ ์๋ง
public function scopeActive($query)
{
return $query->where('status', 'active')
->where('voting_starts_at', '<=', Carbon::now())
->where('voting_ends_at', '>=', Carbon::now());
}
// Scope: ์ข
๋ฃ๋ ์ ์
public function scopeEnded($query)
{
return $query->where('voting_ends_at', '<', Carbon::now())
->whereIn('status', ['active', 'pending']);
}
}
๐ณ๏ธ Vote ๋ชจ๋ธ
// app/Models/Vote.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Vote extends Model
{
protected $fillable = [
'proposal_id',
'voter_address',
'vote_choice',
'voting_power',
'blockchain_tx_hash',
'block_number',
'voted_at',
'signature',
'metadata'
];
protected $casts = [
'voted_at' => 'datetime',
'metadata' => 'array'
];
// ๊ด๊ณ
public function proposal()
{
return $this->belongsTo(Proposal::class);
}
// ํฌํ ์ ํ์ ์ด๋ชจ์ง๋ก ํ์
public function getVoteEmojiAttribute(): string
{
return match($this->vote_choice) {
'for' => 'โ
',
'against' => 'โ',
'abstain' => 'โธ๏ธ',
default => 'โ'
};
}
// ๋ธ๋ก์ฒด์ธ ํ์๊ธฐ ๋งํฌ
public function getExplorerUrlAttribute(): string
{
$network = config('blockchain.network');
$baseUrl = match($network) {
'polygon_mumbai' => 'https://mumbai.polygonscan.com',
'polygon_mainnet' => 'https://polygonscan.com',
'ethereum_mainnet' => 'https://etherscan.io',
default => 'https://etherscan.io'
};
return "{$baseUrl}/tx/{$this->blockchain_tx_hash}";
}
}
๐ธ VoteSnapshot ๋ชจ๋ธ
// app/Models/VoteSnapshot.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class VoteSnapshot extends Model
{
protected $fillable = [
'proposal_id',
'voter_address',
'token_balance',
'snapshot_block'
];
public function proposal()
{
return $this->belongsTo(Proposal::class);
}
// ํน์ ์ฃผ์์ ํฌํ๊ถ ์กฐํ
public static function getVotingPower(int $proposalId, string $address): int
{
$snapshot = static::where('proposal_id', $proposalId)
->where('voter_address', $address)
->first();
return $snapshot ? $snapshot->token_balance : 0;
}
}
โข Accessor & Mutator: ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋ ์ ์ฅํ ๋ ์๋์ผ๋ก ๋ณํ. getProgressPercentageAttribute() ๊ฐ์ ๋ฉ์๋๊ฐ ๊ทธ ์์์ผ.
โข Scope: ์์ฃผ ์ฌ์ฉํ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค์ด. Proposal::active()->get() ์ด๋ ๊ฒ ๊น๋ํ๊ฒ ์ธ ์ ์์ง!
โข ๊ด๊ณ ์ ์: hasMany, belongsTo ๊ฐ์ ๊ด๊ณ๋ฅผ ๋ช ํํ ์ ์ํ๋ฉด ์กฐ์ธ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค. ๐
๐ Web3 ์๋น์ค ๊ตฌ์ถํ๊ธฐ
์ด์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ํต์ฌ ์๋น์ค๋ฅผ ๋ง๋ค์ด๋ณด์! ๋ผ๋ผ๋ฒจ์ ์๋น์ค ํจํด์ ํ์ฉํ๋ฉด ๋น์ฆ๋์ค ๋ก์ง์ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ ์ ์์ด. ๐ฏ
โก Web3Service ํด๋์ค
// app/Services/Web3Service.php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Exception;
class Web3Service
{
protected $web3;
protected $eth;
protected $votingContract;
protected $tokenContract;
public function __construct()
{
$rpcUrl = config('blockchain.rpc_url');
$this->web3 = new Web3($rpcUrl);
$this->eth = $this->web3->eth;
// ์ค๋งํธ ์ปจํธ๋ํธ ์ด๊ธฐํ
$this->initializeContracts();
}
protected function initializeContracts()
{
// Voting Contract ABI ๋ก๋
$votingAbi = json_decode(
file_get_contents(storage_path('contracts/VotingContract.json')),
true
);
$this->votingContract = new Contract(
$this->web3->provider,
$votingAbi
);
$this->votingContract->at(config('blockchain.voting_contract_address'));
// Token Contract ์ด๊ธฐํ
$tokenAbi = json_decode(
file_get_contents(storage_path('contracts/GovernanceToken.json')),
true
);
$this->tokenContract = new Contract(
$this->web3->provider,
$tokenAbi
);
$this->tokenContract->at(config('blockchain.token_contract_address'));
}
// ํ ํฐ ์์ก ์กฐํ
public function getTokenBalance(string $address): int
{
$balance = 0;
$this->tokenContract->call('balanceOf', $address, function ($err, $result) use (&$balance) {
if ($err) {
throw new Exception("Failed to get token balance: " . $err->getMessage());
}
$balance = hexdec($result[0]->toString());
});
return $balance;
}
// ํน์ ๋ธ๋ก์ ํ ํฐ ์์ก ์กฐํ (์ค๋
์ท์ฉ)
public function getTokenBalanceAtBlock(string $address, int $blockNumber): int
{
// ERC20Snapshot ํ์ฅ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
$balance = 0;
$this->tokenContract->call(
'balanceOfAt',
$address,
'0x' . dechex($blockNumber),
function ($err, $result) use (&$balance) {
if ($err) {
throw new Exception("Failed to get historical balance: " . $err->getMessage());
}
$balance = hexdec($result[0]->toString());
}
);
return $balance;
}
// ์ด ํ ํฐ ๊ณต๊ธ๋
public function getTotalSupply(): int
{
$supply = 0;
$this->tokenContract->call('totalSupply', function ($err, $result) use (&$supply) {
if ($err) {
throw new Exception("Failed to get total supply: " . $err->getMessage());
}
$supply = hexdec($result[0]->toString());
});
return $supply;
}
// ์ ์์๋ฅผ ๋ธ๋ก์ฒด์ธ์ ์์ฑ
public function createProposal(array $proposalData): string
{
$privateKey = config('blockchain.admin_wallet_private_key');
$fromAddress = config('blockchain.admin_wallet_address');
// ํธ๋์ญ์
๋ฐ์ดํฐ ์ค๋น
$data = $this->votingContract->getData(
'createProposal',
$proposalData['ipfs_hash'],
$proposalData['voting_starts_at'],
$proposalData['voting_ends_at'],
$proposalData['quorum_required'],
$proposalData['approval_threshold']
);
// ๊ฐ์ค ์ถ์
$gasEstimate = $this->estimateGas($fromAddress, $data);
// ํธ๋์ญ์
์ ์ก
$txHash = $this->sendTransaction([
'from' => $fromAddress,
'to' => config('blockchain.voting_contract_address'),
'data' => $data,
'gas' => '0x' . dechex($gasEstimate * 1.2), // 20% ๋ฒํผ
'gasPrice' => $this->getGasPrice()
], $privateKey);
return $txHash;
}
// ํฌํ ํธ๋์ญ์
๊ฒ์ฆ
public function verifyVoteTransaction(string $txHash): array
{
$receipt = null;
$this->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
if ($err) {
throw new Exception("Failed to get transaction receipt: " . $err->getMessage());
}
$receipt = $result;
});
if (!$receipt || $receipt->status !== '0x1') {
throw new Exception("Transaction failed or not found");
}
// ์ด๋ฒคํธ ๋ก๊ทธ ํ์ฑ
$logs = $receipt->logs;
$voteData = $this->parseVoteEvent($logs);
return [
'proposal_id' => $voteData['proposalId'],
'voter' => $voteData['voter'],
'choice' => $voteData['choice'],
'voting_power' => $voteData['votingPower'],
'block_number' => hexdec($receipt->blockNumber)
];
}
// Vote ์ด๋ฒคํธ ํ์ฑ
protected function parseVoteEvent(array $logs): array
{
// Vote ์ด๋ฒคํธ ์๊ทธ๋์ฒ
$voteEventSignature = '0x' . hash('sha3-256', 'VoteCast(uint256,address,uint8,uint256)');
foreach ($logs as $log) {
if ($log->topics[0] === $voteEventSignature) {
return [
'proposalId' => hexdec($log->topics[1]),
'voter' => '0x' . substr($log->topics[2], -40),
'choice' => hexdec($log->data),
'votingPower' => hexdec(substr($log->data, 66))
];
}
}
throw new Exception("Vote event not found in transaction logs");
}
// ๊ฐ์ค ๊ฐ๊ฒฉ ์กฐํ
protected function getGasPrice(): string
{
$gasPrice = null;
$this->eth->gasPrice(function ($err, $result) use (&$gasPrice) {
if ($err) {
throw new Exception("Failed to get gas price: " . $err->getMessage());
}
$gasPrice = $result->toString();
});
return $gasPrice;
}
// ๊ฐ์ค ์ถ์
protected function estimateGas(string $from, string $data): int
{
$gas = 0;
$this->eth->estimateGas([
'from' => $from,
'to' => config('blockchain.voting_contract_address'),
'data' => $data
], function ($err, $result) use (&$gas) {
if ($err) {
throw new Exception("Failed to estimate gas: " . $err->getMessage());
}
$gas = hexdec($result->toString());
});
return $gas;
}
// ํธ๋์ญ์
์ ์ก (์๋ช
ํฌํจ)
protected function sendTransaction(array $tx, string $privateKey): string
{
// ๋
ผ์ค ๊ฐ์ ธ์ค๊ธฐ
$nonce = null;
$this->eth->getTransactionCount($tx['from'], 'pending', function ($err, $result) use (&$nonce) {
if ($err) {
throw new Exception("Failed to get nonce: " . $err->getMessage());
}
$nonce = $result->toString();
});
$tx['nonce'] = $nonce;
$tx['chainId'] = config('blockchain.chain_id');
// ํธ๋์ญ์
์๋ช
$signedTx = $this->signTransaction($tx, $privateKey);
// ์ ์ก
$txHash = null;
$this->eth->sendRawTransaction('0x' . $signedTx, function ($err, $result) use (&$txHash) {
if ($err) {
throw new Exception("Failed to send transaction: " . $err->getMessage());
}
$txHash = $result;
});
return $txHash;
}
// ํธ๋์ญ์
์๋ช
(๊ฐ๋จํ ๊ตฌํ)
protected function signTransaction(array $tx, string $privateKey): string
{
// ์ค์ ๋ก๋ ethereum-tx ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์
// ์ฌ๊ธฐ์๋ ๊ฐ๋
์ ์ธ ์ฝ๋๋ง ๋ณด์ฌ์ค๊ฒ
// RLP ์ธ์ฝ๋ฉ ํ ์๋ช
// ... (๋ณต์กํ ์๋ช
๋ก์ง)
return $signedTxHex;
}
// ํ์ฌ ๋ธ๋ก ๋ฒํธ
public function getCurrentBlockNumber(): int
{
$blockNumber = 0;
$this->eth->blockNumber(function ($err, $result) use (&$blockNumber) {
if ($err) {
throw new Exception("Failed to get block number: " . $err->getMessage());
}
$blockNumber = hexdec($result->toString());
});
return $blockNumber;
}
// ์ ์์ ์ต์ข
๊ฒฐ๊ณผ๋ฅผ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก
public function finalizeProposal(int $proposalId, string $result): string
{
$privateKey = config('blockchain.admin_wallet_private_key');
$fromAddress = config('blockchain.admin_wallet_address');
$data = $this->votingContract->getData(
'finalizeProposal',
$proposalId,
$result === 'passed' ? 1 : 0
);
$gasEstimate = $this->estimateGas($fromAddress, $data);
$txHash = $this->sendTransaction([
'from' => $fromAddress,
'to' => config('blockchain.voting_contract_address'),
'data' => $data,
'gas' => '0x' . dechex($gasEstimate * 1.2),
'gasPrice' => $this->getGasPrice()
], $privateKey);
return $txHash;
}
}
๐ฆ IPFS ์๋น์ค
์ ์์ ๋ด์ฉ์ IPFS์ ์ ์ฅํ๋ ์๋น์ค๋ ๋ง๋ค์ด๋ณด์. IPFS๋ ๋ถ์ฐ ํ์ผ ์์คํ ์ด๋ผ ๋ฐ์ดํฐ๊ฐ ์๊ตฌ์ ์ผ๋ก ๋ณด์กด๋ผ! ๐
// app/Services/IpfsService.php
namespace App\Services;
use GuzzleHttp\Client;
use Exception;
class IpfsService
{
protected $client;
protected $apiUrl;
protected $apiKey;
public function __construct()
{
$this->apiUrl = config('ipfs.api_url');
$this->apiKey = config('ipfs.api_key');
$this->client = new Client([
'base_uri' => $this->apiUrl,
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey
]
]);
}
// ์ ์์ ๋ฐ์ดํฐ๋ฅผ IPFS์ ์
๋ก๋
public function uploadProposal(array $proposalData): string
{
$jsonData = json_encode([
'title' => $proposalData['title'],
'description' => $proposalData['description'],
'proposer' => $proposalData['proposer_address'],
'created_at' => now()->toIso8601String(),
'metadata' => $proposalData['metadata'] ?? []
], JSON_PRETTY_PRINT);
try {
$response = $this->client->post('/pinning/pinJSONToIPFS', [
'json' => [
'pinataContent' => json_decode($jsonData),
'pinataMetadata' => [
'name' => 'Proposal: ' . $proposalData['title']
]
]
]);
$result = json_decode($response->getBody(), true);
return $result['IpfsHash'];
} catch (Exception $e) {
throw new Exception("Failed to upload to IPFS: " . $e->getMessage());
}
}
// IPFS์์ ์ ์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
public function getProposal(string $ipfsHash): array
{
try {
$response = $this->client->get(
config('ipfs.gateway') . $ipfsHash
);
return json_decode($response->getBody(), true);
} catch (Exception $e) {
throw new Exception("Failed to fetch from IPFS: " . $e->getMessage());
}
}
// ๋ฐ์ดํฐ ํด์ ๊ฒ์ฆ
public function verifyHash(array $data, string $expectedHash): bool
{
$jsonData = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$calculatedHash = hash('sha256', $jsonData);
return $calculatedHash === $expectedHash;
}
}
๐ฎ ์ปจํธ๋กค๋ฌ ๊ตฌํํ๊ธฐ
์ด์ ์ฌ์ฉ์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค์ด๋ณด์! ๋ผ๋ผ๋ฒจ์ ๋ฆฌ์์ค ์ปจํธ๋กค๋ฌ ํจํด์ ํ์ฉํ๋ฉด RESTful API๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด. ๐ฏ
๐ ProposalController
// app/Http/Controllers/ProposalController.php
namespace App\Http\Controllers;
use App\Models\Proposal;
use App\Models\VoteSnapshot;
use App\Services\Web3Service;
use App\Services\IpfsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
class ProposalController extends Controller
{
protected $web3Service;
protected $ipfsService;
public function __construct(Web3Service $web3Service, IpfsService $ipfsService)
{
$this->web3Service = $web3Service;
$this->ipfsService = $ipfsService;
}
// ์ ์์ ๋ชฉ๋ก ์กฐํ
public function index(Request $request)
{
$status = $request->query('status', 'all');
$perPage = $request->query('per_page', 20);
$query = Proposal::with('votes')
->orderBy('created_at', 'desc');
if ($status !== 'all') {
$query->where('status', $status);
}
$proposals = $query->paginate($perPage);
// ๊ฐ ์ ์์์ ์ถ๊ฐ ์ ๋ณด ์ฒจ๋ถ
$proposals->getCollection()->transform(function ($proposal) {
$proposal->total_votes = $proposal->votes_for +
$proposal->votes_against +
$proposal->votes_abstain;
$proposal->participation_rate = $this->calculateParticipationRate($proposal);
$proposal->approval_rate = $this->calculateApprovalRate($proposal);
return $proposal;
});
return response()->json([
'success' => true,
'data' => $proposals
]);
}
// ์ ์์ ์์ธ ์กฐํ
public function show($id)
{
$proposal = Proposal::with(['votes' => function ($query) {
$query->orderBy('voted_at', 'desc');
}])->findOrFail($id);
// IPFS์์ ์ ์ฒด ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
if ($proposal->ipfs_hash) {
try {
$ipfsData = $this->ipfsService->getProposal($proposal->ipfs_hash);
$proposal->full_content = $ipfsData;
} catch (\Exception $e) {
$proposal->full_content = null;
}
}
// ํฌํ ํต๊ณ
$proposal->vote_statistics = [
'total_votes' => $proposal->votes->count(),
'votes_for' => $proposal->votes_for,
'votes_against' => $proposal->votes_against,
'votes_abstain' => $proposal->votes_abstain,
'participation_rate' => $this->calculateParticipationRate($proposal),
'approval_rate' => $this->calculateApprovalRate($proposal),
'quorum_reached' => $proposal->hasReachedQuorum(),
'is_approved' => $proposal->isApproved()
];
return response()->json([
'success' => true,
'data' => $proposal
]);
}
// ์ ์ ์์ ์์ฑ
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'description' => 'required|string|min:100',
'proposer_address' => 'required|string|size:42',
'voting_duration_hours' => 'required|integer|min:24|max:720',
'quorum_required' => 'nullable|numeric|min:1|max:100',
'approval_threshold' => 'nullable|numeric|min:50|max:100',
'metadata' => 'nullable|array'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
// ์ ์์์ ํ ํฐ ๋ณด์ ๋ ํ์ธ
$proposerBalance = $this->web3Service->getTokenBalance(
$request->proposer_address
);
$minTokenRequired = config('dao.min_token_to_propose', 1000);
if ($proposerBalance < $minTokenRequired) {
return response()->json([
'success' => false,
'message' => "Insufficient tokens. You need at least {$minTokenRequired} tokens to create a proposal."
], 403);
}
DB::beginTransaction();
try {
// IPFS์ ์ ์์ ์
๋ก๋
$ipfsHash = $this->ipfsService->uploadProposal([
'title' => $request->title,
'description' => $request->description,
'proposer_address' => $request->proposer_address,
'metadata' => $request->metadata
]);
// ํฌํ ์์/์ข
๋ฃ ์๊ฐ ๊ณ์ฐ
$votingStartsAt = Carbon::now()->addHours(1); // 1์๊ฐ ํ ์์
$votingEndsAt = $votingStartsAt->copy()->addHours(
$request->voting_duration_hours
);
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์์ ์์ฑ
$proposal = Proposal::create([
'title' => $request->title,
'description' => $request->description,
'ipfs_hash' => $ipfsHash,
'proposer_address' => $request->proposer_address,
'status' => 'pending',
'voting_starts_at' => $votingStartsAt,
'voting_ends_at' => $votingEndsAt,
'quorum_required' => $request->quorum_required ?? 10,
'approval_threshold' => $request->approval_threshold ?? 50,
'metadata' => $request->metadata
]);
// ๋ธ๋ก์ฒด์ธ์ ์ ์์ ์์ฑ (๋น๋๊ธฐ ์ฒ๋ฆฌ)
dispatch(function () use ($proposal) {
try {
$txHash = $this->web3Service->createProposal([
'ipfs_hash' => $proposal->ipfs_hash,
'voting_starts_at' => $proposal->voting_starts_at->timestamp,
'voting_ends_at' => $proposal->voting_ends_at->timestamp,
'quorum_required' => $proposal->quorum_required,
'approval_threshold' => $proposal->approval_threshold
]);
$proposal->update([
'blockchain_tx_hash' => $txHash,
'status' => 'active'
]);
// ์ค๋
์ท ์์ฑ ์ค์ผ์ค๋ง
$this->scheduleSnapshotCreation($proposal);
} catch (\Exception $e) {
\Log::error("Failed to create proposal on blockchain: " . $e->getMessage());
$proposal->update(['status' => 'failed']);
}
})->afterResponse();
DB::commit();
return response()->json([
'success' => true,
'message' => 'Proposal created successfully',
'data' => $proposal
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to create proposal: ' . $e->getMessage()
], 500);
}
}
// ์ค๋
์ท ์์ฑ ์ค์ผ์ค๋ง
protected function scheduleSnapshotCreation(Proposal $proposal)
{
// ํฌํ ์์ ์์ ์ ๋ธ๋ก ๋ฒํธ์์ ์ค๋
์ท ์์ฑ
dispatch(function () use ($proposal) {
// ํฌํ ์์๊น์ง ๋๊ธฐ
sleep($proposal->voting_starts_at->diffInSeconds(now()));
$snapshotBlock = $this->web3Service->getCurrentBlockNumber();
// ๋ชจ๋ ํ ํฐ ํ๋์ ์์ก ์ค๋
์ท (์ค์ ๋ก๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์ฒ๋ฆฌ)
// ์ฌ๊ธฐ์๋ ๊ฐ๋
์ ์ธ ์ฝ๋๋ง ๋ณด์ฌ์ค๊ฒ
$proposal->update([
'metadata->snapshot_block' => $snapshotBlock
]);
})->delay($proposal->voting_starts_at);
}
// ์ฐธ์ฌ์จ ๊ณ์ฐ
protected function calculateParticipationRate(Proposal $proposal): float
{
$totalVotes = $proposal->votes_for +
$proposal->votes_against +
$proposal->votes_abstain;
$totalSupply = $this->web3Service->getTotalSupply();
if ($totalSupply == 0) return 0;
return round(($totalVotes / $totalSupply) * 100, 2);
}
// ์ฐฌ์ฑ๋ฅ ๊ณ์ฐ
protected function calculateApprovalRate(Proposal $proposal): float
{
$totalVotes = $proposal->votes_for + $proposal->votes_against;
if ($totalVotes == 0) return 0;
return round(($proposal->votes_for / $totalVotes) * 100, 2);
}
}
๐ณ๏ธ VoteController
// app/Http/Controllers/VoteController.php
namespace App\Http\Controllers;
use App\Models\Proposal;
use App\Models\Vote;
use App\Models\VoteSnapshot;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class VoteController extends Controller
{
protected $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
// ํฌํํ๊ธฐ
public function store(Request $request, $proposalId)
{
$validator = Validator::make($request->all(), [
'voter_address' => 'required|string|size:42',
'vote_choice' => 'required|in:for,against,abstain',
'transaction_hash' => 'required|string|size:66',
'signature' => 'nullable|string'
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$proposal = Proposal::findOrFail($proposalId);
// ํฌํ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
if (!$proposal->isVotingActive()) {
return response()->json([
'success' => false,
'message' => 'Voting is not active for this proposal'
], 403);
}
// ์ค๋ณต ํฌํ ํ์ธ
$existingVote = Vote::where('proposal_id', $proposalId)
->where('voter_address', $request->voter_address)
->first();
if ($existingVote) {
return response()->json([
'success' => false,
'message' => 'You have already voted on this proposal'
], 403);
}
try {
// ๋ธ๋ก์ฒด์ธ์์ ํธ๋์ญ์
๊ฒ์ฆ
$txData = $this->web3Service->verifyVoteTransaction(
$request->transaction_hash
);
// ํธ๋์ญ์
๋ฐ์ดํฐ์ ์์ฒญ ๋ฐ์ดํฐ ์ผ์น ํ์ธ
if ($txData['proposal_id'] != $proposalId ||
strtolower($txData['voter']) !== strtolower($request->voter_address)) {
return response()->json([
'success' => false,
'message' => 'Transaction data does not match request'
], 400);
}
// ํฌํ๊ถ ํ์ธ (์ค๋
์ท ๊ธฐ๋ฐ)
$votingPower = VoteSnapshot::getVotingPower(
$proposalId,
$request->voter_address
);
if ($votingPower == 0) {
// ์ค๋
์ท์ด ์์ผ๋ฉด ํ์ฌ ์์ก์ผ๋ก ์์ฑ
$votingPower = $this->web3Service->getTokenBalance(
$request->voter_address
);
if ($votingPower == 0) {
return response()->json([
'success' => false,
'message' => 'You do not have voting power'
], 403);
}
// ์ค๋
์ท ์์ฑ
VoteSnapshot::create([
'proposal_id' => $proposalId,
'voter_address' => $request->voter_address,
'token_balance' => $votingPower,
'snapshot_block' => $txData['block_number']
]);
}
// ํฌํ ๊ธฐ๋ก ์์ฑ
$vote = Vote::create([
'proposal_id' => $proposalId,
'voter_address' => $request->voter_address,
'vote_choice' => $request->vote_choice,
'voting_power' => $votingPower,
'blockchain_tx_hash' => $request->transaction_hash,
'block_number' => $txData['block_number'],
'voted_at' => now(),
'signature' => $request->signature
]);
// ์ ์์ ํฌํ ์นด์ดํธ ์
๋ฐ์ดํธ
$this->updateProposalVotes($proposal, $request->vote_choice, $votingPower);
return response()->json([
'success' => true,
'message' => 'Vote recorded successfully',
'data' => $vote
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to record vote: ' . $e->getMessage()
], 500);
}
}
// ์ ์์ ํฌํ ์นด์ดํธ ์
๋ฐ์ดํธ
protected function updateProposalVotes(Proposal $proposal, string $choice, int $power)
{
switch ($choice) {
case 'for':
$proposal->increment('votes_for', $power);
break;
case 'against':
$proposal->increment('votes_against', $power);
break;
case 'abstain':
$proposal->increment('votes_abstain', $power);
break;
}
}
// ํน์ ์ ์์์ ํฌํ ๋ชฉ๋ก
public function index($proposalId)
{
$proposal = Proposal::findOrFail($proposalId);
$votes = Vote::where('proposal_id', $proposalId)
->orderBy('voted_at', 'desc')
->paginate(50);
return response()->json([
'success' => true,
'data' => $votes
]);
}
// ํน์ ์ฃผ์์ ํฌํ ๋ด์ญ
public function userVotes(Request $request)
{
$address = $request->query('address');
if (!$address) {
return response()->json([
'success' => false,
'message' => 'Address parameter is required'
], 400);
}
$votes = Vote::with('proposal')
->where('voter_address', $address)
->orderBy('voted_at', 'desc')
->paginate(20);
return response()->json([
'success' => true,
'data' => $votes
]);
}
// ํฌํ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
public function checkEligibility($proposalId, Request $request)
{
$address = $request->query('address');
if (!$address) {
return response()->json([
'success' => false,
'message' => 'Address parameter is required'
], 400);
}
$proposal = Proposal::findOrFail($proposalId);
// ํฌํ ๊ธฐ๊ฐ ํ์ธ
if (!$proposal->isVotingActive()) {
return response()->json([
'success' => false,
'eligible' => false,
'reason' => 'Voting period has ended or not started yet'
]);
}
// ์ค๋ณต ํฌํ ํ์ธ
$hasVoted = Vote::where('proposal_id', $proposalId)
->where('voter_address', $address)
->exists();
if ($hasVoted) {
return response()->json([
'success' => false,
'eligible' => false,
'reason' => 'You have already voted'
]);
}
// ํฌํ๊ถ ํ์ธ
$votingPower = VoteSnapshot::getVotingPower($proposalId, $address);
if ($votingPower == 0) {
$votingPower = $this->web3Service->getTokenBalance($address);
}
if ($votingPower == 0) {
return response()->json([
'success' => false,
'eligible' => false,
'reason' => 'You do not have any voting power'
]);
}
return response()->json([
'success' => true,
'eligible' => true,
'voting_power' => $votingPower
]);
}
}
โข ๊ฒ์ฆ ๋ ์ด์ด: ๋ชจ๋ ์ ๋ ฅ๊ฐ์ ์ฒ ์ ํ ๊ฒ์ฆํด. ๋ธ๋ก์ฒด์ธ ์ฃผ์ ํ์, ํธ๋์ญ์ ํด์ ๋ฑ์ ์ฒดํฌํ๋ ๊ฑฐ์ง.
โข ๋น๋๊ธฐ ์ฒ๋ฆฌ: ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋๊น dispatch()->afterResponse()๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌํด.
โข ์๋ฌ ํธ๋ค๋ง: ๋ธ๋ก์ฒด์ธ ํต์ ์ ์คํจํ ์ ์์ผ๋ try-catch๋ก ๊ฐ์ธ๊ณ ๋ช ํํ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ฐํํด. ๐ก๏ธ
โฐ ์ค์ผ์ค๋ฌ์ ํ ์์ ์ค์ ํ๊ธฐ
DAO ํฌํ ์์คํ ์ ์๋ํ๊ฐ ์๋ช ์ด์ผ! ํฌํ ๊ธฐ๊ฐ ์ข ๋ฃ, ๊ฒฐ๊ณผ ์ง๊ณ, ์ค๋ ์ท ์์ฑ ๋ฑ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ๊ฑฐ๋ . ๋ผ๋ผ๋ฒจ์ Task Scheduling๊ณผ Queue ์์คํ ์ ํ์ฉํด๋ณด์. โ๏ธ
๐ ์ค์ผ์ค๋ฌ ์ค์
// app/Console/Kernel.php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// ๋งค 5๋ถ๋ง๋ค ์ข
๋ฃ๋ ์ ์์ ํ์ธ ๋ฐ ๊ฒฐ๊ณผ ์ง๊ณ
$schedule->command('dao:finalize-proposals')
->everyFiveMinutes()
->withoutOverlapping();
// ๋งค ์๊ฐ๋ง๋ค ํ์ฑ ์ ์์ ์ํ ๋๊ธฐํ
$schedule->command('dao:sync-proposals')
->hourly()
->withoutOverlapping();
// ๋งค์ผ ์์ ์ ํต๊ณ ์
๋ฐ์ดํธ
$schedule->command('dao:update-statistics')
->daily()
->at('00:00');
// ๋งค 10๋ถ๋ง๋ค ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋
$schedule->command('dao:listen-events')
->everyTenMinutes()
->withoutOverlapping();
}
}
๐จ Artisan ์ปค๋งจ๋ ๊ตฌํ
// app/Console/Commands/FinalizeProposals.php
namespace App\Console\Commands;
use App\Models\Proposal;
use App\Services\Web3Service;
use Illuminate\Console\Command;
use Carbon\Carbon;
class FinalizeProposals extends Command
{
protected $signature = 'dao:finalize-proposals';
protected $description = 'Finalize ended proposals and record results on blockchain';
protected $web3Service;
public function __construct(Web3Service $web3Service)
{
parent::__construct();
$this->web3Service = $web3Service;
}
public function handle()
{
$this->info('Checking for proposals to finalize...');
// ์ข
๋ฃ๋์์ง๋ง ์์ง ์ต์ข
ํ๋์ง ์์ ์ ์์๋ค
$proposals = Proposal::ended()
->whereIn('status', ['active', 'pending'])
->get();
if ($proposals->isEmpty()) {
$this->info('No proposals to finalize.');
return 0;
}
$this->info("Found {$proposals->count()} proposals to finalize.");
foreach ($proposals as $proposal) {
try {
$this->info("Finalizing proposal #{$proposal->id}: {$proposal->title}");
// ์ต์ข
๊ฒฐ๊ณผ ๊ณ์ฐ
$result = $proposal->calculateResult();
$this->info("Result: {$result}");
$this->info("Votes For: {$proposal->votes_for}");
$this->info("Votes Against: {$proposal->votes_against}");
$this->info("Votes Abstain: {$proposal->votes_abstain}");
// ๋ธ๋ก์ฒด์ธ์ ๊ฒฐ๊ณผ ๊ธฐ๋ก
if ($proposal->proposal_id_onchain) {
$txHash = $this->web3Service->finalizeProposal(
$proposal->proposal_id_onchain,
$result
);
$this->info("Blockchain transaction: {$txHash}");
}
// ์ํ ์
๋ฐ์ดํธ
$proposal->update([
'status' => $result,
'metadata->finalized_at' => Carbon::now()->toIso8601String()
]);
$this->info("โ
Proposal #{$proposal->id} finalized successfully.\n");
} catch (\Exception $e) {
$this->error("โ Failed to finalize proposal #{$proposal->id}: " . $e->getMessage());
continue;
}
}
$this->info('Finalization complete!');
return 0;
}
}
// app/Console/Commands/SyncProposals.php
namespace App\Console\Commands;
use App\Models\Proposal;
use App\Models\Vote;
use App\Services\Web3Service;
use Illuminate\Console\Command;
class SyncProposals extends Command
{
protected $signature = 'dao:sync-proposals';
protected $description = 'Sync proposal data with blockchain';
protected $web3Service;
public function __construct(Web3Service $web3Service)
{
parent::__construct();
$this->web3Service = $web3Service;
}
public function handle()
{
$this->info('Syncing proposals with blockchain...');
$activeProposals = Proposal::active()->get();
if ($activeProposals->isEmpty()) {
$this->info('No active proposals to sync.');
return 0;
}
foreach ($activeProposals as $proposal) {
try {
$this->info("Syncing proposal #{$proposal->id}");
// ๋ธ๋ก์ฒด์ธ์์ ์ต์ ํฌํ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
// (์ค์ ๋ก๋ ์ด๋ฒคํธ ๋ก๊ทธ๋ฅผ ํ์ฑํด์ ๊ฐ์ ธ์์ผ ํด)
// ์ฌ๊ธฐ์๋ ๊ฐ๋
์ ์ธ ์ฝ๋๋ง
$onchainData = $this->web3Service->getProposalData(
$proposal->proposal_id_onchain
);
// ๋ฐ์ดํฐ ๋๊ธฐํ
if ($onchainData) {
$proposal->update([
'votes_for' => $onchainData['votesFor'],
'votes_against' => $onchainData['votesAgainst'],
'votes_abstain' => $onchainData['votesAbstain']
]);
$this->info("โ
Synced proposal #{$proposal->id}");
}
} catch (\Exception $e) {
$this->error("โ Failed to sync proposal #{$proposal->id}: " . $e->getMessage());
continue;
}
}
$this->info('Sync complete!');
return 0;
}
}
๐ฌ ํ ์์ (Jobs)
// app/Jobs/ProcessVoteJob.php
namespace App\Jobs;
use App\Models\Proposal;
use App\Models\Vote;
use App\Services\Web3Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessVoteJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 120;
protected $voteData;
public function __construct(array $voteData)
{
$this->voteData = $voteData;
}
public function handle(Web3Service $web3Service)
{
try {
// ํธ๋์ญ์
๊ฒ์ฆ
$txData = $web3Service->verifyVoteTransaction(
$this->voteData['transaction_hash']
);
// ํฌํ ๊ธฐ๋ก ์์ฑ
$vote = Vote::create([
'proposal_id' => $this->voteData['proposal_id'],
'voter_address' => $this->voteData['voter_address'],
'vote_choice' => $this->voteData['vote_choice'],
'voting_power' => $txData['voting_power'],
'blockchain_tx_hash' => $this->voteData['transaction_hash'],
'block_number' => $txData['block_number'],
'voted_at' => now()
]);
// ์ ์์ ์นด์ดํธ ์
๋ฐ์ดํธ
$proposal = Proposal::find($this->voteData['proposal_id']);
switch ($this->voteData['vote_choice']) {
case 'for':
$proposal->increment('votes_for', $txData['voting_power']);
break;
case 'against':
$proposal->increment('votes_against', $txData['voting_power']);
break;
case 'abstain':
$proposal->increment('votes_abstain', $txData['voting_power']);
break;
}
\Log::info("Vote processed successfully", [
'vote_id' => $vote->id,
'proposal_id' => $proposal->id
]);
} catch (\Exception $e) {
\Log::error("Failed to process vote", [
'error' => $e->getMessage(),
'vote_data' => $this->voteData
]);
throw $e; // ์ฌ์๋๋ฅผ ์ํด ์์ธ๋ฅผ ๋ค์ ๋์ง
}
}
public function failed(\Throwable $exception)
{
// ์์
์ด ์์ ํ ์คํจํ์ ๋ ์ฒ๋ฆฌ
\Log::error("Vote processing failed permanently", [
'error' => $exception->getMessage(),
'vote_data' => $this->voteData
]);
// ๊ด๋ฆฌ์์๊ฒ ์๋ฆผ ๋ฐ์ก ๋ฑ
}
}
// app/Jobs/CreateSnapshotJob.php
namespace App\Jobs;
use App\Models\Proposal;
use App\Models\VoteSnapshot;
use App\Services\Web3Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CreateSnapshotJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $proposal;
public function __construct(Proposal $proposal)
{
$this->proposal = $proposal;
}
public function handle(Web3Service $web3Service)
{
try {
$snapshotBlock = $web3Service->getCurrentBlockNumber();
// ๋ชจ๋ ํ ํฐ ํ๋ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
// (์ค์ ๋ก๋ Transfer ์ด๋ฒคํธ๋ฅผ ํ์ฑํด์ ํ๋ ๋ชฉ๋ก์ ๋ง๋ค์ด์ผ ํด)
$holders = $this->getTokenHolders();
foreach ($holders as $holder) {
$balance = $web3Service->getTokenBalanceAtBlock(
$holder,
$snapshotBlock
);
if ($balance > 0) {
VoteSnapshot::create([
'proposal_id' => $this->proposal->id,
'voter_address' => $holder,
'token_balance' => $balance,
'snapshot_block' => $snapshotBlock
]);
}
}
$this->proposal->update([
'metadata->snapshot_block' => $snapshotBlock,
'metadata->snapshot_created_at' => now()->toIso8601String()
]);
\Log::info("Snapshot created for proposal #{$this->proposal->id}", [
'block' => $snapshotBlock,
'holders_count' => count($holders)
]);
} catch (\Exception $e) {
\Log::error("Failed to create snapshot", [
'proposal_id' => $this->proposal->id,
'error' => $e->getMessage()
]);
throw $e;
}
}
protected function getTokenHolders(): array
{
// ์บ์๋ ํ๋ ๋ชฉ๋ก ๋ฐํ
// ์ค์ ๋ก๋ ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ํ์ฑํด์ ๋ง๋ค์ด์ผ ํด
return cache()->remember('token_holders', 3600, function () {
// Transfer ์ด๋ฒคํธ ํ์ฑ ๋ก์ง
return [];
});
}
}
๐จ ํ๋ก ํธ์๋ ๊ตฌํํ๊ธฐ
์ด์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ฌ์ฉํ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค์ด๋ณด์! Vue.js์ Web3.js๋ฅผ ์กฐํฉํ๋ฉด ์ ๋ง ๋ฉ์ง DApp์ ๋ง๋ค ์ ์์ด. ๐ผ๏ธ
๐ Web3 ์ฐ๊ฒฐ ์ปดํฌ๋ํธ
// resources/js/components/WalletConnect.vue
<template>
<div class="wallet-connect">
<div v-if="!isConnected" class="connect-section">
<button @click="connectWallet" class="connect-btn">
๐ฆ Connect Wallet
</button>
<p class="hint">Connect your MetaMask to participate in DAO voting</p>
</div>
<div v-else class="connected-section">
<div class="wallet-info">
<span class="address">{{ shortAddress }}</span>
<span class="balance">{{ tokenBalance }} GOV</span>
</div>
<button @click="disconnectWallet" class="disconnect-btn">
Disconnect
</button>
</div>
<div v-if="error" class="error-message">
โ ๏ธ {{ error }}
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import detectEthereumProvider from '@metamask/detect-provider';
import Web3 from 'web3';
export default {
name: 'WalletConnect',
setup() {
const isConnected = ref(false);
const account = ref(null);
const tokenBalance = ref(0);
const error = ref(null);
const web3 = ref(null);
const shortAddress = computed(() => {
if (!account.value) return '';
return `${account.value.slice(0, 6)}...${account.value.slice(-4)}`;
});
const connectWallet = async () => {
try {
error.value = null;
const provider = await detectEthereumProvider();
if (!provider) {
error.value = 'Please install MetaMask!';
return;
}
web3.value = new Web3(provider);
// ๊ณ์ ์ฐ๊ฒฐ ์์ฒญ
const accounts = await provider.request({
method: 'eth_requestAccounts'
});
account.value = accounts[0];
isConnected.value = true;
// ๋คํธ์ํฌ ํ์ธ
const chainId = await web3.value.eth.getChainId();
const expectedChainId = import.meta.env.VITE_CHAIN_ID;
if (chainId !== parseInt(expectedChainId)) {
await switchNetwork();
}
// ํ ํฐ ์์ก ์กฐํ
await fetchTokenBalance();
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก
provider.on('accountsChanged', handleAccountsChanged);
provider.on('chainChanged', handleChainChanged);
} catch (err) {
console.error('Failed to connect wallet:', err);
error.value = err.message;
}
};
const disconnectWallet = () => {
isConnected.value = false;
account.value = null;
tokenBalance.value = 0;
};
const switchNetwork = async () => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x' + parseInt(import.meta.env.VITE_CHAIN_ID).toString(16) }]
});
} catch (switchError) {
// ๋คํธ์ํฌ๊ฐ ์์ผ๋ฉด ์ถ๊ฐ
if (switchError.code === 4902) {
await addNetwork();
}
}
};
const addNetwork = async () => {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x' + parseInt(import.meta.env.VITE_CHAIN_ID).toString(16),
chainName: 'Polygon Mumbai Testnet',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
rpcUrls: ['https://rpc-mumbai.maticvigil.com'],
blockExplorerUrls: ['https://mumbai.polygonscan.com']
}]
});
};
const fetchTokenBalance = async () => {
if (!web3.value || !account.value) return;
const tokenContract = new web3.value.eth.Contract(
JSON.parse(import.meta.env.VITE_TOKEN_ABI),
import.meta.env.VITE_TOKEN_ADDRESS
);
const balance = await tokenContract.methods.balanceOf(account.value).call();
tokenBalance.value = web3.value.utils.fromWei(balance, 'ether');
};
const handleAccountsChanged = (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
account.value = accounts[0];
fetchTokenBalance();
}
};
const handleChainChanged = () => {
window.location.reload();
};
onMounted(async () => {
// ์ด์ ์ ์ฐ๊ฒฐํ์๋์ง ํ์ธ
const provider = await detectEthereumProvider();
if (provider && provider.selectedAddress) {
await connectWallet();
}
});
return {
isConnected,
account,
tokenBalance,
error,
shortAddress,
connectWallet,
disconnectWallet
};
}
};
</script>
<style scoped>
.wallet-connect {
padding: 20px;
}
.connect-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.connect-btn:hover {
transform: translateY(-2px);
}
.connected-section {
display: flex;
align-items: center;
gap: 15px;
}
.wallet-info {
background: #f5f7fa;
padding: 10px 15px;
border-radius: 8px;
display: flex;
gap: 10px;
}
.error-message {
margin-top: 10px;
padding: 10px;
background: #fee;
color: #c33;
border-radius: 6px;
}
</style>
๐ณ๏ธ ํฌํ ์ปดํฌ๋ํธ
// resources/js/components/VoteProposal.vue
<template>
<div class="vote-proposal">
<div class="proposal-header">
<h2>{{ proposal.title }}</h2>
<span class="status-badge" :class="proposal.status">
{{ proposal.status }}
</span>
</div>
<div class="proposal-content">
<p>{{ proposal.description }}</p>
</div>
<div class="voting-stats">
<div class="stat-item">
<span class="label">Voting Power:</span>
<span class="value">{{ votingPower }} GOV</span>
</div>
<div class="stat-item">
<span class="label">Ends in:</span>
<span class="value">{{ timeRemaining }}</span>
</div>
</div>
<div class="vote-results">
<div class="result-bar">
<div class="bar-segment for" :style="{ width: forPercentage + '%' }">
<span>โ
For: {{ forPercentage }}%</span>
</div>
<div class="bar-segment against" :style="{ width: againstPercentage + '%' }">
<span>โ Against: {{ againstPercentage }}%</span>
</div>
</div>
<div class="vote-counts">
<div>For: {{ proposal.votes_for }}</div>
<div>Against: {{ proposal.votes_against }}</div>
<div>Abstain: {{ proposal.votes_abstain }}</div>
</div>
</div>
<div v-if="canVote" class="vote-actions">
<button
@click="vote('for')"
class="vote-btn for"
:disabled="isVoting"
>
โ
Vote For
</button>
<button
@click="vote('against')"
class="vote-btn against"
:disabled="isVoting"
>
โ Vote Against
</button>
<button
@click="vote('abstain')"
class="vote-btn abstain"
:disabled="isVoting"
>
โธ๏ธ Abstain
</button>
</div>
<div v-if="hasVoted" class="voted-message">
โ
You have already voted on this proposal
</div>
<div v-if="isVoting" class="loading-overlay">
<div class="spinner"></div>
<p>Processing your vote...</p>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import Web3 from 'web3';
export default {
name: 'VoteProposal',
props: {
proposalId: {
type: Number,
required: true
},
walletAddress: {
type: String,
required: true
}
},
setup(props) {
const proposal = ref(null);
const votingPower = ref(0);
const canVote = ref(false);
const hasVoted = ref(false);
const isVoting = ref(false);
const web3 = ref(null);
const forPercentage = computed(() => {
if (!proposal.value) return 0;
const total = proposal.value.votes_for + proposal.value.votes_against;
if (total === 0) return 0;
return Math.round((proposal.value.votes_for / total) * 100);
});
const againstPercentage = computed(() => {
return 100 - forPercentage.value;
});
const timeRemaining = computed(() => {
if (!proposal.value) return '';
const end = new Date(proposal.value.voting_ends_at);
const now = new Date();
const diff = end - now;
if (diff <= 0) return 'Ended';
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
return `${days}d ${hours}h`;
});
const fetchProposal = async () => {
try {
const response = await axios.get(`/api/proposals/${props.proposalId}`);
proposal.value = response.data.data;
} catch (error) {
console.error('Failed to fetch proposal:', error);
}
};
const checkEligibility = async () => {
try {
const response = await axios.get(
`/api/proposals/${props.proposalId}/check-eligibility`,
{ params: { address: props.walletAddress } }
);
if (response.data.eligible) {
canVote.value = true;
votingPower.value = response.data.voting_power;
} else {
canVote.value = false;
if (response.data.reason.includes('already voted')) {
hasVoted.value = true;
}
}
} catch (error) {
console.error('Failed to check eligibility:', error);
}
};
const vote = async (choice) => {
if (!canVote.value || isVoting.value) return;
try {
isVoting.value = true;
// Web3 ์ด๊ธฐํ
web3.value = new Web3(window.ethereum);
// ์ค๋งํธ ์ปจํธ๋ํธ ์ธ์คํด์ค
const votingContract = new web3.value.eth.Contract(
JSON.parse(import.meta.env.VITE_VOTING_ABI),
import.meta.env.VITE_VOTING_ADDRESS
);
// ํฌํ ์ ํ์ ์ซ์๋ก ๋ณํ (0: for, 1: against, 2: abstain)
const voteChoice = choice === 'for' ? 0 : choice === 'against' ? 1 : 2;
// ํธ๋์ญ์
์ ์ก
const tx = await votingContract.methods
.castVote(proposal.value.proposal_id_onchain, voteChoice)
.send({ from: props.walletAddress });
console.log('Transaction hash:', tx.transactionHash);
// ๋ฐฑ์๋์ ํฌํ ๊ธฐ๋ก
await axios.post(`/api/proposals/${props.proposalId}/vote`, {
voter_address: props.walletAddress,
vote_choice: choice,
transaction_hash: tx.transactionHash
});
// ์ํ ์
๋ฐ์ดํธ
hasVoted.value = true;
canVote.value = false;
// ์ ์์ ๋ฐ์ดํฐ ์๋ก๊ณ ์นจ
await fetchProposal();
alert('โ
Your vote has been recorded successfully!');
} catch (error) {
console.error('Failed to vote:', error);
alert('โ Failed to vote: ' + error.message);
} finally {
isVoting.value = false;
}
};
onMounted(async () => {
await fetchProposal();
if (props.walletAddress) {
await checkEligibility();
}
});
return {
proposal,
votingPower,
canVote,
hasVoted,
isVoting,
forPercentage,
againstPercentage,
timeRemaining,
vote
};
}
};
</script>
<style scoped>
.vote-proposal {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.proposal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
.status-badge.active {
background: #e8f5e9;
color: #2e7d32;
}
.vote-results {
margin: 30px 0;
}
.result-bar {
display: flex;
height: 40px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
}
.bar-segment {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
transition: width 0.3s ease;
}
.bar-segment.for {
background: #66bb6a;
}
.bar-segment.against {
background: #ef5350;
}
.vote-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.vote-btn {
flex: 1;
padding: 15px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.vote-btn:hover:not(:disabled) {
transform: translateY(-2px);
}
.vote-btn.for {
background: #66bb6a;
color: white;
}
.vote-btn.against {
background: #ef5350;
color: white;
}
.vote-btn.abstain {
background: #9e9e9e;
color: white;
}
.vote-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
z-index: 1000;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
โข ์ํ ๊ด๋ฆฌ: ๋ณต์กํ ์ฑ์ด๋ผ๋ฉด Vuex๋ Pinia ๊ฐ์ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์.
โข ์๋ฌ ํธ๋ค๋ง: ๋ฉํ๋ง์คํฌ ๊ฑฐ๋ถ, ๋คํธ์ํฌ ์ค๋ฅ ๋ฑ ๋ค์ํ ์๋ฌ ์ํฉ์ ๊ณ ๋ คํด์ผ ํด.
โข UX ๊ฐ์ : ํธ๋์ญ์ ์ด ์ฒ๋ฆฌ๋๋ ๋์ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , ์ฑ๊ณต/์คํจ ํผ๋๋ฐฑ์ ๋ช ํํ ํด์ค. ๐ฏ
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
DAO ํฌํ ์์คํ ์ ๋๊ณผ ๊ถ๋ ฅ์ด ๊ฑธ๋ฆฐ ๋ฌธ์ ๋ผ ๋ณด์์ด ์ ๋ง ์ค์ํด! ์ค์ ํ๋ก๋์ ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ๋ฐ๋์ ์ฒดํฌํด์ผ ํ ๋ณด์ ์ฌํญ๋ค์ ์ ๋ฆฌํด๋ดค์ด. ๐ก๏ธ
๐จ ์ฃผ์ ๋ณด์ ์ํ
๊ณต๊ฒฉ์๊ฐ ํ๋์ ๋ก ์ผ๋ก ๋๋์ ๊ฑฐ๋ฒ๋์ค ํ ํฐ์ ๋น๋ ค์ ํฌํํ๊ณ , ๊ฐ์ ๋ธ๋ก ๋ด์์ ํ ํฐ์ ๋ฐํํ๋ ๊ณต๊ฒฉ์ด์ผ. ์ด๋ฅผ ๋ฐฉ์งํ๋ ค๋ฉด ์ค๋ ์ท ๋ฉ์ปค๋์ฆ์ ์ฌ์ฉํด์ผ ํด. ํฌํ ์์ ์ ํน์ ๋ธ๋ก์ ํ ํฐ ๋ณด์ ๋์ ๊ธฐ์ค์ผ๋ก ํฌํ๊ถ์ ๋ถ์ฌํ๋ ๊ฑฐ์ง. ๐ธ
ํ ์ฌ๋์ด ์ฌ๋ฌ ๊ฐ์ ์ง๊ฐ์ ๋ง๋ค์ด์ ํฌํ๊ถ์ ๋ถ์ฐ์ํค๋ ๊ณต๊ฒฉ์ด์ผ. ํ ํฐ ๊ธฐ๋ฐ ํฌํ์์๋ ํ ํฐ์ ๋ถ์ฐ์์ผ๋ ์ด ํฌํ๊ถ์ ๊ฐ์ผ๋๊น ์ด๋ ์ ๋ ๋ฐฉ์ด๊ฐ ๋ผ. ํ์ง๋ง ์ถ๊ฐ๋ก ์ต์ ํ ํฐ ๋ณด์ ๋ ์๊ตฌ๋ ์์ ํฌํ ์์คํ ์ ๋์ ํ๋ฉด ๋ ์์ ํด. ๐
๊ณต๊ฒฉ์๊ฐ ๋ฉคํ(mempool)์ ๋ชจ๋ํฐ๋งํด์ ๋ค๋ฅธ ์ฌ๋์ ํฌํ ํธ๋์ญ์ ์ ๋ณด๊ณ , ๋ ๋์ ๊ฐ์ค๋น๋ก ๋จผ์ ํฌํํ๋ ๊ณต๊ฒฉ์ด์ผ. ์ด๊ฑด ์ปค๋ฐ-๋ฆฌ๋น ์คํด(Commit-Reveal Scheme)์ผ๋ก ๋ฐฉ์ดํ ์ ์์ด. ๋จผ์ ํฌํ ๋ด์ฉ์ ํด์๋ง ์ ์ถํ๊ณ , ๋์ค์ ์ค์ ํฌํ ๋ด์ฉ์ ๊ณต๊ฐํ๋ ๊ฑฐ์ง. ๐ญ
๐ ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์
์ค๋งํธ ์ปจํธ๋ํธ๋ ํ ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ๋ถ๊ฐ๋ฅํ๋๊น ๋ฐฐํฌ ์ ์ ์ฒ ์ ํ ๊ฐ์ฌ๊ฐ ํ์ํด! ๐
// ์์ ํ Voting Contract ์์ (Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract DAOVoting is Ownable, ReentrancyGuard, Pausable {
IERC20 public governanceToken;
struct Proposal {
uint256 id;
string ipfsHash;
address proposer;
uint256 startBlock;
uint256 endBlock;
uint256 snapshotBlock;
uint256 votesFor;
uint256 votesAgainst;
uint256 votesAbstain;
bool executed;
bool passed;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
mapping(uint256 => mapping(address => uint256)) public votingPower;
uint256 public proposalCount;
uint256 public quorumPercentage = 10; // 10%
uint256 public approvalThreshold = 50; // 50%
event ProposalCreated(
uint256 indexed proposalId,
string ipfsHash,
address indexed proposer,
uint256 startBlock,
uint256 endBlock
);
event VoteCast(
uint256 indexed proposalId,
address indexed voter,
uint8 choice,
uint256 votingPower
);
event ProposalExecuted(
uint256 indexed proposalId,
bool passed
);
modifier onlyDuringVoting(uint256 proposalId) {
require(
block.number >= proposals[proposalId].startBlock &&
block.number <= proposals[proposalId].endBlock,
"Voting is not active"
);
_;
}
constructor(address _governanceToken) {
governanceToken = IERC20(_governanceToken);
}
function createProposal(
string memory ipfsHash,
uint256 votingDuration
) external whenNotPaused returns (uint256) {
// ์ต์ ํ ํฐ ๋ณด์ ๋ ํ์ธ
require(
governanceToken.balanceOf(msg.sender) >= 1000 * 10**18,
"Insufficient tokens to propose"
);
uint256 proposalId = proposalCount++;
uint256 startBlock = block.number + 100; // 100๋ธ๋ก ํ ์์
uint256 snapshotBlock = startBlock - 1;
proposals[proposalId] = Proposal({
id: proposalId,
ipfsHash: ipfsHash,
proposer: msg.sender,
startBlock: startBlock,
endBlock: startBlock + votingDuration,
snapshotBlock: snapshotBlock,
votesFor: 0,
votesAgainst: 0,
votesAbstain: 0,
executed: false,
passed: false
});
emit ProposalCreated(
proposalId,
ipfsHash,
msg.sender,
startBlock,
startBlock + votingDuration
);
return proposalId;
}
function castVote(
uint256 proposalId,
uint8 choice // 0: for, 1: against, 2: abstain
) external nonReentrant onlyDuringVoting(proposalId) whenNotPaused {
require(choice <= 2, "Invalid vote choice");
require(!hasVoted[proposalId][msg.sender], "Already voted");
Proposal storage proposal = proposals[proposalId];
// ์ค๋
์ท ๋ธ๋ก ๊ธฐ์ค ํฌํ๊ถ ๊ณ์ฐ
uint256 power = getVotingPowerAt(
msg.sender,
proposal.snapshotBlock
);
require(power > 0, "No voting power");
hasVoted[proposalId][msg.sender] = true;
votingPower[proposalId][msg.sender] = power;
if (choice == 0) {
proposal.votesFor += power;
} else if (choice == 1) {
proposal.votesAgainst += power;
} else {
proposal.votesAbstain += power;
}
emit VoteCast(proposalId, msg.sender, choice, power);
}
function executeProposal(uint256 proposalId) external nonReentrant {
Proposal storage proposal = proposals[proposalId];
require(block.number > proposal.endBlock, "Voting still active");
require(!proposal.executed, "Already executed");
uint256 totalVotes = proposal.votesFor +
proposal.votesAgainst +
proposal.votesAbstain;
uint256 totalSupply = governanceToken.totalSupply();
uint256 participationRate = (totalVotes * 100) / totalSupply;
// ์ ์กฑ์ ํ์ธ
require(
participationRate >= quorumPercentage,
"Quorum not reached"
);
// ์น์ธ ์ฌ๋ถ ํ์ธ
uint256 approvalRate = (proposal.votesFor * 100) /
(proposal.votesFor + proposal.votesAgainst);
proposal.passed = approvalRate >= approvalThreshold;
proposal.executed = true;
emit ProposalExecuted(proposalId, proposal.passed);
}
function getVotingPowerAt(
address account,
uint256 blockNumber
) public view returns (uint256) {
// ERC20Snapshot์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
// return governanceToken.balanceOfAt(account, blockNumber);
// ์ผ๋ฐ ERC20์ ๊ฒฝ์ฐ ํ์ฌ ์์ก ๋ฐํ (์ค์ ๋ก๋ ์ค๋
์ท ๊ตฌํ ํ์)
return governanceToken.balanceOf(account);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
- โ Reentrancy ๊ณต๊ฒฉ ๋ฐฉ์ด (ReentrancyGuard ์ฌ์ฉ)
- โ Integer Overflow/Underflow ๋ฐฉ์ง (Solidity 0.8+ ์ฌ์ฉ)
- โ Access Control ๊ฒ์ฆ (Ownable, Role ๊ธฐ๋ฐ ๊ถํ)
- โ ๊ธด๊ธ ์ ์ง ๊ธฐ๋ฅ (Pausable)
- โ ๊ฐ์ค ์ต์ ํ
- โ ์ธ๋ถ ๊ฐ์ฌ (CertiK, OpenZeppelin ๋ฑ)
๐ ๋ฐฑ์๋ ๋ณด์
// ๋ผ๋ผ๋ฒจ ๋ฏธ๋ค์จ์ด๋ก API ๋ณดํธ
// app/Http/Middleware/VerifySignature.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Elliptic\EC;
class VerifySignature
{
public function handle(Request $request, Closure $next)
{
$signature = $request->header('X-Signature');
$address = $request->header('X-Address');
$message = $request->getContent();
if (!$signature || !$address) {
return response()->json([
'error' => 'Missing signature or address'
], 401);
}
// ์๋ช
๊ฒ์ฆ
if (!$this->verifySignature($message, $signature, $address)) {
return response()->json([
'error' => 'Invalid signature'
], 401);
}
$request->merge(['verified_address' => $address]);
return $next($request);
}
protected function verifySignature(
string $message,
string $signature,
string $address
): bool {
try {
$ec = new EC('secp256k1');
// ๋ฉ์์ง ํด์
$messageHash = hash('sha3-256', $message);
// ์๋ช
ํ์ฑ
$r = substr($signature, 2, 64);
$s = substr($signature, 66, 64);
$v = hexdec(substr($signature, 130, 2));
// ๊ณต๊ฐํค ๋ณต๊ตฌ
$publicKey = $ec->recoverPubKey(
$messageHash,
['r' => $r, 's' => $s],
$v
);
// ์ฃผ์ ๊ณ์ฐ
$recoveredAddress = '0x' . substr(
hash('sha3-256', $publicKey->encode('hex')),
-40
);
return strtolower($recoveredAddress) === strtolower($address);
} catch (\Exception $e) {
\Log::error('Signature verification failed: ' . $e->getMessage());
return false;
}
}
}
โข Rate Limiting: API ์์ฒญ ํ์ ์ ํ์ผ๋ก DDoS ๋ฐฉ์ด
โข CORS ์ค์ : ํ์ฉ๋ ๋๋ฉ์ธ์์๋ง API ์ ๊ทผ ๊ฐ๋ฅ
โข ํ๊ฒฝ ๋ณ์ ์ํธํ: ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ํธํํด์ ์ ์ฅ
โข ๋ก๊ทธ ๋ชจ๋ํฐ๋ง: ์์ฌ์ค๋ฌ์ด ํ๋ ์ค์๊ฐ ๊ฐ์ง
โข ์ ๊ธฐ ๋ฐฑ์ : ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ค๋งํธ ์ปจํธ๋ํธ ์ํ ๋ฐฑ์
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ๋ค๋ฉด ์ด์ ์ค์ ์๋น์ค๋ก ๋ฐฐํฌํ ์ฐจ๋ก์ผ! ์์ ์ ์ธ ์ด์์ ์ํ ๋ฐฐํฌ ์ ๋ต๊ณผ ๋ชจ๋ํฐ๋ง ๋ฐฉ๋ฒ์ ์์๋ณด์. ๐
๐ฆ ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ
๋จผ์ ํ ์คํธ๋ท(Mumbai, Goerli ๋ฑ)์์ ์ถฉ๋ถํ ํ ์คํธํ ํ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํด. Hardhat์ด๋ Truffle ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด ํธํด. ๋ฐฐํฌ ํ์๋ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ๋ฐ๋์ ๊ธฐ๋กํด๋๊ณ , Etherscan์์ ์์ค์ฝ๋๋ฅผ ๊ฒ์ฆ(Verify)ํ์! ๐
AWS, DigitalOcean, ๋๋ Laravel Forge๋ฅผ ์ฌ์ฉํด์ ๋ฐฐํฌํ ์ ์์ด. ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ฐ๋์ HTTPS๋ฅผ ์ฌ์ฉํ๊ณ , APP_DEBUG=false๋ก ์ค์ ํด์ผ ํด. ๊ทธ๋ฆฌ๊ณ ์บ์ ์ต์ ํ๋ ์์ง ๋ง์!
// ํ๋ก๋์
๋ฐฐํฌ ๋ช
๋ น์ด
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
// ํ ์์ปค ์คํ (Supervisor๋ก ๊ด๋ฆฌ)
php artisan queue:work --tries=3 --timeout=90
Vue.js ์ฑ์ ํ๋ก๋์ ๋ชจ๋๋ก ๋น๋ํ๊ณ , CDN์ ํตํด ์ ์ ํ์ผ์ ์๋นํ๋ฉด ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋ผ. Cloudflare๋ AWS CloudFront๋ฅผ ์ถ์ฒํด! โ๏ธ
// ํ๋ก ํธ์๋ ๋น๋
npm run build
// ๋น๋๋ ํ์ผ์ public ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ
cp -r dist/* public/
๐ ๋ชจ๋ํฐ๋ง ์ค์
์์คํ ์ด ์ ๋๋ก ์๋ํ๋์ง ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๋ ๊ฒ ์ค์ํด. ๋ผ๋ผ๋ฒจ Telescope์ Horizon์ ํ์ฉํ๋ฉด ์ข์! ๐ญ
// Laravel Telescope ์ค์น
composer require laravel/telescope
php artisan telescope:install
php artisan migrate
// Laravel Horizon ์ค์น (ํ ๋ชจ๋ํฐ๋ง)
composer require laravel/horizon
php artisan horizon:install
โข API ์๋ต ์๊ฐ: ํ๊ท ์๋ต ์๊ฐ์ด 200ms ์ดํ์ธ์ง ํ์ธ
โข ๋ธ๋ก์ฒด์ธ ๋๊ธฐํ ์ํ: ์ต์ ๋ธ๋ก๊ณผ์ ์ฐจ์ด๊ฐ 10๋ธ๋ก ์ด๋ด์ธ์ง ์ฒดํฌ
โข ํ ์์ ์คํจ์จ: ์คํจ์จ์ด 5% ์ด์์ด๋ฉด ์๋ฆผ
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ฑ๋ฅ: ๋๋ฆฐ ์ฟผ๋ฆฌ ๋ก๊น ๋ฐ ์ต์ ํ
โข ๊ฐ์ค๋น ์ถ์ด: ๊ฐ์ค๋น๊ฐ ๊ธ๋ฑํ๋ฉด ํธ๋์ญ์ ์ผ์ ์ค๋จ
๐ CI/CD ํ์ดํ๋ผ์ธ
GitHub Actions๋ฅผ ์ฌ์ฉํด์ ์๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํด๋ณด์! ๐ค
// .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install Dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run Tests
run: php artisan test
- name: Run Static Analysis
run: ./vendor/bin/phpstan analyse
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/dao-voting
git pull origin main
composer install --no-dev
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart
๐ก ์ค์ ํ๊ณผ ์ต์ ํ
์ค์ ๋ก DAO ํฌํ ์์คํ ์ ์ด์ํ๋ฉด์ ๋ฐฐ์ด ๋ ธํ์ฐ๋ค์ ๊ณต์ ํ ๊ฒ! ์ด๋ฐ ํ๋ค์ ์๊ณ ์์ผ๋ฉด ํจ์ฌ ํจ์จ์ ์ผ๋ก ๊ฐ๋ฐํ๊ณ ์ด์ํ ์ ์์ด. ๐ฏ
โก ์ฑ๋ฅ ์ต์ ํ
ํฌํ ์กฐํ ์ฟผ๋ฆฌ๊ฐ ๋ง์ด ๋ฐ์ํ๋๊น ์ ์ ํ ์ธ๋ฑ์ค๋ฅผ ์ค์ ํ๋ ๊ฒ ์ค์ํด. ํนํ proposal_id, voter_address, blockchain_tx_hash ์ปฌ๋ผ์๋ ๋ฐ๋์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์!
// ๋ณตํฉ ์ธ๋ฑ์ค ์ถ๊ฐ ๋ง์ด๊ทธ๋ ์ด์
Schema::table('votes', function (Blueprint $table) {
$table->index(['proposal_id', 'voter_address']);
$table->index(['proposal_id', 'voted_at']);
});
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ์กฐํ๋ ๋๋ฆฌ๋๊น Redis๋ก ์บ์ฑํ๋ฉด ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋ผ. ํนํ ํ ํฐ ์ด ๊ณต๊ธ๋, ์ ์์ ๋ชฉ๋ก ๊ฐ์ ์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ ์บ์ฑ ํ์! ๐
// ์บ์ฑ ์์
public function getActiveProposals()
{
return Cache::remember('active_proposals', 300, function () {
return Proposal::active()
->with('votes')
->get();
});
}
// ํฌํ ํ ์บ์ ๋ฌดํจํ
public function afterVote(Proposal $proposal)
{
Cache::forget('active_proposals');
Cache::forget("proposal_{$proposal->id}_stats");
}
๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ๋ฆฌ์ค๋ํด์ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ ๋ฐ์ดํธํ๋ฉด ์ค์๊ฐ์ฑ์ด ํฅ์๋ผ. Web3.js์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ํ์ฉํ์! ๐
// ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋
const votingContract = new web3.eth.Contract(abi, address);
votingContract.events.VoteCast({
fromBlock: 'latest'
})
.on('data', async (event) => {
const { proposalId, voter, choice, votingPower } = event.returnValues;
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ
await axios.post('/api/internal/sync-vote', {
proposal_id: proposalId,
voter_address: voter,
vote_choice: choice,
voting_power: votingPower,
transaction_hash: event.transactionHash,
block_number: event.blockNumber
});
})
.on('error', console.error);
๐ฐ ๊ฐ์ค๋น ์ต์ ํ
์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์ ๊ฐ์ค๋น๊ฐ ๋น์ธ๋๊น ์ต์ ํ๊ฐ ํ์์ผ! ๋ช ๊ฐ์ง ์ ๋ต์ ์๊ฐํ ๊ฒ. ๐ธ
| ์ ๋ต | ์ค๋ช | ์ ๊ฐ ํจ๊ณผ |
|---|---|---|
| L2 ์๋ฃจ์ ์ฌ์ฉ | Polygon, Arbitrum, Optimism ๋ฑ | 90% ์ด์ |
| ๋ฐฐ์น ์ฒ๋ฆฌ | ์ฌ๋ฌ ํฌํ๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌ | 30-50% |
| ์คํ์ฒด์ธ ์๋ช | EIP-712 ์๋ช ํ์ฉ | 70-80% |
| ์คํ ๋ฆฌ์ง ์ต์ ํ | uint256 ๋์ uint128 ์ฌ์ฉ | 10-20% |
์ฌ์ฉ์๊ฐ ๋ฉํ๋ง์คํฌ๋ก ์๋ช ๋ง ํ๊ณ , ์ค์ ํธ๋์ญ์ ์ ์๋ฒ์์ ๋ฐฐ์น๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด์ผ. ๊ฐ์ค๋น๋ฅผ ์ฌ์ฉ์๊ฐ ์๋ DAO ์ฌ๋ฌด์์ ๋ถ๋ดํ ์ ์์ด์ UX๊ฐ ํฌ๊ฒ ๊ฐ์ ๋ผ! ์ด๊ฑธ "Gasless Transaction" ๋๋ "Meta Transaction"์ด๋ผ๊ณ ํด. ๐ซ
๐จ UX ๊ฐ์ ์์ด๋์ด
๊ธฐ์ ์ ์ผ๋ก ์๋ฒฝํด๋ ์ฌ์ฉ์ ๊ฒฝํ์ด ๋์๋ฉด ์๋ฌด๋ ์ ์จ. ์ค์ ์ฌ์ฉ์๋ค์ด ์ข์ํ๋ ๊ธฐ๋ฅ๋ค์ ์ ๋ฆฌํด๋ดค์ด! ๐
โข ํฌํ ์์: ๋ฐ์ ์ฌ์ฉ์๋ ์ ๋ขฐํ๋ ์ฌ๋์๊ฒ ํฌํ๊ถ์ ์์ํ ์ ์์ด
โข ํ ๋ก ํฌ๋ผ: ์ ์์์ ๋ํ ์ปค๋ฎค๋ํฐ ํ ๋ก ๊ณต๊ฐ
โข ์๋ฆผ ์์คํ : ์ ์ ์, ํฌํ ๋ง๊ฐ ์๋ฐ ๋ฑ์ ์ด๋ฉ์ผ/ํ ๋ ๊ทธ๋จ์ผ๋ก ์๋ฆผ
โข ํฌํ ํ์คํ ๋ฆฌ: ๊ณผ๊ฑฐ ํฌํ ๋ด์ญ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ํ๋์ ๋ณผ ์ ์๋ ๋์๋ณด๋
โข ์๋ฎฌ๋ ์ด์ : ์ ์์ด ํต๊ณผ๋๋ฉด ์ด๋ค ๋ณํ๊ฐ ์๊ธฐ๋์ง ๋ฏธ๋ฆฌ ๋ณด๊ธฐ
โข ๋ชจ๋ฐ์ผ ์ฑ: ์ธ์ ์ด๋์๋ ํฌํํ ์ ์๋๋ก ๋ค์ดํฐ๋ธ ์ฑ ์ ๊ณต
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ด๋ฐ DAO ๊ฑฐ๋ฒ๋์ค ์์คํ ์ ๋์ ํ๋ฉด ์ ๋ง ๋ฉ์ง ๊ฒ ๊ฐ์! ์ฌ์ฉ์๋ค์ด ์ง์ ํ๋ซํผ์ ์ ์ฑ ์ ๊ฒฐ์ ํ๊ณ , ์์๋ฃ ๊ตฌ์กฐ๋ฅผ ํฌํ๋ก ์ ํ๊ณ , ์๋ก์ด ๊ธฐ๋ฅ ๊ฐ๋ฐ ์ฐ์ ์์๋ฅผ ์ ํ ์ ์์์? ์์ ํ์ ์ ์ด์ง! ๐
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
DAO์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ๊ณ์ ์งํํ๋ ๋ถ์ผ์ผ. ์ต์ ํธ๋ ๋๋ฅผ ๋ฐ๋ผ๊ฐ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ํ์ฉํด๋ด! ๐
๊ณต์ ๋ฌธ์
โข Laravel ๊ณต์ ๋ฌธ์: https://laravel.com/docs
โข Web3.js ๋ฌธ์: https://web3js.readthedocs.io
โข Solidity ๋ฌธ์: https://docs.soliditylang.org
โข OpenZeppelin Contracts: https://docs.openzeppelin.com
์จ๋ผ์ธ ๊ฐ์
โข Udemy: "Ethereum and Solidity: The Complete Developer's Guide"
โข Coursera: "Blockchain Specialization"
โข YouTube: Patrick Collins, Dapp University ์ฑ๋
์ปค๋ฎค๋ํฐ
โข Discord: Laravel, Ethereum, Polygon ๊ณต์ ์๋ฒ
โข Reddit: r/ethdev, r/laravel
โข Stack Overflow: ethereum, laravel ํ๊ทธ
โข GitHub: ์คํ์์ค DAO ํ๋ก์ ํธ๋ค ์ฐธ๊ณ
๐ ์ ๋ช DAO ํ๋ก์ ํธ ๋ถ์
์ค์ ์ด์ ์ค์ธ DAO๋ค์ ๋ถ์ํ๋ฉด ๋ง์ ๊ฑธ ๋ฐฐ์ธ ์ ์์ด. ๊ฐ ํ๋ก์ ํธ์ GitHub ์ ์ฅ์๋ฅผ ์ดํด๋ณด๋ฉด ์ค์ ์ฝ๋๋ฅผ ๋ณผ ์ ์์ง! ๐
โข Compound: ๊ฐ์ฅ ์ฑ๊ณต์ ์ธ DeFi DAO ์ค ํ๋. ๊ฑฐ๋ฒ๋์ค ํ ํฐ COMP๋ก ํ๋กํ ์ฝ ํ๋ผ๋ฏธํฐ๋ฅผ ํฌํ๋ก ๊ฒฐ์ ํด.
โข Uniswap: UNI ํ ํฐ ํ๋๋ค์ด ํ๋กํ ์ฝ ์ ๊ทธ๋ ์ด๋์ ์์๋ฃ ์ ์ฑ ์ ๊ฒฐ์ . ์์ ํฌํ ์์คํ ์ด ์ ๊ตฌํ๋์ด ์์ด.
โข Snapshot: ์คํ์ฒด์ธ ํฌํ ํ๋ซํผ. ๊ฐ์ค๋น ์์ด ํฌํํ ์ ์์ด์ ๋ง์ DAO๋ค์ด ์ฌ์ฉํด. IPFS ๊ธฐ๋ฐ์ด๋ผ ์์ ํ์ค์ํ์ผ.
โข Aragon: DAO ์์ฑ ํ๋ซํผ. ์ฝ๋ ์์ด๋ DAO๋ฅผ ๋ง๋ค ์ ์๋ ๋ ธ์ฝ๋ ์๋ฃจ์ ์ ์ ๊ณตํด.
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ๋ผ๋ผ๋ฒจ๋ก DAO ํฌํ ์์คํ
์ ๊ตฌํํ๋ ์ ์ฒด ๊ณผ์ ์ ์ดํด๋ดค์ด! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
์ฐ๋ฆฌ๊ฐ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด๋ฉด:
โ
DAO์ ๊ฐ๋
๊ณผ ์ ์ค์ํ์ง
โ
ํ์ด๋ธ๋ฆฌ๋ ์ํคํ
์ฒ ์ค๊ณ (์จ์ฒด์ธ + ์คํ์ฒด์ธ)
โ
๋ผ๋ผ๋ฒจ ๋ฐฑ์๋ ๊ตฌํ (๋ชจ๋ธ, ์ปจํธ๋กค๋ฌ, ์๋น์ค)
โ
์ค๋งํธ ์ปจํธ๋ํธ ์์ฑ ๋ฐ ๋ณด์
โ
Vue.js ํ๋ก ํธ์๋์ Web3 ์ฐ๋
โ
์ค์ผ์ค๋ฌ์ ํ๋ก ์๋ํ
โ
๋ณด์ ๊ณ ๋ ค์ฌํญ๊ณผ ์ต์ ํ
โ
๋ฐฐํฌ ๋ฐ ์ด์ ์ ๋ต
์ด ๋ชจ๋ ๊ฑธ ์๋ฒฝํ๊ฒ ๊ตฌํํ๋ ค๋ฉด ์๊ฐ์ด ์ข ๊ฑธ๋ฆฌ๊ฒ ์ง๋ง, ํ ๋จ๊ณ์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์งํํ๋ฉด ์ถฉ๋ถํ ํ ์ ์์ด! ๐ช
ํนํ ๊ธฐ์ตํด์ผ ํ ํต์ฌ ํฌ์ธํธ๋:
1. ๋ณด์์ด ์ต์ฐ์ ์ด์ผ. ์ค๋งํธ ์ปจํธ๋ํธ๋ ํ ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ๋ถ๊ฐ๋ฅํ๋๊น ๋ฐฐํฌ ์ ์ ์ฒ ์ ํ ๊ฐ์ฌ๊ฐ ํ์์ผ. ๐
2. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ณ ๋ คํด์ผ ํด. ์๋ฌด๋ฆฌ ๊ธฐ์ ์ ์ผ๋ก ์๋ฒฝํด๋ ์ฌ์ฉํ๊ธฐ ์ด๋ ค์ฐ๋ฉด ์๋ฌด๋ ์ ์จ. ๋ฉํ๋ง์คํฌ ์ฐ๊ฒฐ, ํธ๋์ญ์
์๋ช
, ๊ฒฐ๊ณผ ํ์ธ๊น์ง ๋ชจ๋ ๊ณผ์ ์ด ์ง๊ด์ ์ด์ด์ผ ํด. ๐จ
3. ์ ์ง์ ํ์ค์ํ๋ฅผ ์ถ๊ตฌํ์. ์ฒ์๋ถํฐ ์์ ํ ํ์ค์ํ๋ฅผ ๋ชฉํ๋ก ํ๋ฉด ๋ณต์ก๋๊ฐ ๋๋ฌด ๋์์ ธ. ์ด๊ธฐ์๋ ์ค์ํ๋ ์์๋ฅผ ์ผ๋ถ ๋จ๊ฒจ๋๊ณ , ์์คํ
์ด ์์ ํ๋๋ฉด ์ ์ ๋ ํ์ค์ํํ๋ ๊ฒ ํ์ค์ ์ด์ผ. ๐
4. ์ปค๋ฎค๋ํฐ๊ฐ ํต์ฌ์ด์ผ. DAO๋ ๊ฒฐ๊ตญ ์ฌ๋๋ค์ ์ฐธ์ฌ๋ก ์๋ํ๋ ์์คํ
์ด๊ฑฐ๋ . ํ๋ฐํ ํ ๋ก ๊ณผ ํฌ๋ช
ํ ์์ฌ๊ฒฐ์ ๊ณผ์ ์ด DAO์ ์ฑ๊ณต์ ์ข์ฐํด. ๐ฅ
๋ธ๋ก์ฒด์ธ๊ณผ Web3๋ ์ ๋ง ๋น ๋ฅด๊ฒ ๋ฐ์ ํ๋ ๋ถ์ผ์ผ. ์ค๋ ๋ฐฐ์ด ๋ด์ฉ๋ ๋ช ๋ฌ ํ๋ฉด ๋ ์ข์ ๋ฐฉ๋ฒ์ด ๋์ฌ ์ ์์ด. ๊ทธ๋ฌ๋๊น ํญ์ ์ต์ ํธ๋ ๋๋ฅผ ์ฃผ์ํ๊ณ , ์ปค๋ฎค๋ํฐ์ ์ํตํ๋ฉด์ ๊ณ์ ๋ฐฐ์๋๊ฐ๋ ๊ฒ ์ค์ํด! ๐
๋ง์ง๋ง์ผ๋ก, ์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ DAO ํ๋ก์ ํธ์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ง ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ด. ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๋ ์ ๋ง ์น์ ํ๊ณ ๋์์ ์ ์ฃผ๊ฑฐ๋ ! ๐
๊ทธ๋ผ ์ฌ๋ฌ๋ถ์ ๋ฉ์ง DAO ํ๋ก์ ํธ๋ฅผ ์์ํ ๊ฒ! ํ์ดํ
! ๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.