
개요
해당 포스트는 2023년 2월 22일 Dynamic Finance에서 일어난 약 23,000 달러의 해킹 사건을 분석한 포스트입니다.
컨트랙트 취약점과 해킹 기법, 취약점 수정에 초점을 맞추어 포스트를 작성하였습니다.
취약한 컨트랙트
해킹 대상이 된 컨트랙트는 StakingDYNA로 링크를 통해 확인할 수 있습니다.
아래는 해당 컨트랙트의 소스코드입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract StakingDYNA is Ownable {
using SafeMath for uint256;
uint256 public apr = 3200;
uint256 constant RATE_PRECISION = 10000;
uint256 constant ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
uint256 constant ONE_DAY_IN_SECONDS = 24 * 60 * 60;
uint256 constant PERIOD_PRECISION = 10000;
IERC20 public token;
bool public enabled;
event Deposit(address indexed user, uint256 amount);
event Redeem(address indexed user, uint256 amount);
constructor(IERC20 _token) {
token = _token;
}
struct StakeDetail {
uint256 principal;
uint256 lastProcessAt;
uint256 pendingReward;
uint256 firstStakeAt;
}
mapping(address => StakeDetail) public stakers;
function setEnabled(bool _enabled) external onlyOwner {
enabled = _enabled;
}
function updateAPR(uint256 _apr) external onlyOwner {
apr = _apr;
}
function emergencyWithdraw(uint256 _amount) external onlyOwner {
token.transfer(msg.sender, _amount);
}
function getStakeDetail(address _staker)
public
view
returns (
uint256 principal,
uint256 pendingReward,
uint256 lastProcessAt,
uint256 firstStakeAt
)
{
StakeDetail memory stakeDetail = stakers[_staker];
return (
stakeDetail.principal,
stakeDetail.pendingReward,
stakeDetail.lastProcessAt,
stakeDetail.firstStakeAt
);
}
function getInterest(address _staker) public view returns (uint256) {
StakeDetail memory stakeDetail = stakers[_staker];
uint256 duration = block.timestamp.sub(stakeDetail.lastProcessAt);
uint256 interest = stakeDetail
.principal
.mul(apr)
.mul(duration)
.div(ONE_YEAR_IN_SECONDS)
.div(RATE_PRECISION);
return interest.add(stakeDetail.pendingReward);
}
function deposit(uint256 _stakeAmount) external {
require(enabled, "Staking is not enabled");
require(
_stakeAmount > 0,
"StakingDYNA: stake amount must be greater than 0"
);
token.transferFrom(msg.sender, address(this), _stakeAmount);
StakeDetail storage stakeDetail = stakers[msg.sender];
if (stakeDetail.firstStakeAt == 0) {
stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
stakeDetail.firstStakeAt = stakeDetail.firstStakeAt == 0
? block.timestamp
: stakeDetail.firstStakeAt;
stakeDetail.lastProcessAt = block.timestamp;
} else {
stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
}
emit Deposit(msg.sender, _stakeAmount);
}
function redeem(uint256 _redeemAmount) external {
require(enabled, "Staking is not enabled");
StakeDetail storage stakeDetail = stakers[msg.sender];
require(stakeDetail.firstStakeAt > 0, "StakingDYNA: no stake");
uint256 interest = getInterest(msg.sender);
uint256 claimAmount = interest.mul(_redeemAmount).div(
stakeDetail.principal
);
uint256 remainAmount = interest.sub(claimAmount);
stakeDetail.lastProcessAt = block.timestamp;
require(
stakeDetail.principal >= _redeemAmount,
"StakingDYNA: redeem amount must be less than principal"
);
stakeDetail.principal = stakeDetail.principal.sub(_redeemAmount);
stakeDetail.pendingReward = remainAmount;
require(
token.transfer(msg.sender, _redeemAmount.add(claimAmount)),
"StakingDYNA: transfer failed"
);
emit Redeem(msg.sender, _redeemAmount.add(claimAmount));
}
}
컨트랙트 기능 분석
해당 컨트랙트의 취약점을 분석하기 위해서는 2개의 함수를 보아야 합니다.
deposit 함수
function deposit(uint256 _stakeAmount) external {
require(enabled, "Staking is not enabled");
require(
_stakeAmount > 0,
"StakingDYNA: stake amount must be greater than 0"
);
token.transferFrom(msg.sender, address(this), _stakeAmount);
StakeDetail storage stakeDetail = stakers[msg.sender];
if (stakeDetail.firstStakeAt == 0) {
stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
stakeDetail.firstStakeAt = stakeDetail.firstStakeAt == 0
? block.timestamp
: stakeDetail.firstStakeAt;
stakeDetail.lastProcessAt = block.timestamp;
} else {
stakeDetail.principal = stakeDetail.principal.add(_stakeAmount);
}
emit Deposit(msg.sender, _stakeAmount);
}
deposit 함수는 staker가 토큰을 전송하면 그 토큰을 받아 stakeDetail을 업데이트하는 기능을 가지고 있습니다.
이전에 한번도 deposit을 하지 않은 경우 stakeDetail를 전체적으로 업데이트하고, deposit을 한 경우는 principal만 업데이트해줍니다.
redeem 함수
function redeem(uint256 _redeemAmount) external {
require(enabled, "Staking is not enabled");
StakeDetail storage stakeDetail = stakers[msg.sender];
require(stakeDetail.firstStakeAt > 0, "StakingDYNA: no stake");
uint256 interest = getInterest(msg.sender);
uint256 claimAmount = interest.mul(_redeemAmount).div(
stakeDetail.principal
);
uint256 remainAmount = interest.sub(claimAmount);
stakeDetail.lastProcessAt = block.timestamp;
require(
stakeDetail.principal >= _redeemAmount,
"StakingDYNA: redeem amount must be less than principal"
);
stakeDetail.principal = stakeDetail.principal.sub(_redeemAmount);
stakeDetail.pendingReward = remainAmount;
require(
token.transfer(msg.sender, _redeemAmount.add(claimAmount)),
"StakingDYNA: transfer failed"
);
emit Redeem(msg.sender, _redeemAmount.add(claimAmount));
}
redeem 함수는 기존에 msg.sender가 deposit한 token을 인출할 때 getInterest를 통해 계산된 이자도 비율에 맞추어 함께 지급하는 기능을 가진 함수입니다.
getInterest 함수는 deposit 함수에서 설정된 stakeDetail을 통해 계산되며, 결과값은 principal에 비례한다는 점이 중요합니다.
취약점 분석
이 컨트랙트의 취약점은 deposit 함수에 존재합니다.
이전에 deposit한 msg.sender가 다시 deposit을 한 경우 stakeDetail의 principal만 변경하게 되는데, 이를 활용해서 getInterest를 통해 계산되는 이자 결과값을 원하는 값으로 얻을 수 있습니다.
공격 순서는 다음과 같습니다.(취약한 컨트랙트의 token 개수 : token_amount)
1. 공격 시점의 1~2일 전쯤, deposit(0)을 실행합니다.
(이를 통해 공격자의 stakeDetail을 업데이트합니다.)
2. 공격 시점에서 Exploit.sol 컨트랙트를 디플로이하고 Attack 함수를 실행합니다.
Attack 함수의 기능은 아래와 같습니다.
2-1. Flashloan, Swap을 활용하여 token X개를 확보합니다.(다른 컨트랙트들의 함수를 활용합니다.)
2-2. deposit(X)를 통해 공격자의 stakeDetail의 principal 값을 업데이트합니다.(stakeDetail.principal = X)
2-3. redeem(X)를 실행하여 컨트랙트의 token을 모두 가져옵니다.
(getInterest(msg.sender) = aX = token_amount)
해당 취약점을 통해 컨트랙트의 모든 토큰을 해킹할 수 있습니다.
'Blockchain-Hacking > Hacking-analysis' 카테고리의 다른 글
| Dexible Hacking Analysis (0) | 2023.03.05 |
|---|---|
| LaunchZone(LZ finance) Hacking Analysis (0) | 2023.03.05 |
| Hakuna Matata($TATA) Hacking Analysis (0) | 2023.03.03 |