Skip to content
This repository has been archived by the owner on Oct 26, 2022. It is now read-only.

Commit

Permalink
Merge pull request #58 from pinkiebell/pinkie/harness
Browse files Browse the repository at this point in the history
WIP: test harness
  • Loading branch information
boringcrypto committed Feb 9, 2021
2 parents 59beed3 + 6f31410 commit 355ed19
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 1 deletion.
65 changes: 64 additions & 1 deletion contracts/BentoBox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ contract BentoBox is MasterContractManager, BoringBatchable {
struct StrategyData {
uint64 strategyStartDate;
uint64 targetPercentage;
uint128 balance;
uint128 balance; // the actual balance of the strategy that can differ from `totals[token]`
}

// ******************************** //
Expand Down Expand Up @@ -135,6 +135,11 @@ contract BentoBox is MasterContractManager, BoringBatchable {
// *** PUBLIC FUNCTIONS *** //
// ************************ //

/// @dev Helper function to represent an `amount` of `token` in shares.
/// @param token The ERC-20 token.
/// @param amount The `token` amount.
/// @param roundUp If the result `share` should be rounded up.
/// @return share The token amount represented in shares.
function toShare(
IERC20 token,
uint256 amount,
Expand All @@ -143,6 +148,11 @@ contract BentoBox is MasterContractManager, BoringBatchable {
share = totals[token].toBase(amount, roundUp);
}

/// @dev Helper function represent shares back into the `token` amount.
/// @param token The ERC-20 token.
/// @param share The amount of shares.
/// @param roundUp If the result should be rounded up.
/// @return amount The share amount back into native representation.
function toAmount(
IERC20 token,
uint256 share,
Expand All @@ -151,6 +161,14 @@ contract BentoBox is MasterContractManager, BoringBatchable {
amount = totals[token].toElastic(share, roundUp);
}

/// @notice Deposit an amount of `token` represented in either `amount` or `share`.
/// @param token_ The ERC-20 token to deposit.
/// @param from which account to pull the tokens.
/// @param to which account to push the tokens.
/// @param amount Token amount in native representation to deposit.
/// @param share Token amount represented in shares to deposit. Takes precedence over `amount`.
/// @return amountOut The amount deposited.
/// @return shareOut The deposited amount repesented in shares.
function deposit(
IERC20 token_,
address from,
Expand Down Expand Up @@ -208,6 +226,12 @@ contract BentoBox is MasterContractManager, BoringBatchable {
shareOut = share;
}

/// @notice Withdraws an amount of `token` from a user account.
/// @param token_ The ERC-20 token to withdraw.
/// @param from which user to pull the tokens.
/// @param to which user to push the tokens.
/// @param amount of tokens. Either one of `amount` or `share` needs to be supplied.
/// @param share Like above, but `share` takes precedence over `amount`.
function withdraw(
IERC20 token_,
address from,
Expand Down Expand Up @@ -253,6 +277,11 @@ contract BentoBox is MasterContractManager, BoringBatchable {
shareOut = share;
}

/// @notice Transfer shares from a user account to another one.
/// @param token The ERC-20 token to transfer.
/// @param from which user to pull the tokens.
/// @param to which user to push the tokens.
/// @param share The amount of `token` in shares.
// Clones of master contracts can transfer from any account that has approved them
// F3 - Can it be combined with another similar function?
// F3: This isn't combined with transferMultiple for gas optimization
Expand All @@ -272,6 +301,11 @@ contract BentoBox is MasterContractManager, BoringBatchable {
emit LogTransfer(token, from, to, share);
}

/// @notice Transfer shares from a user account to multiple other ones.
/// @param token The ERC-20 token to transfer.
/// @param from which user to pull the tokens.
/// @param tos The receivers of the tokens.
/// @param shares The amount of `token` in shares for each receiver in `tos`.
// F3 - Can it be combined with another similar function?
// F3: This isn't combined with transfer for gas optimization
function transferMultiple(
Expand All @@ -295,6 +329,12 @@ contract BentoBox is MasterContractManager, BoringBatchable {
balanceOf[token][from] = balanceOf[token][from].sub(totalAmount);
}

/// @notice Flashloan ability.
/// @param borrower The address of the contract that implements and conforms to `IFlashBorrower` and handles the flashloan.
/// @param receiver Address of the token receiver.
/// @param token The address of the token to receive.
/// @param amount of the tokens to receive.
/// @param data The calldata to pass to the `borrower` contract.
// F5 - Checks-Effects-Interactions pattern followed? (SWC-107)
// F5: Not possible to follow this here, reentrancy has been reviewed
// F6 - Check for front-running possibilities, such as the approve function (SWC-114)
Expand All @@ -315,6 +355,12 @@ contract BentoBox is MasterContractManager, BoringBatchable {
emit LogFlashLoan(address(borrower), token, amount, fee, receiver);
}

/// @notice Support for batched flashloans. Useful to request multiple different `tokens` in a single transaction.
/// @param borrower The address of the contract that implements and conforms to `IBatchFlashBorrower` and handles the flashloan.
/// @param receivers An array of the token receivers. A one-to-one mapping with `tokens` and `amounts`.
/// @param tokens The addresses of the tokens.
/// @param amounts of the tokens for each receiver.
/// @param data The calldata to pass to the `borrower` contract.
// F5 - Checks-Effects-Interactions pattern followed? (SWC-107)
// F5: Not possible to follow this here, reentrancy has been reviewed
// F6 - Check for front-running possibilities, such as the approve function (SWC-114)
Expand Down Expand Up @@ -345,6 +391,10 @@ contract BentoBox is MasterContractManager, BoringBatchable {
}
}

/// @notice Sets the target percentage of the strategy for `token`.
/// @dev Only the owner of this contract is allowed to change this.
/// @param token The address of the token that maps to a strategy to change.
/// @param targetPercentage_ The new target in percent. Must be lesser or equal to `MAX_TARGET_PERCENTAGE`.
function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) public onlyOwner {
// Checks
require(targetPercentage_ <= MAX_TARGET_PERCENTAGE, "StrategyManager: Target too high");
Expand All @@ -354,6 +404,12 @@ contract BentoBox is MasterContractManager, BoringBatchable {
emit LogStrategyTargetPercentage(token, targetPercentage_);
}

/// @notice Sets the contract address of a new strategy that conforms to `IStrategy` for `token`.
/// Must be called twice with the same arguments.
/// A new strategy becomes pending first and can be activated once `STRATEGY_DELAY` is over.
/// @dev Only the owner of this contract is allowed to change this.
/// @param token The address of the token that maps to a strategy to change.
/// @param newStrategy The address of the contract that conforms to `IStrategy`.
// F5 - Checks-Effects-Interactions pattern followed? (SWC-107)
// F5: Total amount is updated AFTER interaction. But strategy is under our control.
// C4 - Use block.timestamp only for long intervals (SWC-116)
Expand Down Expand Up @@ -393,6 +449,12 @@ contract BentoBox is MasterContractManager, BoringBatchable {
}
}

/// @notice The actual process of yield farming. Executes the strategy of `token`.
/// Optionally does housekeeping if `balance` is true.
/// `maxChangeAmount` is relevant for skimming or withdrawing if `balance` is true.
/// @param token The address of the token for which a strategy is deployed.
/// @param balance True if housekeeping should be done.
/// @param maxChangeAmount The maximum amount for either pulling or pushing from/to the `IStrategy` contract.
// F5 - Checks-Effects-Interactions pattern followed? (SWC-107)
// F5: Total amount is updated AFTER interaction. But strategy is under our control.
// F5: Not followed to prevent reentrancy issues with flashloans and BentoBox skims?
Expand Down Expand Up @@ -428,6 +490,7 @@ contract BentoBox is MasterContractManager, BoringBatchable {

if (balance) {
uint256 targetBalance = totalElastic.mul(data.targetPercentage) / 100;
// if data.balance == targetBalance there is nothing to update
if (data.balance < targetBalance) {
uint256 amountOut = targetBalance.sub(data.balance);
if (maxChangeAmount != 0 && amountOut > maxChangeAmount) {
Expand Down
70 changes: 70 additions & 0 deletions contracts/mocks/DummyStrategyMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
import "../interfaces/IStrategy.sol";
import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol";
import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol";

// solhint-disable not-rely-on-time

contract DummyStrategyMock is IStrategy {
using BoringMath for uint256;
using BoringERC20 for IERC20;

IERC20 private immutable token;
address private immutable bentoBox;

int256 public _harvestProfit;

modifier onlyBentoBox() {
require(msg.sender == bentoBox, "Ownable: caller is not the owner");
_;
}

constructor(address bentoBox_, IERC20 token_) public {
bentoBox = bentoBox_;
token = token_;
}

function setHarvestProfit(int256 val) public {
_harvestProfit = val;
}

// Send the assets to the Strategy and call skim to invest them
function skim(uint256) external override onlyBentoBox {
return;
}

// Harvest any profits made converted to the asset and pass them to the caller
function harvest(
uint256 balance,
address /*sender*/
) external override onlyBentoBox returns (int256 amountAdded) {
if (_harvestProfit > 0) {
amountAdded = int256(balance) + _harvestProfit;
} else {
amountAdded = int256(balance) - _harvestProfit;
}
}

// Withdraw assets. The returned amount can differ from the requested amount due to rounding or if the request was more than there is.
function withdraw(uint256 amount) external override onlyBentoBox returns (uint256 actualAmount) {
actualAmount = token.balanceOf(address(this));
if (amount > actualAmount) {
actualAmount = amount;
}
token.safeTransfer(msg.sender, actualAmount);
}

// Withdraw all assets in the safest way possible. This shouldn't fail.
function exit(uint256 balance) external override onlyBentoBox returns (int256 amountAdded) {
uint256 moneyToBeTransferred = token.balanceOf(address(this));
amountAdded = int256(moneyToBeTransferred) - int256(balance);
// that is here to reach some branches in BentoBox
if (_harvestProfit > 0) {
amountAdded += _harvestProfit;
} else {
amountAdded -= _harvestProfit;
}
token.safeTransfer(msg.sender, moneyToBeTransferred);
}
}
51 changes: 51 additions & 0 deletions test/HelloWorld.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const assert = require("assert")
const {
getBigNumber,
prepare,
setMasterContractApproval,
deploymentsFixture,
} = require("./utilities")

describe("HelloWorld", function () {
const APPROVAL_AMOUNT = 1000

before(async function () {
await prepare(this, [
"HelloWorld",
"ReturnFalseERC20Mock",
])
})

it("Setup", async function () {
await deploymentsFixture(this, async (cmd) => {
await cmd.addToken("tokenA", "Token A", "A", 18, this.ReturnFalseERC20Mock)
})

await this.HelloWorld.new("helloWorld", this.bentoBox.address, this.tokenA.address)
})

it("should reject deposit: no token- nor master contract approval", async function () {
await assert.rejects(this.helloWorld.deposit(APPROVAL_AMOUNT))
})

it("approve BentoBox", async function () {
await this.tokenA.approve(this.bentoBox.address, getBigNumber(APPROVAL_AMOUNT, await this.tokenA.decimals()))
})

it("should reject deposit: user approved, master contract not approved", async function () {
await assert.rejects(this.helloWorld.deposit(APPROVAL_AMOUNT))
})

it("approve master contract", async function () {
await setMasterContractApproval(this.bentoBox, this.alice, this.alice, this.alicePrivateKey, this.helloWorld.address, true)
})

it("should allow deposit", async function () {
await this.helloWorld.deposit(APPROVAL_AMOUNT)
assert.equal((await this.helloWorld.balance()).toString(), APPROVAL_AMOUNT.toString())
})

it("should allow withdraw", async function () {
await this.helloWorld.withdraw()
})
})
Loading

0 comments on commit 355ed19

Please sign in to comment.