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

Làm thế nào để upgrade một smart contract trên ethereum ? (Phần 2 - Phần cuối)

0 0 106

Người đăng: thiên thần gãy cánh

Theo Viblo Asia

4. Triển khai một upgradeable contract theo cách Unstructured Storage

Đây là cách được Openzepplin chuẩn hóa và publish miễn phí tại đây và gọi nôm na cách triển khai này là Proxy. Toàn bộ code thực hành của bài này mình để tại đây.

Cốt lõi của cách làm này là họ sẽ thực hiện delegatecall trong assembly{} và đặt nó trong fallback() của contract:

// SampleProxy
pragma solidity >=0.6.0 <0.8.0; contract SampleProxy { constructor (address _logic, bytes memory _data) public payable { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); _setImplementation(_logic); if(_data.length > 0) { // solhint-disable-next-line avoid-low-level-calls (bool success,) = _logic.delegatecall(_data); require(success); } } /** * @dev Emitted when the implementation is upgraded. */ event Upgraded(address indexed implementation); bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; function _implementation() internal view returns (address impl) { bytes32 slot = _IMPLEMENTATION_SLOT; // solhint-disable-next-line no-inline-assembly assembly { impl := sload(slot) } } function _fallback() internal { _delegate(_implementation()); } function _delegate(address implementation) internal { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } function upgradeToAndCall(address newImplementation, bytes calldata data) external payable { _upgradeTo(newImplementation); // solhint-disable-next-line avoid-low-level-calls (bool success,) = newImplementation.delegatecall(data); require(success); } /** * @dev Upgrades the proxy to a new implementation. * * Emits an {Upgraded} event. */ function _upgradeTo(address newImplementation) internal { _setImplementation(newImplementation); emit Upgraded(newImplementation); } /** * @dev Stores a new address in the EIP1967 implementation slot. */ function _setImplementation(address newImplementation) private { bytes32 slot = _IMPLEMENTATION_SLOT; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, newImplementation) } } /** * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other * function in the contract matches the call data. */ fallback () external payable { _fallback(); }
}
pragma solidity >=0.6.0 <0.8.0;
contract SampleLogic { address public owner; bool public initialized; mapping(address => uint256) public balances; modifier initializer() { require(!initialized, "Initializable: contract is already initialized"); _; } modifier onlyOwner() { require(msg.sender == owner, "Caller is not owner"); _; } constructor () public { } function initialize() public initializer { owner = msg.sender; initialized = true; } function mint(address _recipient, uint256 _amount) public onlyOwner { balances[_recipient] = balances[_recipient] + _amount; }
}

Như ta đã biết:

  • khi gọi đến một function không được định nghĩa trong contract X nào đó, thì nó sẽ được tự gọi đến function fallback() của contract X
  • mặt khác, tất cả các cuộc gọi đến function của contract dù là function chỉ có chức năng view hay là function thay đổi storage thì nó đều được mã hóa trong biến msg.data

Kết hợp 2 điều này, Openzeppelin đã đặt một đoạn code assembly{} bên trong fallback() để triển khải lõi của Proxy, Nhìn vào 2 contract mẫu ở trên, ta thấy:

  • SampleProxy có các function:

    • constructor(address _logic, bytes memory _data)
    • upgradeToAndCall(address newImplementation, bytes calldata data)
    • fallback()

    mặt khác nó còn một giá trị _implementation được lưu ở slot ngẫu nhiên 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc . Lý do ta không khai báo một biến để lưu giá trị _implementation mà lại lưu trữ theo kiểu ghi vào slot là vì nếu khai báo biến, rất có thể sẽ bị xung đột lưu trữ với các biến của contract SampleLogic (xem lại Phần 1).

  • SampleLogic có các function:

    • constructor()
    • initialize()
    • mint(address _recipient, uint256 _amount)

Thứ tự thực hiện sẽ là như sau:

  • Deploy SampleLogic ta thu được địa chỉ của SampleLogic là sample_logic_address
  • Deploy SampleProxy cùng với các tham số sample_logic_address, 0x8129fc1c (trong đó 0x8129fc1c là selector của function initialize() được định nghĩa trong SampleLogic), ta thu được địa chỉ sample_proxy_address.
  • Thực hiện SampleLogic(sample_proxy_address).mint(user, '1000')
  • Kiểm tra số dư của user SampleLogic(sample_proxy_address).balances(user) và thấy nó bằng 1000.

Mặc dù trong code của SampleProxy không có các function mint() cũng như balances() thế nhưng khi tạo instance SampleLogic tại địa chỉ sample_proxy_address vẫn có thể gọi được các function đó, lý do ta đã nói ở trên.

Bây giờ ta sẽ upgrade SampleProxy sang một SampleLogic2 có mã nguồn dưới đây, nó khác với SampleLogic là nó sẽ mint cho user gấp đôi amount truyền vào:

pragma solidity >=0.6.0 <0.8.0; contract SampleLogic2 { address public owner; bool public initialized; mapping(address => uint256) public balances; modifier initializer() { require(!initialized, "Initializable: contract is already initialized"); _; } modifier onlyOwner() { require(msg.sender == owner, "Caller is not owner"); _; } constructor () public { } function initialize() public initializer { owner = msg.sender; initialized = true; } function mint(address _recipient, uint256 _amount) public onlyOwner { balances[_recipient] = balances[_recipient] + _amount * 2; }
}

Ta gọi function upgradeToAndCall() của SampleProxt và truyền vào địa chỉ của SampleLogic2 vừa deploy cùng với một chuỗi bytes có ý nghĩa mint cho user 1000.

Sau đó ta check lại balances(user) sẽ nhận được kết quả là 3000.

Đây là toàn code để test:

const SampleProxy = artifacts.require('SampleProxy');
const SampleLogic2 = artifacts.require('SampleLogic2');
const SampleLogic = artifacts.require('SampleLogic'); contract('Sample', ([owner, user, someuser]) => { it('SampleProxy test', async () => { // deploy SampleLogic let sampleLogic = await SampleLogic.new({ from: someuser }); // get selector of initialize() function let data = sampleLogic.contract.methods.initialize().encodeABI(); // deploy SampleProxy let sampleProxy = await SampleProxy.new(sampleLogic.address, data, { from: owner }); // create SampleLogic instance at SampleProxy's address let realSampleLogic = await SampleLogic.at(sampleProxy.address); // check owner assert.equal(await realSampleLogic.owner(), owner); // mint for user 1000 await realSampleLogic.mint(user, '1000', { from: owner }); // check user's balance assert.equal(await realSampleLogic.balances(user), '1000'); // deploy SampleLogic2 let sampleLogic_2 = await SampleLogic2.new({ from: someuser }); data = sampleLogic_2.contract.methods.mint(user, '1000').encodeABI(); // upgrade _implementation of Proxy to SampleLogic2's address and mint to user 2000 await sampleProxy.upgradeToAndCall(sampleLogic_2.address, data); // check user's balance assert.equal(await realSampleLogic.balances(user), '3000'); });
});

5. Triển khai một upgradeable ERC20 theo tiêu chuẩn của Openzepplin

Bên trên là cách triển khai một Proxy đơn giản, tuy nhiên trong thực tế việc quản lý Proxy không đơn giản như thế, nó phải có phân quyền admin, người mà có quyền upgrade _implemmentation của Proxy, vì nếu để ai cũng upgrade được thì Proxy hoàn toàn vô tác dụng.

Rất may là Openzeplin đã chuẩn hóa điều này cho chúng ta, mình chỉ cần implement nó. Đây là repos mình chuẩn bị sẵn để các bạn có thể tham khảo xem tại đây.

Mình sẽ hướng dẫn các bạn cách tạo một upgradeable ERC20 (tạm gọi là DAI) bằng cách kế thừa code đã được chuẩn hóa của Openzepplin:

pragma solidity >=0.6.0 <0.8.0; import "./token/ERC20/ERC20Upgradeable.sol";
import "./proxy/Initializable.sol";
import "./access/OwnableUpgradeable.sol"; contract DAI is ERC20Upgradeable, OwnableUpgradeable { function initialize(string memory name_, string memory symbol_) initializer public { __ERC20_init(name_, symbol_); __Ownable_init(); } function mint(address recipient, uint256 amount) public onlyOwner { _mint(recipient, amount); }
}

Đây là logic đầu tiên của DAI, các file được import là mã nguồn đã được chuẩn hóa của Openzeplin, logic này sẽ mint sẽ mint một lượng amount cho recipient, contract này có thể deploy bởi bất kỳ user nào (someuser) vì nó chỉ đóng vai trò logic.

pragma solidity >=0.6.0 <0.8.0; import "./proxy/ProxyAdmin.sol"; contract DAIProxyAdmin is ProxyAdmin { constructor () public { }
}

Đây là contract DaiProxyAdmin, nó import ProxyAdmin.sol đã được chuẩn hóa của Openzepplin, nó quản lý và có quyền upgrade giá trị _implimentation của các DAIProxy mà nó quản lý. Tại sao không dùng trực tiếp một địa chỉ ví để quản lý các Proxy mà lại phải thông qua một contract ProxyAdmin? Các bạn tự tìm hiểu nhé ?

pragma solidity >=0.6.0 <0.8.0; import "./proxy/TransparentUpgradeableProxy.sol"; contract DAIProxy is TransparentUpgradeableProxy { constructor (address _logic, address admin_, bytes memory _data) public TransparentUpgradeableProxy(_logic, admin_, _data) { }
}

Đây là file DaiProxy, sau này nó sẽ hoạt động như một ERC20, nó import TransparentUpgradeableProxy.sol đã được chuẩn hóa bởi Openzepplin, contract này phải được deploy bởi daiOwner

pragma solidity >=0.6.0 <0.8.0; import "./token/ERC20/ERC20Upgradeable.sol";
import "./proxy/Initializable.sol";
import "./access/OwnableUpgradeable.sol"; contract DAI_2 is ERC20Upgradeable, OwnableUpgradeable { function initialize(string memory name_, string memory symbol_) initializer public { __ERC20_init(name_, symbol_); __Ownable_init(); } function mint(address recipient, uint256 amount) public onlyOwner { _mint(recipient, amount * 2); }
}

Đây là file DAI_2, nó chỉ khác so với DAI là nó mint một lương amount * cho recipient.

Bây giờ mình sẽ thực hiện các bước:

    1. Deploy DAI bằng account someuser, được dai
    1. Deploy DAIProxyAdmin bằng account proxyAdmin, được daiProxyAdmin
    1. Lấy dạng bytes của initialize('DAI', 'DAI') (calldata)
    1. Deploy DAIProxy bằng account daiOwner và truyền vào các tham số: dai.Address, daiProxyAdmin.address, calldata, được daiProxy.
    1. Tạo một instance DAI khác tại địa chỉ daiProxy.address, được realDai.
    1. Gọi realDAI.mint(user, '1000') bằng account daiOwner
    1. Gọi kiểm tra realDAI.balanceOf(user) thì nhận được kết quả là 1000.
    1. Deploy DAI2 bằng account someuser, được dai_2
    1. Upgrade lại implementation của daiProxy bằng cách gọi daiProxyAdmin.upgrade(daiProxy.address, dai_2.address) bằng account proxyAdmin .
    1. Gọi realDAI.mint(user, '1000') bằng account daiOwner.
    1. Gọi kiểm tra realDAI.balanceOf(user) thì nhận được kết quả là 3000.

Đây là toàn bộ code chạy test:

const DAI = artifacts.require('DAI');
const DAI_2 = artifacts.require('DAI_2'); const DAIProxy = artifacts.require('DAIProxy');
const DAIProxyAdmin = artifacts.require('DAIProxyAdmin'); contract('DAI Proxy', ([proxyAdmin, daiOwner, user, someuser]) => { it('DAI Proxy', async () => { // deploy DAI let dai = await DAI.new({ from: someuser }); // deploy DAIProxyAdmin let daiProxyAdmin = await DAIProxyAdmin.new({ from: proxyAdmin }); // Get bytes of initialize('DAI', 'DAI') let data = await dai.contract.methods.initialize('DAI', 'DAI').encodeABI(); // deploy DAIProxy let daiProxy = await DAIProxy.new(dai.address, daiProxyAdmin.address, data, { from: daiOwner, }); // create DAI instance at daiProxy.address let realDai = await DAI.at(daiProxy.address); // Mint 1000 for user await realDai.mint(user, '1000', { from: daiOwner }); // check user's balance = 1000 assert.equal(await realDai.balanceOf(user), '1000'); // deploy DAI_2 let dai_2 = await DAI_2.new({ from: someuser }); // upgrade daiProxy's implementation to dai_2.address await daiProxyAdmin.upgrade(daiProxy.address, dai_2.address); // mint 1000 for user await realDai.mint(user, '1000', { from: daiOwner }); // check user's balance = 3000 assert.equal(await realDai.balanceOf(user), '3000'); });
});

Tổng kết

Vấn để Upgradeable Contract hay còn gọi là Proxy trong solidity là một vấn đề tương đối khó, đòi hỏi phải thực hành nhiều lần thì mới hiểu được, nhưng lợi ích nó mang lại là cực kỳ lớn, nó giúp chúng ta bảo vệ ứng dụng ứng dụng và tài sản của mình tốt hơn, tránh những incident đáng tiếc xảy ra. Và để một smart contract có thể upgrade được thì chúng ta phải làm nó có thể upgrade ngay từ đầu chứ không thể nào upgrade một smart contract đã được deploy từ rất lâu mà không có cơ chế upgrade.

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 78

- 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