Deploy to Local Chain

Deploy lên local chain thông qua RPC URL (là endpoint dùng để giao tiếp với một node trong chain):

forge create SimpleStorage --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Nếu chạy anvil (local chain của Foundry) thì không cần specify rpc-url.

Ngoài ra, có thể dùng flag --interactive để nhập password thay vì specify trong câu lệnh để tránh việc password được lưu trong bash history. Ngoài --interactive, ta cũng có thể tạo file .env (nhớ thêm vào .gitignore):

PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
 
RPC_URL=http://127.0.0.1:8545

Sau đó, ta sử dụng lệnh source .env (trong Linux) để nạp các biến môi trường từ file .env vào shell.

Cuối cùng, thay thế các giá trị trong command trên bằng các biến môi trường:

forge create SimpleStorage --rpc-url $RPC_URL --private-key $PRIVATE_KEY

Keystore

Việc lưu private key ở dạng bản rõ trong .env như đã đề cập ở Deploy to Local Chain là không an toàn. Foundry cung cấp lệnh keystore để tạo và quản lý key.

Cách import private key vào keystore:

cast wallet import <ACCOUNT_NAME> --interactive
Enter private key:
Enter password:
`<ACCOUNT_NAME>` keystore was saved successfully. Address: address-corresponding-to-private-key

Key sẽ được lưu vào thư mục ~/.foundry/keystores. Ví dụ:

cat C:\Users\quan.m.le\.foundry\keystores\account1
{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"847efed5e7b0fa2f8b6c91d0b445c2e4"},"ciphertext":"403e3bb18e45f2ca3d2424143bf47582589c9c2b20651e1ab38b9d6710ac93a9","kdf":"scrypt","kdfparams":{"dklen":32,"n":8192,"p":1,"r":8,"salt":"568b5df60d94d78b7ba38794ae1352426dd1c6fd8bae65beb241e6191cd716ea"},"mac":"c7eb923b53e2b4f488122da77652c468598562433a4968d3eb5e25fddf7eac67"},"id":"2c801f03-d8e5-42aa-b82e-84d076ba8ac2","version":3}

Sau khi có keystore thì ta deploy sử dụng account name như sau:

forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --account <ACCOUNT_NAME> --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Khi đó, ta sẽ cần phải nhập password để decrypt private key.

Deploy to Sepolia

Để deploy lên Sepolia, ta cần RPC URL của Sepolia network thông qua một node thuộc một dịch vụ Node as a Service (NaaS) chẳng hạn như Infura hoặc Alchemy. Nếu dùng Alchemy, chỉ cần tạo một ứng dụng và chọn mạng Etherum sau đó ta sẽ có được RPC URL. Kế đến, import private key của ví metamask thông qua cast và tiến hành deploy như sau:

forge script script/DeploySimpleStorage.s.sol --rpc-url https://eth-sepolia.g.alchemy.com/v2/me-e2dRMLIRdJi_Bf4nyX --broadcast --account account2 --sender 0xD88fa55281904436f2416F57bdFd4A563d8184e4

Note

Câu lệnh trên dùng subcommand script, xem thêm Foundry - Scripting.

Multi-Chain Deployment

Xét hàm sau:

function getVersion() public view returns (uint256) {
	AggregatorV3Interface priceFeed = AggregatorV3Interface(
		0x694AA1769357215DE4FAC081bf1f309aDC325306
	);
	return priceFeed.version();
}

Việc gán cứng address của contract implement AggregatorV3Interface là bad practice trong trường hợp ta deploy contract lên nhiều chain. Khi đó, ta phải thay đổi address mỗi lần deploy lên một chain khác nhau.

Để giải quyết vấn đề này, ta sẽ truyền vào address thông qua constructor của contract:

AggregatorV3Interface private s_priceFeed;
 
constructor(address _priceFeed) {
	i_owner = msg.sender;
	s_priceFeed = AggregatorV3Interface(_priceFeed);
}

Với tiền tố s_ là để chỉ state variable.

Gọi sử dụng các hàm từ interfact như sau:

function getVersion() public view returns (uint256) {
	AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeed);
	return priceFeed.version();
}

Ví dụ sử dụng constructor trong script và test:

contract DeployFundMe is Script {
    function run() external returns (FundMe) {
        vm.startBroadcast();
        FundMe fundMe = new FundMe(0x694AA1769357215DE4FAC081bf1f309aDC325306);
        vm.stopBroadcast();
        return fundMe;
    }
}

Tái sử dụng script trong test:

contract FundMeTest is Test {
    FundMe private fundMe;
    DeployFundMe private deployFundMe;
 
    function setUp() external {
        deployFundMe = new DeployFundMe();
        fundMe = deployFundMe.run();
    }
 
    function testMinimumDollarIsFive() public view {
        assertEq(fundMe.MINIMUM_USD(), 5e18);
    }
 
    function testOwnerIsMsgSender() public {
        assertEq(fundMe.i_owner(), msg.sender);
    }
 
    function testPriceFeedVersionIsAccurate() public {
        uint256 version = fundMe.getVersion();
        assertEq(version, 4);
    }
}

Important

Lúc này, owner của fundMe không còn là địa chỉ của test contract nữa. Thay vào đó, nó sẽ là địa chỉ của sender mà gọi hàm của test contract.

Seealso

Do đó, owner của fundMe trong ngữ cảnh này sẽ là msg.sender.

Để viết smart contract mà có thể được deploy trong ngữ cảnh multichain. Ta có thể viết một helper script như sau:

import {Script} from "forge-std/Script.sol";
 
contract HelperConfig {
    NetworkConfig public activeNetworkConfig;
 
    struct NetworkConfig {
        address priceFeed; // ETH/USD price feed address
    }
 
    constructor() {
        if (block.chainid == 11155111) {
            activeNetworkConfig = getSepoliaEthConfig();
        } else {
            activeNetworkConfig = getAnvilEthConfig();
        }
    }
 
    function getSepoliaEthConfig() public pure returns (NetworkConfig memory) {
        NetworkConfig memory sepoliaConfig = NetworkConfig({
            priceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306
        });
        return sepoliaConfig;
    }
 
    function getAnvilEthConfig() public pure returns (NetworkConfig memory) {}
}

Khi được deploy, contract này sẽ dựa vào chain ID từ block.chainid để lựa chọn config phù hợp. Cụ thể hơn, nếu là Sepolia testnet thì nó sẽ trả về cấu hình có địa chỉ của price feed ở trên Sepolia testnet.

Seealso

Tra cứu chain ID: ChainList

Gọi sử dụng script trên ở trong deploy script như sau:

contract DeployFundMe is Script {
    function run() external returns (FundMe) {
        HelperConfig helperConfig = new HelperConfig();
        address priceFeedAddress = helperConfig.activeNetworkConfig();
        vm.startBroadcast();
        FundMe fundMe = new FundMe(priceFeedAddress);
        vm.stopBroadcast();
        return fundMe;
    }
}

Bằng cách này, chúng ta sẽ không phải gán cứng địa chỉ của price feed contract trong deployment script nữa. Ngoài ra, deployment script có thể tự động xác định địa chỉ của price feed contract tùy thuộc vào chain đang được deploy.

Để cấu hình địa chỉ của price feed contract trên các chain khác, ta chỉ cần thêm vào các hàm get<chain_name>Config trong contract HelperConfig.

Resources