Skip to content

Commit

Permalink
Merge the develop branch to the master branch, preparation to v1.1.0-rc0
Browse files Browse the repository at this point in the history
This update for the `master` branch contains the following set of changes:
  * [Improvement] Add support on interest earning using Compound (#47)
  * [Other] Bump package and contracts interfaces version prior to 1.1.0-rc0 (#49)
  • Loading branch information
akolotov authored May 14, 2021
2 parents 84b1116 + ac80c43 commit b658c7c
Show file tree
Hide file tree
Showing 29 changed files with 1,036 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- run: yarn ${{ matrix.task }}
coverage:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags')
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags') || contains(github.event.head_commit.message, 'coverage')
steps:
- uses: actions/setup-node@v1
with:
Expand Down
28 changes: 28 additions & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
const Web3 = require('web3')

module.exports = {
mocha: {
timeout: 30000
},
forceBackupServer: true,
providerOptions: {
port: 8545,
seed: 'TestRPC is awesome!'
},
onServerReady: async (config) => {
const web3 = new Web3(config.provider)
const abi = [{
inputs: [{ name: "", type: "address"}],
outputs: [{ name: "", type: "uint256" }],
name: "balanceOf",
stateMutability: "view",
type: "function"
}]
const cDai = new web3.eth.Contract(abi, '0x615cba17EE82De39162BB87dBA9BcfD6E8BcF298')
const faucet = (await web3.eth.getAccounts())[6]
while (true) {
try {
if (await cDai.methods.balanceOf(faucet).call() !== '0') {
break
}
} catch (e) {
await new Promise(res => setTimeout(res, 1000))
}
}
},
skipFiles: ['mocks']
}
19 changes: 19 additions & 0 deletions contracts/interfaces/ICToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pragma solidity 0.7.5;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface ICToken is IERC20 {
function underlying() external returns (address);

function mint(uint256 mintAmount) external returns (uint256);

function redeem(uint256 redeemAmount) external returns (uint256);

function redeemUnderlying(uint256 redeemAmount) external returns (uint256);

function balanceOfUnderlying(address account) external returns (uint256);

function borrow(uint256 borrowAmount) external returns (uint256);

function repayBorrow(uint256 borrowAmount) external returns (uint256);
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IComptroller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pragma solidity 0.7.5;

interface IComptroller {
function claimComp(
address[] calldata holders,
address[] calldata cTokens,
bool borrowers,
bool suppliers
) external;

function compAccrued(address holder) external view returns (uint256);
}
5 changes: 5 additions & 0 deletions contracts/interfaces/IHarnessComptroller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity 0.7.5;

interface IHarnessComptroller {
function fastForward(uint256 blocks) external;
}
15 changes: 15 additions & 0 deletions contracts/interfaces/IInterestImplementation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pragma solidity 0.7.5;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IInterestImplementation {
function isInterestSupported(address _token) external view returns (bool);

function invest(address _token, uint256 _amount) external;

function withdraw(address _token, uint256 _amount) external;

function investedAmount(address _token) external view returns (uint256);

function claimCompAndPay(address[] calldata _markets) external;
}
5 changes: 5 additions & 0 deletions contracts/interfaces/IInterestReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity 0.7.5;

interface IInterestReceiver {
function onInterestReceived(address _token) external;
}
5 changes: 5 additions & 0 deletions contracts/interfaces/IOwnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity 0.7.5;

interface IOwnable {
function owner() external view returns (address);
}
20 changes: 20 additions & 0 deletions contracts/mocks/CompoundInterestERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity 0.7.5;

import "../upgradeable_contracts/modules/interest/CompoundInterestERC20.sol";

contract CompoundInterestERC20Mock is CompoundInterestERC20 {
constructor(
address _omnibridge,
address _owner,
uint256 _minCompPaid,
address _compReceiver
) CompoundInterestERC20(_omnibridge, _owner, _minCompPaid, _compReceiver) {}

function comptroller() public pure override returns (IComptroller) {
return IComptroller(0x85e855b22F01BdD33eE194490c7eB16b7EdaC019);
}

function compToken() public pure override returns (IERC20) {
return IERC20(0x6f51036Ec66B08cBFdb7Bd7Fb7F40b184482d724);
}
}
16 changes: 12 additions & 4 deletions contracts/upgradeable_contracts/BasicOmnibridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,8 @@ abstract contract BasicOmnibridge is
{
require(isRegisteredAsNativeToken(_token));

uint256 balance = IERC677(_token).balanceOf(address(this));
uint256 expectedBalance = mediatorBalance(_token);
require(balance > expectedBalance);
uint256 diff = balance - expectedBalance;
uint256 diff = _unaccountedBalance(_token);
require(diff > 0);
uint256 available = maxAvailablePerTx(_token);
require(available > 0);
if (diff > available) {
Expand Down Expand Up @@ -488,6 +486,16 @@ abstract contract BasicOmnibridge is
return result;
}

/**
* @dev Internal function for counting excess balance which is not tracked within the bridge.
* Represents the amount of forced tokens on this contract.
* @param _token address of the token contract.
* @return amount of excess tokens.
*/
function _unaccountedBalance(address _token) internal view virtual returns (uint256) {
return IERC677(_token).balanceOf(address(this)).sub(mediatorBalance(_token));
}

function _handleTokens(
address _token,
bool _isNative,
Expand Down
37 changes: 36 additions & 1 deletion contracts/upgradeable_contracts/ForeignOmnibridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ pragma solidity 0.7.5;

import "./BasicOmnibridge.sol";
import "./components/common/GasLimitManager.sol";
import "./components/common/InterestConnector.sol";
import "../libraries/SafeMint.sol";

/**
* @title ForeignOmnibridge
* @dev Foreign side implementation for multi-token mediator intended to work on top of AMB bridge.
* It is designed to be used as an implementation contract of EternalStorageProxy contract.
*/
contract ForeignOmnibridge is BasicOmnibridge, GasLimitManager {
contract ForeignOmnibridge is BasicOmnibridge, GasLimitManager, InterestConnector {
using SafeERC20 for IERC677;
using SafeMint for IBurnableMintableERC677Token;
using SafeMath for uint256;
Expand Down Expand Up @@ -136,11 +137,33 @@ contract ForeignOmnibridge is BasicOmnibridge, GasLimitManager {
uint256 _balanceChange
) internal override {
if (_isNative) {
// There are two edge cases related to withdrawals on the foreign side of the bridge.
// 1) Minting of extra STAKE tokens, if supply on the Home side exceeds total bridge amount on the Foreign side.
// 2) Withdrawal of the invested tokens back from the Compound-like protocol, if currently available funds are insufficient.
// Most of the time, these cases do not intersect. However, in case STAKE tokens are also invested (e.g. via EasyStaking),
// the situation can be the following:
// - 20 STAKE are bridged through the OB. 15 STAKE of which are invested into EasyStaking, and 5 STAKE are locked directly on the bridge.
// - 5 STAKE are mistakenly locked on the bridge via regular transfer, they are not accounted in mediatorBalance(STAKE)
// - User requests withdrawal of 30 STAKE from the Home side.
// Correct sequence of actions should be the following:
// - Mint new STAKE tokens (value - mediatorBalance(STAKE) = 30 STAKE - 20 STAKE = 10 STAKE)
// - Set local variable balance to 30 STAKE
// - Withdraw all invested STAKE tokens (value - (balance - investedAmount(STAKE)) = 30 STAKE - (30 STAKE - 15 STAKE) = 15 STAKE)

uint256 balance = mediatorBalance(_token);
if (_token == address(0x0Ae055097C6d159879521C384F1D2123D1f195e6) && balance < _value) {
IBurnableMintableERC677Token(_token).safeMint(address(this), _value - balance);
balance = _value;
}

IInterestImplementation impl = interestImplementation(_token);
if (Address.isContract(address(impl))) {
uint256 availableBalance = balance.sub(impl.investedAmount(_token));
if (_value > availableBalance) {
impl.withdraw(_token, (_value - availableBalance).add(minCashThreshold(_token)));
}
}

_setMediatorBalance(_token, balance.sub(_balanceChange));
IERC677(_token).safeTransfer(_recipient, _value);
} else {
Expand All @@ -159,4 +182,16 @@ contract ForeignOmnibridge is BasicOmnibridge, GasLimitManager {

return bridgeContract().requireToPassMessage(mediatorContractOnOtherSide(), _data, requestGasLimit());
}

/**
* @dev Internal function for counting excess balance which is not tracked within the bridge.
* Represents the amount of forced tokens on this contract.
* @param _token address of the token contract.
* @return amount of excess tokens.
*/
function _unaccountedBalance(address _token) internal view override returns (uint256) {
IInterestImplementation impl = interestImplementation(_token);
uint256 invested = Address.isContract(address(impl)) ? impl.investedAmount(_token) : 0;
return IERC677(_token).balanceOf(address(this)).sub(mediatorBalance(_token).sub(invested));
}
}
13 changes: 11 additions & 2 deletions contracts/upgradeable_contracts/Upgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ pragma solidity 0.7.5;
import "../interfaces/IUpgradeabilityOwnerStorage.sol";

contract Upgradeable {
// Avoid using onlyUpgradeabilityOwner name to prevent issues with implementation from proxy contract
/**
* @dev Throws if called by any account other than the upgradeability owner.
*/
modifier onlyIfUpgradeabilityOwner() {
require(msg.sender == IUpgradeabilityOwnerStorage(address(this)).upgradeabilityOwner());
_onlyIfUpgradeabilityOwner();
_;
}

/**
* @dev Internal function for reducing onlyIfUpgradeabilityOwner modifier bytecode overhead.
*/
function _onlyIfUpgradeabilityOwner() internal view {
require(msg.sender == IUpgradeabilityOwnerStorage(address(this)).upgradeabilityOwner());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
pragma solidity 0.7.5;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "../../Ownable.sol";
import "../../../interfaces/IInterestReceiver.sol";
import "../../../interfaces/IInterestImplementation.sol";
import "../native/MediatorBalanceStorage.sol";

/**
* @title InterestConnector
* @dev This contract gives an abstract way of receiving interest on locked tokens.
*/
contract InterestConnector is Ownable, MediatorBalanceStorage {
using SafeMath for uint256;

/**
* @dev Tells address of the interest earning implementation for the specific token contract.
* If interest earning is disabled, will return 0x00..00.
* Can be an address of the deployed CompoundInterestERC20 contract.
* @param _token address of the locked token contract.
* @return address of the implementation contract.
*/
function interestImplementation(address _token) public view returns (IInterestImplementation) {
return IInterestImplementation(addressStorage[keccak256(abi.encodePacked("interestImpl", _token))]);
}

/**
* @dev Initializes interest receiving functionality for the particular locked token.
* Only owner can call this method.
* @param _token address of the token for interest earning.
* @param _impl address of the interest earning implementation contract.
* @param _minCashThreshold minimum amount of underlying tokens that are not invested.
*/
function initializeInterest(
address _token,
address _impl,
uint256 _minCashThreshold
) external onlyOwner {
require(address(interestImplementation(_token)) == address(0));
_setInterestImplementation(_token, _impl);
_setMinCashThreshold(_token, _minCashThreshold);
}

/**
* @dev Sets minimum amount of tokens that cannot be invested.
* Only owner can call this method.
* @param _token address of the token contract.
* @param _minCashThreshold minimum amount of underlying tokens that are not invested.
*/
function setMinCashThreshold(address _token, uint256 _minCashThreshold) external onlyOwner {
_setMinCashThreshold(_token, _minCashThreshold);
}

/**
* @dev Tells minimum amount of tokens that are not being invested.
* @param _token address of the invested token contract.
* @return amount of tokens.
*/
function minCashThreshold(address _token) public view returns (uint256) {
return uintStorage[keccak256(abi.encodePacked("minCashThreshold", _token))];
}

/**
* @dev Disables interest for locked funds.
* Only owner can call this method.
* Prior to calling this function, consider to call payInterest and claimCompAndPay.
* @param _token of token to disable interest for.
*/
function disableInterest(address _token) external onlyOwner {
interestImplementation(_token).withdraw(_token, uint256(-1));
_setInterestImplementation(_token, address(0));
}

/**
* @dev Invests all excess tokens. Leaves only minCashThreshold in underlying tokens.
* Requires interest for the given token to be enabled first.
* @param _token address of the token contract considered.
*/
function invest(address _token) external {
IInterestImplementation impl = interestImplementation(_token);
// less than _token.balanceOf(this), since it does not take into account mistakenly locked tokens that should be processed via fixMediatorBalance.
uint256 balance = mediatorBalance(_token).sub(impl.investedAmount(_token));
uint256 minCash = minCashThreshold(_token);

require(balance > minCash);
uint256 amount = balance - minCash;

IERC20(_token).transfer(address(impl), amount);
impl.invest(_token, amount);
}

/**
* @dev Internal function for setting interest earning implementation contract for some token.
* Also acts as an interest enabled flag.
* @param _token address of the token contract.
* @param _impl address of the implementation contract.
*/
function _setInterestImplementation(address _token, address _impl) internal {
require(_impl == address(0) || IInterestImplementation(_impl).isInterestSupported(_token));
addressStorage[keccak256(abi.encodePacked("interestImpl", _token))] = _impl;
}

/**
* @dev Internal function for setting minimum amount of tokens that cannot be invested.
* @param _token address of the token contract.
* @param _minCashThreshold minimum amount of underlying tokens that are not invested.
*/
function _setMinCashThreshold(address _token, uint256 _minCashThreshold) internal {
uintStorage[keccak256(abi.encodePacked("minCashThreshold", _token))] = _minCashThreshold;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract OmnibridgeInfo is VersionableBridge {
uint64 patch
)
{
return (3, 0, 0);
return (3, 1, 0);
}

/**
Expand Down
Loading

0 comments on commit b658c7c

Please sign in to comment.