- vừa được xem lúc

WTF Solidity 104

0 0 8

Người đăng: Le Thanh Cong

Theo Viblo Asia

40. WETH

image.png

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 fallbackreceive, 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 Timelock
  • value: không gửi ETH nên truyền vào 0
  • signature: 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

  1. Proxy contract: Lưu trữ dữ liệu, các biến trạng thái
  2. 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.

image

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

47-2.png 47-3.png

2. Deploy proxy contract với implementation là địa chỉ logic contract cũ

47-4.png

3. Gọi hàm foo với selector 0xc2985578 => biến words bây giờ có giá trị là "old"

47-5.png

4. Upgrade contract (truyền địa chỉ Logic contract mới vào)

47-6.png

5. Gọi hàm foo, giá trị words được thay đổi thành 'new'

47-7.png

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

48-2.png 48-3.png

2. Deploy Proxy

48-4.png

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.

48-5.png

4. Đổi ví, gọi lại foo()

48-6.png

5. Dùng ví admin để gọi hàm upgrade()

48-7.png

6. Sử dụng ví người dùng, gọi foo để kiểm tra trạng thái mới

48-8.png

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

demo

2. Deploy UUPSProxy

demo

3. Gọi hàm foo với selector, biến words được đặt thành old

demo

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)

encoding demo

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

demo

2. Chuyển 1 ETH vào multisig contract

50-3.png

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.

50-4.png

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

50-5-1.png

50-5-2.png

50-5-3.png

5. Gọi hàm execTransaction()

Do 2 chữ ký đều hợp lệ nên giao dịch sẽ được thực thi.

50-6.png

Nguồn, tài liệu tham khảo

https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en

https://www.wtf.academy/en/solidity-start/

Bình luận

Bài viết tương tự

- vừa được xem lúc

[Blockchain] Road to Bitcoin

. Chắc mọi người hẳn đã không còn xa lạ gì với anh chàng tỷ phú đã ném vỡ cửa kính ô tô nhà mình cùng với siêu năng lực điều khiển vật giá chỉ bằng lời nói, người đã đẩy định giá Bitcoin trên thị trường vượt ngưỡng 50K dolar/coin với những bài twitter để đời . .

0 0 44

- vừa được xem lúc

Khi Ethereum có chi phí giao dịch quá đắt đỏ - Tương lai cho layer2 ?

Với sự phát triển như vũ bão của Blockchain, ETH dường như đang quá tải và hệ quả là chi phí Gas đã lên đến 1000Gwei, phí để tạo những transaction phức tạp đã xấp xỉ 500$ . Và một giải pháp cứu cánh cho các sản phẩm Defi trên ETH chính là Layer2, và trong nhiệm vụ lần này Matic đang thể hiện khả năn

0 0 73

- vừa được xem lúc

Blockchain với Java - Tại sao không?

Cuộc cách mạng công nghiệp 4.0 ra đời kéo theo nhiều sự thay đổi và xu hướng mới được hình thành. Riêng đối với lĩnh vực CNTT cũng không nằm ngoài vùng ảnh hưởng mạnh mẽ. Chính làn sóng 4.

0 0 79

- vừa được xem lúc

Phân loại và tầm quan trọng của các node trong mạng blockchain

Trước khi đi vào phân loại và nêu rõ được tầm quan trọng của các node trọng mạng blockchain thì mình xin được trích dẫn khái niệm về blockchain từ Wikipedia như sau:. .

0 1 46

- vừa được xem lúc

Code Smart Contract bằng Assembly ?

Introduction. Hồi còn học trong ghế nhà trường bộ môn lập trình tốn nhiều não nhất của mình là code assembly. Nôm na thì bất cứ ngôn ngữ bậc cao nào như C , Go, Java,... được sinh ra để người dễ hiểu và dễ code , tuy nhiên chúng đều sẽ được compiled down xuống assembly một ngôn ngữ bậc thấp để máy h

0 0 43

- vừa được xem lúc

Dextool - Công cụ phân tích Decentralized Exchange tuyệt vời

. Trend Defi mặc dù đã bớt nhiệt nhưng những sản phẩm nổi bật của làn sóng này mang lại thì vẫn rất được người dùng ưa chuộng. Đặc biệt là các nền tảng Decentralized Exchange, tiêu biểu là Uniswap, SushiSwap, 1inch Exchange, FalconSwap,... Nhưng khi đã sử dụng các nền tảng DEx này mà không biết đến

0 0 92