tags: Exploit Analysis Report

EVO DeFi Analysis

Copyright © 2022 by Verilog Solutions. All rights reserved.
April 29, 2022
by Verilog Solutions

Inspired by the recent token bridge exploits, Verilog research team surveyed the existing cross-chain bridge solutions and decided to perform an independent, external security analysis on EVO DeFi Bridge. EVO DeFi Bridge is a cross-chain bridge connected to multiple EVM-compatible chains, including Ethereum, BSC, Fantom, Avalanche, and Oasis Emerald ParaTime. Verilog Solutions has conducted on-chain analytics and external analysis on the open-sourced portion of EVO DeFi Bridge in hope of improving the safety of funds and transparency across the crypto ecosystem.

To conduct the analysis, the Verilog team used various techniques including but not limited to event emission extraction, cross-chain fuzzy matching, and byte code comparison. The goal of this analysis is to discover whether proper collateralization and governance are in place for EVO DeFi Bridge. It is important to note that this analysis was conducted unilaterally without correspondence from EVO DeFi Bridge, and some parts of the source code are not available to the Verilog team. Therefore, this analysis is based on incomplete information and best-effort conjugation.

This report is not intended to be used as any financial advice.


Table of Content


Smart Contract Analysis

The EVO DeFi Bridge Contract Analysis

  1. address 0x06c30Af8A82AAf9cFd319f8644584276Bfbec42f

    This address has been used on BSC, Polygon, Oasis, Fantom, Heco, Ethereum, Avalanche, Arbitrum, Cronos, Moonriver and OKEXchain.
    However, only the contract on BSC has been verified, thus this is the only contract we can analyze.

  2. address 0x7cca0859058aa03301106ab8aba1707a16a30821

    This address has been used on HarmonyONE, Gnosis and Optimism. However, none of the addresses has verified contracts.

Bytecode Analysis

We inspect the bytecode and source code of EVO DeFi Bridge contract in all the aforementioned chains. The code is available at our GitHub repo.

  1. We start with the only available source code, EVO DeFi Bridge @ BSC. We also obtain its opcode and bytecode.

  2. We obtain the opcode representation of EVO DeFi Bridge @ Arbitrum, Ethereum, Fantom and Polygon.
    a. We compare the opcode diff between BSC's and every other. We observe that Arbitrum, Fantom and Polygon have the exact same opcode with BSC, so we confirm that within these three blockchain, EVO DeFi Bridge operates using the same logic.
    b. Comparing the opcode in BSC and in Ethereum, we observe a few different opcodes (as follows). Given only 27 out of 4864 opcodes are different and the others remain the same, the Ethereum version is very likely to be identical with

    ​​​​Unknown Opcode 3d => RETURNDATASIZE
    ​​​​Unknown Opcode 3e => RETURNDATACOPY
    ​​​​Unknown Opcode 1b => SHL
    ​​​​Unknown Opcode 47 => SELFBALANCE
    
  3. We obtain the bytecode of representation of EVO DeFi Bridge @ Avalanche, Cronos, Gnosis, HarmonyONE, Heco, Moonriver and Optimistic.
    a. We observe that the bytecode of EVO DeFi Bridge @ Avalanche is identical to that in BSC, so we confirm EVO DeFi Bridge shares the same logic with Avalanche and BSC.
    b. Although Heco, Cronos, Moonriver, OKEX, HarmonyOne, Gnosis, Optimism are different from the original lBSC version. {Heco, Cronos, Moonriver} share the same set of bytecode; {OKEX, HarmonyOne, Gnosis, Optimism} share another same set of bytecode.

  4. We continue to explore similarities among the BSC version, Heco version and OKEX version. We decompile the bytecode using Panoramix decompiler.

  5. We found that the Heco version and OKEX version implement the same set of ABI as the BSC version:

    ​​​​decompiled interface => source code interface
    ​​​​withdrawn(bytes32 _param1) => mapping(bytes32 => bool) public withdrawn;
    ​​​​owner() => address public owner
    ​​​​operator(address _operator) => mapping(address => uint8) public operator
    ​​​​_fallback() payable => receive() external payable
    ​​​​setOwner(address _new) => setOwner(address newOwner)
    ​​​​unknown4b63d0a1(uint256 _param1, uint256 _param2) => setOperatorMode(address account, uint8 mode)
    ​​​​unknown94241002() => withdraw(Withdraw[] calldata ws)
    ​​​​unknown8033d687(?) => take(IERC20 token, uint amount, address payable to)
    ​​​​unknown90cfe778(?) => deposit(IERC20 token, uint amount, uint8 to, bool bonus, bytes calldata recipient)
    

Owner Analysis( )

