DEFI COVER 被黑过程分析(storage赋值给memo
有漏洞的合约代码:etherscan需要翻墙
/// @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);
}
function claimRewardsForPools(address[] calldata _lpTokens) external override {
for (uint256 i = 0; i < _lpTokens.length; i++) {
claimRewards(_lpTokens[i]);
}
}
function deposit(address _lpToken, uint256 _amount) external override {
require(block.timestamp >= START_TIME , "Blacksmith: not started");
require(_amount > 0, "Blacksmith: amount is 0");
//主要原因就是先将storage赋值给memory,再改变storage的值,然而最后pool.accRewardsPerToken却用的是memory的值
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);
}
整个攻击过程如下图所示:

攻击者首先发起一笔交易,调用deposit函数向Blacksmith存入1525.5552810089260015362LP代币。接着攻击者发起另一笔交易,调用withdraw函数取回1525.5552810089260015361LP代币。这样攻击者在Blacksmith中只剩下1e-18LP代币。但由于此时还有别的用户存入这种LP代币,此时的lpTotal还是很大。
接着,攻击者等待别的用户把他们的LP代币都取走。攻击者很快就等到了一个攻击机会:一位用户通过交易取走了自己的LP代币,此时lpTotal的值为1,即代表1e-18LP代币。
攻击者立即发起了一笔关键的攻击交易,调用deposit函数。根据前面说的攻击原理,此时pool.accRewardsPerToken的旧值为3.37e9,新值为2.67e27。而用旧值错误计算出的miner.rewardWriteoff为5.14e19,远小于正确值4.08e37。
攻击者最后简单的发起一笔交易,调用claimRewards函数,获得了4.08e19COVER代币