攻击者利用StakingPool合约中没有对意料之外的值做处理,循环利用获得大量Token,最后通过Pancake砸盘获利。
攻击者部署了2个合约,2个攻击合约相似度高,以其中一个攻击合约为例,攻击者主要的攻击步骤为三步:
invest
方法Withdraw
方法WithdrawToken
方法分别对应选取一笔交易进行分析,先来看调用invest
方法的这笔交易
https://bscscan.com/tx/0x63e8185c867de4c0d09b8df7813647d0aca0596a0e786cd8c6f4202053edc0bc
其实际调用了被攻击合约的invest
方法:
参照对应的StakingPool中的invest合约代码,攻击者给形参end_date
传入了0,给形参qty_ort
传入了1,过了require(qty_ort > 0, "amount cannot be 0")
的判断,之后由于end_date
输入了意外之外的值0,直接过了if...eles if...
,并调用内部_Check_reward
方法开始计算奖励,并写入结构体中。
function invest(uint256 end_date, uint256 qty_ort) external whenNotPaused {
require(qty_ort > 0, "amount cannot be 0");
//transfer token to this smart contract
stakeToken.approve(address(this), qty_ort);
stakeToken.safeTransferFrom(msg.sender, address(this), qty_ort);
//save their staking duration for further use
if (end_date == 3) {
end_staking[msg.sender] = block.timestamp + 90 days;
duration[msg.sender] = 3;
} else if (end_date == 6) {
end_staking[msg.sender] = block.timestamp + 180 days;
duration[msg.sender] = 6;
} else if (end_date == 12) {
end_staking[msg.sender] = block.timestamp + 365 days;
duration[msg.sender] = 12;
} else if (end_date == 24) {
end_staking[msg.sender] = block.timestamp + 730 days;
duration[msg.sender] = 24;
}
//calculate reward tokens for further use
uint256 check_reward = _Check_reward(duration[msg.sender], qty_ort) /
1 ether;
//save values in array
tokens_staking.push(
staking(
msg.sender,
qty_ort,
end_staking[msg.sender],
duration[msg.sender],
true,
check_reward
)
);
//save array index in map
uint256 lockId = tokens_staking.length - 1;
userStake[msg.sender].push(lockId);
emit Invest(
msg.sender,
lockId,
end_staking[msg.sender],
duration[msg.sender],
qty_ort,
check_reward
);
}
跟进计算奖励的_Check_reward
方法,内部写了一个if...else if...
来计算奖励,但由于攻击者传入的end_date
为0,因此duration[msg.sender]
对应的值也为0,导致_Check_reward
直接返回了total_percent,从Events事件可以看到total_percent值为8930620160000000000000000:
以上就是调用invest
方法的分析,接着再来看下withdraw
的这笔交易:
https://bscscan.com/tx/0xd90f2ccfcd0fdbd002af7b21b51732a7a08d0bd3af2d298ec275062a11eb3019
实际是调用了被攻击合约的withdrawAndClaim方法,其会调用内部2个方法_withdraw
、_claim
,需要传入的是lockId,lockId,在invest
交易的Log日志中有输出。
function withdrawAndClaim(uint256 lockId)
external
nonReentrant
whenNotPaused
onlyStakeOwner(lockId)
{
_withdraw(lockId);
_claim(lockId);
}
分析_withdraw方法其作用是用于领取之前质押进去的Token。
function _withdraw(uint256 lockId) internal {
require(tokens_staking[lockId].balances > 0, "not an investor");
require(
block.timestamp >= tokens_staking[lockId].end_staking,
"too early"
);
//Change status of invester for reward
uint256 invested_balance = 0;
invested_balance = tokens_staking[lockId].balances;
tokens_staking[lockId].changeClaimed = false;
tokens_staking[lockId].balances = 0;
tokens_staking[lockId].duration = 0;
tokens_staking[lockId].end_staking = 0;
//transfer back amount to user
stakeToken.safeTransfer(msg.sender, invested_balance);
emit Withdraw(msg.sender, lockId, invested_balance);
}
分析_claim方法其作用是用于领取奖励。
function _claim(uint256 lockId) internal {
require(tokens_staking[lockId].check_claim > 0, "No Reward Available");
require(
tokens_staking[lockId].changeClaimed == false,
"Already claimed"
);
//change status of invested user
tokens_staking[lockId].changeClaimed = true;
uint256 claimed_amount = 0;
claimed_amount = tokens_staking[lockId].check_claim;
tokens_staking[lockId].check_claim = 0;
//mint new ort token
rewardToken.mint(msg.sender, claimed_amount);
emit Claim(msg.sender, lockId, claimed_amount);
}
最后调用withdrawToken
方法将攻击合约中的Token全部取出。
https://bscscan.com/tx/0x7aac05fecd19b19c28929b29262fb848eb00323d9f2b496d422e795fd9869f70
攻击者循环利用3步,将获得的Token通过Pancake砸盘获利。 https://bscscan.com/tx/0xa68442bd5ae35711e4e7f26d02787a3d2aed9db7084f48e3b7ed722a3487c629
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!