There are in total 5 write functions & 3 read functions in EVO DeFi Bridge BSC Contract:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import './interfaces/IBridgePool.sol'; contract BridgePool is IBridgePool { address public owner; /* operator modes: 1 - contract:creator 2 - contract:withdrawer 4 - withdrawer 8 - taker */ mapping(address => uint8) public operator; mapping(bytes32 => bool) public withdrawn; bool private entered = false; modifier nonReentrant() { require(!entered, 'reentrant call'); entered = true; _; entered = false; } constructor () { owner = tx.origin; } function setOwner(address newOwner) external { require(msg.sender == owner, 'forbidden'); owner = newOwner; } .....

The above contract shows that deployer of the contract has been set as the native owner of this contract, where owner can transfer ownership to another wallet address.

The deployer - 0x40e0dcd7024030c7b5e1d474fe95aaf7bb880ad0

found out in the contract deployment transaction tx 0x2a788fa2eb1c4313d4bff1ae19e24ac5bfbd6c15b56c75ff512b16993905612d on BSCscan.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

After contract deployment, the deployer did not set new owner. Thus, until now, this address is still the owner of contract address 0x06c30Af8A82AAf9cFd319f8644584276Bfbec42f on BSC, Polygon, Oasis, Fantom, Heco, Ethereum, Avalanche, Arbitrum, Cronos, Moonriver and OKEXchain.

setOperatorMode( )

function setOperatorMode(address account, uint8 mode) external { require(msg.sender == owner, 'forbidden'); operator[account] = mode; }

The owner of this smart contract can set operator mode for input account.

deposit( )

function deposit(IERC20 token, uint amount, uint8 to, bool bonus, bytes calldata recipient) override external payable nonReentrant() { // // ed only direct call or 'contract:creator' or 'contract:withdrawer' require(tx.origin == msg.sender || (operator[msg.sender] & (1 | 2) > 0), 'call from unauthorized contract'); require(address(token) != address(0) && amount > 0 && recipient.length > 0, 'invalid input'); if (address(token) == address(1)) { require(amount == msg.value, 'value must equal amount'); } else { safeTransferFrom(token, msg.sender, address(this), amount); } emit Deposited(msg.sender, address(token), to, amount, bonus, recipient); }

This deposit function allows users to deposit certain token to this smart contract. There are in total 5 inputs:

  1. IERC20 token - deposit ERC20 token address
  2. uint amount - amount of ERC20 token
  3. to - might be chainID
  4. bool bonus - small amount of gas incentive trigger
  5. bytes calldata recipient - recipient address on other chains

withdraw( )

function withdraw(Withdraw[] calldata ws) override external nonReentrant() { // allowed only 'withdrawer' or 'withdrawer' through 'contract:withdrawer' require(operator[msg.sender] == 4 || (operator[tx.origin] == 4 && operator[msg.sender] == 2), 'forbidden'); for (uint i = 0; i < ws.length; i++) { Withdraw memory w = ws[i]; require(!withdrawn[w.id], 'already withdrawn'); withdrawn[w.id] = true; if (address(w.token) == address(1)) { require(address(this).balance >= w.amount + w.bonus, 'too low token balance'); (bool success, ) = w.recipient.call{value: w.amount}(''); require(success, 'native transfer error'); } else { require( w.token.balanceOf(address(this)) >= w.amount && address(this).balance >= w.bonus, 'too low token balance' ); safeTransfer(w.token, w.recipient, w.amount); } if (w.bonus > 0) { // may fail on contracts w.recipient.call{value: w.bonus}(''); } if (address(w.token) != address(1) && w.feeAmounts.length > 0) { for (uint j = 0; j < w.feeAmounts.length; j++) { require(w.token.balanceOf(address(this)) >= w.feeAmounts[j], 'too low token balance'); safeTransfer(w.token, w.feeTargets[j], w.feeAmounts[j]); } } emit Withdrawn(w.id, address(w.token), w.recipient, w.amount); } }

withdraw function to allow users to receive the token from the dst chain.

take( ) - ** high centralization risks **

function take(IERC20 token, uint amount, address payable to) external override nonReentrant() { // allowed only 'taker' require(operator[msg.sender] == 8, 'forbidden'); if (address(token) == address(1)) { to.transfer(amount); } else { safeTransfer(token, to, amount); } }

As we previously mentioned, setOperatorMode() function can set input address account to different mode options. This take() function only allows msg.sender is 8 to pass the require conditions. Thus, 8 should be taker mode.

The taker has the right to send tokens out from this contract.


Taker Activity Analysis

The on-chain transaction tx shows the following addresses have the taker right on Avalanche,BSC,Ethereum,Fantom,Polygon,Heco and Cronos Chain:

Name Address
taker1 0xf758E719d88862F87585F20Acc81417Cae72Df22
taker2 0x40E0DcD7024030c7b5E1d474fe95Aaf7Bb880ad0
taker3 0x2074fF8f6625A9eEE2e3Eac2Fd658df18A5C5166

NOTE: 0x40E0DcD7024030c7b5E1d474fe95Aaf7Bb880ad0 is also the contract deployer

A more detialed Tx list can be found here:
https://docs.google.com/spreadsheets/d/1plFLA7_W4rEa6Orgi2s5zMNvZiA5W_XhY7oMjRkaGCs/edit?usp=sharing

Summary of Taker Activity

Network Taker Name Token Value
Avalanche taker1 USDC 205,200.0
Avalanche taker1 USDT 225,000.0
Avalanche taker1 WETH 370.5
Avalanche taker2 USDT 20,000.0
BSC taker1 USDC 7,654,200.0
BSC taker1 USDT 25,280,600.0
BSC taker1 WBTC 172.3
BSC taker1 WETH 1,201.0
BSC taker2 USDC 1,620.0
BSC taker2 USDT 401,006.368
Cronos taker1 USDC 263,000.0
Cronos taker1 USDT 258,500.0
Cronos taker1 WETH 4.0
Ethereum taker1 USDC 91,261.2513
Ethereum taker1 USDT 2,925,264.94
Ethereum taker1 WBTC 7.0303518
Ethereum taker1 WETH 376.174583
Ethereum taker2 USDT 100.0
Fantom taker1 USDC 505,000.0
Fantom taker1 USDT 3,310,000.0
Fantom taker1 WBTC 60.0
Fantom taker1 WETH 610.0
Heco taker1 USDT 82,000.0
Polygon taker1 USDC 539,200.0
Polygon taker1 USDT 302,000.0
Polygon taker1 WETH 20.0
Polygon taker2 USDT 12.0

Summary of Set Taker Activity

Network Taker Name Txn Hash
Avalanche taker3 0xfb7d7405a22eb052cbb0457c1460ad45b7b8ff70282e542549438edb14e3d9b6
Avalanche taker2 0x69eed4f24f2a405b053eeb43ae3f3623817208495755a14a772fffde9e717d7e
Avalanche taker1 0x45adc2829b62ce6585efddd89026fc990e6a5b37abefb591e014e74be6cbe4f4
BSC taker3 0xba5c0add385bf81c91201e0a6d1cf5c7ea794ce3e52eae69445cc00461f22285
BSC taker2 0x65e34d693f446938d97d4bb9a8b5d42a29dd10263c608e0a0ab58ef23a14b949
BSC taker1 0xdfaec4de42dd10b46b722107dd742b3b958cac24d59f6ab7ded5dd5d61c4579b
Ethereum taker3 0xb25236a24f19f56dc6007153b6810ada0109625f007dec1122027b6e7d7afe61
Ethereum taker2 0x9c4b385f9dd15a431dac06770e5c9c1bacf0fd637129b59a23bb43ef9dfce649
Ethereum taker1 0x1380619a43d61e79bbebab768f0f4bb444ed113b25b32ee32e01cc6018364cad
Fantom taker3 0xfa640d40549a6bda93cd17d1e2ec9c622fa4036110a19ee12b55195564090bcb
Fantom taker2 0xf016860a66b8f3aa803e02e84a7297b04a61b919c3e4230808aa1fc8fb4e830a
Fantom taker1 0xba9f32a3d5a0956835fd6d40d28fa1824517d95119319896331c1fa2a30b6551
Polygon taker3 0x162c70e61c78cba6dae84fb7aa906e9b1f26b0208ed4f80b1201347f64d69caa
Polygon taker2 0x25cfe73cfaa932bbe604f4ff4d85592725d584066b94e56e5431907c618ca30c
Polygon taker1 0x8312d3f6b97bb9b27e5012434bb1fc293b43220d1c6f03a7a78621ed53d0c34f
Cronos taker3 0x04793b215cc49b650504b761203e6f7465432a32bce3e49faaff815fc48ed34a
Cronos taker2 0xbcac54315fd871694a6cf85941b250ff5ecc90ca36dfb077715a46e51997adbb
Cronos taker1 0x085ee90a637f6cfdcc482cdc2393b30737a456167faf332ba3a281edb465d7bc
Heco taker3 0xc61707f6f64e25d807d70d71f324d55d39873912b9ddf99310463dbbc566d7ba
Heco taker2 0x6a2e78154dfbaf29a4a8da5baad721f83d46514d024b206c58180b1ed33d00a7
Heco taker1 0xa1f051496d186cfb5d18a50b9bb97a47f2a6bc33fe3536e4a0f7440332762099

Disclaimer

The scope of our audit is limited to a review of code and only the code we note as being within the scope of our audit detailed in this report. It is important to note that the Solidity code itself presents unique and unquantifiable risks since the Solidity language itself remains under current development and is subject to unknown risks and flaws. Verilog in no way claims any guarantee of security or functionality of the technology we agree to analyze.

In addition, Verilog reports do not provide any indication of the technologies proprietors, business, business model or legal compliance. As such, reports do not provide investment advice and should not be used to make decisions about investment or involvement with any particular project. Verilog has the right to distribute the Report through other means, including via Verilog publications and other distributions. Verilog makes the reports available to parties other than the Clients (i.e., “third parties”).