40. WETH
WETH (viết tắt của Wrapped ETH) là phiên bản ERC-20 của ETH, tỷ lệ quy đổi là 1:1. Với các tính năng của ERC-20, WETH giúp cho việc trao đổi ETH được linh hoạt, tiện lợi hơn thông qua các giao dịch như chuyển tiền giữa các blockchain khác nhau, swap ....
WETH Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract WETH is ERC20 { // Events event Deposit(address indexed dst, uint wad); event Withdrawal(address indexed src, uint wad); // Constructor: Khởi tạo tên, symbol token constructor() ERC20("WETH", "WETH") {} // Callback function fallback() external payable { deposit(); } // Callback function receive() external payable { deposit(); } // Deposit function, người dùng gửi ETH vào sẽ được mint ra lượng WETH tương ứng function deposit() public payable { _mint(msg.sender, msg.value); emit Deposit(msg.sender, msg.value); } // Withdrawal function, người dùng rút ETH về và WETH của người dùng với số lượng tương ứng bị đốt function withdraw(uint amount) public { require(balanceOf(msg.sender) >= amount); _burn(msg.sender, amount); payable(msg.sender).transfer(amount); emit Withdrawal(msg.sender, amount); }
}
Sự khác nhau giữa fallback
và receive
, chúng ta có thể xem lại ở đây.
41. Payment Splitting
Payment Splitting
Payment splitting là một khái niệm liên quan đến việc phân chia thanh toán hoặc chi trả giữa nhiều bên hoặc các đối tượng khác nhau trong một giao dịch tài chính. Nôm na nghĩa là chia tiền cho nhiều bên theo tỷ lệ khác nhau.
Payment Split Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract PaymentSplit { // event event PayeeAdded(address account, uint256 shares); event PaymentReleased(address to, uint256 amount); event PaymentReceived(address from, uint256 amount); uint256 public totalShares; // Tổng số người nhận uint256 public totalReleased; // Tổng tiền chi trả mapping(address => uint256) public shares; // số tiền thụ hưởng đối với mỗi địa chỉ trong danh sách mapping(address => uint256) public released; // số tiền mỗi địa chỉ đã được tri chả address[] public payees; // Danh sách người nhận // Khởi tạo danh sách người nhận và số tiền tương ứng với mỗi địa chỉ constructor(address[] memory _payees, uint256[] memory _shares) payable { require( _payees.length == _shares.length, "PaymentSplitter: payees and shares length mismatch" ); require(_payees.length > 0, "PaymentSplitter: no payees"); for (uint256 i = 0; i < _payees.length; i++) { _addPayee(_payees[i], _shares[i]); } } /** * @dev Callback function, hàm nhận ETH */ receive() external payable virtual { emit PaymentReceived(msg.sender, msg.value); } /** * @dev Trả tiền cho người nhận */ function release(address payable _account) public virtual { // Địa chỉ phải có trong danh sách require(shares[_account] > 0, "PaymentSplitter: account has no shares"); // Tính toán lượng tiền sẽ trả uint256 payment = releasable(_account); // số tiền trả phải lớn hơn 0 require(payment != 0, "PaymentSplitter: account is not due payment"); // Cập nhật các biến trạng thái totalReleased += payment; released[_account] += payment; // Chuyển tiền cho người nhận _account.transfer(payment); emit PaymentReleased(_account, payment); } /** * @dev Tính toán lượng tiền địa chỉ sẽ được nhận * gọi pendingPayment() */ function releasable(address _account) public view returns (uint256) { // Calculate the total income of the profit-sharing contract uint256 totalReceived = address(this).balance + totalReleased; // Call _pendingPayment to calculate the amount of ETH that account is entitled to return pendingPayment(_account, totalReceived, released[_account]); } /** * @dev Tính toán số tiền còn lại mà người nhận chưa được hưởng */ function pendingPayment( address _account, uint256 _totalReceived, uint256 _alreadyReleased ) public view returns (uint256) { // Số lượng ETH = Total ETH due - ETH received return (_totalReceived * shares[_account]) / totalShares - _alreadyReleased; } /** * @dev thêm người nhận vào danh sách, hàm này chỉ được gọi từ constructor khi contract được triển khai */ function _addPayee(address _account, uint256 _accountShares) private { require( _account != address(0), "PaymentSplitter: account is the zero address" ); require(_accountShares > 0, "PaymentSplitter: shares are 0"); require( shares[_account] == 0, "PaymentSplitter: account already has shares" ); // Cập nhật các biến payees.push(_account); shares[_account] = _accountShares; totalShares += _accountShares; // emit add payee event emit PayeeAdded(_account, _accountShares); }
}
Triển khai
1. Deploy contract, khởi tạo với 2 địa chỉ nhận
2. Kiểm tra các biến trạng thái
3. Gọi hàm release
để nhận tiền
4. Kiểm tra lại trạng thái mới
42. Token Vesting
Vesting
Token Vesting là hình thức trả thưởng theo từng đợt, mỗi đợt cách nhau một khoảng thời gian. Việc phân phối thành nhiều đợt giúp giảm áp lực bán ra cho token và duy trì sự cam kết lâu dài của đội ngũ hay các nhà đầu tư đối với dự án.
Smart contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TokenVesting { // Event event ERC20Released(address indexed token, uint256 amount); // Withdraw event // số lượng đã trả ứng với mỗi token mapping(address => uint256) public erc20Released; address public immutable beneficiary; // địa chỉ nhận token uint256 public immutable start; // thời gian bắt đầu (tính theo timestamp) uint256 public immutable duration; // Khoảng thời gian khóa token /** * @dev Khởi tạo các biến trạng thái khi deploy */ constructor(address beneficiaryAddress, uint256 durationSeconds) { require( beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address" ); beneficiary = beneficiaryAddress; start = block.timestamp; duration = durationSeconds; } /** * @dev Rút tiền * Emit {ERC20Released} event. */ function release(address token) public { // Gọi vestedAmount để tính toán số tiền có thể nhận uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - erc20Released[token]; erc20Released[token] += releasable; // Chuyển token emit ERC20Released(token, releasable); IERC20(token).transfer(beneficiary, releasable); } /** * @param token: địa chỉ Token rút * @param timestamp: thời điểm rút */ function vestedAmount( address token, uint256 timestamp ) public view returns (uint256) { uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + erc20Released[token]; if (timestamp < start) { return 0; } else if (timestamp > start + duration) { return totalAllocation; } else { // Dựa trên thời gian rút, tính toán xem đã qua được bao nhiêu chu kỳ (duration) để tính toán lượng token return (totalAllocation * (timestamp - start)) / duration; } }
}
43. Token Locker
Khác với Token Vesting mở khóa theo từng khoảng thời gian. Token Locker sẽ khóa toàn bộ token trong khoảng thời gian được chỉ định và chỉ có thể được rút sau khoảng thời gian đó.
Contract Token Locker có thể ứng dụng trong việc khóa các LP Token, tránh tình trạng rug-pull, rút cạn thanh khoản của cặp giao dịch trên sàn phi tập trung (DEX).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TokenLocker { // Event event TokenLockStart( address indexed beneficiary, address indexed token, uint256 startTime, uint256 lockTime ); event Release( address indexed beneficiary, address indexed token, uint256 releaseTime, uint256 amount ); // địa chỉ contract ERC20 sẽ được khóa IERC20 public immutable token; // địa chỉ người dùng address address public immutable beneficiary; // Thời gian khóa (giây) uint256 public immutable lockTime; // Thời gian bắt đầu khóa (giây) uint256 public immutable startTime; // Khởi tạo constructor(IERC20 token_, address beneficiary_, uint256 lockTime_) { require(lockTime_ > 0, "TokenLock: lock time should greater than 0"); token = token_; beneficiary = beneficiary_; lockTime = lockTime_; startTime = block.timestamp; emit TokenLockStart( beneficiary_, address(token_), block.timestamp, lockTime_ ); } /** * @dev Sau khi thời gian khóa đã hết, người dùng có thể gọi để rút token */ function release() public { require( block.timestamp >= startTime + lockTime, "TokenLock: current time is before release time" ); uint256 amount = token.balanceOf(address(this)); require(amount > 0, "TokenLock: no tokens to release"); token.transfer(beneficiary, amount); emit Release(msg.sender, address(token), block.timestamp, amount); }
}
44. TimeLock
TimeLock (Khóa thời gian) là một cơ chế thường thấy ở các ngân hàng hay những nơi cần sự mật cao. Là một bộ đếm thời gian được thiết kế để ngăn chặn việc mở két sắt trong một thời gian nhất định, ngay cả khi người mở khóa biết mật khẩu chính xác.
Trong blockchain, TimeLock được sử dụng rộng rãi trong DeFi và DAO. Việc trì hoãn giao dịch trong một khoảng thời gian giúp phòng tránh và giảm thiểu rủi ro trong các ứng dụng tài chính. Ví dụ: nếu kẻ xấu hack được đa chữ ký của Uniswap và có ý định rút tiền từ vault, nhưng phải chờ 2 ngày vì nó áp dụng timelock, hacker cần đợi 2 ngày kể từ khi tạo giao dịch rút tiền để thực sự rút được tiền. Trong giai đoạn này, bên dự án có thể tìm ra biện pháp đối phó và nhà đầu tư có thể bán token trước để giảm lỗ.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract Timelock { // Event // transaction cancel event event CancelTransaction( bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime ); // transaction execution event event ExecuteTransaction( bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime ); // transaction created and queued event event QueueTransaction( bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime ); // Event to change administrator address event NewAdmin(address indexed newAdmin); address public admin; // Admin address uint public constant GRACE_PERIOD = 7 days; // Thời gian giao dịch còn hiệu lực, sau thời gian này nếu thực thi giao dịch sẽ lỗi uint public delay; // thời gian khóa giao dịch (giây) mapping (bytes32 => bool) public queuedTransactions; // Lưu trạng thái của tất cả giao dịch timelock modifier onlyOwner() { require(msg.sender == admin, "Timelock: Caller not admin"); _; } modifier onlyTimelock() { require(msg.sender == address(this), "Timelock: Caller not Timelock"); _; } constructor(uint delay_) { delay = delay_; admin = msg.sender; } // Chỉ có thể được gọi từ chính nó, đây là giao dịch gọi hàm được áp dụng timelock function changeAdmin(address newAdmin) public onlyTimelock { admin = newAdmin; emit NewAdmin(newAdmin); } /** * @dev Tạo giao dịch và thêm vào hàng đợi timelock * @param target: địa chỉ contract đích của giao dịch * @param value: lượng ETH * @param signature: function signature * @param data * @param executeTime: Thời gian thực thi giao dịch * * Yêu cầu: executeTime phải lớn hơn block.timestamp + delay */ function queueTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime ) public onlyOwner returns (bytes32) { require( executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay." ); // định danh giao dịch bằng mã băm bytes32 txHash = getTxHash(target, value, signature, data, executeTime); // thêm vào hàng đợi queuedTransactions[txHash] = true; emit QueueTransaction( txHash, target, value, signature, data, executeTime ); return txHash; } /** * @dev Hủy giao dịch * yêu cầu: giao dịch phải đang trạng thái chờ trong hàng đợi timelock */ function cancelTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime ) public onlyOwner { bytes32 txHash = getTxHash(target, value, signature, data, executeTime); require( queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued." ); // dequed queuedTransactions[txHash] = false; emit CancelTransaction( txHash, target, value, signature, data, executeTime ); } /** * @dev Thực thi * * 1. Giao dịch ở trong hàng đợi timelock * 2. Hết thời gian khóa * 3. Thời gian hiệu lực vẫn còn (chưa quá 7 ngày) */ function executeTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime ) public payable onlyOwner returns (bytes memory) { bytes32 txHash = getTxHash(target, value, signature, data, executeTime); require( queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued." ); // Kiểm tra executeTime so với thời gian hiện tại require( getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock." ); // Kiểm tra xem hết thời gian hiệu lực chưa require( getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale." ); // xóa khỏi hàng đợi queuedTransactions[txHash] = false; // get callData bytes memory callData; if (bytes(signature).length == 0) { callData = data; } else { callData = abi.encodePacked( bytes4(keccak256(bytes(signature))), data ); } // Thực thi giao dịch (bool success, bytes memory returnData) = target.call{value: value}( callData ); // kiểm tra trạng thái require( success, "Timelock::executeTransaction: Transaction execution reverted." ); emit ExecuteTransaction( txHash, target, value, signature, data, executeTime ); return returnData; } /** * @dev Get the current blockchain timestamp */ function getBlockTimestamp() public view returns (uint) { return block.timestamp; } /** * @dev transaction identifier */ function getTxHash( address target, uint value, string memory signature, bytes memory data, uint executeTime ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, signature, data, executeTime)); }
}
Chạy thử
1. Deploy với delay = 120
2. Gọi hàm changeAdmin
=> lỗi vì không thể gọi từ bên ngoài
3.
target
: địa chỉ contract Timelockvalue
: không gửi ETH nên truyền vào 0signature
: gọi hàm changeAdmin nên chữ ký sẽ là"changeAdmin(address)"
data
: encode tham số sẽ truyền vào khi gọi hàm
Address before encoding:0xd9145CCE52D386f254917e481eB44e9943F39138
encoded address:0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
executeTime
: lấy block.timestamp hiện tại cộng thêm 150 (>120)
4. Gọi hàm queueTransaction
với các thông số ở trên
5. Chưa hết thời gian khóa và gọi hàm excuteTransaction
=> lỗi
6. Chờ hết thời gian khóa và gọi hàm excuteTransaction
7. Kiểm tra địa chỉ admin => địa chỉ mới (giao dịch thành công)
45. ProxyContract
Proxy pattern
Smart contract sau khi đã được triển khai sẽ không thể thay đổi. Đây là một ưu điểm nhưng đồng thời cũng là một hạn chế.
- Ưu điểm: An toàn khi không ai có thể sửa đổi logic hợp đồng để chuộc lợi.
- Hạn chế: Khi phát hiện lỗi hoặc muốn nâng cấp phiên bản thì phải triển khai một hợp đồng hoàn toàn mới. Các dữ liệu trên hợp đồng cũ nếu mới chuyển sang hợp đồng mới cũng tốn rất nhiều chi phí và thời gian.
Proxy pattern được đề xuất giúp có thể "sửa đổi" và nâng cấp hợp đồng thông minh. Nó sẽ bao gồm 2 hợp đồng
- Proxy contract: Lưu trữ dữ liệu, các biến trạng thái
- Logic contract: Chứa logic, các hàm
Khi người dùng gọi tới Proxy contract, nó sẽ gọi bằng lệnh delegate
đến Logic contract để thực thi.
Chúng ta có thể đọc lại về delegateCall ở đây
Lợi ích của proxy pattern:
- Upgradeable (Khả năng nâng cấp): Khi cần thay đổi logic của hợp đồng, chỉ cần trỏ Proxy contract sang Logic contract mới.
- Gas saving (tiết kiệm gas)
Proxy contract
Proxy contract không dài, nhưng chứa nhiều lệnh inline assembly khá phức tạp và khó hiểu.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract Proxy { // Address of the logic contract address public implementation; constructor(address implementation_) { implementation = implementation_; } /** * @dev Khi proxy contract được gọi, chuyển hướng gọi đến logic contract bằng delegateCall */ fallback() external payable { address _implementation = implementation; assembly { // copy msg.data vào memory calldatacopy(0, 0, calldatasize()) // dùng delegatecall để gọi implementation contract (logic contract) // các tham số của opcode delegatecall lần lượt là: gas, target contract address, start position of input memory, length of input memory, start position of output memory, length of output memory // đặt vị trí bắt đầu của bộ nhớ đầu ra và độ dài của bộ nhớ đầu ra thành 0 // delegatecall trả về 1 nếu thành công, 0 nếu lỗi let result := delegatecall( gas(), _implementation, 0, calldatasize(), 0, 0 ) // copy returndata vô memory // đối số của opcode returndata: start position of memory, start position of returndata, length of retundata returndatacopy(0, 0, returndatasize()) switch result // nếu delegateCall lỗi thì revert case 0 { revert(0, returndatasize()) } // Nếu thành công thì trả về kết quả default { return(0, returndatasize()) } } }
}
Logic contract
// Tạo 1 logic cơ
contract Logic { address public implementation; uint public x = 99; event CallSuccess(); function increment() external returns(uint) { emit CallSuccess(); return x + 1; }
}
Caller contract
contract Caller{ address public proxy; // proxy contract address constructor(address proxy_){ proxy = proxy_; } // gọi hàm increment() thông qua proxy contract function increment() external returns(uint) { ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()")); return abi.decode(data,(uint)); }
}
Chạy thử
1. Deploy Logic contract
2. Gọi thẳng hàm increment()
từ logic contract => trả về 100
3. Deploy proxy contract
4. Gọi increment
thông qua proxy
5. Deploy Caller
6. Gọi increment
từ Caller, trả về 1
46. Upgrade
Bây giờ chúng ta thử thay đổi logic contract với proxy và xem điều gì sẽ xảy ra.
Proxy contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract SimpleUpgrade { // logic contract's address address public implementation; // admin address address public admin; // string variable, could be changed by logic contract's function string public words; constructor(address _implementation){ admin = msg.sender; implementation = _implementation; } // fallback function fallback() external payable { (bool success, bytes memory data) = implementation.delegatecall(msg.data); } // upgrade function,thay đổi địa chỉ logic contract function upgrade(address newImplementation) external { require(msg.sender == admin); implementation = newImplementation; }
}
Logic contract cũ
// Logic contract 1
contract Logic1 { address public implementation; address public admin; string public words; // Change state variables in Proxy contract, selector: 0xc2985578 function foo() public { words = "old"; }
}
Logic contract mới
contract Logic2 { address public implementation; address public admin; string public words; // Change state variables in Proxy contract, selector: 0xc2985578 function foo() public{ words = "new"; }
}
Chạy thử
1. Deploy contract Logic1 và Logic2
2. Deploy proxy contract với implementation
là địa chỉ logic contract cũ
3. Gọi hàm foo
với selector 0xc2985578
=> biến words
bây giờ có giá trị là "old"
4. Upgrade contract (truyền địa chỉ Logic contract mới vào)
5. Gọi hàm foo
, giá trị words
được thay đổi thành 'new'
47. Transparent Proxy
Selector Clash (trùng selector)
function signature trong Solidity gồm 4 bytes. Ví dụ như mint(address account)
sẽ là 0x6a627842
.
Xem lại về selector ở đây
Do không gian chỉ có 4 bytes nên việc 2 hàm trùng selector sẽ không khó gặp
// Đều là 0x42966c68
// Việc hai hàm có cùng giá trị selector gọi là Selector Clash
contract Foo { function burn(uint256) external {} function collate_propagate_storage(bytes16) external {}
}
Xuất hiện vấn đề là Proxy và Logic contract có thể xuất hiện 2 hàm có giá trị selector trùng nhau. Trong trường hợp 1 hàm a
nào đó trên Logic contract trùng với hàm upgrade
trên proxy contract. Như vậy, do trùng selector nên hàm a sẽ không gọi được, thay vào đó là hàm upgrade
, đây là 1 rủi ro bảo mật lớn.
Transparent
Transparent là 1 giải pháp giúp giải quyết vấn đề selector clash nêu trên với ý tưởng rất đơn giản. Chỉ admin mới có thể gọi hàm upgrade
, người dùng thông thường sẽ không thể gọi hàm upgrade
.
Proxy Contract
// DO NOT USE IN PRODUCTION
contract TransparentProxy { // logic contract's address address implementation; // admin address address admin; string public words; constructor(address _implementation){ admin = msg.sender; implementation = _implementation; } fallback() external payable { require(msg.sender != admin); (bool success, bytes memory data) = implementation.delegatecall(msg.data); } // chỉ có thể gọi bởi admin function upgrade(address newImplementation) external { if (msg.sender != admin) revert(); implementation = newImplementation; }
}
Logic contract
// logic contract cũ
contract Logic1 { address public implementation; address public admin; string public words; // selector 0xc2985578 function foo() public{ words = "old"; }
} // logic contract mới
contract Logic2 { address public implementation; address public admin; string public words; // selector 0xc2985578 function foo() public{ words = "new"; }
}
Chạy thử
1. Deploy Logic1 và Logic2
2. Deploy Proxy
3. Sử dụng selector 0xc2985578
để gọi foo()
trong Logic1 bằng tài khoản admin
Giao dịch bị revert do admin không thể gọi hàm của Logic contract.
4. Đổi ví, gọi lại foo()
5. Dùng ví admin để gọi hàm upgrade()
6. Sử dụng ví người dùng, gọi foo
để kiểm tra trạng thái mới
48. UUPS
UUPS là một giải pháp khác giúp giải quyết vấn đề selector clash cùng với Transparent Proxy.
Ý tưởng của UUPS là chuyển hàm upgrade
sang Logic contract thay vì nằm tại Proxy contract. Do đó, nếu 2 hàm bất kỳ trong Logic contract bị trùng selector, quá trình biên dịch sẽ báo lỗi
UUPS proxy contract
contract UUPSProxy { // Address of the logic contract address public implementation; // Address of admin address public admin; string public words; constructor(address _implementation){ admin = msg.sender; implementation = _implementation; } // Fallback function fallback() external payable { (bool success, bytes memory data) = implementation.delegatecall(msg.data); }
}
UUPS Logic Contract
contract UUPS1 { address public implementation; address public admin; string public words; // selector: 0xc2985578 function foo() public{ words = "old"; } // upgrade function, chỉ admin mới thực thi được. selector: 0x0900f010 function upgrade(address newImplementation) external { require(msg.sender == admin); implementation = newImplementation; }
} // UUPS logic contract mới
contract UUPS2{ address public implementation; address public admin; string public words; // selector: 0xc2985578 function foo() public{ words = "new"; } // upgrade function function upgrade(address newImplementation) external { require(msg.sender == admin); implementation = newImplementation; }
}
Chạy thử
1. Deploy UUPS1 và UUPS2
2. Deploy UUPSProxy
3. Gọi hàm foo
với selector, biến words
được đặt thành old
4. Upgrade sang UUPS2
Tính toán data để gọi hàm upgrade
(vẫn phải gọi qua fallback function từ contract proxy)
5.Gọi hàm foo()
với logic contract mới
49. Multisig Wallet
Khác với các ví người dùng thông thường khác, ví multisig yêu cầu từ 2 chữ ký trở lên để có thể thực hiện giao dịch. Ví Multisig có thể ngăn chặn rủi ro như private key hay ý đồ xấu từ số ít cá nhân và được sử dụng phổ biến trong các DAO.
Vitalik từng đang 1 tweet đại ý rằng ví Multisig an toàn hơn ví cứng
Multisig wallet contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract MultisigWallet { event ExecutionSuccess(bytes32 txHash); event ExecutionFailure(bytes32 txHash); // danh sách các chủ sở hữu ví multisig address[] public owners; // mapping kiểm tra xem địa chỉ truyền vào có phải là 1 trong các chủ sở hữu không ? mapping(address => bool) public isOwner; // số lượng chủ của ví uint256 public ownerCount; // số lượng chữ ký cần tối thiểu để thực thi giao dịch (threshold <= ownerCount) uint256 public threshold; uint256 public nonce; // nonce,prevent signature replay attack receive() external payable {} constructor(address[] memory _owners, uint256 _threshold) { _setupOwners(_owners, _threshold); } // Khởi tạo các giá trị của biến trạng thái function _setupOwners( address[] memory _owners, uint256 _threshold ) internal { require(threshold == 0, "WTF5000"); require(_threshold <= _owners.length, "WTF5001"); // số lượng chữ ký tối thiểu phải lớn hơn 1 require(_threshold >= 1, "WTF5002"); for (uint256 i = 0; i < _owners.length; i++) { address owner = _owners[i]; require( owner != address(0) && owner != address(this) && !isOwner[owner], "WTF5003" ); owners.push(owner); isOwner[owner] = true; } ownerCount = _owners.length; threshold = _threshold; } // dựa trên dataHash và chữ ký, xác thực các chữ ký function checkSignatures( bytes32 dataHash, bytes memory signatures ) public view { uint256 _threshold = threshold; require(_threshold > 0, "WTF5005"); // kiểm tra xem có đủ số chữ ký tối thiểu không (mỗi chữ ký dài 65 bytes) require(signatures.length >= _threshold * 65, "WTF5006"); address lastOwner = address(0); address currentOwner; uint8 v; bytes32 r; bytes32 s; uint256 i; for (i = 0; i < _threshold; i++) { (v, r, s) = signatureSplit(signatures, i); // sử dụng ECDSA để xác thực chữ ký // địa chỉ ký phải có trong danh sách owners currentOwner = ecrecover( keccak256( abi.encodePacked( "\x19Ethereum Signed Message:\n32", dataHash ) ), v, r, s ); require( currentOwner > lastOwner && isOwner[currentOwner], "WTF5007" ); lastOwner = currentOwner; } } // tách chữ ký dạng bytes thành dạng (v, r, s) function signatureSplit( bytes memory signatures, uint256 pos ) internal pure returns (uint8 v, bytes32 r, bytes32 s) { // signature format: {bytes32 r}{bytes32 s}{uint8 v} assembly { let signaturePos := mul(0x41, pos) r := mload(add(signatures, add(signaturePos, 0x20))) s := mload(add(signatures, add(signaturePos, 0x40))) v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) } } function encodeTransactionData( address to, uint256 value, bytes memory data, uint256 _nonce, uint256 chainid ) public pure returns (bytes32) { bytes32 safeTxHash = keccak256( abi.encode(to, value, keccak256(data), _nonce, chainid) ); return safeTxHash; } /* thực thi giao dịch khi số lượng chữ ký đã đủ - signatures: biểu diễn bằng bytes tất cả các chữ ký của người sở hữu Transaction hash: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66 Multisig person A signature: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c Multisig person B signature: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b Packaged signatures:
0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b */ function execTransaction( address to, uint256 value, bytes memory data, bytes memory signatures ) public payable virtual returns (bool success) { bytes32 txHash = encodeTransactionData( to, value, data, nonce, block.chainid ); nonce++; // Check signatures checkSignatures(txHash, signatures); // Thực thi giao dịch và kiểm tra kết quả (success, ) = to.call{value: value}(data); require(success, "WTF5004"); if (success) emit ExecutionSuccess(txHash); else emit ExecutionFailure(txHash); }
}
Chạy thử trên Remix
1. Deploy contract multisig
Có 2 người tham gia và cần cả hai chữ ký để thực hiện giao dịch.
Người dùng 1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Người dùng 2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
2. Chuyển 1 ETH vào multisig contract
3. Gọi hàm encodeTransaction
với tham số
Tham số:
to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
value: 1000000000000000000
data: 0x
_nonce: 0
chainid: 1 Kết quả:
Transaction hash: 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231fc39b
=> Tạo giao dịch gửi 1 ETH từ multisig tới ví của người dùng 1.
4. Ký
Chữ ký của người dùng 1: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11c Chữ ký của người dùng 2: 0xbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c Ghép 2 chữ ký lại: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c
5. Gọi hàm execTransaction()
Do 2 chữ ký đều hợp lệ nên giao dịch sẽ được thực thi.
Nguồn, tài liệu tham khảo
https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en