From 052153cd04d65ea8fbf5d34abe1a0f1ba747c709 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 6 Jun 2018 17:09:32 -0700 Subject: [PATCH 01/21] add bucketlender --- .../external/BucketLender/BucketLender.sol | 817 ++++++++++++++++++ .../interfaces/LoanOfferingVerifier.sol | 97 +++ test/helpers/MarginHelper.js | 26 +- test/margin/external/TestBucketLender.js | 283 ++++++ 4 files changed, 1213 insertions(+), 10 deletions(-) create mode 100644 contracts/margin/external/BucketLender/BucketLender.sol create mode 100644 test/margin/external/TestBucketLender.js diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol new file mode 100644 index 00000000..1969c9d0 --- /dev/null +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -0,0 +1,817 @@ +/* + + Copyright 2018 dYdX Trading Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental "v0.5.0"; + +import { ReentrancyGuard } from "zeppelin-solidity/contracts/ReentrancyGuard.sol"; +import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; +import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; +import { Margin } from "../../Margin.sol"; +import { MathHelpers } from "../../../lib/MathHelpers.sol"; +import { TokenInteract } from "../../../lib/TokenInteract.sol"; +import { MarginCommon } from "../../impl/MarginCommon.sol"; +import { LoanOfferingVerifier } from "../../interfaces/LoanOfferingVerifier.sol"; +import { OnlyMargin } from "../../interfaces/OnlyMargin.sol"; +import { CancelMarginCallDelegator } from "../../interfaces/lender/CancelMarginCallDelegator.sol"; +/* solium-disable-next-line max-len*/ +import { ForceRecoverCollateralDelegator } from "../../interfaces/lender/ForceRecoverCollateralDelegator.sol"; +import { IncreaseLoanDelegator } from "../../interfaces/lender/IncreaseLoanDelegator.sol"; +import { LoanOwner } from "../../interfaces/lender/LoanOwner.sol"; +import { MarginCallDelegator } from "../../interfaces/lender/MarginCallDelegator.sol"; +import { MarginHelper } from "../lib/MarginHelper.sol"; + + +/** + * @title BucketLender + * @author dYdX + * + * On-chain shared lender that allows anyone to deposit tokens into this contract to be used to + * lend tokens for a particular position. + + * - Deposits go into a particular bucket, determined by time since the start of the position. + * - When lending money, earlier buckets are used to lend first. + * - When money is paid back, later buckets are paid back first. + * - Over time, this gives higher interest to earlier buckets, but locks-up those funds for longer. + * - Deposits in the same bucket earn the same interest. + * - Lenders can withdraw their funds at any time if the funds are not being lent. + */ +contract BucketLender is + OnlyMargin, + LoanOwner, + IncreaseLoanDelegator, + MarginCallDelegator, + CancelMarginCallDelegator, + ForceRecoverCollateralDelegator, + LoanOfferingVerifier, + ReentrancyGuard +{ + using SafeMath for uint256; + + // ============ Events ============ + + // TODO + + // ============ State Variables ============ + + // Available token to lend + mapping(uint256 => uint256) public availableForBkt; + uint256 public availableTotal; + + // Current allocated principal for each bucket + mapping(uint256 => uint256) public principalForBkt; + uint256 public principalTotal; + + // Bucket accounting for which accounts have deposited into that bucket + mapping(uint256 => mapping(address => uint256)) public weightForBktForAct; + mapping(uint256 => uint256) public weightForBkt; + + // Latest recorded value for totalOwedTokenRepaidToLender + uint256 public cachedRepaidAmount = 0; + + // ============ Constants ============ + + // Address of the token being lent + address public OWED_TOKEN; + + // Address of the token held in the position as collateral + address public HELD_TOKEN; + + // Time between new buckets + uint32 public BUCKET_TIME; + + // Unique ID of the position + bytes32 public POSITION_ID; + + // Accounts that are permitted to margin-call positions (or cancel the margin call) + mapping(address => bool) public TRUSTED_MARGIN_CALLERS; + + // ============ Constructor ============ + + constructor( + address margin, + bytes32 positionId, + address heldToken, + address owedToken, + uint32 bucketTime, + address[] trustedMarginCallers + ) + public + OnlyMargin(margin) + { + POSITION_ID = positionId; + HELD_TOKEN = heldToken; + OWED_TOKEN = owedToken; + BUCKET_TIME = bucketTime; + + for (uint256 i = 0; i < trustedMarginCallers.length; i = i.add(1)) { + TRUSTED_MARGIN_CALLERS[trustedMarginCallers[i]] = true; + } + + TokenInteract.approve( + OWED_TOKEN, + Margin(DYDX_MARGIN).getProxyAddress(), + MathHelpers.maxUint256() + ); + } + + // ============ Modifiers ============ + + modifier onlyPosition(bytes32 positionId) { + require( + POSITION_ID == positionId + ); + _; + } + + modifier onlyWhileOpen() { + require( + !Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID) + ); + _; + } + + // ============ Margin-Only State-Changing Functions ============ + + /** + * Function a smart contract must implement to be able to consent to a loan. The loan offering + * will be generated off-chain and signed by a signer. The Margin contract will verify that + * the signature for the loan offering was made by signer. The "loan owner" address will own the + * loan-side of the resulting position. + * + * If true is returned, and no errors are thrown by the Margin contract, the loan will have + * occurred. This means that verifyLoanOffering can also be used to update internal contract + * state on a loan. + * + * @param addresses Array of addresses: + * + * [0] = owedToken + * [1] = heldToken + * [2] = loan payer + * [3] = loan signer + * [4] = loan owner + * [5] = loan taker + * [6] = loan fee recipient + * [7] = loan lender fee token + * [8] = loan taker fee token + * + * @param values256 Values corresponding to: + * + * [0] = loan maximum amount + * [1] = loan minimum amount + * [2] = loan minimum heldToken + * [3] = loan lender fee + * [4] = loan taker fee + * [5] = loan expiration timestamp (in seconds) + * [6] = loan salt + * + * @param values32 Values corresponding to: + * + * [0] = loan call time limit (in seconds) + * [1] = loan maxDuration (in seconds) + * [2] = loan interest rate (annual nominal percentage times 10**6) + * [3] = loan interest update period (in seconds) + * + * @param positionId Unique ID of the position + * @return This address to accept, a different address to ask that contract + */ + function verifyLoanOffering( + address[10] addresses, + uint256[7] values256, + uint32[4] values32, + bytes32 positionId + ) + external + onlyMargin + nonReentrant + returns (address) + { + LoanOffering memory loanOffering = parseLoanOffering(addresses, values256, values32); + + /* CHECK POSITIONID */ + require(positionId == POSITION_ID); + + /* CHECK ADDRESSES */ + require(loanOffering.owedToken == OWED_TOKEN); + require(loanOffering.heldToken == HELD_TOKEN); + require(loanOffering.payer == address(this)); + // no need to require anything about loanOffering.signer + require(loanOffering.owner == address(this)); + // no need to require anything about loanOffering.taker + // no need to require anything about loanOffering.positionOwner + // no need to require anything about loanOffering.feeRecipient + // no need to require anything about loanOffering.lenderFeeToken + // no need to require anything about loanOffering.takerFeeToken + + /* CHECK VALUES256 */ + // no need to require anything about loanOffering.maximumAmount + // no need to require anything about loanOffering.minimumAmount + // no need to require anything about loanOffering.minimumHeldToken + require(loanOffering.lenderFee == 0); + // no need to require anything about loanOffering.takerFee + // no need to require anything about loanOffering.expirationTimestamp + // no need to require anything about loanOffering.salt + + /* CHECK VALUES32 */ + // no need to require anything about loanOffering.callTimeLimit + // no need to require anything about loanOffering.maxDuration + // no need to require anything about loanOffering.interestRate + // no need to require anything about loanOffering.interestPeriod + + return address(this); + } + + /** + * Called by the Margin contract when anyone transfers ownership of a loan to this contract. + * This function initializes this contract and returns this address to indicate to Margin + * that it is willing to take ownership of the loan. + * + * @param from (unused) + * @param positionId Unique ID of the position + * @return This address on success, throw otherwise + */ + function receiveLoanOwnership( + address from, + bytes32 positionId + ) + external + onlyMargin + onlyPosition(positionId) + returns (address) + { + MarginCommon.Position memory position = MarginHelper.getPosition(DYDX_MARGIN, POSITION_ID); + + assert(position.principal > 0); + assert(position.owedToken == OWED_TOKEN); + assert(position.heldToken == HELD_TOKEN); + + // set relevant constants + uint256 initialPrincipal = position.principal; + principalForBkt[0] = initialPrincipal; + principalTotal = initialPrincipal; + weightForBkt[0] = weightForBkt[0].add(initialPrincipal); + weightForBktForAct[0][from] = weightForBktForAct[0][from].add(initialPrincipal); + + return address(this); + } + + /** + * Called by Margin when additional value is added onto the position this contract + * is lending for. Balance is added to the address that loaned the additional tokens. + * + * @param payer Address that loaned the additional tokens + * @param positionId Unique ID of the position + * @param principalAdded Amount that was added to the position + * param lentAmount (unused) + * @return This address to accept, a different address to ask that contract + */ + function increaseLoanOnBehalfOf( + address payer, + bytes32 positionId, + uint256 principalAdded, + uint256 lentAmount + ) + external + onlyMargin + onlyPosition(positionId) + returns (address) + { + // Don't allow other lenders + require(payer == address(this)); + + // p2 is the principal after the add (p2 > p1) + // p1 is the principal before the add + uint256 principalAfterIncrease = getCurrentPrincipalFromMargin(); + uint256 principalBeforeIncrease = principalAfterIncrease.sub(principalAdded); + + accountForClose(principalTotal.sub(principalBeforeIncrease)); + + accountForIncrease(principalAdded, lentAmount); + + assert(principalTotal == principalAfterIncrease); + + return address(this); + } + + /** + * Function a contract must implement in order to let other addresses call marginCall(). + * + * @param caller Address of the caller of the marginCall function + * @param positionId Unique ID of the position + * @param depositAmount Amount of heldToken deposit that will be required to cancel the call + * @return This address to accept, a different address to ask that contract + */ + function marginCallOnBehalfOf( + address caller, + bytes32 positionId, + uint256 depositAmount + ) + external + onlyMargin + onlyPosition(positionId) + returns (address) + { + require(TRUSTED_MARGIN_CALLERS[caller]); + require(depositAmount == 0); + + return address(this); + } + + /** + * Function a contract must implement in order to let other addresses call cancelMarginCall(). + * + * @param canceler Address of the caller of the cancelMarginCall function + * @param positionId Unique ID of the position + * @return This address to accept, a different address to ask that contract + */ + function cancelMarginCallOnBehalfOf( + address canceler, + bytes32 positionId + ) + external + onlyMargin + onlyPosition(positionId) + returns (address) + { + require(TRUSTED_MARGIN_CALLERS[canceler]); + + return address(this); + } + + /** + * Function a contract must implement in order to let other addresses call + * forceRecoverCollateral(). + * + * param recoverer Address of the caller of the forceRecoverCollateral() function + * @param positionId Unique ID of the position + * @param recipient Address to send the recovered tokens to + * @return This address to accept, a different address to ask that contract + */ + function forceRecoverCollateralOnBehalfOf( + address /* recoverer */, + bytes32 positionId, + address recipient + ) + external + onlyMargin + onlyPosition(positionId) + returns (address) + { + require(recipient == address(this)); + + rebalanceBuckets(); + + return address(this); + } + + // ============ Public State-Changing Functions ============ + + /** + * Allows users to deposit owedToken into this contract. Allowance must be set on this contract + * for "token" in at least the amount "amount". + * + * @param beneficiary The account that will be entitled to this depoit + * @param amount The amount of owedToken to deposit + * @return The bucket number that was deposited into + */ + function deposit( + address beneficiary, + uint256 amount + ) + external + onlyWhileOpen + returns (uint256) + { + rebalanceBuckets(); + + TokenInteract.transferFrom( + OWED_TOKEN, + msg.sender, + address(this), + amount + ); + + uint256 bucket = getBucketNumber(); + + uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); + + uint256 weightToAdd = 0; + if (effectiveAmount == 0) { + weightToAdd = amount; // first deposit in bucket + } else { + weightToAdd = MathHelpers.getPartialAmount( + amount, + effectiveAmount, + weightForBkt[bucket] + ); + } + + accountForDeposit(bucket, beneficiary, weightToAdd); + + changeAvailable(bucket, amount, true); + + return bucket; + } + + function deposit2( + address beneficiary, + uint256 amount + ) + external + onlyWhileOpen + returns (uint256) + { + rebalanceBuckets(); + + TokenInteract.transferFrom( + OWED_TOKEN, + msg.sender, + address(this), + amount + ); + } + + function deposit3( + address beneficiary, + uint256 amount + ) + external + onlyWhileOpen + returns (uint256) + { + rebalanceBuckets(); + + TokenInteract.transferFrom( + OWED_TOKEN, + msg.sender, + address(this), + amount + ); + + uint256 bucket = getBucketNumber(); + + uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); + + uint256 weightToAdd = 0; + if (effectiveAmount == 0) { + weightToAdd = amount; // first deposit in bucket + } else { + weightToAdd = MathHelpers.getPartialAmount( + amount, + effectiveAmount, + weightForBkt[bucket] + ); + } + } + + /** + * Allow anyone to refresh the bucket amounts if part of the position was closed since the last + * position increase. Favors earlier buckets. + */ + function rebalanceBuckets() + public + onlyWhileOpen + { + uint256 marginPrincipal = getCurrentPrincipalFromMargin(); + + accountForClose(principalTotal.sub(marginPrincipal)); + + assert(principalTotal == marginPrincipal); + } + + /** + * Allows users to withdraw their lent funds. + * + * @param buckets The bucket numbers to withdraw from + * @return The number of owedTokens withdrawn + */ + function withdraw( + uint256[] buckets + ) + external + returns (uint256) + { + // running total amount of tokens to withdraw + uint256 runningTotal = 0; + + if (!Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)) { + rebalanceBuckets(); + } + + for (uint256 i = 0; i < buckets.length; i = i.add(1)) { + uint256 bucket = buckets[i]; + + // calculate the bucket's share + uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); + + // calculate the user's share + uint256 bucketWeight = weightForBkt[bucket]; + uint256 userWeight = accountForWithdraw(bucket, msg.sender); // deletes the users share + uint256 amountToWithdraw = MathHelpers.getPartialAmount( + userWeight, + bucketWeight, + effectiveAmount + ); + + // check that there is enough token to give back + require(amountToWithdraw <= availableForBkt[bucket]); + + // update amounts + changeAvailable(bucket, amountToWithdraw, false); + runningTotal = runningTotal.add(amountToWithdraw); + } + + TokenInteract.transfer(OWED_TOKEN, msg.sender, runningTotal); + + return runningTotal; + } + + /** + * Allows lenders to withdraw heldToken in the event that the position was force-recovered. + * + * @param buckets The bucket numbers to withdraw from + * @return The number of heldTokens withdrawn + */ + function withdrawHeldToken( + uint256[] buckets + ) + external + returns(uint256) + { + // running total amount of tokens to withdraw + uint256 runningUserPrincipal = 0; + uint256 originalPrincipalTotal = principalTotal; + + for (uint256 i = 0; i < buckets.length; i = i.add(1)) { + uint256 bucket = buckets[i]; + + // calculate the user's share + uint256 bucketWeight = weightForBkt[bucket]; + uint256 userWeight = accountForWithdraw(bucket, msg.sender); // deletes the users share + + // calculate the user's principal for the bucket + uint256 userPrincipal = + userWeight + .mul(principalForBkt[bucket]) + .div(bucketWeight); + + // update amounts + changePrincipal(bucket, userPrincipal, false); + + runningUserPrincipal = runningUserPrincipal.add(userPrincipal); + } + + uint256 tokenAmount = MathHelpers.getPartialAmount( + runningUserPrincipal, + originalPrincipalTotal, + TokenInteract.balanceOf(HELD_TOKEN, address(this)) + ); + + TokenInteract.transfer(HELD_TOKEN, msg.sender, tokenAmount); + + return tokenAmount; + } + + // ============ Helper Functions ============ + + /** + * Updates the state variables at any time. Only does anything after the position has been + * closed or partially-closed since the last time this function was called. + * + * - Increases the available amount in the highest bucket with outstanding principal + * - Decreases the principal amount in that bucket + * + * @param principalRemoved Amount of principal closed since the last update + */ + function accountForClose( + uint256 principalRemoved + ) + internal + { + if (principalRemoved == 0) { + return; + } + + uint256 newRepaidAmount = Margin(DYDX_MARGIN).getTotalOwedTokenRepaidToLender(POSITION_ID); + assert(newRepaidAmount.sub(cachedRepaidAmount) >= principalRemoved); + + // find highest bucket with outstanding principal + uint256 bucket = getBucketNumber(); + while (principalForBkt[bucket] == 0) { + bucket = bucket.sub(1); + } + + // (available up / principal down) starting at the highest bucket + uint256 p_total = principalRemoved; + uint256 a_total = newRepaidAmount.sub(cachedRepaidAmount); + while (p_total > 0) { + uint256 p_i = Math.min256(p_total, principalForBkt[bucket]); + if (p_i == 0) { + continue; + } + uint256 a_i = MathHelpers.getPartialAmount(a_total, p_total, p_i); + + changeAvailable(bucket, a_i, true); + changePrincipal(bucket, p_i, false); + + p_total = p_total.sub(p_i); + a_total = a_total.sub(a_i); + + if (bucket == 0) { + break; + } else { + bucket = bucket.sub(1); + } + } + + assert(p_total == 0); + assert(a_total == 0); + + cachedRepaidAmount = newRepaidAmount; + } + + /** + * Updates the state variables when a position is increased. + * + * - Decreases the available amount in the lowest bucket with available token + * - Increases the principal amount in that bucket + * + * @param principalAdded Amount of principal added to the position + * @param lentAmount Amount of owedToken lent + */ + function accountForIncrease( + uint256 principalAdded, + uint256 lentAmount + ) + internal + { + uint256 p_total = principalAdded; + uint256 a_total = lentAmount; + + for (uint256 bucket = 0; p_total > 0; bucket = bucket.add(1)) { + uint256 a_i = Math.min256(a_total, availableForBkt[bucket]); + if (a_i == 0) { + continue; + } + uint256 p_i = MathHelpers.getPartialAmount(p_total, a_total, a_i); + + changeAvailable(bucket, a_i, false); + changePrincipal(bucket, p_i, true); + + p_total = p_total.sub(p_i); + a_total = a_total.sub(a_i); + } + + assert(p_total == 0); + assert(a_total == 0); + } + + // ============ Setter Functions ============ + + /** + * Changes the available owedToken amount. This changes both the variable to track the total + * amount as well as the variable to track a particular bucket. + * + * @param bucket The bucket number + * @param amount The amount to change the available amount by + * @param increase True if positive change, false if negative change + */ + function changeAvailable( + uint256 bucket, + uint256 amount, + bool increase + ) + internal + { + require(amount > 0); + if (increase) { + availableTotal = availableTotal.add(amount); + availableForBkt[bucket] = availableForBkt[bucket].add(amount); + } else { + availableTotal = availableTotal.sub(amount); + availableForBkt[bucket] = availableForBkt[bucket].sub(amount); + } + } + + /** + * Changes the principal amount. This changes both the variable to track the total + * amount as well as the variable to track a particular bucket. + * + * @param bucket The bucket number + * @param amount The amount to change the principal amount by + * @param increase True if positive change, false if negative change + */ + function changePrincipal( + uint256 bucket, + uint256 amount, + bool increase + ) + internal + { + require(amount > 0); + if (increase) { + principalTotal = principalTotal.add(amount); + principalForBkt[bucket] = principalForBkt[bucket].add(amount); + } else { + principalTotal = principalTotal.sub(amount); + principalForBkt[bucket] = principalForBkt[bucket].sub(amount); + } + } + + function accountForDeposit( + uint256 bucket, + address account, + uint256 weightToAdd + ) + internal + { + weightForBktForAct[bucket][account] = weightForBktForAct[bucket][account].add(weightToAdd); + weightForBkt[bucket] = weightForBkt[bucket].add(weightToAdd); + } + + function accountForWithdraw( + uint256 bucket, + address account + ) + internal + returns (uint256) + { + uint256 userWeight = weightForBktForAct[bucket][account]; + + weightForBkt[bucket] = weightForBkt[bucket].sub(userWeight); + delete weightForBktForAct[bucket][account]; + + return userWeight; + } + + // ============ Getter Functions ============ + + /** + * Get the current bucket number that funds will be deposited into. This is the highest bucket + * so far. + */ + function getBucketNumber() + internal + view + returns (uint256) + { + uint256 marginTimestamp = Margin(DYDX_MARGIN).getPositionStartTimestamp(POSITION_ID); + + // position not created, allow deposits in the first bucket + if (marginTimestamp == 0) { + return 0; + } + + return block.timestamp.sub(marginTimestamp).div(BUCKET_TIME); + } + + /** + * Gets the outstanding amount of owedToken owed to a bucket. This is the principal amount of + * the bucket multiplied by the interest accrued in the position. + */ + function getBucketOwedAmount( + uint256 bucket + ) + internal + view + returns (uint256) + { + uint256 lentPrincipal = principalForBkt[bucket]; + + if (lentPrincipal == 0) { + return 0; + } + + return Margin(DYDX_MARGIN).getPositionOwedAmountAtTime( + POSITION_ID, + lentPrincipal, + uint32(block.timestamp) + ); + } + + /** + * Gets the principal amount of the position from the Margin contract + */ + function getCurrentPrincipalFromMargin() + internal + view + returns (uint256) + { + return Margin(DYDX_MARGIN).getPositionPrincipal(POSITION_ID); + } +} diff --git a/contracts/margin/interfaces/LoanOfferingVerifier.sol b/contracts/margin/interfaces/LoanOfferingVerifier.sol index b7583c97..8b450afd 100644 --- a/contracts/margin/interfaces/LoanOfferingVerifier.sol +++ b/contracts/margin/interfaces/LoanOfferingVerifier.sol @@ -31,6 +31,35 @@ pragma experimental "v0.5.0"; * to these functions */ contract LoanOfferingVerifier { + + // ============ Structs ============ + + struct LoanOffering { + address owedToken; + address heldToken; + address payer; + address signer; + address owner; + address taker; + address positionOwner; + address feeRecipient; + address lenderFeeToken; + address takerFeeToken; + uint256 maximumAmount; + uint256 minimumAmount; + uint256 minimumHeldToken; + uint256 lenderFee; + uint256 takerFee; + uint256 expirationTimestamp; + uint256 salt; + uint32 callTimeLimit; + uint32 maxDuration; + uint32 interestRate; + uint32 interestPeriod; + } + + // ============ Margin-Only State-Changing Functions ============ + /** * Function a smart contract must implement to be able to consent to a loan. The loan offering * will be generated off-chain. The "loan owner" address will own the loan-side of the resulting @@ -82,4 +111,72 @@ contract LoanOfferingVerifier { external /* onlyMargin */ returns (address); + + // ============ Parsing Functions ============ + + function parseLoanOffering( + address[10] addresses, + uint256[7] values256, + uint32[4] values32 + ) + internal + pure + returns (LoanOffering memory) + { + LoanOffering memory loanOffering; + + fillLoanOfferingAddresses(loanOffering, addresses); + fillLoanOfferingValues256(loanOffering, values256); + fillLoanOfferingValues32(loanOffering, values32); + + return loanOffering; + } + + function fillLoanOfferingAddresses( + LoanOffering memory loanOffering, + address[10] addresses + ) + private + pure + { + loanOffering.owedToken = addresses[0]; + loanOffering.heldToken = addresses[1]; + loanOffering.payer = addresses[2]; + loanOffering.signer = addresses[3]; + loanOffering.owner = addresses[4]; + loanOffering.taker = addresses[5]; + loanOffering.positionOwner = addresses[6]; + loanOffering.feeRecipient = addresses[7]; + loanOffering.lenderFeeToken = addresses[8]; + loanOffering.takerFeeToken = addresses[9]; + } + + function fillLoanOfferingValues256( + LoanOffering memory loanOffering, + uint256[7] values256 + ) + private + pure + { + loanOffering.maximumAmount = values256[0]; + loanOffering.minimumAmount = values256[1]; + loanOffering.minimumHeldToken = values256[2]; + loanOffering.lenderFee = values256[3]; + loanOffering.takerFee = values256[4]; + loanOffering.expirationTimestamp = values256[5]; + loanOffering.salt = values256[6]; + } + + function fillLoanOfferingValues32( + LoanOffering memory loanOffering, + uint32[4] values32 + ) + private + pure + { + loanOffering.callTimeLimit = values32[0]; + loanOffering.maxDuration = values32[1]; + loanOffering.interestRate = values32[2]; + loanOffering.interestPeriod = values32[3]; + } } diff --git a/test/helpers/MarginHelper.js b/test/helpers/MarginHelper.js index 219ee79a..39b65c9e 100644 --- a/test/helpers/MarginHelper.js +++ b/test/helpers/MarginHelper.js @@ -289,7 +289,7 @@ async function callIncreasePosition(dydxMargin, tx) { tx.loanOffering.maxDuration ]; - const order = zeroExOrderToBytes(tx.buyOrder); + const order = orderToBytes(tx.buyOrder); const [principal, balance] = await Promise.all([ dydxMargin.getPositionPrincipal.call(positionId), @@ -320,7 +320,8 @@ async function callIncreasePosition(dydxMargin, tx) { async function expectIncreasePositionLog(dydxMargin, tx, response, start) { const positionId = tx.id; - const [time1, time2, principal, endingBalance] = await Promise.all([ + const [owner, time1, time2, principal, endingBalance] = await Promise.all([ + dydxMargin.getPositionOwner.call(positionId), dydxMargin.getPositionStartTimestamp.call(positionId), getBlockTimestamp(response.receipt.blockNumber), dydxMargin.getPositionPrincipal.call(positionId), @@ -339,13 +340,18 @@ async function expectIncreasePositionLog(dydxMargin, tx, response, start) { tx.principal, true ); - const heldTokenFromSell = tx.depositInHeldToken ? - getPartialAmount( - owed, - tx.buyOrder.takerTokenAmount, - tx.buyOrder.makerTokenAmount - ) - : minTotalDeposit; + let heldTokenFromSell; + if (tx.buyOrder.type === ORDER_TYPE.ZERO_EX) { + heldTokenFromSell = tx.depositInHeldToken ? + getPartialAmount( + owed, + tx.buyOrder.takerTokenAmount, + tx.buyOrder.makerTokenAmount + ) + : minTotalDeposit; + } else if (tx.buyOrder.type === ORDER_TYPE.DIRECT) { + heldTokenFromSell = 0; + } const depositAmount = tx.depositInHeldToken ? minTotalDeposit.minus(heldTokenFromSell) : getPartialAmount( @@ -359,7 +365,7 @@ async function expectIncreasePositionLog(dydxMargin, tx, response, start) { positionId: positionId, trader: tx.trader, lender: tx.loanOffering.payer, - positionOwner: tx.owner, + positionOwner: owner, loanOwner: tx.loanOffering.owner, loanHash: tx.loanOffering.loanHash, loanFeeRecipient: tx.loanOffering.feeRecipient, diff --git a/test/margin/external/TestBucketLender.js b/test/margin/external/TestBucketLender.js new file mode 100644 index 00000000..6ee33944 --- /dev/null +++ b/test/margin/external/TestBucketLender.js @@ -0,0 +1,283 @@ +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-bignumber')()); +const Web3 = require('web3'); +const BigNumber = require('bignumber.js'); + +const SharedLoan = artifacts.require("SharedLoan"); +const Margin = artifacts.require("Margin"); +const HeldToken = artifacts.require("TokenA"); +const OwedToken = artifacts.require("TokenB"); +const BucketLender = artifacts.require("BucketLender"); +const ERC20ShortCreator = artifacts.require("ERC20ShortCreator"); +const OpenDirectlyExchangeWrapper = artifacts.require("OpenDirectlyExchangeWrapper"); + +const { transact } = require('../../helpers/ContractHelper'); +const { ADDRESSES, BIGNUMBERS, ORDER_TYPE } = require('../../helpers/Constants'); +const { expectThrow } = require('../../helpers/ExpectHelper'); +const { issueAndSetAllowance } = require('../../helpers/TokenHelper'); +const { signLoanOffering } = require('../../helpers/LoanHelper'); +const { + issueTokenToAccountInAmountAndApproveProxy, + doOpenPosition, + getPosition, + callIncreasePosition +} = require('../../helpers/MarginHelper'); +const { wait } = require('@digix/tempo')(web3); + +const OT = new BigNumber('1e18'); + +const web3Instance = new Web3(web3.currentProvider); + +const INTEREST_PERIOD = new BigNumber(60 * 60); +const INTEREST_RATE = new BigNumber(10 * 1000000); +const MAX_DURATION = new BigNumber(60 * 60 * 24 * 365); +const CALL_TIMELIMIT = new BigNumber(60 * 60 * 24); +const BUCKET_TIME = new BigNumber(60 * 60 * 24); +let POSITION_ID; + +let margin, heldToken, owedToken; +let bucketLender; + +let alice; +const aliceAmount = OT; + +async function doDeposit(account, amount) { + console.log("A"); + await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); + console.log("B"); + await bucketLender.deposit(account, amount, { from: account }); + console.log("C"); +} + +async function doDeposit2(account, amount) { + console.log("A"); + await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); + console.log("B"); + await bucketLender.deposit2(account, amount, { from: account }); + console.log("C"); +} + +async function runAliceBot() { + console.log(" runnning alice bot..."); + console.log(" depositing..."); + await issueAndSetAllowance(owedToken, alice, aliceAmount, bucketLender.address); + const bucket = await transact(bucketLender.deposit, alice, aliceAmount, { from: alice }); + console.log(" withdrawing (bucket " + bucket.result.toString() + ")..."); + const withdrawn = await transact(bucketLender.withdraw, [bucket.result], { from: alice }); + expect(withdrawn.result).to.be.bignumber.lte(aliceAmount); + console.log(" done."); +} + +async function setUpPosition(accounts) { + const nonce = Math.floor(Math.random() * 12983748912748); + POSITION_ID = web3Instance.utils.soliditySha3(accounts[0], nonce); + + bucketLender = await BucketLender.new( + Margin.address, + POSITION_ID, + heldToken.address, + owedToken.address, + BUCKET_TIME, + [accounts[0]] // trusted margin-callers + ); + + const principal = new BigNumber('22e18'); + const deposit = new BigNumber('60e18'); + + await Promise.all([ + issueTokenToAccountInAmountAndApproveProxy(heldToken, accounts[0], deposit), + ]); + + await margin.openWithoutCounterparty( + [ + ERC20ShortCreator.address, + owedToken.address, + heldToken.address, + bucketLender.address + ], + [ + principal, + deposit, + nonce + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ] + ); +} + +contract('BucketLender', accounts => { + + alice = accounts[9]; + + // ============ Before ============ + + beforeEach('Set up contracts', async () => { + [ + margin, + heldToken, + owedToken + ] = await Promise.all([ + Margin.deployed(), + HeldToken.new(), + OwedToken.new(), + ]); + + await setUpPosition(accounts); + }); + + // ============ Constructor ============ + + describe('Constructor', () => { + it('sets constants correctly', async () => { + const [ + c_margin, + c_owedToken, + c_heldToken, + c_bucketTime, + c_positionId, + c_isTrusted, + c_isTrusted2, + + c_available, + c_available2, + c_principal, + c_principal2, + c_weight, + c_weight2, + + principal + ] = await Promise.all([ + bucketLender.DYDX_MARGIN.call(), + bucketLender.OWED_TOKEN.call(), + bucketLender.HELD_TOKEN.call(), + bucketLender.BUCKET_TIME.call(), + bucketLender.POSITION_ID.call(), + bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[0]), + bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[1]), + + bucketLender.availableTotal.call(), + bucketLender.availableForBkt.call(0), + + bucketLender.principalTotal.call(), + bucketLender.principalForBkt.call(0), + + bucketLender.weightForBkt.call(0), + bucketLender.weightForBktForAct.call(0, accounts[0]), + + margin.getPositionPrincipal.call(POSITION_ID) + ]); + + expect(c_margin).to.eq(Margin.address); + expect(c_owedToken).to.eq(owedToken.address); + expect(c_heldToken).to.eq(heldToken.address); + expect(c_bucketTime).to.be.bignumber.eq(BUCKET_TIME); + expect(c_positionId).to.eq(POSITION_ID); + expect(c_isTrusted).to.be.true; + expect(c_isTrusted2).to.be.false; + + expect(c_available).to.be.bignumber.eq(0); + expect(c_available).to.be.bignumber.eq(c_available2); + expect(c_principal).to.be.bignumber.eq(principal); + expect(c_principal).to.be.bignumber.eq(c_principal2); + expect(c_weight).to.be.bignumber.eq(principal); + expect(c_weight).to.be.bignumber.eq(c_weight2); + }); + }); + + // ============ Complicated case ============ + + describe('Constructor', () => { + it('sets constants correctly', async () => { + await runAliceBot(); + await runAliceBot(); + await runAliceBot(); + }); + }); + + describe('IncreasePosition', () => { + it('sets constants correctly', async () => { + const lender = accounts[5]; + const uselessLender = accounts[7]; + const trader = accounts[6]; + await runAliceBot(); + await doDeposit(lender, OT.times(20)); + await runAliceBot(); + await issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)); + + let tx = await createIncreaseTx(trader, OT.times(2)) + + console.log(" increasing position..."); + await callIncreasePosition(margin, tx); + console.log(" done."); + await runAliceBot(); + + wait(60 * 60 * 24 * 4); + + console.log(" depositing from useless lender..."); + await doDeposit(uselessLender, OT.times(20)); + console.log(" done."); + await runAliceBot(); + + wait(60 * 60 * 24 * 4); + + await issueTokenToAccountInAmountAndApproveProxy(owedToken, trader, OT.times(1000)); + + console.log(" closing position..."); + await margin.closePositionDirectly( + POSITION_ID, + tx.principal, + trader, + { from: trader } + ); + console.log(" done."); + await bucketLender.rebalanceBuckets(); + console.log(" depositing from useless lender..."); + await doDeposit2(uselessLender, OT.times(20)); + console.log(" done."); + await runAliceBot(); + }); + }); +}); + +async function createIncreaseTx(trader, principal) { + const tx = { + trader: trader, + id: POSITION_ID, + principal: principal, + exchangeWrapper: OpenDirectlyExchangeWrapper.address, + depositInHeldToken: true, + buyOrder: { type: ORDER_TYPE.DIRECT }, + loanOffering: { + owedToken: owedToken.address, + heldToken: heldToken.address, + payer: bucketLender.address, + signer: trader, + owner: bucketLender.address, + taker: ADDRESSES.ZERO, + positionOwner: ADDRESSES.ZERO, + feeRecipient: ADDRESSES.ZERO, + lenderFeeTokenAddress: ADDRESSES.ZERO, + takerFeeTokenAddress: ADDRESSES.ZERO, + rates: { + maxAmount: OT.times(1000), + minAmount: BIGNUMBERS.ZERO, + minHeldToken: BIGNUMBERS.ZERO, + lenderFee: BIGNUMBERS.ZERO, + takerFee: BIGNUMBERS.ZERO, + interestRate: INTEREST_RATE, + interestPeriod: INTEREST_PERIOD + }, + expirationTimestamp: 1000000000000, + callTimeLimit: CALL_TIMELIMIT.toNumber(), + maxDuration: MAX_DURATION.toNumber(), + salt: 0 + } + }; + tx.loanOffering.signature = await signLoanOffering(tx.loanOffering, margin); + return tx; +} From 2bfab0873cfab674c5303690bebf07b108bb711e Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 11 Jun 2018 11:24:17 -0700 Subject: [PATCH 02/21] more changes --- .../external/BucketLender/BucketLender.sol | 768 ++++++++++++------ contracts/margin/external/SharedLoan.sol | 6 +- .../margin/external/SharedLoanCreator.sol | 6 +- .../interfaces/LoanOfferingVerifier.sol | 74 +- test/helpers/MarginHelper.js | 2 + test/margin/external/TestBucketLender.js | 209 +++-- 6 files changed, 694 insertions(+), 371 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 1969c9d0..97c6169f 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -42,14 +42,48 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * @author dYdX * * On-chain shared lender that allows anyone to deposit tokens into this contract to be used to - * lend tokens for a particular position. - - * - Deposits go into a particular bucket, determined by time since the start of the position. - * - When lending money, earlier buckets are used to lend first. - * - When money is paid back, later buckets are paid back first. - * - Over time, this gives higher interest to earlier buckets, but locks-up those funds for longer. - * - Deposits in the same bucket earn the same interest. - * - Lenders can withdraw their funds at any time if the funds are not being lent. + * lend tokens for a particular margin position. + * + * - Each bucket has three variables: + * - Available Amount (AA) + * - The available amount of tokens that the bucket has to lend out + * - Outstanding Principal (OP) + * - The amount of principal that the bucket is responsible for in the margin position + * - Weight + * - Used to keep track of each account's weighted ownership within a bucket + * - Relative weight between buckets is meaningless + * - Only accounts' relative weight within a bucket matters + * + * - Token Deposits: + * - Go into a particular bucket, determined by time since the start of the position + * - If the position has not started: bucket = 0 + * - If the position has started: bucket = ceiling(time_since_start / BUCKET_TIME) + * - This is always the highest bucket; that is, all higher buckets have no Weight, AA or OP + * - Increase the bucket's AA + * - Increase the bucket's weight and the account's weight in that bucket + * + * - Token Withdrawals: + * - Can be from any bucket with available amount + * - Decrease the bucket's AA (if it has enough, otherwise throw) + * - Decrease the bucket's weight and the account's weight in that bucket + * + * - Increasing the Position (Lending): + * - The lowest buckets with AA are used first + * - Decreases AA + * - Increases OP + * + * - Decreasing the Position (Being Paid-Back) + * - The highest buckets with OP are paid back first + * - Decreases OP + * - Increases AA + * + * + * - Over time, this gives highest interest rates to earlier buckets, but disallows withdrawals from + * those buckets for a longer period of time. + * - Deposits in the same bucket earn the same interest rate. + * - Lenders can withdraw their funds at any time if they are not being lent (and are therefore not + * making the maximum interest). + * - The highest bucket with OP is always less-than-or-equal-to the lowest bucket with AA */ contract BucketLender is OnlyMargin, @@ -65,23 +99,71 @@ contract BucketLender is // ============ Events ============ - // TODO + event Deposit( + address beneficiary, + uint256 bucket, + uint256 amount, + uint256 weight + ); + + event Withdraw( + address withdrawer, + uint256 bucket, + uint256 weight, + uint256 owedTokenWithdrawn, + uint256 heldTokenWithdrawn + ); // ============ State Variables ============ - // Available token to lend - mapping(uint256 => uint256) public availableForBkt; + /** + * Available Amount (AA) is the amount of tokens that is available to be lent by each bucket. + * These tokens are also available to be withdrawn by the accounts that have weight in the + * bucket. + */ + // AA for each bucket + mapping(uint256 => uint256) public availableForBucket; + // Total AA uint256 public availableTotal; - // Current allocated principal for each bucket - mapping(uint256 => uint256) public principalForBkt; + /** + * Outstanding Principal (OP) is the share of the margin position's principal that each bucket + * is responsible for. That is, each bucket with OP is owed (OP)*E^(RT) owedTokens in repayment. + */ + // OP for each bucket + mapping(uint256 => uint256) public principalForBucket; + // Total OP uint256 public principalTotal; - // Bucket accounting for which accounts have deposited into that bucket - mapping(uint256 => mapping(address => uint256)) public weightForBktForAct; - mapping(uint256 => uint256) public weightForBkt; + /** + * Weight determines an account's proportional share of a bucket. Relative weights have no + * meaning if they are not for the same bucket. Likewise, the relative weight of two buckets has + * no meaning. However, the relative weight of two accounts within the same bucket is equal to + * the accounts' shares in the bucket and are therefore proportional to the payout that they + * should expect from withdrawing from that bucket. + */ + // Weight for each account in each bucket + mapping(uint256 => mapping(address => uint256)) public weightForBucketForAccount; + // Total Weight for each bucket + mapping(uint256 => uint256) public weightForBucket; + + /** + * The critical bucket is: + * - Greater-than-or-equal-to The highest bucket with OP + * - Less-than-or-equal-to the lowest bucket with AA + * + * It is equal to both of these values in most cases except in an edge cases where the two + * buckets are different. This value is cached to find such a bucket faster than looping through + * all possible buckets. + */ + uint256 public criticalBucket = 0; - // Latest recorded value for totalOwedTokenRepaidToLender + /** + * Latest cached value for totalOwedTokenRepaidToLender. + * This number updates on the dYdX Margin base protocol whenever the position is + * partially-closed, but this contract is not notified at that time. Therefore, it is updated + * upon increasing the position or when depositing/withdrawing + */ uint256 public cachedRepaidAmount = 0; // ============ Constants ============ @@ -119,29 +201,19 @@ contract BucketLender is OWED_TOKEN = owedToken; BUCKET_TIME = bucketTime; - for (uint256 i = 0; i < trustedMarginCallers.length; i = i.add(1)) { + for (uint256 i = 0; i < trustedMarginCallers.length; i++) { TRUSTED_MARGIN_CALLERS[trustedMarginCallers[i]] = true; } - TokenInteract.approve( - OWED_TOKEN, - Margin(DYDX_MARGIN).getProxyAddress(), - MathHelpers.maxUint256() - ); + setMaximumAllowanceOnProxy(); } // ============ Modifiers ============ modifier onlyPosition(bytes32 positionId) { require( - POSITION_ID == positionId - ); - _; - } - - modifier onlyWhileOpen() { - require( - !Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID) + POSITION_ID == positionId, + "BucketLender#onlyPosition: Incorrect position" ); _; } @@ -150,22 +222,17 @@ contract BucketLender is /** * Function a smart contract must implement to be able to consent to a loan. The loan offering - * will be generated off-chain and signed by a signer. The Margin contract will verify that - * the signature for the loan offering was made by signer. The "loan owner" address will own the - * loan-side of the resulting position. - * - * If true is returned, and no errors are thrown by the Margin contract, the loan will have - * occurred. This means that verifyLoanOffering can also be used to update internal contract - * state on a loan. + * will be generated off-chain. The "loan owner" address will own the loan-side of the resulting + * position. * * @param addresses Array of addresses: * * [0] = owedToken * [1] = heldToken * [2] = loan payer - * [3] = loan signer - * [4] = loan owner - * [5] = loan taker + * [3] = loan owner + * [4] = loan taker + * [5] = loan positionOwner * [6] = loan fee recipient * [7] = loan lender fee token * [8] = loan taker fee token @@ -188,50 +255,63 @@ contract BucketLender is * [3] = loan interest update period (in seconds) * * @param positionId Unique ID of the position + * @param signature Arbitrary bytes; may or may not be an ECDSA signature * @return This address to accept, a different address to ask that contract */ function verifyLoanOffering( - address[10] addresses, + address[9] addresses, uint256[7] values256, uint32[4] values32, - bytes32 positionId + bytes32 positionId, + bytes signature ) external onlyMargin nonReentrant returns (address) { - LoanOffering memory loanOffering = parseLoanOffering(addresses, values256, values32); + MarginCommon.LoanOffering memory loanOffering = parseLoanOffering( + addresses, + values256, + values32, + signature + ); - /* CHECK POSITIONID */ + // CHECK POSITIONID require(positionId == POSITION_ID); - /* CHECK ADDRESSES */ + // CHECK ADDRESSES require(loanOffering.owedToken == OWED_TOKEN); require(loanOffering.heldToken == HELD_TOKEN); require(loanOffering.payer == address(this)); - // no need to require anything about loanOffering.signer require(loanOffering.owner == address(this)); - // no need to require anything about loanOffering.taker - // no need to require anything about loanOffering.positionOwner - // no need to require anything about loanOffering.feeRecipient - // no need to require anything about loanOffering.lenderFeeToken - // no need to require anything about loanOffering.takerFeeToken - - /* CHECK VALUES256 */ - // no need to require anything about loanOffering.maximumAmount - // no need to require anything about loanOffering.minimumAmount - // no need to require anything about loanOffering.minimumHeldToken - require(loanOffering.lenderFee == 0); - // no need to require anything about loanOffering.takerFee - // no need to require anything about loanOffering.expirationTimestamp - // no need to require anything about loanOffering.salt - - /* CHECK VALUES32 */ - // no need to require anything about loanOffering.callTimeLimit - // no need to require anything about loanOffering.maxDuration - // no need to require anything about loanOffering.interestRate - // no need to require anything about loanOffering.interestPeriod + + /* Can un-comment these after testing + Margin margin = Margin(DYDX_MARGIN); + + require(loanOffering.taker == address(0)); + require(loanOffering.positionOwner == margin.getPositionOwner(POSITION_ID)); + require(loanOffering.lenderFeeToken == address(0)); + require(loanOffering.takerFeeToken == address(0)); + + // CHECK VALUES256 + require(loanOffering.maximumAmount == MathHelpers.maxUint256()); + require(loanOffering.minimumAmount == 0); + require(loanOffering.minimumHeldToken == 0); + require(loanOffering.rates.lenderFee == 0); + require(loanOffering.rates.takerFee == 0); + require(loanOffering.expirationTimestamp == MathHelpers.maxUint256()); + require(loanOffering.salt == 0); + + // CHECK VALUES32 + require(loanOffering.callTimeLimit == margin.getPositionCallTimeLimit(POSITION_ID)); + require(loanOffering.maxDuration == margin.getPositionMaxDuration(POSITION_ID)); + require(loanOffering.interestRate == margin.getPositionInterestRate(POSITION_ID)); + require(loanOffering.interestPeriod == margin.getPositioninterestPeriod(POSITION_ID)); + + // CHECK SIGNATURE + // no need to require anything about loanOffering.signature + */ return address(this); } @@ -256,16 +336,18 @@ contract BucketLender is { MarginCommon.Position memory position = MarginHelper.getPosition(DYDX_MARGIN, POSITION_ID); + assert(principalTotal == 0); assert(position.principal > 0); assert(position.owedToken == OWED_TOKEN); assert(position.heldToken == HELD_TOKEN); // set relevant constants uint256 initialPrincipal = position.principal; - principalForBkt[0] = initialPrincipal; + principalForBucket[0] = initialPrincipal; principalTotal = initialPrincipal; - weightForBkt[0] = weightForBkt[0].add(initialPrincipal); - weightForBktForAct[0][from] = weightForBktForAct[0][from].add(initialPrincipal); + weightForBucket[0] = weightForBucket[0].add(initialPrincipal); + weightForBucketForAccount[0][from] = + weightForBucketForAccount[0][from].add(initialPrincipal); return address(this); } @@ -291,14 +373,23 @@ contract BucketLender is onlyPosition(positionId) returns (address) { - // Don't allow other lenders - require(payer == address(this)); + require( + payer == address(this), + "BucketLender#increaseLoanOnBehalfOf: Other lenders cannot lend for this position" + ); + require( + !Margin(DYDX_MARGIN).isPositionCalled(POSITION_ID), + "BucketLender#increaseLoanOnBehalfOf: No lending while the position is margin-called" + ); + require( + lentAmount <= availableTotal, + "BucketLender#increaseLoanOnBehalfOf: No lending not-accounted-for funds" + ); - // p2 is the principal after the add (p2 > p1) - // p1 is the principal before the add uint256 principalAfterIncrease = getCurrentPrincipalFromMargin(); uint256 principalBeforeIncrease = principalAfterIncrease.sub(principalAdded); + // principalTotal was the principal after the last increase accountForClose(principalTotal.sub(principalBeforeIncrease)); accountForIncrease(principalAdded, lentAmount); @@ -326,8 +417,14 @@ contract BucketLender is onlyPosition(positionId) returns (address) { - require(TRUSTED_MARGIN_CALLERS[caller]); - require(depositAmount == 0); + require( + TRUSTED_MARGIN_CALLERS[caller], + "BucketLender#marginCallOnBehalfOf: Margin-caller must be trusted" + ); + require( + depositAmount == 0, + "BucketLender#marginCallOnBehalfOf: Deposit amount must be zero" + ); return address(this); } @@ -348,7 +445,10 @@ contract BucketLender is onlyPosition(positionId) returns (address) { - require(TRUSTED_MARGIN_CALLERS[canceler]); + require( + TRUSTED_MARGIN_CALLERS[canceler], + "BucketLender#cancelMarginCallOnBehalfOf: Margin-call-canceler must be trusted" + ); return address(this); } @@ -372,7 +472,10 @@ contract BucketLender is onlyPosition(positionId) returns (address) { - require(recipient == address(this)); + require( + recipient == address(this), + "BucketLender#forceRecoverCollateralOnBehalfOf: Recipient must be this contract" + ); rebalanceBuckets(); @@ -394,9 +497,17 @@ contract BucketLender is uint256 amount ) external - onlyWhileOpen returns (uint256) { + require( + !Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID), + "BucketLender#deposit: Cannot deposit after the position is closed" + ); + require( + Margin(DYDX_MARGIN).getPositionCallTimestamp(POSITION_ID) == 0, + "BucketLender#deposit: Cannot deposit while the position is margin-called" + ); + rebalanceBuckets(); TokenInteract.transferFrom( @@ -408,7 +519,7 @@ contract BucketLender is uint256 bucket = getBucketNumber(); - uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); + uint256 effectiveAmount = availableForBucket[bucket].add(getBucketOwedAmount(bucket)); uint256 weightToAdd = 0; if (effectiveAmount == 0) { @@ -417,7 +528,7 @@ contract BucketLender is weightToAdd = MathHelpers.getPartialAmount( amount, effectiveAmount, - weightForBkt[bucket] + weightForBucket[bucket] ); } @@ -425,68 +536,96 @@ contract BucketLender is changeAvailable(bucket, amount, true); + emit Deposit( + beneficiary, + bucket, + amount, + weightToAdd + ); + return bucket; } - function deposit2( - address beneficiary, - uint256 amount + /** + * Allows users to withdraw their lent funds. An account can withdraw its weighted share of the + * bucket. + * + * While the position is open, a bucket's share is equal to: + * Owed Token: AA + OP * (1 + interest) + * Held Token: 0 + * + * After the position is closed, a bucket's share is equal to: + * Owed Token: AA + * Held Token: (Held Token Balance) * (OP / Total OP) + * + * @param buckets The bucket numbers to withdraw from + * @param maxWeights The maximum weight to withdraw from each bucket. The amount of tokens + * withdrawn will be at least this amount, but not necessarily more. + * Withdrawing the same weight from different buckets does not necessarily + * return the same amounts from those buckets. In order to withdraw as many + * tokens as possible, use the maximum uint256. + * @param beneficiary The address to send the tokens to + * @return 1) The number of owedTokens withdrawn + * 2) The number of heldTokens withdrawn + */ + function withdraw( + uint256[] buckets, + uint256[] maxWeights, + address beneficiary ) external - onlyWhileOpen - returns (uint256) + returns (uint256, uint256) { - rebalanceBuckets(); - - TokenInteract.transferFrom( - OWED_TOKEN, - msg.sender, - address(this), - amount + require( + beneficiary != address(0), + "BucketLender#withdraw: Beneficiary cannot be the zero address" + ); + require( + buckets.length == maxWeights.length, + "BucketLender#withdraw: The lengths of the input arrays must match" ); - } - function deposit3( - address beneficiary, - uint256 amount - ) - external - onlyWhileOpen - returns (uint256) - { rebalanceBuckets(); - TokenInteract.transferFrom( - OWED_TOKEN, - msg.sender, - address(this), - amount - ); - - uint256 bucket = getBucketNumber(); + uint256 totalOwedToken = 0; + uint256 totalHeldToken = 0; - uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); + uint256 maxHeldToken = + Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID) ? + TokenInteract.balanceOf(HELD_TOKEN, address(this)) : + 0; - uint256 weightToAdd = 0; - if (effectiveAmount == 0) { - weightToAdd = amount; // first deposit in bucket - } else { - weightToAdd = MathHelpers.getPartialAmount( - amount, - effectiveAmount, - weightForBkt[bucket] + for (uint256 i = 0; i < buckets.length; i++) { + (uint256 owedTokenForBucket, uint256 heldTokenForBucket) = withdrawInternal( + buckets[i], + maxWeights[i], + maxHeldToken ); + + totalOwedToken = totalOwedToken.add(owedTokenForBucket); + totalHeldToken = totalHeldToken.add(heldTokenForBucket); } + + // Transfer share of owedToken + TokenInteract.transfer(OWED_TOKEN, beneficiary, totalOwedToken); + TokenInteract.transfer(HELD_TOKEN, beneficiary, totalHeldToken); + + return (totalOwedToken, totalHeldToken); } + // ============ Public State-Changing Functions ============ + /** * Allow anyone to refresh the bucket amounts if part of the position was closed since the last - * position increase. Favors earlier buckets. + * position increase. */ function rebalanceBuckets() public - onlyWhileOpen { + if (Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)) { + return; + } + uint256 marginPrincipal = getCurrentPrincipalFromMargin(); accountForClose(principalTotal.sub(marginPrincipal)); @@ -495,96 +634,16 @@ contract BucketLender is } /** - * Allows users to withdraw their lent funds. - * - * @param buckets The bucket numbers to withdraw from - * @return The number of owedTokens withdrawn - */ - function withdraw( - uint256[] buckets - ) - external - returns (uint256) - { - // running total amount of tokens to withdraw - uint256 runningTotal = 0; - - if (!Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)) { - rebalanceBuckets(); - } - - for (uint256 i = 0; i < buckets.length; i = i.add(1)) { - uint256 bucket = buckets[i]; - - // calculate the bucket's share - uint256 effectiveAmount = availableForBkt[bucket].add(getBucketOwedAmount(bucket)); - - // calculate the user's share - uint256 bucketWeight = weightForBkt[bucket]; - uint256 userWeight = accountForWithdraw(bucket, msg.sender); // deletes the users share - uint256 amountToWithdraw = MathHelpers.getPartialAmount( - userWeight, - bucketWeight, - effectiveAmount - ); - - // check that there is enough token to give back - require(amountToWithdraw <= availableForBkt[bucket]); - - // update amounts - changeAvailable(bucket, amountToWithdraw, false); - runningTotal = runningTotal.add(amountToWithdraw); - } - - TokenInteract.transfer(OWED_TOKEN, msg.sender, runningTotal); - - return runningTotal; - } - - /** - * Allows lenders to withdraw heldToken in the event that the position was force-recovered. - * - * @param buckets The bucket numbers to withdraw from - * @return The number of heldTokens withdrawn + * Helper function to make sure allowance is set on the dYdX proxy. Callable by anyone. */ - function withdrawHeldToken( - uint256[] buckets - ) - external - returns(uint256) + function setMaximumAllowanceOnProxy() + public { - // running total amount of tokens to withdraw - uint256 runningUserPrincipal = 0; - uint256 originalPrincipalTotal = principalTotal; - - for (uint256 i = 0; i < buckets.length; i = i.add(1)) { - uint256 bucket = buckets[i]; - - // calculate the user's share - uint256 bucketWeight = weightForBkt[bucket]; - uint256 userWeight = accountForWithdraw(bucket, msg.sender); // deletes the users share - - // calculate the user's principal for the bucket - uint256 userPrincipal = - userWeight - .mul(principalForBkt[bucket]) - .div(bucketWeight); - - // update amounts - changePrincipal(bucket, userPrincipal, false); - - runningUserPrincipal = runningUserPrincipal.add(userPrincipal); - } - - uint256 tokenAmount = MathHelpers.getPartialAmount( - runningUserPrincipal, - originalPrincipalTotal, - TokenInteract.balanceOf(HELD_TOKEN, address(this)) + TokenInteract.approve( + OWED_TOKEN, + Margin(DYDX_MARGIN).getProxyAddress(), + MathHelpers.maxUint256() ); - - TokenInteract.transfer(HELD_TOKEN, msg.sender, tokenAmount); - - return tokenAmount; } // ============ Helper Functions ============ @@ -593,8 +652,8 @@ contract BucketLender is * Updates the state variables at any time. Only does anything after the position has been * closed or partially-closed since the last time this function was called. * - * - Increases the available amount in the highest bucket with outstanding principal - * - Decreases the principal amount in that bucket + * - Increases the available amount in the highest buckets with outstanding principal + * - Decreases the principal amount in those buckets * * @param principalRemoved Amount of principal closed since the last update */ @@ -610,37 +669,39 @@ contract BucketLender is uint256 newRepaidAmount = Margin(DYDX_MARGIN).getTotalOwedTokenRepaidToLender(POSITION_ID); assert(newRepaidAmount.sub(cachedRepaidAmount) >= principalRemoved); - // find highest bucket with outstanding principal - uint256 bucket = getBucketNumber(); - while (principalForBkt[bucket] == 0) { - bucket = bucket.sub(1); - } - - // (available up / principal down) starting at the highest bucket - uint256 p_total = principalRemoved; - uint256 a_total = newRepaidAmount.sub(cachedRepaidAmount); - while (p_total > 0) { - uint256 p_i = Math.min256(p_total, principalForBkt[bucket]); - if (p_i == 0) { + uint256 principalToSub = principalRemoved; + uint256 availableToAdd = newRepaidAmount.sub(cachedRepaidAmount); + uint256 mostRecentlyUsedBucket; + + // loop over buckets in reverse order starting with the critical bucket + for ( + uint256 bucket = criticalBucket; + principalToSub > 0 && bucket <= criticalBucket; + bucket-- + ) { + uint256 principalTemp = Math.min256(principalToSub, principalForBucket[bucket]); + if (principalTemp == 0) { continue; } - uint256 a_i = MathHelpers.getPartialAmount(a_total, p_total, p_i); + uint256 availableTemp = MathHelpers.getPartialAmount( + principalTemp, + principalToSub, + availableToAdd + ); - changeAvailable(bucket, a_i, true); - changePrincipal(bucket, p_i, false); + changeAvailable(bucket, availableTemp, true); + changePrincipal(bucket, principalTemp, false); - p_total = p_total.sub(p_i); - a_total = a_total.sub(a_i); + principalToSub = principalToSub.sub(principalTemp); + availableToAdd = availableToAdd.sub(availableTemp); - if (bucket == 0) { - break; - } else { - bucket = bucket.sub(1); - } + mostRecentlyUsedBucket = bucket; } - assert(p_total == 0); - assert(a_total == 0); + assert(principalToSub == 0); + assert(availableToAdd == 0); + + setCriticalBucket(mostRecentlyUsedBucket); cachedRepaidAmount = newRepaidAmount; } @@ -648,8 +709,8 @@ contract BucketLender is /** * Updates the state variables when a position is increased. * - * - Decreases the available amount in the lowest bucket with available token - * - Increases the principal amount in that bucket + * - Decreases the available amount in the lowest buckets with available token + * - Increases the principal amount in those buckets * * @param principalAdded Amount of principal added to the position * @param lentAmount Amount of owedToken lent @@ -660,29 +721,163 @@ contract BucketLender is ) internal { - uint256 p_total = principalAdded; - uint256 a_total = lentAmount; - - for (uint256 bucket = 0; p_total > 0; bucket = bucket.add(1)) { - uint256 a_i = Math.min256(a_total, availableForBkt[bucket]); - if (a_i == 0) { + uint256 principalToAdd = principalAdded; + uint256 availableToSub = lentAmount; + uint256 mostRecentlyUsedBucket; + + // loop over buckets in order starting from the critical bucket + uint256 lastBucket = getBucketNumber(); + for ( + uint256 bucket = criticalBucket; + principalToAdd > 0 && bucket <= lastBucket; + bucket++ + ) { + uint256 availableTemp = Math.min256(availableToSub, availableForBucket[bucket]); + if (availableTemp == 0) { continue; } - uint256 p_i = MathHelpers.getPartialAmount(p_total, a_total, a_i); + uint256 principalTemp = MathHelpers.getPartialAmount( + availableTemp, + availableToSub, + principalToAdd + ); - changeAvailable(bucket, a_i, false); - changePrincipal(bucket, p_i, true); + changeAvailable(bucket, availableTemp, false); + changePrincipal(bucket, principalTemp, true); - p_total = p_total.sub(p_i); - a_total = a_total.sub(a_i); + principalToAdd = principalToAdd.sub(principalTemp); + availableToSub = availableToSub.sub(availableTemp); + + mostRecentlyUsedBucket = bucket; } - assert(p_total == 0); - assert(a_total == 0); + assert(principalToAdd == 0); + assert(availableToSub == 0); + + setCriticalBucket(mostRecentlyUsedBucket); + } + + function withdrawInternal( + uint256 bucket, + uint256 maxWeight, + uint256 maxHeldToken + ) + internal + returns(uint256, uint256) + { + // calculate the user's share + uint256 bucketWeight = weightForBucket[bucket]; + uint256 userWeight = accountForWithdraw(bucket, msg.sender, maxWeight); + + uint256 owedTokenToWithdraw = withdrawInternalOwedToken( + bucket, + userWeight, + bucketWeight + ); + + // calculate for heldToken + uint256 heldTokenToWithdraw = withdrawInternalHeldToken( + bucket, + userWeight, + bucketWeight, + maxHeldToken + ); + + emit Withdraw( + msg.sender, + bucket, + userWeight, + owedTokenToWithdraw, + heldTokenToWithdraw + ); + + return (owedTokenToWithdraw, heldTokenToWithdraw); + } + + function withdrawInternalOwedToken( + uint256 bucket, + uint256 userWeight, + uint256 bucketWeight + ) + internal + returns (uint256) + { + // amount to return for the bucket + uint256 owedTokenToWithdraw = MathHelpers.getPartialAmount( + userWeight, + bucketWeight, + availableForBucket[bucket].add(getBucketOwedAmount(bucket)) + ); + + if (owedTokenToWithdraw == 0) { + return 0; + } + + // check that there is enough token to give back + require( + owedTokenToWithdraw <= availableForBucket[bucket], + "BucketLender#withdrawInternalOwedToken: There must be enough available owedToken" + ); + + // update amounts + changeAvailable(bucket, owedTokenToWithdraw, false); + + return owedTokenToWithdraw; + } + + function withdrawInternalHeldToken( + uint256 bucket, + uint256 userWeight, + uint256 bucketWeight, + uint256 maxHeldToken + ) + internal + returns (uint256) + { + if (maxHeldToken == 0) { + return 0; + } + + // user's principal for the bucket + uint256 principalForBucketForAccount = MathHelpers.getPartialAmount( + userWeight, + bucketWeight, + principalForBucket[bucket] + ); + + if (principalForBucketForAccount == 0) { + return 0; + } + + uint256 heldTokenToWithdraw = MathHelpers.getPartialAmount( + principalForBucketForAccount, + principalTotal, + maxHeldToken + ); + + changePrincipal(bucket, principalForBucketForAccount, false); + + return heldTokenToWithdraw; } // ============ Setter Functions ============ + /** + * Changes the critical bucket variable + * + * @param bucket The value to set criticalBucket to + */ + function setCriticalBucket( + uint256 bucket + ) + internal + { + // don't spend the gas to sstore unless we need to change the value + if (criticalBucket != bucket) { + criticalBucket = bucket; + } + } + /** * Changes the available owedToken amount. This changes both the variable to track the total * amount as well as the variable to track a particular bucket. @@ -698,13 +893,16 @@ contract BucketLender is ) internal { - require(amount > 0); + if (amount == 0) { + return; + } + if (increase) { availableTotal = availableTotal.add(amount); - availableForBkt[bucket] = availableForBkt[bucket].add(amount); + availableForBucket[bucket] = availableForBucket[bucket].add(amount); } else { availableTotal = availableTotal.sub(amount); - availableForBkt[bucket] = availableForBkt[bucket].sub(amount); + availableForBucket[bucket] = availableForBucket[bucket].sub(amount); } } @@ -723,16 +921,26 @@ contract BucketLender is ) internal { - require(amount > 0); + if (amount == 0) { + return; + } + if (increase) { principalTotal = principalTotal.add(amount); - principalForBkt[bucket] = principalForBkt[bucket].add(amount); + principalForBucket[bucket] = principalForBucket[bucket].add(amount); } else { principalTotal = principalTotal.sub(amount); - principalForBkt[bucket] = principalForBkt[bucket].sub(amount); + principalForBucket[bucket] = principalForBucket[bucket].sub(amount); } } + /** + * Increases the 'weight' values for a bucket and an account within that bucket + * + * @param bucket The bucket number + * @param account The account to remove weight from + * @param weightToAdd Adds this amount of weight + */ function accountForDeposit( uint256 bucket, address account, @@ -740,30 +948,42 @@ contract BucketLender is ) internal { - weightForBktForAct[bucket][account] = weightForBktForAct[bucket][account].add(weightToAdd); - weightForBkt[bucket] = weightForBkt[bucket].add(weightToAdd); + weightForBucketForAccount[bucket][account] = + weightForBucketForAccount[bucket][account].add(weightToAdd); + weightForBucket[bucket] = weightForBucket[bucket].add(weightToAdd); } + /** + * Decreases the 'weight' values for a bucket and an account within that bucket. + * + * @param bucket The bucket number + * @param account The account to remove weight from + * @param maximumWeight Removes up-to this amount of weight + * @return The amount of weight removed + */ function accountForWithdraw( uint256 bucket, - address account + address account, + uint256 maximumWeight ) internal returns (uint256) { - uint256 userWeight = weightForBktForAct[bucket][account]; + uint256 userWeight = weightForBucketForAccount[bucket][account]; + uint256 weightToWithdraw = Math.min256(userWeight, maximumWeight); - weightForBkt[bucket] = weightForBkt[bucket].sub(userWeight); - delete weightForBktForAct[bucket][account]; + weightForBucket[bucket] = weightForBucket[bucket].sub(weightToWithdraw); + weightForBucketForAccount[bucket][account] = userWeight.sub(weightToWithdraw); - return userWeight; + return weightToWithdraw; } // ============ Getter Functions ============ /** * Get the current bucket number that funds will be deposited into. This is the highest bucket - * so far. + * so far. All lent funds before the position open will go into bucket 0. All lent funds after + * position open will go into buckets 1+. */ function getBucketNumber() internal @@ -777,7 +997,7 @@ contract BucketLender is return 0; } - return block.timestamp.sub(marginTimestamp).div(BUCKET_TIME); + return block.timestamp.sub(marginTimestamp).div(BUCKET_TIME).add(1); } /** @@ -791,17 +1011,31 @@ contract BucketLender is view returns (uint256) { - uint256 lentPrincipal = principalForBkt[bucket]; + // if the position is completely closed, then the outstanding principal will never be repaid + if (Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)) { + return 0; + } + uint256 lentPrincipal = principalForBucket[bucket]; + + // the bucket has no outstanding principal if (lentPrincipal == 0) { return 0; } - return Margin(DYDX_MARGIN).getPositionOwedAmountAtTime( + // get the total amount of owedToken that would be paid back at this time + uint256 owedAmount = Margin(DYDX_MARGIN).getPositionOwedAmountAtTime( POSITION_ID, - lentPrincipal, + principalTotal, uint32(block.timestamp) ); + + // return the bucket's share + return MathHelpers.getPartialAmount( + lentPrincipal, + principalTotal, + owedAmount + ); } /** diff --git a/contracts/margin/external/SharedLoan.sol b/contracts/margin/external/SharedLoan.sol index b58f48dd..70eb748f 100644 --- a/contracts/margin/external/SharedLoan.sol +++ b/contracts/margin/external/SharedLoan.sol @@ -147,7 +147,7 @@ contract SharedLoan is bytes32 positionId, address margin, address initialLender, - address[] trustedLoanCallers + address[] trustedMarginCallers ) public OnlyMargin(margin) @@ -156,8 +156,8 @@ contract SharedLoan is state = State.UNINITIALIZED; INITIAL_LENDER = initialLender; - for (uint256 i = 0; i < trustedLoanCallers.length; i++) { - TRUSTED_MARGIN_CALLERS[trustedLoanCallers[i]] = true; + for (uint256 i = 0; i < trustedMarginCallers.length; i++) { + TRUSTED_MARGIN_CALLERS[trustedMarginCallers[i]] = true; } } diff --git a/contracts/margin/external/SharedLoanCreator.sol b/contracts/margin/external/SharedLoanCreator.sol index dbd4a9b8..c855acc1 100644 --- a/contracts/margin/external/SharedLoanCreator.sol +++ b/contracts/margin/external/SharedLoanCreator.sol @@ -57,13 +57,13 @@ contract SharedLoanCreator is constructor( address margin, - address[] trustedLoanCallers + address[] trustedMarginCallers ) public OnlyMargin(margin) { - for (uint256 i = 0; i < trustedLoanCallers.length; i++) { - TRUSTED_MARGIN_CALLERS.push(trustedLoanCallers[i]); + for (uint256 i = 0; i < trustedMarginCallers.length; i++) { + TRUSTED_MARGIN_CALLERS.push(trustedMarginCallers[i]); } } diff --git a/contracts/margin/interfaces/LoanOfferingVerifier.sol b/contracts/margin/interfaces/LoanOfferingVerifier.sol index 8b450afd..14187bc7 100644 --- a/contracts/margin/interfaces/LoanOfferingVerifier.sol +++ b/contracts/margin/interfaces/LoanOfferingVerifier.sol @@ -19,6 +19,8 @@ pragma solidity 0.4.24; pragma experimental "v0.5.0"; +import { MarginCommon } from "../impl/MarginCommon.sol"; + /** * @title LoanOfferingVerifier @@ -32,32 +34,6 @@ pragma experimental "v0.5.0"; */ contract LoanOfferingVerifier { - // ============ Structs ============ - - struct LoanOffering { - address owedToken; - address heldToken; - address payer; - address signer; - address owner; - address taker; - address positionOwner; - address feeRecipient; - address lenderFeeToken; - address takerFeeToken; - uint256 maximumAmount; - uint256 minimumAmount; - uint256 minimumHeldToken; - uint256 lenderFee; - uint256 takerFee; - uint256 expirationTimestamp; - uint256 salt; - uint32 callTimeLimit; - uint32 maxDuration; - uint32 interestRate; - uint32 interestPeriod; - } - // ============ Margin-Only State-Changing Functions ============ /** @@ -99,6 +75,7 @@ contract LoanOfferingVerifier { * [3] = loan interest update period (in seconds) * * @param positionId Unique ID of the position + * @param signature Arbitrary bytes; may or may not be an ECDSA signature * @return This address to accept, a different address to ask that contract */ function verifyLoanOffering( @@ -115,26 +92,28 @@ contract LoanOfferingVerifier { // ============ Parsing Functions ============ function parseLoanOffering( - address[10] addresses, + address[9] addresses, uint256[7] values256, - uint32[4] values32 + uint32[4] values32, + bytes signature ) internal pure - returns (LoanOffering memory) + returns (MarginCommon.LoanOffering memory) { - LoanOffering memory loanOffering; + MarginCommon.LoanOffering memory loanOffering; fillLoanOfferingAddresses(loanOffering, addresses); fillLoanOfferingValues256(loanOffering, values256); fillLoanOfferingValues32(loanOffering, values32); + loanOffering.signature = signature; return loanOffering; } function fillLoanOfferingAddresses( - LoanOffering memory loanOffering, - address[10] addresses + MarginCommon.LoanOffering memory loanOffering, + address[9] addresses ) private pure @@ -142,33 +121,32 @@ contract LoanOfferingVerifier { loanOffering.owedToken = addresses[0]; loanOffering.heldToken = addresses[1]; loanOffering.payer = addresses[2]; - loanOffering.signer = addresses[3]; - loanOffering.owner = addresses[4]; - loanOffering.taker = addresses[5]; - loanOffering.positionOwner = addresses[6]; - loanOffering.feeRecipient = addresses[7]; - loanOffering.lenderFeeToken = addresses[8]; - loanOffering.takerFeeToken = addresses[9]; + loanOffering.owner = addresses[3]; + loanOffering.taker = addresses[4]; + loanOffering.positionOwner = addresses[5]; + loanOffering.feeRecipient = addresses[6]; + loanOffering.lenderFeeToken = addresses[7]; + loanOffering.takerFeeToken = addresses[8]; } function fillLoanOfferingValues256( - LoanOffering memory loanOffering, + MarginCommon.LoanOffering memory loanOffering, uint256[7] values256 ) private pure { - loanOffering.maximumAmount = values256[0]; - loanOffering.minimumAmount = values256[1]; - loanOffering.minimumHeldToken = values256[2]; - loanOffering.lenderFee = values256[3]; - loanOffering.takerFee = values256[4]; + loanOffering.rates.maxAmount = values256[0]; + loanOffering.rates.minAmount = values256[1]; + loanOffering.rates.minHeldToken = values256[2]; + loanOffering.rates.lenderFee = values256[3]; + loanOffering.rates.takerFee = values256[4]; loanOffering.expirationTimestamp = values256[5]; loanOffering.salt = values256[6]; } function fillLoanOfferingValues32( - LoanOffering memory loanOffering, + MarginCommon.LoanOffering memory loanOffering, uint32[4] values32 ) private @@ -176,7 +154,7 @@ contract LoanOfferingVerifier { { loanOffering.callTimeLimit = values32[0]; loanOffering.maxDuration = values32[1]; - loanOffering.interestRate = values32[2]; - loanOffering.interestPeriod = values32[3]; + loanOffering.rates.interestRate = values32[2]; + loanOffering.rates.interestPeriod = values32[3]; } } diff --git a/test/helpers/MarginHelper.js b/test/helpers/MarginHelper.js index 39b65c9e..2ce854f8 100644 --- a/test/helpers/MarginHelper.js +++ b/test/helpers/MarginHelper.js @@ -361,6 +361,8 @@ async function expectIncreasePositionLog(dydxMargin, tx, response, start) { true ).minus(owed); + setLoanHash(tx.loanOffering); + expectLog(response.logs[0], 'PositionIncreased', { positionId: positionId, trader: tx.trader, diff --git a/test/margin/external/TestBucketLender.js b/test/margin/external/TestBucketLender.js index 6ee33944..aec2881b 100644 --- a/test/margin/external/TestBucketLender.js +++ b/test/margin/external/TestBucketLender.js @@ -4,7 +4,6 @@ chai.use(require('chai-bignumber')()); const Web3 = require('web3'); const BigNumber = require('bignumber.js'); -const SharedLoan = artifacts.require("SharedLoan"); const Margin = artifacts.require("Margin"); const HeldToken = artifacts.require("TokenA"); const OwedToken = artifacts.require("TokenB"); @@ -13,7 +12,7 @@ const ERC20ShortCreator = artifacts.require("ERC20ShortCreator"); const OpenDirectlyExchangeWrapper = artifacts.require("OpenDirectlyExchangeWrapper"); const { transact } = require('../../helpers/ContractHelper'); -const { ADDRESSES, BIGNUMBERS, ORDER_TYPE } = require('../../helpers/Constants'); +const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../helpers/Constants'); const { expectThrow } = require('../../helpers/ExpectHelper'); const { issueAndSetAllowance } = require('../../helpers/TokenHelper'); const { signLoanOffering } = require('../../helpers/LoanHelper'); @@ -38,38 +37,61 @@ let POSITION_ID; let margin, heldToken, owedToken; let bucketLender; +let lender1, lender2, uselessLender, trader, alice; -let alice; -const aliceAmount = OT; - +// grants tokens to a lender and has them deposit them into the bucket lender async function doDeposit(account, amount) { - console.log("A"); + console.log(" depositing..."); await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); - console.log("B"); + console.log(" ..."); await bucketLender.deposit(account, amount, { from: account }); - console.log("C"); + console.log("done."); } -async function doDeposit2(account, amount) { - console.log("A"); - await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); - console.log("B"); - await bucketLender.deposit2(account, amount, { from: account }); - console.log("C"); +// withdraws for a bucket from an account +async function doWithdraw(account, bucket) { + const tx = await transact( + bucketLender.withdraw, + [bucket], + [BIGNUMBERS.ONES_255], + account, + { from: account } + ); + const [owedWithdrawn, heldWithdrawn] = tx.result; + const remainingWeight = await bucketLender.weightForBucketForAccount.call(bucket, account); + return {owedWithdrawn, heldWithdrawn, remainingWeight}; } async function runAliceBot() { + const aliceAmount = OT; console.log(" runnning alice bot..."); console.log(" depositing..."); await issueAndSetAllowance(owedToken, alice, aliceAmount, bucketLender.address); const bucket = await transact(bucketLender.deposit, alice, aliceAmount, { from: alice }); console.log(" withdrawing (bucket " + bucket.result.toString() + ")..."); - const withdrawn = await transact(bucketLender.withdraw, [bucket.result], { from: alice }); - expect(withdrawn.result).to.be.bignumber.lte(aliceAmount); + const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(alice, bucket.result); + expect(owedWithdrawn).to.be.bignumber.lte(aliceAmount); + expect(owedWithdrawn.plus(1)).to.bignumber.gte(aliceAmount); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); console.log(" done."); } async function setUpPosition(accounts) { + [ + lender1, + lender2, + uselessLender, + trader, + alice + ] = [ + accounts[5], + accounts[6], + accounts[7], + accounts[8], + accounts[9], + ]; + const nonce = Math.floor(Math.random() * 12983748912748); POSITION_ID = web3Instance.utils.soliditySha3(accounts[0], nonce); @@ -87,6 +109,8 @@ async function setUpPosition(accounts) { await Promise.all([ issueTokenToAccountInAmountAndApproveProxy(heldToken, accounts[0], deposit), + doDeposit(lender1, OT.times(2)), + doDeposit(lender2, OT.times(3)), ]); await margin.openWithoutCounterparty( @@ -112,8 +136,6 @@ async function setUpPosition(accounts) { contract('BucketLender', accounts => { - alice = accounts[9]; - // ============ Before ============ beforeEach('Set up contracts', async () => { @@ -143,14 +165,21 @@ contract('BucketLender', accounts => { c_isTrusted, c_isTrusted2, + c_criticalBucket, + c_cachedRepaidAmount, + c_available, c_available2, + c_available3, c_principal, c_principal2, c_weight, c_weight2, - principal + principal, + + bucketLenderOwedToken, + bucketLenderHeldToken, ] = await Promise.all([ bucketLender.DYDX_MARGIN.call(), bucketLender.OWED_TOKEN.call(), @@ -160,16 +189,21 @@ contract('BucketLender', accounts => { bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[0]), bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[1]), - bucketLender.availableTotal.call(), - bucketLender.availableForBkt.call(0), + bucketLender.criticalBucket.call(), + bucketLender.cachedRepaidAmount.call(), + bucketLender.availableTotal.call(), + bucketLender.availableForBucket.call(0), + bucketLender.availableForBucket.call(1), bucketLender.principalTotal.call(), - bucketLender.principalForBkt.call(0), + bucketLender.principalForBucket.call(0), + bucketLender.weightForBucket.call(0), + bucketLender.weightForBucketForAccount.call(0, accounts[0]), - bucketLender.weightForBkt.call(0), - bucketLender.weightForBktForAct.call(0, accounts[0]), + margin.getPositionPrincipal.call(POSITION_ID), - margin.getPositionPrincipal.call(POSITION_ID) + owedToken.balanceOf.call(bucketLender.address), + heldToken.balanceOf.call(bucketLender.address), ]); expect(c_margin).to.eq(Margin.address); @@ -180,50 +214,59 @@ contract('BucketLender', accounts => { expect(c_isTrusted).to.be.true; expect(c_isTrusted2).to.be.false; - expect(c_available).to.be.bignumber.eq(0); - expect(c_available).to.be.bignumber.eq(c_available2); + expect(c_criticalBucket).to.be.bignumber.eq(0); + expect(c_cachedRepaidAmount).to.be.bignumber.eq(0); + + expect(c_available).to.be.bignumber.eq(bucketLenderOwedToken); + expect(c_available2).to.be.bignumber.eq(c_available); + expect(c_available3).to.be.bignumber.eq(0); expect(c_principal).to.be.bignumber.eq(principal); expect(c_principal).to.be.bignumber.eq(c_principal2); - expect(c_weight).to.be.bignumber.eq(principal); - expect(c_weight).to.be.bignumber.eq(c_weight2); + expect(c_weight).to.be.bignumber.eq(principal.plus(c_available)); + expect(c_weight2).to.be.bignumber.eq(c_principal); + + expect(bucketLenderHeldToken).to.be.bignumber.eq(0); }); }); // ============ Complicated case ============ - describe('Constructor', () => { - it('sets constants correctly', async () => { + describe('Alice Bot GOOOO', () => { + it('runs alice bot several times', async () => { await runAliceBot(); await runAliceBot(); await runAliceBot(); }); }); - describe('IncreasePosition', () => { - it('sets constants correctly', async () => { - const lender = accounts[5]; - const uselessLender = accounts[7]; - const trader = accounts[6]; + describe('Integration Test', () => { + it('does the complicated integration test', async () => { await runAliceBot(); - await doDeposit(lender, OT.times(20)); + + console.log(" depositing from good lender..."); + await doDeposit(lender1, OT.times(3)); + console.log(" done."); + await runAliceBot(); - await issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)); - let tx = await createIncreaseTx(trader, OT.times(2)) + await issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)); console.log(" increasing position..."); + let tx = createIncreaseTx(trader, OT.times(3)); await callIncreasePosition(margin, tx); console.log(" done."); + await runAliceBot(); - wait(60 * 60 * 24 * 4); + await wait(60 * 60 * 24 * 4); console.log(" depositing from useless lender..."); - await doDeposit(uselessLender, OT.times(20)); + await doDeposit(uselessLender, OT.times(3)); console.log(" done."); + await runAliceBot(); - wait(60 * 60 * 24 * 4); + await wait(60 * 60 * 24 * 4); await issueTokenToAccountInAmountAndApproveProxy(owedToken, trader, OT.times(1000)); @@ -237,15 +280,83 @@ contract('BucketLender', accounts => { console.log(" done."); await bucketLender.rebalanceBuckets(); console.log(" depositing from useless lender..."); - await doDeposit2(uselessLender, OT.times(20)); + await doDeposit(uselessLender, OT.times(3)); + console.log(" done."); + + await wait(60 * 60 * 24 * 1); + + await runAliceBot(); + + console.log(" increasing position..."); + tx = createIncreaseTx(trader, OT.times(3)) + await callIncreasePosition(margin, tx); + tx = createIncreaseTx(trader, OT.times(3)) + await callIncreasePosition(margin, tx); console.log(" done."); + + // expect that the lenders can no longer withdraw their owedToken isnce it has been lent + await expectThrow(doWithdraw(lender1, 0)); + await expectThrow(doWithdraw(lender2, 0)); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + console.log(" closing position..."); + await margin.closePositionDirectly( + POSITION_ID, + tx.principal.div(2), + trader, + { from: trader } + ); + console.log(" done."); + + await runAliceBot(); + + await margin.marginCall(POSITION_ID, 0); + + await wait(60 * 60 * 24 * 1); + + await margin.cancelMarginCall(POSITION_ID); + + await wait(60 * 60 * 24 * 1); + + await margin.marginCall(POSITION_ID, 0); + + console.log(" closing position..."); + await margin.closePositionDirectly( + POSITION_ID, + BIGNUMBERS.ONES_255, + trader, + { from: trader } + ); + console.log(" done."); + + await wait(60 * 60 * 24 * 1); + + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + + for(let a = 0; a < 10; a++) { + let act = accounts[a]; + for(let b = 0; b < 20; b++) { + const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); + if (!hasWeight.isZero()) { + console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); + const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + console.log(" owed: " + owedWithdrawn.toString()); + console.log(" held: " + heldWithdrawn.toString()); + console.log(" remw: " + remainingWeight.toString()); + console.log(" done."); + } + } + } + }); }); }); -async function createIncreaseTx(trader, principal) { - const tx = { +function createIncreaseTx(trader, principal) { + return { trader: trader, id: POSITION_ID, principal: principal, @@ -256,7 +367,6 @@ async function createIncreaseTx(trader, principal) { owedToken: owedToken.address, heldToken: heldToken.address, payer: bucketLender.address, - signer: trader, owner: bucketLender.address, taker: ADDRESSES.ZERO, positionOwner: ADDRESSES.ZERO, @@ -264,7 +374,7 @@ async function createIncreaseTx(trader, principal) { lenderFeeTokenAddress: ADDRESSES.ZERO, takerFeeTokenAddress: ADDRESSES.ZERO, rates: { - maxAmount: OT.times(1000), + maxAmount: BIGNUMBERS.ONES_255, minAmount: BIGNUMBERS.ZERO, minHeldToken: BIGNUMBERS.ZERO, lenderFee: BIGNUMBERS.ZERO, @@ -275,9 +385,8 @@ async function createIncreaseTx(trader, principal) { expirationTimestamp: 1000000000000, callTimeLimit: CALL_TIMELIMIT.toNumber(), maxDuration: MAX_DURATION.toNumber(), - salt: 0 + salt: 0, + signature: BYTES.EMPTY } }; - tx.loanOffering.signature = await signLoanOffering(tx.loanOffering, margin); - return tx; } From 3a790681c9e9331b367ce1533cf60d603f613312 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 13 Jun 2018 17:11:30 -0700 Subject: [PATCH 03/21] add WETH9 --- contracts/external/weth/README.md | 3 + contracts/external/weth/WETH9.sol | 63 ++++++++++++++++ .../EthWrapperForBucketLender.sol | 74 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 contracts/external/weth/README.md create mode 100644 contracts/external/weth/WETH9.sol create mode 100644 contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol diff --git a/contracts/external/weth/README.md b/contracts/external/weth/README.md new file mode 100644 index 00000000..f23d127f --- /dev/null +++ b/contracts/external/weth/README.md @@ -0,0 +1,3 @@ +All contracts in this folder are taken from the official MakerDao GitHub. +They have been upgraded to a more recent Solidity version without changing functionality. +They are only included for testing purposes. diff --git a/contracts/external/weth/WETH9.sol b/contracts/external/weth/WETH9.sol new file mode 100644 index 00000000..65102488 --- /dev/null +++ b/contracts/external/weth/WETH9.sol @@ -0,0 +1,63 @@ +pragma solidity 0.4.24; + + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping (address => uint) public balanceOf; + mapping (address => mapping (address => uint)) public allowance; + + function() public payable { + deposit(); + } + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) + public + returns (bool) + { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol new file mode 100644 index 00000000..17a38a38 --- /dev/null +++ b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol @@ -0,0 +1,74 @@ +/* + + Copyright 2018 dYdX Trading Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental "v0.5.0"; + +import { BucketLender } from "./BucketLender.sol"; +import { WETH9 } from "../../../external/weth/WETH9.sol"; +import { TokenInteract } from "../../../lib/TokenInteract.sol"; + + +/** + * @title EthWrapperForBucketLender + * @author dYdX + * + * Takes ETH directly, wraps it, then sends it to a bucket lender on behalf of a user. + */ +contract EthWrapperForBucketLender +{ + // ============ Constants ============ + + // Address of the WETH token + address public WETH; + + // ============ Constructor ============ + + constructor( + address weth + ) + public + { + WETH = weth; + } + + // ============ Functions ============ + + function depositEth( + address bucketLender, + address beneficiary + ) + external + payable + returns (uint256) + { + uint256 amount = msg.value; + + // wrap the eth + WETH9(WETH).deposit.value(amount)(); + assert(TokenInteract.balanceOf(WETH, address(this)) >= amount); + + // approve for "unlimited amount". WETH9 leaves this value as-is when doing transferFrom + TokenInteract.approve(WETH, bucketLender, uint256(-1)); + + // deposit the tokens + uint256 bucket = BucketLender(bucketLender).deposit(beneficiary, amount); + + return bucket; + } +} From 399f4b44f3e6dfe903c3028cc1c0b799c8761632 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Thu, 14 Jun 2018 14:41:25 -0700 Subject: [PATCH 04/21] test EthWrapper --- .../external/BucketLender/BucketLender.sol | 198 ++++++++++++------ .../EthWrapperForBucketLender.sol | 22 +- contracts/testing/TestBucketLender.sol | 90 ++++++++ test/margin/TestPositionGetters.js | 13 +- .../{ => bucketlender}/TestBucketLender.js | 141 ++++++++++--- .../TestEthWrapperForBucketLender.js | 129 ++++++++++++ 6 files changed, 495 insertions(+), 98 deletions(-) create mode 100644 contracts/testing/TestBucketLender.sol rename test/margin/external/{ => bucketlender}/TestBucketLender.js (71%) create mode 100644 test/margin/external/bucketlender/TestEthWrapperForBucketLender.js diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 97c6169f..7dd6e7f3 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -22,6 +22,7 @@ pragma experimental "v0.5.0"; import { ReentrancyGuard } from "zeppelin-solidity/contracts/ReentrancyGuard.sol"; import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; +import { HasNoEther } from "zeppelin-solidity/contracts/ownership/HasNoEther.sol"; import { Margin } from "../../Margin.sol"; import { MathHelpers } from "../../../lib/MathHelpers.sol"; import { TokenInteract } from "../../../lib/TokenInteract.sol"; @@ -58,13 +59,13 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * - Go into a particular bucket, determined by time since the start of the position * - If the position has not started: bucket = 0 * - If the position has started: bucket = ceiling(time_since_start / BUCKET_TIME) - * - This is always the highest bucket; that is, all higher buckets have no Weight, AA or OP + * - This is always the highest bucket; no higher bucket yet exists * - Increase the bucket's AA * - Increase the bucket's weight and the account's weight in that bucket * * - Token Withdrawals: * - Can be from any bucket with available amount - * - Decrease the bucket's AA (if it has enough, otherwise throw) + * - Decrease the bucket's AA * - Decrease the bucket's weight and the account's weight in that bucket * * - Increasing the Position (Lending): @@ -86,6 +87,7 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * - The highest bucket with OP is always less-than-or-equal-to the lowest bucket with AA */ contract BucketLender is + HasNoEther, OnlyMargin, LoanOwner, IncreaseLoanDelegator, @@ -166,19 +168,31 @@ contract BucketLender is */ uint256 public cachedRepaidAmount = 0; + // True if the position was closed from force-recovering the collateral + bool public wasForceClosed = false; + // ============ Constants ============ - // Address of the token being lent - address public OWED_TOKEN; + // Unique ID of the position + bytes32 public POSITION_ID; // Address of the token held in the position as collateral address public HELD_TOKEN; + uint32 public MAX_DURATION; + uint32 public CALL_TIMELIMIT; + + // Address of the token being lent + address public OWED_TOKEN; + // Time between new buckets uint32 public BUCKET_TIME; - // Unique ID of the position - bytes32 public POSITION_ID; + uint32 public INTEREST_RATE; + uint32 public INTEREST_PERIOD; + + uint256 public MIN_HELD_TOKEN_NUMERATOR; + uint256 public MIN_HELD_TOKEN_DENOMINATOR; // Accounts that are permitted to margin-call positions (or cancel the margin call) mapping(address => bool) public TRUSTED_MARGIN_CALLERS; @@ -191,6 +205,12 @@ contract BucketLender is address heldToken, address owedToken, uint32 bucketTime, + uint32 interestRate, + uint32 interestPeriod, + uint32 maxDuration, + uint32 callTimelimit, + uint256 minHeldTokenNumerator, + uint256 minHeldTokenDenominator, address[] trustedMarginCallers ) public @@ -199,13 +219,26 @@ contract BucketLender is POSITION_ID = positionId; HELD_TOKEN = heldToken; OWED_TOKEN = owedToken; + BUCKET_TIME = bucketTime; + INTEREST_RATE = interestRate; + INTEREST_PERIOD = interestPeriod; + MAX_DURATION = maxDuration; + CALL_TIMELIMIT = callTimelimit; + + MIN_HELD_TOKEN_NUMERATOR = minHeldTokenNumerator; + MIN_HELD_TOKEN_DENOMINATOR = minHeldTokenDenominator; for (uint256 i = 0; i < trustedMarginCallers.length; i++) { TRUSTED_MARGIN_CALLERS[trustedMarginCallers[i]] = true; } - setMaximumAllowanceOnProxy(); + // Set maximum allowance on proxy + TokenInteract.approve( + OWED_TOKEN, + Margin(DYDX_MARGIN).getProxyAddress(), + MathHelpers.maxUint256() + ); } // ============ Modifiers ============ @@ -268,8 +301,14 @@ contract BucketLender is external onlyMargin nonReentrant + onlyPosition(positionId) returns (address) { + require( + Margin(DYDX_MARGIN).containsPosition(POSITION_ID), + "BucketLender#verifyLoanOffering: This contract should not open a new position" + ); + MarginCommon.LoanOffering memory loanOffering = parseLoanOffering( addresses, values256, @@ -277,41 +316,32 @@ contract BucketLender is signature ); - // CHECK POSITIONID - require(positionId == POSITION_ID); - // CHECK ADDRESSES - require(loanOffering.owedToken == OWED_TOKEN); - require(loanOffering.heldToken == HELD_TOKEN); - require(loanOffering.payer == address(this)); - require(loanOffering.owner == address(this)); - - /* Can un-comment these after testing - Margin margin = Margin(DYDX_MARGIN); - + assert(loanOffering.owedToken == OWED_TOKEN); + assert(loanOffering.heldToken == HELD_TOKEN); + assert(loanOffering.payer == address(this)); + assert(loanOffering.owner == address(this)); require(loanOffering.taker == address(0)); - require(loanOffering.positionOwner == margin.getPositionOwner(POSITION_ID)); + require(loanOffering.positionOwner == address(0)); require(loanOffering.lenderFeeToken == address(0)); require(loanOffering.takerFeeToken == address(0)); // CHECK VALUES256 - require(loanOffering.maximumAmount == MathHelpers.maxUint256()); - require(loanOffering.minimumAmount == 0); - require(loanOffering.minimumHeldToken == 0); + require(loanOffering.rates.maxAmount == MathHelpers.maxUint256()); + require(loanOffering.rates.minAmount == 0); + require(loanOffering.rates.minHeldToken == 0); require(loanOffering.rates.lenderFee == 0); require(loanOffering.rates.takerFee == 0); require(loanOffering.expirationTimestamp == MathHelpers.maxUint256()); require(loanOffering.salt == 0); // CHECK VALUES32 - require(loanOffering.callTimeLimit == margin.getPositionCallTimeLimit(POSITION_ID)); - require(loanOffering.maxDuration == margin.getPositionMaxDuration(POSITION_ID)); - require(loanOffering.interestRate == margin.getPositionInterestRate(POSITION_ID)); - require(loanOffering.interestPeriod == margin.getPositioninterestPeriod(POSITION_ID)); + require(loanOffering.callTimeLimit == CALL_TIMELIMIT); + require(loanOffering.maxDuration == MAX_DURATION); + require(loanOffering.rates.interestRate == INTEREST_RATE); + require(loanOffering.rates.interestPeriod == INTEREST_PERIOD); - // CHECK SIGNATURE // no need to require anything about loanOffering.signature - */ return address(this); } @@ -321,7 +351,7 @@ contract BucketLender is * This function initializes this contract and returns this address to indicate to Margin * that it is willing to take ownership of the loan. * - * @param from (unused) + * @param from Address of the previous owner * @param positionId Unique ID of the position * @return This address on success, throw otherwise */ @@ -341,6 +371,20 @@ contract BucketLender is assert(position.owedToken == OWED_TOKEN); assert(position.heldToken == HELD_TOKEN); + // assert enough heldToken + assert( + Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= + MathHelpers.getPartialAmount( + MIN_HELD_TOKEN_NUMERATOR, + MIN_HELD_TOKEN_DENOMINATOR, + position.principal + ) + ); + + // assert that the position was opened without using funds from this position + // (i.e. that it was opened using openWithoutCounterparty()) + assert(from != address(this)); + // set relevant constants uint256 initialPrincipal = position.principal; principalForBucket[0] = initialPrincipal; @@ -359,7 +403,7 @@ contract BucketLender is * @param payer Address that loaned the additional tokens * @param positionId Unique ID of the position * @param principalAdded Amount that was added to the position - * param lentAmount (unused) + * @param lentAmount Amount of owedToken lent * @return This address to accept, a different address to ask that contract */ function increaseLoanOnBehalfOf( @@ -422,7 +466,7 @@ contract BucketLender is "BucketLender#marginCallOnBehalfOf: Margin-caller must be trusted" ); require( - depositAmount == 0, + depositAmount == 0, // disallows any deposit amount to cancel the margin-call "BucketLender#marginCallOnBehalfOf: Deposit amount must be zero" ); @@ -479,6 +523,8 @@ contract BucketLender is rebalanceBuckets(); + wasForceClosed = true; + return address(this); } @@ -499,6 +545,10 @@ contract BucketLender is external returns (uint256) { + require( + amount != 0, + "BucketLender#deposit: Cannot deposit zero tokens" + ); require( !Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID), "BucketLender#deposit: Cannot deposit after the position is closed" @@ -622,7 +672,9 @@ contract BucketLender is function rebalanceBuckets() public { - if (Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)) { + // if force-closed, don't update the outstanding principal values; they are needed to repay + // lenders with heldToken + if (wasForceClosed) { return; } @@ -633,19 +685,6 @@ contract BucketLender is assert(principalTotal == marginPrincipal); } - /** - * Helper function to make sure allowance is set on the dYdX proxy. Callable by anyone. - */ - function setMaximumAllowanceOnProxy() - public - { - TokenInteract.approve( - OWED_TOKEN, - Margin(DYDX_MARGIN).getProxyAddress(), - MathHelpers.maxUint256() - ); - } - // ============ Helper Functions ============ /** @@ -660,7 +699,7 @@ contract BucketLender is function accountForClose( uint256 principalRemoved ) - internal + private { if (principalRemoved == 0) { return; @@ -671,14 +710,16 @@ contract BucketLender is uint256 principalToSub = principalRemoved; uint256 availableToAdd = newRepaidAmount.sub(cachedRepaidAmount); - uint256 mostRecentlyUsedBucket; + uint256 criticalBucketTemp = criticalBucket; // loop over buckets in reverse order starting with the critical bucket for ( - uint256 bucket = criticalBucket; - principalToSub > 0 && bucket <= criticalBucket; + uint256 bucket = criticalBucketTemp; + principalToSub > 0; bucket-- ) { + assert(bucket <= criticalBucketTemp); // no underflow on bucket + uint256 principalTemp = Math.min256(principalToSub, principalForBucket[bucket]); if (principalTemp == 0) { continue; @@ -695,13 +736,13 @@ contract BucketLender is principalToSub = principalToSub.sub(principalTemp); availableToAdd = availableToAdd.sub(availableTemp); - mostRecentlyUsedBucket = bucket; + criticalBucketTemp = bucket; } assert(principalToSub == 0); assert(availableToAdd == 0); - setCriticalBucket(mostRecentlyUsedBucket); + setCriticalBucket(criticalBucketTemp); cachedRepaidAmount = newRepaidAmount; } @@ -719,19 +760,21 @@ contract BucketLender is uint256 principalAdded, uint256 lentAmount ) - internal + private { uint256 principalToAdd = principalAdded; uint256 availableToSub = lentAmount; - uint256 mostRecentlyUsedBucket; + uint256 criticalBucketTemp; // loop over buckets in order starting from the critical bucket uint256 lastBucket = getBucketNumber(); for ( uint256 bucket = criticalBucket; - principalToAdd > 0 && bucket <= lastBucket; + principalToAdd > 0; bucket++ ) { + assert(bucket <= lastBucket); // should never go past the last bucket + uint256 availableTemp = Math.min256(availableToSub, availableForBucket[bucket]); if (availableTemp == 0) { continue; @@ -748,13 +791,13 @@ contract BucketLender is principalToAdd = principalToAdd.sub(principalTemp); availableToSub = availableToSub.sub(availableTemp); - mostRecentlyUsedBucket = bucket; + criticalBucketTemp = bucket; } assert(principalToAdd == 0); assert(availableToSub == 0); - setCriticalBucket(mostRecentlyUsedBucket); + setCriticalBucket(criticalBucketTemp); } function withdrawInternal( @@ -762,8 +805,8 @@ contract BucketLender is uint256 maxWeight, uint256 maxHeldToken ) - internal - returns(uint256, uint256) + private + returns (uint256, uint256) { // calculate the user's share uint256 bucketWeight = weightForBucket[bucket]; @@ -794,12 +837,20 @@ contract BucketLender is return (owedTokenToWithdraw, heldTokenToWithdraw); } + /** + * Helper function to withdraw earned owedToken from this contract. + * + * @param bucket The bucket number to withdraw from + * @param userWeight The amount of weight the user is using to withdraw + * @param bucketWeight The total weight of the bucket + * @return The amount of owedToken being withdrawn + */ function withdrawInternalOwedToken( uint256 bucket, uint256 userWeight, uint256 bucketWeight ) - internal + private returns (uint256) { // amount to return for the bucket @@ -825,13 +876,22 @@ contract BucketLender is return owedTokenToWithdraw; } + /** + * Helper function to withdraw heldToken from this contract. + * + * @param bucket The bucket number to withdraw from + * @param userWeight The amount of weight the user is using to withdraw + * @param bucketWeight The total weight of the bucket + * @param maxHeldToken The total amount of heldToken available to withdraw + * @return The amount of heldToken being withdrawn + */ function withdrawInternalHeldToken( uint256 bucket, uint256 userWeight, uint256 bucketWeight, uint256 maxHeldToken ) - internal + private returns (uint256) { if (maxHeldToken == 0) { @@ -870,7 +930,7 @@ contract BucketLender is function setCriticalBucket( uint256 bucket ) - internal + private { // don't spend the gas to sstore unless we need to change the value if (criticalBucket != bucket) { @@ -891,7 +951,7 @@ contract BucketLender is uint256 amount, bool increase ) - internal + private { if (amount == 0) { return; @@ -919,7 +979,7 @@ contract BucketLender is uint256 amount, bool increase ) - internal + private { if (amount == 0) { return; @@ -946,7 +1006,7 @@ contract BucketLender is address account, uint256 weightToAdd ) - internal + private { weightForBucketForAccount[bucket][account] = weightForBucketForAccount[bucket][account].add(weightToAdd); @@ -966,7 +1026,7 @@ contract BucketLender is address account, uint256 maximumWeight ) - internal + private returns (uint256) { uint256 userWeight = weightForBucketForAccount[bucket][account]; @@ -986,10 +1046,12 @@ contract BucketLender is * position open will go into buckets 1+. */ function getBucketNumber() - internal + private view returns (uint256) { + assert(!Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)); + uint256 marginTimestamp = Margin(DYDX_MARGIN).getPositionStartTimestamp(POSITION_ID); // position not created, allow deposits in the first bucket @@ -1007,7 +1069,7 @@ contract BucketLender is function getBucketOwedAmount( uint256 bucket ) - internal + private view returns (uint256) { @@ -1042,7 +1104,7 @@ contract BucketLender is * Gets the principal amount of the position from the Margin contract */ function getCurrentPrincipalFromMargin() - internal + private view returns (uint256) { diff --git a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol index 17a38a38..6004d8f4 100644 --- a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol +++ b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol @@ -49,6 +49,19 @@ contract EthWrapperForBucketLender // ============ Functions ============ + /** + * In the current version of solidity, the fallback function is non-payable, so we do not need + * to define it in order to prevent sending eth to this contract directly. + */ + + /** + * Allows users to send eth directly to this contract and have it be wrapped and sent to a + * BucketLender to be lent for some margin position. + * + * @param bucketLender The address of the BucketLender contract to deposit money into + * @param beneficiary The address that will retain rights to the deposit + * @return The bucket number that was deposited into + */ function depositEth( address bucketLender, address beneficiary @@ -59,6 +72,11 @@ contract EthWrapperForBucketLender { uint256 amount = msg.value; + require( + amount != 0, + "EthWrapperForBucketLender#depositEth: Cannot deposit zero amount" + ); + // wrap the eth WETH9(WETH).deposit.value(amount)(); assert(TokenInteract.balanceOf(WETH, address(this)) >= amount); @@ -67,8 +85,6 @@ contract EthWrapperForBucketLender TokenInteract.approve(WETH, bucketLender, uint256(-1)); // deposit the tokens - uint256 bucket = BucketLender(bucketLender).deposit(beneficiary, amount); - - return bucket; + return BucketLender(bucketLender).deposit(beneficiary, amount); } } diff --git a/contracts/testing/TestBucketLender.sol b/contracts/testing/TestBucketLender.sol new file mode 100644 index 00000000..b634e05f --- /dev/null +++ b/contracts/testing/TestBucketLender.sol @@ -0,0 +1,90 @@ +/* + + Copyright 2018 dYdX Trading Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental "v0.5.0"; + +import { BucketLender } from "../margin/external/BucketLender/BucketLender.sol"; + + +contract TestBucketLender is BucketLender { + + uint256 SSTOREBUCKET = 0; + + constructor( + address margin, + bytes32 positionId, + address heldToken, + address owedToken, + uint32 bucketTime, + uint32 interestRate, + uint32 interestPeriod, + uint32 maxDuration, + uint32 callTimelimit, + uint256 minHeldTokenNumerator, + uint256 minHeldTokenDenominator, + address[] trustedMarginCallers + ) + public + BucketLender( + margin, + positionId, + heldToken, + owedToken, + bucketTime, + interestRate, + interestPeriod, + maxDuration, + callTimelimit, + minHeldTokenNumerator, + minHeldTokenDenominator, + trustedMarginCallers + ) + { + } + + + // should return fine + function checkInvariants() + external + view + { + uint256 cb = criticalBucket; + uint256 principalSum = 0; + uint256 availableSum = 0; + uint i = 0; + + for(i = 0; (principalSum != principalTotal || availableSum != availableTotal); i++) { + uint256 aa = availableForBucket[i]; + uint256 op = principalForBucket[i]; + require(i >= cb || aa == 0); + require(i <= cb || op == 0); + principalSum += op; + availableSum += aa; + } + + require(principalSum == principalTotal); + require(availableSum == availableTotal); + + for (uint j = i; j < i + 10; j++) { + uint256 aa = availableForBucket[i]; + uint256 op = principalForBucket[i]; + require(aa == 0 && op == 0); + } + } +} diff --git a/test/margin/TestPositionGetters.js b/test/margin/TestPositionGetters.js index b8f110fd..6bf2ca0e 100644 --- a/test/margin/TestPositionGetters.js +++ b/test/margin/TestPositionGetters.js @@ -276,9 +276,18 @@ contract('PositionGetters', (accounts) => { describe('#getPositionPrincipal and #getPositionBalance', () => { it('check values for principal and balance', async () => { + const [principal0, balance0] = await Promise.all([ + dydxMargin.getPositionPrincipal.call(BYTES32.BAD_ID), + dydxMargin.getPositionBalance.call(BYTES32.BAD_ID), + ]); + expect(principal0).to.be.bignumber.equal(0); + expect(balance0).to.be.bignumber.equal(0); + const { expectedHeldTokenBalance } = getTokenAmountsFromOpen(openTx); - const principal1 = await dydxMargin.getPositionPrincipal.call(positionId); - const balance1 = await dydxMargin.getPositionBalance.call(positionId); + const [principal1, balance1] = await Promise.all([ + dydxMargin.getPositionPrincipal.call(positionId), + dydxMargin.getPositionBalance.call(positionId), + ]); expect(principal1).to.be.bignumber.equal(openTx.principal); expect(balance1).to.be.bignumber.equal(expectedHeldTokenBalance); diff --git a/test/margin/external/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js similarity index 71% rename from test/margin/external/TestBucketLender.js rename to test/margin/external/bucketlender/TestBucketLender.js index aec2881b..7cac8a80 100644 --- a/test/margin/external/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -7,21 +7,18 @@ const BigNumber = require('bignumber.js'); const Margin = artifacts.require("Margin"); const HeldToken = artifacts.require("TokenA"); const OwedToken = artifacts.require("TokenB"); -const BucketLender = artifacts.require("BucketLender"); +const TestBucketLender = artifacts.require("TestBucketLender"); const ERC20ShortCreator = artifacts.require("ERC20ShortCreator"); const OpenDirectlyExchangeWrapper = artifacts.require("OpenDirectlyExchangeWrapper"); -const { transact } = require('../../helpers/ContractHelper'); -const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../helpers/Constants'); -const { expectThrow } = require('../../helpers/ExpectHelper'); -const { issueAndSetAllowance } = require('../../helpers/TokenHelper'); -const { signLoanOffering } = require('../../helpers/LoanHelper'); +const { transact } = require('../../../helpers/ContractHelper'); +const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../../helpers/Constants'); +const { expectThrow } = require('../../../helpers/ExpectHelper'); +const { issueAndSetAllowance } = require('../../../helpers/TokenHelper'); const { issueTokenToAccountInAmountAndApproveProxy, - doOpenPosition, - getPosition, callIncreasePosition -} = require('../../helpers/MarginHelper'); +} = require('../../../helpers/MarginHelper'); const { wait } = require('@digix/tempo')(web3); const OT = new BigNumber('1e18'); @@ -65,6 +62,8 @@ async function doWithdraw(account, bucket) { async function runAliceBot() { const aliceAmount = OT; console.log(" runnning alice bot..."); + console.log(" checking invariants..."); + await bucketLender.checkInvariants(); console.log(" depositing..."); await issueAndSetAllowance(owedToken, alice, aliceAmount, bucketLender.address); const bucket = await transact(bucketLender.deposit, alice, aliceAmount, { from: alice }); @@ -74,6 +73,8 @@ async function runAliceBot() { expect(owedWithdrawn.plus(1)).to.bignumber.gte(aliceAmount); expect(heldWithdrawn).to.be.bignumber.eq(0); expect(remainingWeight).to.be.bignumber.eq(0); + console.log(" checking invariants..."); + await bucketLender.checkInvariants(); console.log(" done."); } @@ -95,18 +96,24 @@ async function setUpPosition(accounts) { const nonce = Math.floor(Math.random() * 12983748912748); POSITION_ID = web3Instance.utils.soliditySha3(accounts[0], nonce); - bucketLender = await BucketLender.new( + const principal = new BigNumber('22e18'); + const deposit = new BigNumber('60e18'); + + bucketLender = await TestBucketLender.new( Margin.address, POSITION_ID, heldToken.address, owedToken.address, BUCKET_TIME, + INTEREST_RATE, + INTEREST_PERIOD, + MAX_DURATION, + CALL_TIMELIMIT, + deposit, // MIN_HELD_TOKEN_NUMERATOR, + principal, // MIN_HELD_TOKEN_DENOMINATOR, [accounts[0]] // trusted margin-callers ); - const principal = new BigNumber('22e18'); - const deposit = new BigNumber('60e18'); - await Promise.all([ issueTokenToAccountInAmountAndApproveProxy(heldToken, accounts[0], deposit), doDeposit(lender1, OT.times(2)), @@ -165,6 +172,7 @@ contract('BucketLender', accounts => { c_isTrusted, c_isTrusted2, + c_wasForceClosed, c_criticalBucket, c_cachedRepaidAmount, @@ -189,6 +197,7 @@ contract('BucketLender', accounts => { bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[0]), bucketLender.TRUSTED_MARGIN_CALLERS.call(accounts[1]), + bucketLender.wasForceClosed.call(), bucketLender.criticalBucket.call(), bucketLender.cachedRepaidAmount.call(), @@ -214,6 +223,7 @@ contract('BucketLender', accounts => { expect(c_isTrusted).to.be.true; expect(c_isTrusted2).to.be.false; + expect(c_wasForceClosed).to.be.false; expect(c_criticalBucket).to.be.bignumber.eq(0); expect(c_cachedRepaidAmount).to.be.bignumber.eq(0); @@ -269,7 +279,7 @@ contract('BucketLender', accounts => { await wait(60 * 60 * 24 * 4); await issueTokenToAccountInAmountAndApproveProxy(owedToken, trader, OT.times(1000)); - + await bucketLender.checkInvariants(); console.log(" closing position..."); await margin.closePositionDirectly( POSITION_ID, @@ -278,22 +288,25 @@ contract('BucketLender', accounts => { { from: trader } ); console.log(" done."); + await bucketLender.rebalanceBuckets(); + await bucketLender.checkInvariants(); console.log(" depositing from useless lender..."); + await expectThrow(doDeposit(uselessLender, 0)); await doDeposit(uselessLender, OT.times(3)); console.log(" done."); await wait(60 * 60 * 24 * 1); await runAliceBot(); - + await bucketLender.checkInvariants(); console.log(" increasing position..."); tx = createIncreaseTx(trader, OT.times(3)) await callIncreasePosition(margin, tx); tx = createIncreaseTx(trader, OT.times(3)) await callIncreasePosition(margin, tx); console.log(" done."); - + await bucketLender.checkInvariants(); // expect that the lenders can no longer withdraw their owedToken isnce it has been lent await expectThrow(doWithdraw(lender1, 0)); await expectThrow(doWithdraw(lender2, 0)); @@ -301,7 +314,7 @@ contract('BucketLender', accounts => { await wait(60 * 60 * 24 * 1); await runAliceBot(); - + await bucketLender.checkInvariants(); console.log(" closing position..."); await margin.closePositionDirectly( POSITION_ID, @@ -310,19 +323,32 @@ contract('BucketLender', accounts => { { from: trader } ); console.log(" done."); - + await bucketLender.checkInvariants(); await runAliceBot(); - + await bucketLender.checkInvariants(); + // margin-call + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender1 })); + await expectThrow(margin.marginCall(POSITION_ID, 1)); await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); + // can't deposit while margin-called + await expectThrow(doDeposit(uselessLender, OT)); await wait(60 * 60 * 24 * 1); - + await bucketLender.checkInvariants(); + // cancel margin-call + await expectThrow(margin.cancelMarginCall(POSITION_ID, { from: lender1 })); await margin.cancelMarginCall(POSITION_ID); + await bucketLender.checkInvariants(); + // can deposit again + await doDeposit(uselessLender, OT); await wait(60 * 60 * 24 * 1); - + await bucketLender.checkInvariants(); + // margin-call again + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender2 })); await margin.marginCall(POSITION_ID, 0); - + await bucketLender.checkInvariants(); console.log(" closing position..."); await margin.closePositionDirectly( POSITION_ID, @@ -333,9 +359,19 @@ contract('BucketLender', accounts => { console.log(" done."); await wait(60 * 60 * 24 * 1); - + await bucketLender.checkInvariants(); + // Force-recover collateral + console.log(" force-recovering collateral..."); + await expectThrow(margin.forceRecoverCollateral(POSITION_ID, accounts[0])); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); - + console.log(" done."); + await bucketLender.checkInvariants(); + // can't deposit after position closed + await expectThrow(doDeposit(uselessLender, OT)); + await bucketLender.checkInvariants(); + // do bad withdrawals + // do all remaining withdrawals + console.log(" doing all remaining withdrawals..."); for(let a = 0; a < 10; a++) { let act = accounts[a]; for(let b = 0; b < 20; b++) { @@ -350,7 +386,62 @@ contract('BucketLender', accounts => { } } } + console.log(" done."); + + // check constants + console.log(" checking constants..."); + const [ + c_wasForceClosed, + c_criticalBucket, + c_cachedRepaidAmount, + actualRepaidAmount, + c_available, + c_available2, + c_available3, + c_principal, + c_principal2, + c_weight, + c_weight2, + + isClosed, + + bucketLenderOwedToken, + bucketLenderHeldToken, + ] = await Promise.all([ + bucketLender.wasForceClosed.call(), + bucketLender.criticalBucket.call(), + bucketLender.cachedRepaidAmount.call(), + margin.getTotalOwedTokenRepaidToLender.call(POSITION_ID), + + bucketLender.availableTotal.call(), + bucketLender.availableForBucket.call(0), + bucketLender.availableForBucket.call(1), + bucketLender.principalTotal.call(), + bucketLender.principalForBucket.call(0), + bucketLender.weightForBucket.call(0), + bucketLender.weightForBucketForAccount.call(0, accounts[0]), + + margin.isPositionClosed.call(POSITION_ID), + + owedToken.balanceOf.call(bucketLender.address), + heldToken.balanceOf.call(bucketLender.address), + ]); + expect(c_wasForceClosed).to.be.true; + expect(c_criticalBucket).to.be.bignumber.eq(0); + expect(c_cachedRepaidAmount).to.be.bignumber.eq(actualRepaidAmount); + expect(c_available).to.be.bignumber.eq(0); + expect(c_available2).to.be.bignumber.eq(0); + expect(c_available3).to.be.bignumber.eq(0); + expect(c_principal).to.be.bignumber.eq(0); + expect(c_principal2).to.be.bignumber.eq(0); + expect(c_weight).to.be.bignumber.eq(0); + expect(c_weight2).to.be.bignumber.eq(0); + expect(isClosed).to.be.true; + expect(bucketLenderOwedToken).to.be.bignumber.eq(0); + expect(bucketLenderHeldToken).to.be.bignumber.eq(0); + console.log(" done."); + await bucketLender.checkInvariants(); }); }); }); @@ -382,7 +473,7 @@ function createIncreaseTx(trader, principal) { interestRate: INTEREST_RATE, interestPeriod: INTEREST_PERIOD }, - expirationTimestamp: 1000000000000, + expirationTimestamp: BIGNUMBERS.ONES_255, callTimeLimit: CALL_TIMELIMIT.toNumber(), maxDuration: MAX_DURATION.toNumber(), salt: 0, diff --git a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js new file mode 100644 index 00000000..77587b8f --- /dev/null +++ b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js @@ -0,0 +1,129 @@ +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-bignumber')()); +const BigNumber = require('bignumber.js'); + +const Margin = artifacts.require("Margin"); +const HeldToken = artifacts.require("TokenA"); +const WETH9 = artifacts.require("WETH9"); +const BucketLender = artifacts.require("BucketLender"); +const EthWrapperForBucketLender = artifacts.require("EthWrapperForBucketLender"); + +const { transact } = require('../../../helpers/ContractHelper'); +const { BIGNUMBERS, BYTES32 } = require('../../../helpers/Constants'); +const { expectThrow } = require('../../../helpers/ExpectHelper'); + +let heldToken, weth, bucketLender, ethWrapper; +const value = new BigNumber('1e10'); +const INTEREST_PERIOD = new BigNumber(60 * 60); +const INTEREST_RATE = new BigNumber(10 * 1000000); +const MAX_DURATION = new BigNumber(60 * 60 * 24 * 365); +const CALL_TIMELIMIT = new BigNumber(60 * 60 * 24); +const BUCKET_TIME = new BigNumber(60 * 60 * 24); +const PRINCIPAL = new BigNumber('1e18'); +const DEPOSIT = PRINCIPAL.times(2); +let POSITION_ID; + +contract('BucketLender', accounts => { + + // ============ Before ============ + + beforeEach('Set up contracts', async () => { + [ + heldToken, + weth, + ] = await Promise.all([ + HeldToken.new(), + WETH9.new(), + ]); + + [ + ethWrapper, + bucketLender + ] = await Promise.all([ + EthWrapperForBucketLender.new( + weth.address + ), + BucketLender.new( + Margin.address, + POSITION_ID, + heldToken.address, + weth.address, + BUCKET_TIME, + INTEREST_RATE, + INTEREST_PERIOD, + MAX_DURATION, + CALL_TIMELIMIT, + DEPOSIT, // MIN_HELD_TOKEN_NUMERATOR, + PRINCIPAL, // MIN_HELD_TOKEN_DENOMINATOR, + [] // trusted margin-callers + ) + ]); + }); + + // ============ Constructor ============ + + describe('Constructor', () => { + it('sets constants correctly', async () => { + const c_weth = await ethWrapper.WETH.call(); + expect(c_weth).to.eq(weth.address); + }); + }); + + describe('Fallback Function', () => { + it('fails', async () => { + await expectThrow(ethWrapper.send(value)); + }); + }); + + describe('#depositEth', () => { + it('succeeds for normal case', async () => { + const sender = accounts[1]; + const beneficiary = accounts[2]; + const result = await transact( + ethWrapper.depositEth, + bucketLender.address, + beneficiary, + { from: sender, value: value } + ); + + // expect bucket 0 + expect(result.result).to.be.bignumber.eq(0); + + const [ + weight1, + weight2 + ] = await Promise.all([ + bucketLender.weightForBucket.call(0), + bucketLender.weightForBucketForAccount.call(0, beneficiary), + ]); + expect(weight1).to.be.bignumber.eq(value); + expect(weight2).to.be.bignumber.eq(value); + }); + + it('fails for zero amount', async () => { + const sender = accounts[1]; + const beneficiary = accounts[2]; + await expectThrow(ethWrapper.depositEth( + bucketLender.address, + beneficiary, + { from: sender, value: BIGNUMBERS.ZERO } + )); + }); + + it('fails for bad bucketLender address', async () => { + const sender = accounts[1]; + const beneficiary = accounts[2]; + await expectThrow(ethWrapper.depositEth( + Margin.address, + beneficiary, + { from: sender, value: value } + )); + await expectThrow(ethWrapper.depositEth( + sender, + beneficiary, + { from: sender, value: value } + )); + }); + }); +}); From 121e0390ef55876fbf65e9396166a864eb41eeb1 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 18 Jun 2018 14:03:48 -0700 Subject: [PATCH 05/21] more tests --- .../external/BucketLender/BucketLender.sol | 304 ++-- .../EthWrapperForBucketLender.sol | 3 +- contracts/testing/TestBucketLender.sol | 10 +- contracts/testing/TestMarginCallDelegator.sol | 27 +- test/margin/TestVault.js | 2 +- .../external/bucketlender/TestBucketLender.js | 1262 +++++++++++++++-- 6 files changed, 1379 insertions(+), 229 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 7dd6e7f3..7499c0f0 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -46,9 +46,9 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * lend tokens for a particular margin position. * * - Each bucket has three variables: - * - Available Amount (AA) + * - Available Amount * - The available amount of tokens that the bucket has to lend out - * - Outstanding Principal (OP) + * - Outstanding Principal * - The amount of principal that the bucket is responsible for in the margin position * - Weight * - Used to keep track of each account's weighted ownership within a bucket @@ -60,23 +60,23 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * - If the position has not started: bucket = 0 * - If the position has started: bucket = ceiling(time_since_start / BUCKET_TIME) * - This is always the highest bucket; no higher bucket yet exists - * - Increase the bucket's AA + * - Increase the bucket's Available Amount * - Increase the bucket's weight and the account's weight in that bucket * * - Token Withdrawals: * - Can be from any bucket with available amount - * - Decrease the bucket's AA + * - Decrease the bucket's Available Amount * - Decrease the bucket's weight and the account's weight in that bucket * * - Increasing the Position (Lending): - * - The lowest buckets with AA are used first - * - Decreases AA - * - Increases OP + * - The lowest buckets with Available Amount are used first + * - Decreases Available Amount + * - Increases Outstanding Principal * * - Decreasing the Position (Being Paid-Back) - * - The highest buckets with OP are paid back first - * - Decreases OP - * - Increases AA + * - The highest buckets with Outstanding Principal are paid back first + * - Decreases Outstanding Principal + * - Increases Available Amount * * * - Over time, this gives highest interest rates to earlier buckets, but disallows withdrawals from @@ -84,7 +84,8 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; * - Deposits in the same bucket earn the same interest rate. * - Lenders can withdraw their funds at any time if they are not being lent (and are therefore not * making the maximum interest). - * - The highest bucket with OP is always less-than-or-equal-to the lowest bucket with AA + * - The highest bucket with Outstanding Principal is always less-than-or-equal-to the lowest bucket + with Available Amount */ contract BucketLender is HasNoEther, @@ -119,22 +120,23 @@ contract BucketLender is // ============ State Variables ============ /** - * Available Amount (AA) is the amount of tokens that is available to be lent by each bucket. + * Available Amount is the amount of tokens that is available to be lent by each bucket. * These tokens are also available to be withdrawn by the accounts that have weight in the * bucket. */ - // AA for each bucket + // Available Amount for each bucket mapping(uint256 => uint256) public availableForBucket; - // Total AA + // Total Available Amount uint256 public availableTotal; /** - * Outstanding Principal (OP) is the share of the margin position's principal that each bucket - * is responsible for. That is, each bucket with OP is owed (OP)*E^(RT) owedTokens in repayment. + * Outstanding Principal is the share of the margin position's principal that each bucket + * is responsible for. That is, each bucket with Outstanding Principal is owed + * (Outstanding Principal)*E^(RT) owedTokens in repayment. */ - // OP for each bucket + // Outstanding Principal for each bucket mapping(uint256 => uint256) public principalForBucket; - // Total OP + // Total Outstanding Principal uint256 public principalTotal; /** @@ -151,8 +153,8 @@ contract BucketLender is /** * The critical bucket is: - * - Greater-than-or-equal-to The highest bucket with OP - * - Less-than-or-equal-to the lowest bucket with AA + * - Greater-than-or-equal-to The highest bucket with Outstanding Principal + * - Less-than-or-equal-to the lowest bucket with Available Amount * * It is equal to both of these values in most cases except in an edge cases where the two * buckets are different. This value is cached to find such a bucket faster than looping through @@ -179,20 +181,27 @@ contract BucketLender is // Address of the token held in the position as collateral address public HELD_TOKEN; - uint32 public MAX_DURATION; - uint32 public CALL_TIMELIMIT; - // Address of the token being lent address public OWED_TOKEN; // Time between new buckets uint32 public BUCKET_TIME; + // Interest rate of the position uint32 public INTEREST_RATE; + + // Interest period of the position uint32 public INTEREST_PERIOD; - uint256 public MIN_HELD_TOKEN_NUMERATOR; - uint256 public MIN_HELD_TOKEN_DENOMINATOR; + // Maximum duration of the position + uint32 public MAX_DURATION; + + // Margin-call time-limit of the position + uint32 public CALL_TIMELIMIT; + + // (NUMERATOR/DENOMINATOR) denotes the minimum collateralization ratio of the position + uint32 public MIN_HELD_TOKEN_NUMERATOR; + uint32 public MIN_HELD_TOKEN_DENOMINATOR; // Accounts that are permitted to margin-call positions (or cancel the margin call) mapping(address => bool) public TRUSTED_MARGIN_CALLERS; @@ -209,8 +218,8 @@ contract BucketLender is uint32 interestPeriod, uint32 maxDuration, uint32 callTimelimit, - uint256 minHeldTokenNumerator, - uint256 minHeldTokenDenominator, + uint32 minHeldTokenNumerator, + uint32 minHeldTokenDenominator, address[] trustedMarginCallers ) public @@ -322,6 +331,7 @@ contract BucketLender is assert(loanOffering.payer == address(this)); assert(loanOffering.owner == address(this)); require(loanOffering.taker == address(0)); + require(loanOffering.feeRecipient == address(0)); require(loanOffering.positionOwner == address(0)); require(loanOffering.lenderFeeToken == address(0)); require(loanOffering.takerFeeToken == address(0)); @@ -338,8 +348,8 @@ contract BucketLender is // CHECK VALUES32 require(loanOffering.callTimeLimit == CALL_TIMELIMIT); require(loanOffering.maxDuration == MAX_DURATION); - require(loanOffering.rates.interestRate == INTEREST_RATE); - require(loanOffering.rates.interestPeriod == INTEREST_PERIOD); + assert(loanOffering.rates.interestRate == INTEREST_RATE); + assert(loanOffering.rates.interestPeriod == INTEREST_PERIOD); // no need to require anything about loanOffering.signature @@ -361,32 +371,33 @@ contract BucketLender is ) external onlyMargin + nonReentrant onlyPosition(positionId) returns (address) { MarginCommon.Position memory position = MarginHelper.getPosition(DYDX_MARGIN, POSITION_ID); + uint256 initialPrincipal = position.principal; assert(principalTotal == 0); - assert(position.principal > 0); - assert(position.owedToken == OWED_TOKEN); - assert(position.heldToken == HELD_TOKEN); - - // assert enough heldToken - assert( - Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= - MathHelpers.getPartialAmount( - MIN_HELD_TOKEN_NUMERATOR, - MIN_HELD_TOKEN_DENOMINATOR, - position.principal - ) + assert(initialPrincipal > 0); + + // lenders should have certain guarantees about how the position is collateralized + require(position.owedToken == OWED_TOKEN); + require(position.heldToken == HELD_TOKEN); + + // require enough heldToken + uint256 minStartingHeldToken = MathHelpers.getPartialAmount( + uint256(MIN_HELD_TOKEN_NUMERATOR), + uint256(MIN_HELD_TOKEN_DENOMINATOR), + initialPrincipal ); + require(Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= minStartingHeldToken); // assert that the position was opened without using funds from this position // (i.e. that it was opened using openWithoutCounterparty()) assert(from != address(this)); // set relevant constants - uint256 initialPrincipal = position.principal; principalForBucket[0] = initialPrincipal; principalTotal = initialPrincipal; weightForBucket[0] = weightForBucket[0].add(initialPrincipal); @@ -414,6 +425,7 @@ contract BucketLender is ) external onlyMargin + nonReentrant onlyPosition(positionId) returns (address) { @@ -430,10 +442,12 @@ contract BucketLender is "BucketLender#increaseLoanOnBehalfOf: No lending not-accounted-for funds" ); - uint256 principalAfterIncrease = getCurrentPrincipalFromMargin(); + // This function is only called after the state has been updated in the base protocol; + // thus, the principal in the base protocol will equal the principal after the increase + uint256 principalAfterIncrease = getPositionPrincipal(); uint256 principalBeforeIncrease = principalAfterIncrease.sub(principalAdded); - // principalTotal was the principal after the last increase + // principalTotal was the principal after the previous increase accountForClose(principalTotal.sub(principalBeforeIncrease)); accountForIncrease(principalAdded, lentAmount); @@ -458,6 +472,7 @@ contract BucketLender is ) external onlyMargin + nonReentrant onlyPosition(positionId) returns (address) { @@ -466,7 +481,7 @@ contract BucketLender is "BucketLender#marginCallOnBehalfOf: Margin-caller must be trusted" ); require( - depositAmount == 0, // disallows any deposit amount to cancel the margin-call + depositAmount == 0, // prevents depositing from canceling the margin-call "BucketLender#marginCallOnBehalfOf: Deposit amount must be zero" ); @@ -486,6 +501,7 @@ contract BucketLender is ) external onlyMargin + nonReentrant onlyPosition(positionId) returns (address) { @@ -513,6 +529,7 @@ contract BucketLender is ) external onlyMargin + nonReentrant onlyPosition(positionId) returns (address) { @@ -521,7 +538,7 @@ contract BucketLender is "BucketLender#forceRecoverCollateralOnBehalfOf: Recipient must be this contract" ); - rebalanceBuckets(); + rebalanceBucketsInternal(); wasForceClosed = true; @@ -530,6 +547,17 @@ contract BucketLender is // ============ Public State-Changing Functions ============ + /** + * Allow anyone to recalculate the Outstanding Principal and Available Amount for the buckets if + * part of the position has been closed since the last position increase. + */ + function rebalanceBuckets() + external + nonReentrant + { + rebalanceBucketsInternal(); + } + /** * Allows users to deposit owedToken into this contract. Allowance must be set on this contract * for "token" in at least the amount "amount". @@ -543,8 +571,13 @@ contract BucketLender is uint256 amount ) external + nonReentrant returns (uint256) { + require( + beneficiary != address(0), + "BucketLender#deposit: Beneficiary cannot be the zero address" + ); require( amount != 0, "BucketLender#deposit: Cannot deposit zero tokens" @@ -554,11 +587,11 @@ contract BucketLender is "BucketLender#deposit: Cannot deposit after the position is closed" ); require( - Margin(DYDX_MARGIN).getPositionCallTimestamp(POSITION_ID) == 0, + !Margin(DYDX_MARGIN).isPositionCalled(POSITION_ID), "BucketLender#deposit: Cannot deposit while the position is margin-called" ); - rebalanceBuckets(); + rebalanceBucketsInternal(); TokenInteract.transferFrom( OWED_TOKEN, @@ -567,7 +600,7 @@ contract BucketLender is amount ); - uint256 bucket = getBucketNumber(); + uint256 bucket = getCurrentBucket(); uint256 effectiveAmount = availableForBucket[bucket].add(getBucketOwedAmount(bucket)); @@ -582,9 +615,16 @@ contract BucketLender is ); } - accountForDeposit(bucket, beneficiary, weightToAdd); + require( + weightToAdd != 0, + "BucketLender#deposit: Cannot deposit for zero weight" + ); - changeAvailable(bucket, amount, true); + // update state + updateAvailable(bucket, amount, true); + weightForBucketForAccount[bucket][beneficiary] = + weightForBucketForAccount[bucket][beneficiary].add(weightToAdd); + weightForBucket[bucket] = weightForBucket[bucket].add(weightToAdd); emit Deposit( beneficiary, @@ -601,12 +641,12 @@ contract BucketLender is * bucket. * * While the position is open, a bucket's share is equal to: - * Owed Token: AA + OP * (1 + interest) + * Owed Token: (Available Amount) + (Outstanding Principal) * (1 + interest) * Held Token: 0 * * After the position is closed, a bucket's share is equal to: - * Owed Token: AA - * Held Token: (Held Token Balance) * (OP / Total OP) + * Owed Token: (Available Amount) + * Held Token: (Held Token Balance) * (Outstanding Principal) / (Total Outstanding Principal) * * @param buckets The bucket numbers to withdraw from * @param maxWeights The maximum weight to withdraw from each bucket. The amount of tokens @@ -624,6 +664,7 @@ contract BucketLender is address beneficiary ) external + nonReentrant returns (uint256, uint256) { require( @@ -635,18 +676,18 @@ contract BucketLender is "BucketLender#withdraw: The lengths of the input arrays must match" ); - rebalanceBuckets(); + rebalanceBucketsInternal(); uint256 totalOwedToken = 0; uint256 totalHeldToken = 0; - uint256 maxHeldToken = - Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID) ? - TokenInteract.balanceOf(HELD_TOKEN, address(this)) : - 0; + uint256 maxHeldToken = 0; + if (wasForceClosed) { + maxHeldToken = TokenInteract.balanceOf(HELD_TOKEN, address(this)); + } for (uint256 i = 0; i < buckets.length; i++) { - (uint256 owedTokenForBucket, uint256 heldTokenForBucket) = withdrawInternal( + (uint256 owedTokenForBucket, uint256 heldTokenForBucket) = withdrawSingleBucket( buckets[i], maxWeights[i], maxHeldToken @@ -663,14 +704,14 @@ contract BucketLender is return (totalOwedToken, totalHeldToken); } - // ============ Public State-Changing Functions ============ + // ============ Helper Functions ============ /** - * Allow anyone to refresh the bucket amounts if part of the position was closed since the last - * position increase. + * Recalculates the Outstanding Principal and Available Amount for the buckets. Only changes the + * state if part of the position has been closed since the last position increase. */ - function rebalanceBuckets() - public + function rebalanceBucketsInternal() + internal { // if force-closed, don't update the outstanding principal values; they are needed to repay // lenders with heldToken @@ -678,15 +719,13 @@ contract BucketLender is return; } - uint256 marginPrincipal = getCurrentPrincipalFromMargin(); + uint256 marginPrincipal = getPositionPrincipal(); accountForClose(principalTotal.sub(marginPrincipal)); assert(principalTotal == marginPrincipal); } - // ============ Helper Functions ============ - /** * Updates the state variables at any time. Only does anything after the position has been * closed or partially-closed since the last time this function was called. @@ -730,8 +769,8 @@ contract BucketLender is availableToAdd ); - changeAvailable(bucket, availableTemp, true); - changePrincipal(bucket, principalTemp, false); + updateAvailable(bucket, availableTemp, true); + updatePrincipal(bucket, principalTemp, false); principalToSub = principalToSub.sub(principalTemp); availableToAdd = availableToAdd.sub(availableTemp); @@ -767,7 +806,7 @@ contract BucketLender is uint256 criticalBucketTemp; // loop over buckets in order starting from the critical bucket - uint256 lastBucket = getBucketNumber(); + uint256 lastBucket = getCurrentBucket(); for ( uint256 bucket = criticalBucket; principalToAdd > 0; @@ -785,8 +824,8 @@ contract BucketLender is principalToAdd ); - changeAvailable(bucket, availableTemp, false); - changePrincipal(bucket, principalTemp, true); + updateAvailable(bucket, availableTemp, false); + updatePrincipal(bucket, principalTemp, true); principalToAdd = principalToAdd.sub(principalTemp); availableToSub = availableToSub.sub(availableTemp); @@ -800,7 +839,16 @@ contract BucketLender is setCriticalBucket(criticalBucketTemp); } - function withdrawInternal( + /** + * Withdraw + * + * @param bucket The bucket number to withdraw from + * @param maxWeight The maximum weight to withdraw + * @param maxHeldToken The total amount of heldToken that has been force-recovered + * @return 1) The number of owedTokens withdrawn + * 2) The number of heldTokens withdrawn + */ + function withdrawSingleBucket( uint256 bucket, uint256 maxWeight, uint256 maxHeldToken @@ -810,18 +858,31 @@ contract BucketLender is { // calculate the user's share uint256 bucketWeight = weightForBucket[bucket]; - uint256 userWeight = accountForWithdraw(bucket, msg.sender, maxWeight); + if (bucketWeight == 0) { + return (0, 0); + } + + uint256 userWeight = weightForBucketForAccount[bucket][msg.sender]; + uint256 weightToWithdraw = Math.min256(maxWeight, userWeight); + if (weightToWithdraw == 0) { + return (0, 0); + } + + // update state + weightForBucket[bucket] = weightForBucket[bucket].sub(weightToWithdraw); + weightForBucketForAccount[bucket][msg.sender] = userWeight.sub(weightToWithdraw); - uint256 owedTokenToWithdraw = withdrawInternalOwedToken( + // calculate for owedToken + uint256 owedTokenToWithdraw = withdrawOwedToken( bucket, - userWeight, + weightToWithdraw, bucketWeight ); // calculate for heldToken - uint256 heldTokenToWithdraw = withdrawInternalHeldToken( + uint256 heldTokenToWithdraw = withdrawHeldToken( bucket, - userWeight, + weightToWithdraw, bucketWeight, maxHeldToken ); @@ -829,7 +890,7 @@ contract BucketLender is emit Withdraw( msg.sender, bucket, - userWeight, + weightToWithdraw, owedTokenToWithdraw, heldTokenToWithdraw ); @@ -845,7 +906,7 @@ contract BucketLender is * @param bucketWeight The total weight of the bucket * @return The amount of owedToken being withdrawn */ - function withdrawInternalOwedToken( + function withdrawOwedToken( uint256 bucket, uint256 userWeight, uint256 bucketWeight @@ -867,11 +928,11 @@ contract BucketLender is // check that there is enough token to give back require( owedTokenToWithdraw <= availableForBucket[bucket], - "BucketLender#withdrawInternalOwedToken: There must be enough available owedToken" + "BucketLender#withdrawOwedToken: There must be enough available owedToken" ); // update amounts - changeAvailable(bucket, owedTokenToWithdraw, false); + updateAvailable(bucket, owedTokenToWithdraw, false); return owedTokenToWithdraw; } @@ -885,7 +946,7 @@ contract BucketLender is * @param maxHeldToken The total amount of heldToken available to withdraw * @return The amount of heldToken being withdrawn */ - function withdrawInternalHeldToken( + function withdrawHeldToken( uint256 bucket, uint256 userWeight, uint256 bucketWeight, @@ -915,7 +976,7 @@ contract BucketLender is maxHeldToken ); - changePrincipal(bucket, principalForBucketForAccount, false); + updatePrincipal(bucket, principalForBucketForAccount, false); return heldTokenToWithdraw; } @@ -946,7 +1007,7 @@ contract BucketLender is * @param amount The amount to change the available amount by * @param increase True if positive change, false if negative change */ - function changeAvailable( + function updateAvailable( uint256 bucket, uint256 amount, bool increase @@ -974,7 +1035,7 @@ contract BucketLender is * @param amount The amount to change the principal amount by * @param increase True if positive change, false if negative change */ - function changePrincipal( + function updatePrincipal( uint256 bucket, uint256 amount, bool increase @@ -994,77 +1055,36 @@ contract BucketLender is } } - /** - * Increases the 'weight' values for a bucket and an account within that bucket - * - * @param bucket The bucket number - * @param account The account to remove weight from - * @param weightToAdd Adds this amount of weight - */ - function accountForDeposit( - uint256 bucket, - address account, - uint256 weightToAdd - ) - private - { - weightForBucketForAccount[bucket][account] = - weightForBucketForAccount[bucket][account].add(weightToAdd); - weightForBucket[bucket] = weightForBucket[bucket].add(weightToAdd); - } - - /** - * Decreases the 'weight' values for a bucket and an account within that bucket. - * - * @param bucket The bucket number - * @param account The account to remove weight from - * @param maximumWeight Removes up-to this amount of weight - * @return The amount of weight removed - */ - function accountForWithdraw( - uint256 bucket, - address account, - uint256 maximumWeight - ) - private - returns (uint256) - { - uint256 userWeight = weightForBucketForAccount[bucket][account]; - uint256 weightToWithdraw = Math.min256(userWeight, maximumWeight); - - weightForBucket[bucket] = weightForBucket[bucket].sub(weightToWithdraw); - weightForBucketForAccount[bucket][account] = userWeight.sub(weightToWithdraw); - - return weightToWithdraw; - } - // ============ Getter Functions ============ /** - * Get the current bucket number that funds will be deposited into. This is the highest bucket - * so far. All lent funds before the position open will go into bucket 0. All lent funds after - * position open will go into buckets 1+. + * Get the current bucket number that funds will be deposited into. This is also the highest + * bucket so far. */ - function getBucketNumber() + function getCurrentBucket() private view returns (uint256) { assert(!Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID)); - uint256 marginTimestamp = Margin(DYDX_MARGIN).getPositionStartTimestamp(POSITION_ID); - - // position not created, allow deposits in the first bucket - if (marginTimestamp == 0) { + // if position not created, allow deposits in the first bucket + if (!Margin(DYDX_MARGIN).containsPosition(POSITION_ID)) { return 0; } + // return the number of BUCKET_TIME periods elapsed since the position start, rounded-up + uint256 marginTimestamp = Margin(DYDX_MARGIN).getPositionStartTimestamp(POSITION_ID); return block.timestamp.sub(marginTimestamp).div(BUCKET_TIME).add(1); } /** * Gets the outstanding amount of owedToken owed to a bucket. This is the principal amount of - * the bucket multiplied by the interest accrued in the position. + * the bucket multiplied by the interest accrued in the position. If the position is closed, + * then any outstanding principal will never be repaid in the form of owedToken. + * + * @param bucket The bucket number + * @return The amount of owedToken that this bucket expects to be paid-back if the posi */ function getBucketOwedAmount( uint256 bucket @@ -1101,9 +1121,9 @@ contract BucketLender is } /** - * Gets the principal amount of the position from the Margin contract + * Gets the position's current principal amount from the Margin contract */ - function getCurrentPrincipalFromMargin() + function getPositionPrincipal() private view returns (uint256) diff --git a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol index 6004d8f4..a623e78a 100644 --- a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol +++ b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol @@ -21,6 +21,7 @@ pragma experimental "v0.5.0"; import { BucketLender } from "./BucketLender.sol"; import { WETH9 } from "../../../external/weth/WETH9.sol"; +import { MathHelpers } from "../../../lib/MathHelpers.sol"; import { TokenInteract } from "../../../lib/TokenInteract.sol"; @@ -82,7 +83,7 @@ contract EthWrapperForBucketLender assert(TokenInteract.balanceOf(WETH, address(this)) >= amount); // approve for "unlimited amount". WETH9 leaves this value as-is when doing transferFrom - TokenInteract.approve(WETH, bucketLender, uint256(-1)); + TokenInteract.approve(WETH, bucketLender, MathHelpers.maxUint256()); // deposit the tokens return BucketLender(bucketLender).deposit(beneficiary, amount); diff --git a/contracts/testing/TestBucketLender.sol b/contracts/testing/TestBucketLender.sol index b634e05f..ca3028ce 100644 --- a/contracts/testing/TestBucketLender.sol +++ b/contracts/testing/TestBucketLender.sol @@ -19,6 +19,8 @@ pragma solidity 0.4.24; pragma experimental "v0.5.0"; +import { Margin } from "../margin/Margin.sol"; +import { TokenInteract } from "../lib/TokenInteract.sol"; import { BucketLender } from "../margin/external/BucketLender/BucketLender.sol"; @@ -36,8 +38,8 @@ contract TestBucketLender is BucketLender { uint32 interestPeriod, uint32 maxDuration, uint32 callTimelimit, - uint256 minHeldTokenNumerator, - uint256 minHeldTokenDenominator, + uint32 minHeldTokenNumerator, + uint32 minHeldTokenDenominator, address[] trustedMarginCallers ) public @@ -78,6 +80,10 @@ contract TestBucketLender is BucketLender { availableSum += aa; } + require(TokenInteract.balanceOf(OWED_TOKEN, address(this)) >= availableTotal); + if (principalTotal > 0) { + require(Margin(DYDX_MARGIN).getPositionPrincipal(POSITION_ID) <= principalTotal); + } require(principalSum == principalTotal); require(availableSum == availableTotal); diff --git a/contracts/testing/TestMarginCallDelegator.sol b/contracts/testing/TestMarginCallDelegator.sol index 95c429ef..ec9e914b 100644 --- a/contracts/testing/TestMarginCallDelegator.sol +++ b/contracts/testing/TestMarginCallDelegator.sol @@ -34,6 +34,7 @@ contract TestMarginCallDelegator is address public CALLER; address public CANCELER; + address public TO_RETURN; constructor( address margin, @@ -45,6 +46,7 @@ contract TestMarginCallDelegator is { CALLER = caller; CANCELER = canceler; + TO_RETURN = address(this); } function receiveLoanOwnership( @@ -68,7 +70,7 @@ contract TestMarginCallDelegator is returns (address) { require(caller == CALLER); - return address(this); + return TO_RETURN; } function cancelMarginCallOnBehalfOf( @@ -80,6 +82,27 @@ contract TestMarginCallDelegator is returns (address) { require(canceler == CANCELER); - return address(this); + return TO_RETURN; + } + + function forceRecoverCollateralOnBehalfOf( + address, + bytes32, + address + ) + onlyMargin + external + view + returns (address) + { + return TO_RETURN; + } + + function setToReturn( + address toReturn + ) + external + { + TO_RETURN = toReturn; } } diff --git a/test/margin/TestVault.js b/test/margin/TestVault.js index c80e2a98..6660d348 100644 --- a/test/margin/TestVault.js +++ b/test/margin/TestVault.js @@ -264,7 +264,7 @@ contract('Vault', accounts => { expect(nToken.result).to.be.bignumber.equal(expected); } - expectThrow( + await expectThrow( vault.withdrawExcessToken(token.address, to, { from }) ); } diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index 7cac8a80..da67810b 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -8,6 +8,8 @@ const Margin = artifacts.require("Margin"); const HeldToken = artifacts.require("TokenA"); const OwedToken = artifacts.require("TokenB"); const TestBucketLender = artifacts.require("TestBucketLender"); +const TestLoanOwner = artifacts.require("TestLoanOwner"); +const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); const ERC20ShortCreator = artifacts.require("ERC20ShortCreator"); const OpenDirectlyExchangeWrapper = artifacts.require("OpenDirectlyExchangeWrapper"); @@ -15,13 +17,17 @@ const { transact } = require('../../../helpers/ContractHelper'); const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../../helpers/Constants'); const { expectThrow } = require('../../../helpers/ExpectHelper'); const { issueAndSetAllowance } = require('../../../helpers/TokenHelper'); +const { signLoanOffering } = require('../../../helpers/LoanHelper'); const { issueTokenToAccountInAmountAndApproveProxy, - callIncreasePosition + issueTokensAndSetAllowances, + callIncreasePosition, + callOpenPosition, + createOpenTx, } = require('../../../helpers/MarginHelper'); const { wait } = require('@digix/tempo')(web3); -const OT = new BigNumber('1e18'); +let OT = new BigNumber('1e18'); const web3Instance = new Web3(web3.currentProvider); @@ -30,42 +36,117 @@ const INTEREST_RATE = new BigNumber(10 * 1000000); const MAX_DURATION = new BigNumber(60 * 60 * 24 * 365); const CALL_TIMELIMIT = new BigNumber(60 * 60 * 24); const BUCKET_TIME = new BigNumber(60 * 60 * 24); -let POSITION_ID; +let POSITION_ID, NONCE; +let testLoanOwner, testMarginCallDelegator; let margin, heldToken, owedToken; let bucketLender; -let lender1, lender2, uselessLender, trader, alice; +let TRUSTED_PARTY, lender1, lender2, uselessLender, trader, alice; + +function gcd(a, b) { + if (!b) { + return a; + } + return gcd(b, a % b); +} // grants tokens to a lender and has them deposit them into the bucket lender async function doDeposit(account, amount) { console.log(" depositing..."); await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); - console.log(" ..."); - await bucketLender.deposit(account, amount, { from: account }); - console.log("done."); + console.log(" ..."); + const tx = await transact(bucketLender.deposit, account, amount, { from: account }); + console.log(" done (depositing)."); + return tx.result; } // withdraws for a bucket from an account -async function doWithdraw(account, bucket) { +async function doWithdraw(account, bucket, args) { + args = args || {}; + args.beneficiary = args.beneficiary || account; + args.throws = args.throws || false; + args.weight = args.weight || BIGNUMBERS.ONES_255; + + if (args.throws) { + await expectThrow( + bucketLender.withdraw( + [bucket], + [args.weight], + args.beneficiary, + { from: account } + ) + ); + return; + } + + const[owed0, held0] = await Promise.all([ + owedToken.balanceOf.call(args.beneficiary), + heldToken.balanceOf.call(args.beneficiary), + ]); + const tx = await transact( bucketLender.withdraw, [bucket], - [BIGNUMBERS.ONES_255], - account, + [args.weight], + args.beneficiary, { from: account } ); + const [owedWithdrawn, heldWithdrawn] = tx.result; + const[owed1, held1] = await Promise.all([ + owedToken.balanceOf.call(args.beneficiary), + heldToken.balanceOf.call(args.beneficiary), + ]); + expect(owed1.minus(owed0)).to.be.bignumber.eq(owedWithdrawn); + expect(held1.minus(held0)).to.be.bignumber.eq(heldWithdrawn); + const remainingWeight = await bucketLender.weightForBucketForAccount.call(bucket, account); return {owedWithdrawn, heldWithdrawn, remainingWeight}; } -async function runAliceBot() { +async function doIncrease(amount, args) { + args = args || {}; + args.throws = args.throws || false; + + const incrTx = createIncreaseTx(trader, amount); + + if(args.throws) { + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + return; + } + + console.log(" increasing..."); + await callIncreasePosition(margin, incrTx); + console.log(" done (increasing)."); +} + +async function doClose(amount) { + console.log(" closing..."); + await margin.closePositionDirectly( + POSITION_ID, + amount, + trader, + { from: trader } + ); + console.log(" done (closing)."); +} + +async function runAliceBot(expectThrow = false) { const aliceAmount = OT; console.log(" runnning alice bot..."); console.log(" checking invariants..."); await bucketLender.checkInvariants(); console.log(" depositing..."); await issueAndSetAllowance(owedToken, alice, aliceAmount, bucketLender.address); + + if (expectThrow) { + await expectThrow(bucketLender.deposit(alice, aliceAmount, { from: alice })); + console.log(" done (alice bot)."); + return; + } + const bucket = await transact(bucketLender.deposit, alice, aliceAmount, { from: alice }); console.log(" withdrawing (bucket " + bucket.result.toString() + ")..."); const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(alice, bucket.result); @@ -75,17 +156,28 @@ async function runAliceBot() { expect(remainingWeight).to.be.bignumber.eq(0); console.log(" checking invariants..."); await bucketLender.checkInvariants(); - console.log(" done."); + console.log(" done (alice bot)."); +} + +async function giveAPositionTo(contract, accounts) { + let openTx = await createOpenTx(accounts); + openTx.loanOffering.owner = contract.address; + openTx.loanOffering.signature = await signLoanOffering(openTx.loanOffering); + await issueTokensAndSetAllowances(openTx); + openTx = await callOpenPosition(margin, openTx); + return openTx.id; } -async function setUpPosition(accounts) { +async function setUpPosition(accounts, openThePosition = true) { [ + TRUSTED_PARTY, lender1, lender2, uselessLender, trader, alice ] = [ + accounts[0], accounts[5], accounts[6], accounts[7], @@ -93,11 +185,12 @@ async function setUpPosition(accounts) { accounts[9], ]; - const nonce = Math.floor(Math.random() * 12983748912748); - POSITION_ID = web3Instance.utils.soliditySha3(accounts[0], nonce); + NONCE = Math.floor(Math.random() * 12983748912748); + POSITION_ID = web3Instance.utils.soliditySha3(TRUSTED_PARTY, NONCE); - const principal = new BigNumber('22e18'); - const deposit = new BigNumber('60e18'); + const principal = OT.times(2); + const deposit = OT.times(6); + const g = gcd(principal.toNumber(), deposit.toNumber()); bucketLender = await TestBucketLender.new( Margin.address, @@ -109,17 +202,36 @@ async function setUpPosition(accounts) { INTEREST_PERIOD, MAX_DURATION, CALL_TIMELIMIT, - deposit, // MIN_HELD_TOKEN_NUMERATOR, - principal, // MIN_HELD_TOKEN_DENOMINATOR, - [accounts[0]] // trusted margin-callers + deposit.div(g), // MIN_HELD_TOKEN_NUMERATOR, + principal.div(g), // MIN_HELD_TOKEN_DENOMINATOR, + [TRUSTED_PARTY] // trusted margin-callers ); await Promise.all([ issueTokenToAccountInAmountAndApproveProxy(heldToken, accounts[0], deposit), doDeposit(lender1, OT.times(2)), doDeposit(lender2, OT.times(3)), + issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)), + issueTokenToAccountInAmountAndApproveProxy(owedToken, trader, OT.times(1000)), + ]); + + [testLoanOwner, testMarginCallDelegator] = await Promise.all([ + TestLoanOwner.new( + Margin.address, + bucketLender.address, + bucketLender.address + ), + TestMarginCallDelegator.new( + Margin.address, + TRUSTED_PARTY, + TRUSTED_PARTY, + ), ]); + if (!openThePosition) { + return; + } + await margin.openWithoutCounterparty( [ ERC20ShortCreator.address, @@ -130,7 +242,7 @@ async function setUpPosition(accounts) { [ principal, deposit, - nonce + NONCE ], [ CALL_TIMELIMIT, @@ -143,7 +255,7 @@ async function setUpPosition(accounts) { contract('BucketLender', accounts => { - // ============ Before ============ + // ============ Before/After ============ beforeEach('Set up contracts', async () => { [ @@ -159,10 +271,15 @@ contract('BucketLender', accounts => { await setUpPosition(accounts); }); + afterEach('make checks', async () => { + await bucketLender.checkInvariants(); + }); + // ============ Constructor ============ describe('Constructor', () => { it('sets constants correctly', async () => { + await setUpPosition(accounts, false); const [ c_margin, c_owedToken, @@ -249,127 +366,1110 @@ contract('BucketLender', accounts => { }); }); + // ============ Margin-Only State-Changing Functions ============ + + describe('#verifyLoanOffering', () => { + it('prevents lending to other positions', async () => { + const tempId = POSITION_ID; + await setUpPosition(accounts); + let tx = createIncreaseTx(trader, OT); + tx.id = tempId; + await expectThrow(callIncreasePosition(margin, tx)); + }); + + it('prevents opening a position', async () => { + await setUpPosition(accounts, false); + await expectThrow( + margin.openPosition( + [ + ERC20ShortCreator.address, + owedToken.address, + heldToken.address, + bucketLender.address, + bucketLender.address, + ADDRESSES.ZERO, + ADDRESSES.ZERO, + ADDRESSES.ZERO, + ADDRESSES.ZERO, + ADDRESSES.ZERO, + OpenDirectlyExchangeWrapper.address, + ], + [ + BIGNUMBERS.ONES_255, + BIGNUMBERS.ZERO, + BIGNUMBERS.ZERO, + BIGNUMBERS.ZERO, + BIGNUMBERS.ZERO, + BIGNUMBERS.ONES_255, + BIGNUMBERS.ZERO, + OT, + OT.times(3), + NONCE + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ], + true, + BYTES.EMPTY, + BYTES.EMPTY + ) + ); + }); + + it('prevents different addresses', async () => { + let incrTx; + + // works once + incrTx = createIncreaseTx(trader, OT); + await callIncreasePosition(margin, incrTx); + + // taker + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.taker = incrTx.trader; + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // feeRecipient + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.feeRecipient = alice; + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // positionOwner + const erc20contract = await margin.getPositionOwner.call(POSITION_ID); + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.positionOwner = erc20contract; + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // lenderFeeToken + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.lenderFeeTokenAddress = heldToken.address; + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // takerFeeToken + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.takerFeeTokenAddress = heldToken.address; + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // works again + incrTx = createIncreaseTx(trader, OT); + await callIncreasePosition(margin, incrTx); + }); + + it('prevents different values', async () => { + let incrTx; + + // works once + incrTx = createIncreaseTx(trader, OT); + await callIncreasePosition(margin, incrTx); + + // maxAmount + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.rates.maxAmount = OT.times(1000); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // minAmount + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.rates.minAmount = new BigNumber(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // minHeldToken + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.rates.minHeldToken = new BigNumber(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // lenderFee + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.rates.lenderFee = new BigNumber(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // takerFee + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.rates.takerFee = new BigNumber(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // expirationTimestamp + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.expirationTimestamp = BIGNUMBERS.ONES_255.minus(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // salt + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.salt = new BigNumber(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // callTimeLimit + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.callTimeLimit = CALL_TIMELIMIT.plus(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // maxDuration + incrTx = createIncreaseTx(trader, OT); + incrTx.loanOffering.maxDuration = MAX_DURATION.plus(1); + await expectThrow( + callIncreasePosition(margin, incrTx) + ); + + // works again + incrTx = createIncreaseTx(trader, OT); + await callIncreasePosition(margin, incrTx); + }); + }); + + describe('#receiveLoanOwnership', () => { + const ogPrincipal = OT.times(2); + const ogDeposit = OT.times(2); + + it('succeeds under normal conditions', async () => { + await setUpPosition(accounts); + const owner = await margin.getPositionLender.call(POSITION_ID); + expect(owner).to.be.equal(bucketLender.address); + }); + + it('fails for the wrong heldToken', async () => { + await setUpPosition(accounts, false); + const badToken = await HeldToken.new(); + await issueTokenToAccountInAmountAndApproveProxy(badToken, accounts[0], OT.times(1000)), + await expectThrow( + margin.openWithoutCounterparty( + [ + ERC20ShortCreator.address, + owedToken.address, + badToken.address, + bucketLender.address + ], + [ + ogPrincipal, + ogDeposit, + NONCE + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ] + ) + ); + }); + + it('fails for the wrong owedToken', async () => { + await setUpPosition(accounts, false); + const badToken = await OwedToken.new(); + await expectThrow( + margin.openWithoutCounterparty( + [ + ERC20ShortCreator.address, + badToken.address, + heldToken.address, + bucketLender.address + ], + [ + ogPrincipal, + ogDeposit, + NONCE + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ] + ) + ); + }); + + it('fails for insufficient collateral', async () => { + await setUpPosition(accounts, false); + await expectThrow( + margin.openWithoutCounterparty( + [ + ERC20ShortCreator.address, + owedToken.address, + heldToken.address, + bucketLender.address + ], + [ + ogPrincipal.plus(1), + ogDeposit, + NONCE + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ] + ) + ); + }); + + it('fails for the wrong position ID during open', async () => { + await expectThrow(giveAPositionTo(testLoanOwner, accounts)); + }); + + it('fails for the wrong position ID during transfer', async () => { + await setUpPosition(accounts, false); + const openTx = await createOpenTx(accounts, { nonce: NONCE }); + await issueTokensAndSetAllowances(openTx); + await callOpenPosition(margin, openTx); + const lender = openTx.loanOffering.owner; + await margin.transferLoan(POSITION_ID, accounts[9], { from: lender }); + }); + }); + + describe('#increaseLoanOnBehalfOf', () => { + it('succeeds under normal conditions', async () => { + await doIncrease(OT); + }); + + it('fails for the wrong position ID', async () => { + await testLoanOwner.setToReturn(ADDRESSES.ONE); + const positionId = await giveAPositionTo(testLoanOwner, accounts); + + const incrTx = await createOpenTx(accounts); + incrTx.loanOffering.owner = testLoanOwner.address; + incrTx.loanOffering.rates.minHeldToken = new BigNumber(0); + incrTx.loanOffering.signature = await signLoanOffering(incrTx.loanOffering); + await issueTokensAndSetAllowances(incrTx); + incrTx.id = positionId; + incrTx.principal = incrTx.principal.div(2).floor(); + await issueTokenToAccountInAmountAndApproveProxy( + heldToken, + incrTx.trader, + incrTx.depositAmount.times(4) + ); + await expectThrow(callIncreasePosition(margin, incrTx)); + }); + + it('prevents other lenders from lending for this position', async () => { + let tx = createIncreaseTx(trader, OT); + tx.loanOffering.payer = uselessLender; + tx.loanOffering.signature = await signLoanOffering(tx.loanOffering); + await issueTokenToAccountInAmountAndApproveProxy(owedToken, uselessLender, OT.times(2)); + await expectThrow(callIncreasePosition(margin, tx)); + }); + + it('prevents lending while the position is margin-called', async () => { + await margin.marginCall(POSITION_ID, 0); + let tx = createIncreaseTx(trader, OT); + await expectThrow(callIncreasePosition(margin, tx)); + }); + + it('prevents lending of non-accounted-for-funds', async () => { + const largeAmount = OT.times(10); + await owedToken.issueTo(bucketLender.address, largeAmount); + let tx = createIncreaseTx(trader, largeAmount); + await expectThrow(callIncreasePosition(margin, tx)); + }); + }); + + describe('#marginCallOnBehalfOf', () => { + it('succeeds under normal conditions', async () => { + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + const isCalled = await margin.isPositionCalled.call(POSITION_ID); + expect(isCalled).to.be.true; + }); + + it('fails for non-zero deposit', async () => { + await expectThrow( + margin.marginCall(POSITION_ID, 1, { from: TRUSTED_PARTY }) + ); + }); + + it('fails for the wrong position ID', async () => { + const id = await giveAPositionTo(testMarginCallDelegator, accounts); + await testMarginCallDelegator.setToReturn(bucketLender.address); + await expectThrow( + margin.marginCall(id, 0, { from: TRUSTED_PARTY }) + ); + }); + + it('fails for an unauthorized account', async () => { + await expectThrow( + margin.marginCall(POSITION_ID, 0, { from: uselessLender }) + ); + }); + }); + + describe('#cancelMarginCallOnBehalfOf', () => { + beforeEach('margin-call the position', async () => { + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + const isCalled = await margin.isPositionCalled.call(POSITION_ID); + expect(isCalled).to.be.true; + await wait(1); + }); + + it('succeeds under normal conditions', async () => { + await margin.cancelMarginCall(POSITION_ID, { from: TRUSTED_PARTY }); + const isCalled = await margin.isPositionCalled.call(POSITION_ID); + expect(isCalled).to.be.false; + }); + + it('fails for an unauthorized account', async () => { + await expectThrow( + margin.cancelMarginCall(POSITION_ID, { from: uselessLender }) + ); + }); + + it('fails for the wrong position ID', async () => { + const id = await giveAPositionTo(testMarginCallDelegator, accounts); + await margin.marginCall(id, 0, { from: TRUSTED_PARTY }); + await testMarginCallDelegator.setToReturn(bucketLender.address); + await expectThrow( + margin.cancelMarginCall(id, { from: TRUSTED_PARTY }) + ); + }); + }); + + describe('#forceRecoverCollateralOnBehalfOf', () => { + beforeEach('margin-call the position', async () => { + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + await wait(MAX_DURATION.toNumber()); + }); + + it('succeeds under normal conditions', async () => { + await margin.forceRecoverCollateral( + POSITION_ID, + bucketLender.address, + { from: TRUSTED_PARTY } + ); + const closed = await margin.isPositionClosed.call(POSITION_ID); + expect(closed).to.be.true; + }); + + it('fails for the wrong recipient', async () => { + await expectThrow( + margin.forceRecoverCollateral( + POSITION_ID, + TRUSTED_PARTY, + { from: TRUSTED_PARTY } + ) + ); + }); + + it('fails for the wrong position ID', async () => { + const id = await giveAPositionTo(testMarginCallDelegator, accounts); + await margin.marginCall(id, 0, { from: TRUSTED_PARTY }); + await testMarginCallDelegator.setToReturn(bucketLender.address); + await expectThrow( + margin.forceRecoverCollateral( + id, + bucketLender.address, + { from: TRUSTED_PARTY } + ) + ); + }); + }); + + // ============ Public State-Changing Functions ============ + + describe('#deposit', () => { + it('succeeds before the position is opened', async () => { + let bucket, weight; + await setUpPosition(accounts, false); + bucket = await doDeposit(alice, OT); + expect(bucket).to.be.bignumber.eq(0); + weight = await bucketLender.weightForBucketForAccount.call(bucket, alice); + expect(weight).to.be.bignumber.eq(OT); + }); + + it('succeeds after the position is opened', async () => { + let bucket, weight; + bucket = await doDeposit(lender1, OT); + expect(bucket).to.be.bignumber.eq(1); + weight = await bucketLender.weightForBucket.call(bucket); + expect(weight).to.be.bignumber.eq(OT); + weight = await bucketLender.weightForBucketForAccount.call(bucket, lender1); + expect(weight).to.be.bignumber.eq(OT); + + await wait(BUCKET_TIME.toNumber()); + + bucket = await doDeposit(lender1, OT); + expect(bucket).to.be.bignumber.eq(2); + weight = await bucketLender.weightForBucket.call(bucket); + expect(weight).to.be.bignumber.eq(OT); + weight = await bucketLender.weightForBucketForAccount.call(bucket, lender1); + expect(weight).to.be.bignumber.eq(OT); + }); + + it('gives less weight for buckets that have already earned interest', async () => { + let bucket, weight; + await doDeposit(lender1, OT); + await doIncrease(OT.times(6)); + + await wait(60 * 60 * 12); + + bucket = await doDeposit(lender2, OT); + expect(bucket).to.be.bignumber.eq(1); + weight = await bucketLender.weightForBucket.call(bucket); + expect(weight).to.be.bignumber.lt(OT.times(2)); + weight = await bucketLender.weightForBucketForAccount.call(bucket, lender2); + expect(weight).to.be.bignumber.lt(OT); + expect(weight).to.be.bignumber.gt(0); + }); + + it('fails for zero weight', async () => { + let bucket, weight; + await doDeposit(lender1, OT); + await doIncrease(OT.times(6)); + await wait(60 * 60 * 12); + + bucket = await doDeposit(lender2, 2); + expect(bucket).to.be.bignumber.eq(1); + weight = await bucketLender.weightForBucketForAccount.call(bucket, lender2); + expect(weight).to.be.bignumber.eq(1); + + // throw for depositing 1 token (zero weight) + await expectThrow(doDeposit(lender2, 1)); + }); + + it('fails for zero deposit', async () => { + await expectThrow( + doDeposit(lender1, BIGNUMBERS.ZERO) + ); + }); + + it('fails for zero address', async () => { + await issueAndSetAllowance(owedToken, lender1, OT, bucketLender.address); + await expectThrow( + bucketLender.deposit(ADDRESSES.ZERO, OT, { from: lender1 }) + ); + }); + + it('fails if position is margin-called', async () => { + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + await expectThrow( + doDeposit(lender1, OT) + ); + }); + + it('fails if position is closed for force-close', async () => { + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + await wait(MAX_DURATION.toNumber()); + await margin.forceRecoverCollateral( + POSITION_ID, + bucketLender.address, + { from: TRUSTED_PARTY } + ); + await expectThrow( + doDeposit(lender1, OT) + ); + }); + + it('fails if position is closed from full-close', async () => { + await issueTokenToAccountInAmountAndApproveProxy(owedToken, TRUSTED_PARTY, OT.times(1000)); + await margin.closePositionDirectly( + POSITION_ID, + BIGNUMBERS.ONES_255, + TRUSTED_PARTY, + { from: TRUSTED_PARTY } + ); + await expectThrow( + doDeposit(lender1, OT) + ); + }); + }); + + describe('#withdraw', () => { + it('succeeds in withdrawing from bucket 0', async () => { + await doWithdraw(lender1, 0); + await doWithdraw(lender1, 0, { weight: 0 }); + }); + + it('succeeds for withdrawing what was just put in', async () => { + const bucket = await doDeposit(uselessLender, OT); + expect(bucket).to.be.bignumber.not.eq(0); + const {owedWithdrawn, heldWithdrawn, remainingWeight} = + await doWithdraw(uselessLender, bucket); + expect(owedWithdrawn).to.be.bignumber.eq(OT); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); + }); + + it('succeeds for withdrawing after some interest has been repaid', async () => { + const amount = OT.times(5); + await doIncrease(amount); + await wait(60 * 60 * 11.5); + await doClose(amount); + + { // lender1 + const {owedWithdrawn, heldWithdrawn, remainingWeight} = + await doWithdraw(lender1, 0); + expect(owedWithdrawn).to.be.bignumber.gt(OT.times(2)); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); + } + + { // lender2 + const {owedWithdrawn, heldWithdrawn, remainingWeight} = + await doWithdraw(lender2, 0); + expect(owedWithdrawn).to.be.bignumber.gt(OT.times(3)); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); + } + }); + + it('succeeds but returns no tokens for random buckets', async () => { + const {owedWithdrawn, heldWithdrawn, remainingWeight} = await doWithdraw(lender1, 1000); + expect(owedWithdrawn).to.be.bignumber.eq(0); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); + }); + + it('succeeds after full-close', async () => { + //TODO + }); + + it('succeeds after force-closing', async () => { + //TODO + }); + + it('fails for withdrawing from the current bucket', async () => { + //TODO + }); + + it('fails for no available amount', async () => { + //TODO + }); + + it('fails to withdraw to the zero address', async () => { + await doWithdraw(lender1, 0, { weight: 0, throws: true, beneficiary: ADDRESSES.ZERO }); + }); + + it('fails if the array lengths dont match', async () => { + await expectThrow( + bucketLender.withdraw( + [0], + [BIGNUMBERS.ONES_255, BIGNUMBERS.ONES_255], + lender1, + { from: lender1 } + ) + ); + await expectThrow( + bucketLender.withdraw( + [0, 1], + [BIGNUMBERS.ONES_255], + lender1, + { from: lender1 } + ) + ); + }); + }); + + describe('#rebalanceBuckets', () => { + it('TODO', async () => { + //TODO + }); + + //TODO: add more tests + }); + + // ============ Integration Tests ============ + describe('Integration Test', () => { - it('does the complicated integration test', async () => { + it('Normal integration test', async () => { await runAliceBot(); - console.log(" depositing from good lender..."); await doDeposit(lender1, OT.times(3)); - console.log(" done."); - await runAliceBot(); - await issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)); - - console.log(" increasing position..."); - let tx = createIncreaseTx(trader, OT.times(3)); - await callIncreasePosition(margin, tx); - console.log(" done."); - + await doIncrease(OT.times(3)); await runAliceBot(); await wait(60 * 60 * 24 * 4); + await runAliceBot(); - console.log(" depositing from useless lender..."); await doDeposit(uselessLender, OT.times(3)); - console.log(" done."); - await runAliceBot(); await wait(60 * 60 * 24 * 4); + await runAliceBot(); - await issueTokenToAccountInAmountAndApproveProxy(owedToken, trader, OT.times(1000)); - await bucketLender.checkInvariants(); - console.log(" closing position..."); - await margin.closePositionDirectly( - POSITION_ID, - tx.principal, - trader, - { from: trader } - ); - console.log(" done."); + await doClose(OT.times(3)); + await runAliceBot(); await bucketLender.rebalanceBuckets(); - await bucketLender.checkInvariants(); - console.log(" depositing from useless lender..."); + await runAliceBot(); + await expectThrow(doDeposit(uselessLender, 0)); await doDeposit(uselessLender, OT.times(3)); - console.log(" done."); + await runAliceBot(); await wait(60 * 60 * 24 * 1); + await runAliceBot(); + await doIncrease(OT.times(3)); + await doIncrease(OT.times(5)); await runAliceBot(); - await bucketLender.checkInvariants(); - console.log(" increasing position..."); - tx = createIncreaseTx(trader, OT.times(3)) - await callIncreasePosition(margin, tx); - tx = createIncreaseTx(trader, OT.times(3)) - await callIncreasePosition(margin, tx); - console.log(" done."); - await bucketLender.checkInvariants(); + // expect that the lenders can no longer withdraw their owedToken isnce it has been lent await expectThrow(doWithdraw(lender1, 0)); await expectThrow(doWithdraw(lender2, 0)); + await runAliceBot(); await wait(60 * 60 * 24 * 1); - await runAliceBot(); - await bucketLender.checkInvariants(); - console.log(" closing position..."); - await margin.closePositionDirectly( - POSITION_ID, - tx.principal.div(2), - trader, - { from: trader } - ); - console.log(" done."); - await bucketLender.checkInvariants(); + + await doClose(OT.times(3)); await runAliceBot(); - await bucketLender.checkInvariants(); + // margin-call await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender1 })); await expectThrow(margin.marginCall(POSITION_ID, 1)); await margin.marginCall(POSITION_ID, 0); await bucketLender.checkInvariants(); + // can't deposit while margin-called await expectThrow(doDeposit(uselessLender, OT)); + // can't increase position while margin-called + await doIncrease(OT, {throws: true}); + await bucketLender.checkInvariants(); + await wait(60 * 60 * 24 * 1); await bucketLender.checkInvariants(); + // cancel margin-call await expectThrow(margin.cancelMarginCall(POSITION_ID, { from: lender1 })); await margin.cancelMarginCall(POSITION_ID); - await bucketLender.checkInvariants(); + await runAliceBot(); + // can deposit again await doDeposit(uselessLender, OT); + await runAliceBot(); + + // can increase again + await doIncrease(OT.times(6)); + await runAliceBot(); await wait(60 * 60 * 24 * 1); - await bucketLender.checkInvariants(); - // margin-call again + await runAliceBot(); + + // margin-call again await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender2 })); await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); - console.log(" closing position..."); - await margin.closePositionDirectly( - POSITION_ID, - BIGNUMBERS.ONES_255, - trader, - { from: trader } - ); + + await doClose(BIGNUMBERS.ONES_255); + + await wait(60 * 60 * 24 * 1); + await bucketLender.checkInvariants(); + + // Force-recover collateral + console.log(" force-recovering collateral..."); + await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + console.log(" done."); + await bucketLender.checkInvariants(); + + // can't deposit after position closed + const closed = await margin.isPositionClosed(POSITION_ID); + expect(closed).to.be.true; + await expectThrow(doDeposit(uselessLender, OT)); + await bucketLender.checkInvariants(); + + // do all remaining withdrawals + console.log(" doing all remaining withdrawals..."); + for(let a = 0; a < 10; a++) { + let act = accounts[a]; + for(let b = 0; b < 20; b++) { + const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); + if (!hasWeight.isZero()) { + console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); + const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + console.log(" owed: " + owedWithdrawn.toString()); + console.log(" held: " + heldWithdrawn.toString()); + console.log(" remw: " + remainingWeight.toString()); + console.log(" done."); + } + } + } + console.log(" done."); + + // check constants + console.log(" checking constants..."); + const [ + c_wasForceClosed, + c_criticalBucket, + c_cachedRepaidAmount, + actualRepaidAmount, + + c_available, + c_available2, + c_available3, + c_principal, + c_principal2, + c_weight, + c_weight2, + + isClosed, + + bucketLenderOwedToken, + bucketLenderHeldToken, + ] = await Promise.all([ + bucketLender.wasForceClosed.call(), + bucketLender.criticalBucket.call(), + bucketLender.cachedRepaidAmount.call(), + margin.getTotalOwedTokenRepaidToLender.call(POSITION_ID), + + bucketLender.availableTotal.call(), + bucketLender.availableForBucket.call(0), + bucketLender.availableForBucket.call(1), + bucketLender.principalTotal.call(), + bucketLender.principalForBucket.call(0), + bucketLender.weightForBucket.call(0), + bucketLender.weightForBucketForAccount.call(0, accounts[0]), + + margin.isPositionClosed.call(POSITION_ID), + + owedToken.balanceOf.call(bucketLender.address), + heldToken.balanceOf.call(bucketLender.address), + ]); + expect(c_wasForceClosed).to.be.true; + expect(c_criticalBucket).to.be.bignumber.eq(0); + expect(c_cachedRepaidAmount).to.be.bignumber.eq(actualRepaidAmount); + expect(c_available).to.be.bignumber.eq(0); + expect(c_available2).to.be.bignumber.eq(0); + expect(c_available3).to.be.bignumber.eq(0); + expect(c_principal).to.be.bignumber.eq(0); + expect(c_principal2).to.be.bignumber.eq(0); + expect(c_weight).to.be.bignumber.eq(0); + expect(c_weight2).to.be.bignumber.eq(0); + expect(isClosed).to.be.true; + expect(bucketLenderOwedToken).to.be.bignumber.eq(0); + expect(bucketLenderHeldToken).to.be.bignumber.eq(0); console.log(" done."); + await bucketLender.checkInvariants(); + }); + + it('Integration test where some lenders get only heldToken paid back', async () => { + await runAliceBot(); + + await doDeposit(lender1, OT.times(3)); + await runAliceBot(); + + await doIncrease(OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 4); + await runAliceBot(); + + await doDeposit(uselessLender, OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 4); + await runAliceBot(); + + await doClose(OT.times(3)); + await runAliceBot(); + + await bucketLender.rebalanceBuckets(); + await runAliceBot(); + + await doDeposit(uselessLender, OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + await doIncrease(OT.times(3)); + await doIncrease(OT.times(5)); + await runAliceBot(); + + // expect that the lenders can no longer withdraw their owedToken isnce it has been lent + await expectThrow(doWithdraw(lender1, 0)); + await expectThrow(doWithdraw(lender2, 0)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + await doClose(OT.times(3)); + await runAliceBot(); + + // margin-call + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender1 })); + await expectThrow(margin.marginCall(POSITION_ID, 1)); + await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); + + // can't deposit while margin-called + await expectThrow(doDeposit(uselessLender, OT)); + + // can't increase position while margin-called + await doIncrease(OT, {throws: true}); + await bucketLender.checkInvariants(); await wait(60 * 60 * 24 * 1); await bucketLender.checkInvariants(); + + // cancel margin-call + await expectThrow(margin.cancelMarginCall(POSITION_ID, { from: lender1 })); + await margin.cancelMarginCall(POSITION_ID); + await runAliceBot(); + + // can deposit again + await doDeposit(uselessLender, OT); + await runAliceBot(); + + // can increase again + await doIncrease(OT.times(6)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + // margin-call again + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender2 })); + await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); + + await doClose(OT.times(3)); + await bucketLender.checkInvariants(); + + await wait(60 * 60 * 24 * 1); + await bucketLender.checkInvariants(); + // Force-recover collateral console.log(" force-recovering collateral..."); - await expectThrow(margin.forceRecoverCollateral(POSITION_ID, accounts[0])); + await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); console.log(" done."); await bucketLender.checkInvariants(); + // can't deposit after position closed + const closed = await margin.isPositionClosed(POSITION_ID); + expect(closed).to.be.true; await expectThrow(doDeposit(uselessLender, OT)); await bucketLender.checkInvariants(); - // do bad withdrawals + + // do all remaining withdrawals + console.log(" doing all remaining withdrawals..."); + for(let a = 0; a < 10; a++) { + let act = accounts[a]; + for(let b = 0; b < 20; b++) { + const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); + if (!hasWeight.isZero()) { + console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); + const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + console.log(" owed: " + owedWithdrawn.toString()); + console.log(" held: " + heldWithdrawn.toString()); + console.log(" remw: " + remainingWeight.toString()); + console.log(" done."); + } + } + } + console.log(" done."); + + // check constants + console.log(" checking constants..."); + const [ + c_wasForceClosed, + c_criticalBucket, + c_cachedRepaidAmount, + actualRepaidAmount, + + c_available, + c_available2, + c_available3, + c_principal, + c_principal2, + c_weight, + c_weight2, + + isClosed, + + bucketLenderOwedToken, + bucketLenderHeldToken, + ] = await Promise.all([ + bucketLender.wasForceClosed.call(), + bucketLender.criticalBucket.call(), + bucketLender.cachedRepaidAmount.call(), + margin.getTotalOwedTokenRepaidToLender.call(POSITION_ID), + + bucketLender.availableTotal.call(), + bucketLender.availableForBucket.call(0), + bucketLender.availableForBucket.call(1), + bucketLender.principalTotal.call(), + bucketLender.principalForBucket.call(0), + bucketLender.weightForBucket.call(0), + bucketLender.weightForBucketForAccount.call(0, accounts[0]), + + margin.isPositionClosed.call(POSITION_ID), + + owedToken.balanceOf.call(bucketLender.address), + heldToken.balanceOf.call(bucketLender.address), + ]); + expect(c_wasForceClosed).to.be.true; + expect(c_criticalBucket).to.be.bignumber.eq(5); + expect(c_cachedRepaidAmount).to.be.bignumber.eq(actualRepaidAmount); + expect(c_available).to.be.bignumber.eq(0); + expect(c_available2).to.be.bignumber.eq(0); + expect(c_available3).to.be.bignumber.eq(0); + expect(c_principal).to.be.bignumber.eq(0); + expect(c_principal2).to.be.bignumber.eq(0); + expect(c_weight).to.be.bignumber.eq(0); + expect(c_weight2).to.be.bignumber.eq(0); + expect(isClosed).to.be.true; + expect(bucketLenderOwedToken).to.be.bignumber.eq(0); + expect(bucketLenderHeldToken).to.be.bignumber.eq(0); + console.log(" done."); + await bucketLender.checkInvariants(); + }); + + it('Integration test with small numbers', async () => { + OT = new BigNumber(1); + await setUpPosition(accounts); + await runAliceBot(); + + await doDeposit(lender1, OT.times(3)); + await runAliceBot(); + await doDeposit(lender1, OT); + await runAliceBot(); + + await doIncrease(OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 4); + await runAliceBot(); + + await doDeposit(uselessLender, OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 4); + await runAliceBot(); + + await doClose(OT.times(3)); + await runAliceBot(); + + await bucketLender.rebalanceBuckets(); + await runAliceBot(); + + await doIncrease(OT.times(3)); + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + + await expectThrow(doDeposit(uselessLender, 0)); + await doDeposit(uselessLender, OT.times(3)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + await doIncrease(OT.times(3)); + await doIncrease(OT.times(5)); + await runAliceBot(); + + // expect that the lenders can no longer withdraw their owedToken isnce it has been lent + await expectThrow(doWithdraw(lender1, 0)); + await expectThrow(doWithdraw(lender2, 0)); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + await doClose(OT); + await runAliceBot(); + + // margin-call + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender1 })); + await expectThrow(margin.marginCall(POSITION_ID, 1)); + await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); + + // can't deposit while margin-called + await expectThrow(doDeposit(uselessLender, OT)); + + // can't increase position while margin-called + await doIncrease(OT, {throws: true}); + await bucketLender.checkInvariants(); + + await wait(60 * 60 * 24 * 1); + await bucketLender.checkInvariants(); + + // cancel margin-call + await expectThrow(margin.cancelMarginCall(POSITION_ID, { from: lender1 })); + await margin.cancelMarginCall(POSITION_ID); + await runAliceBot(); + + // can deposit again + await doDeposit(uselessLender, OT); + await runAliceBot(); + + // can increase again + await doIncrease(OT.times(6)); + await runAliceBot(); + + await wait(60 * 60 * 24 * 1); + await runAliceBot(); + + // margin-call again + await expectThrow(margin.marginCall(POSITION_ID, 0, { from: lender2 })); + await margin.marginCall(POSITION_ID, 0); + await bucketLender.checkInvariants(); + + await doClose(OT.times(3)); + await bucketLender.checkInvariants(); + + await wait(60 * 60 * 24 * 1); + await bucketLender.checkInvariants(); + + // Force-recover collateral + console.log(" force-recovering collateral..."); + await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + console.log(" done."); + await bucketLender.checkInvariants(); + + // can't deposit after position closed + const closed = await margin.isPositionClosed(POSITION_ID); + expect(closed).to.be.true; + await expectThrow(doDeposit(uselessLender, OT)); + await bucketLender.checkInvariants(); + // do all remaining withdrawals console.log(" doing all remaining withdrawals..."); for(let a = 0; a < 10; a++) { From 7b9a5ac5006d0b817b25d4c27de65520b4e47450 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 27 Jun 2018 17:20:43 -0700 Subject: [PATCH 06/21] prevent withdrawing from the critical bucket if it is also the current bucket --- .../external/BucketLender/BucketLender.sol | 52 ++-- .../external/bucketlender/TestBucketLender.js | 223 ++++++++++++++++-- 2 files changed, 238 insertions(+), 37 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 7499c0f0..baa98073 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -382,8 +382,14 @@ contract BucketLender is assert(initialPrincipal > 0); // lenders should have certain guarantees about how the position is collateralized - require(position.owedToken == OWED_TOKEN); - require(position.heldToken == HELD_TOKEN); + require( + position.owedToken == OWED_TOKEN, + "BucketLender#receiveLoanOwnership: Position owedToken mismatch" + ); + require( + position.heldToken == HELD_TOKEN, + "BucketLender#receiveLoanOwnership: Position heldToken mismatch" + ); // require enough heldToken uint256 minStartingHeldToken = MathHelpers.getPartialAmount( @@ -437,10 +443,6 @@ contract BucketLender is !Margin(DYDX_MARGIN).isPositionCalled(POSITION_ID), "BucketLender#increaseLoanOnBehalfOf: No lending while the position is margin-called" ); - require( - lentAmount <= availableTotal, - "BucketLender#increaseLoanOnBehalfOf: No lending not-accounted-for funds" - ); // This function is only called after the state has been updated in the base protocol; // thus, the principal in the base protocol will equal the principal after the increase @@ -678,6 +680,16 @@ contract BucketLender is rebalanceBucketsInternal(); + // decide if some bucket is unable to be withdrawn from (is locked) + // the zero value represents no-lock + uint256 lockedBucket = 0; + if ( + Margin(DYDX_MARGIN).containsPosition(POSITION_ID) && + criticalBucket == getCurrentBucket() + ) { + lockedBucket = criticalBucket; + } + uint256 totalOwedToken = 0; uint256 totalHeldToken = 0; @@ -687,8 +699,15 @@ contract BucketLender is } for (uint256 i = 0; i < buckets.length; i++) { + uint256 bucket = buckets[i]; + + // prevent withdrawing from the current bucket if it is also the critical bucket + if ((bucket != 0) && (bucket == lockedBucket)) { + continue; + } + (uint256 owedTokenForBucket, uint256 heldTokenForBucket) = withdrawSingleBucket( - buckets[i], + bucket, maxWeights[i], maxHeldToken ); @@ -711,7 +730,7 @@ contract BucketLender is * state if part of the position has been closed since the last position increase. */ function rebalanceBucketsInternal() - internal + private { // if force-closed, don't update the outstanding principal values; they are needed to repay // lenders with heldToken @@ -801,6 +820,11 @@ contract BucketLender is ) private { + require( + lentAmount <= availableTotal, + "BucketLender#accountForIncrease: No lending not-accounted-for funds" + ); + uint256 principalToAdd = principalAdded; uint256 availableToSub = lentAmount; uint256 criticalBucketTemp; @@ -1014,10 +1038,6 @@ contract BucketLender is ) private { - if (amount == 0) { - return; - } - if (increase) { availableTotal = availableTotal.add(amount); availableForBucket[bucket] = availableForBucket[bucket].add(amount); @@ -1042,10 +1062,6 @@ contract BucketLender is ) private { - if (amount == 0) { - return; - } - if (increase) { principalTotal = principalTotal.add(amount); principalForBucket[bucket] = principalForBucket[bucket].add(amount); @@ -1060,6 +1076,8 @@ contract BucketLender is /** * Get the current bucket number that funds will be deposited into. This is also the highest * bucket so far. + * + * @return The highest bucket and the one that funds will be deposited into */ function getCurrentBucket() private @@ -1122,6 +1140,8 @@ contract BucketLender is /** * Gets the position's current principal amount from the Margin contract + * + * @return The position's current principal */ function getPositionPrincipal() private diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index da67810b..d1a7dffe 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -52,11 +52,13 @@ function gcd(a, b) { // grants tokens to a lender and has them deposit them into the bucket lender async function doDeposit(account, amount) { + await bucketLender.checkInvariants(); console.log(" depositing..."); await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); console.log(" ..."); const tx = await transact(bucketLender.deposit, account, amount, { from: account }); console.log(" done (depositing)."); + await bucketLender.checkInvariants(); return tx.result; } @@ -66,6 +68,7 @@ async function doWithdraw(account, bucket, args) { args.beneficiary = args.beneficiary || account; args.throws = args.throws || false; args.weight = args.weight || BIGNUMBERS.ONES_255; + await bucketLender.checkInvariants(); if (args.throws) { await expectThrow( @@ -101,12 +104,14 @@ async function doWithdraw(account, bucket, args) { expect(held1.minus(held0)).to.be.bignumber.eq(heldWithdrawn); const remainingWeight = await bucketLender.weightForBucketForAccount.call(bucket, account); + await bucketLender.checkInvariants(); return {owedWithdrawn, heldWithdrawn, remainingWeight}; } async function doIncrease(amount, args) { args = args || {}; args.throws = args.throws || false; + await bucketLender.checkInvariants(); const incrTx = createIncreaseTx(trader, amount); @@ -120,17 +125,23 @@ async function doIncrease(amount, args) { console.log(" increasing..."); await callIncreasePosition(margin, incrTx); console.log(" done (increasing)."); + await bucketLender.checkInvariants(); } -async function doClose(amount) { +async function doClose(amount, args) { + args = args || {}; + args.closer = args.closer || trader; + await bucketLender.checkInvariants(); + console.log(" closing..."); await margin.closePositionDirectly( POSITION_ID, amount, - trader, - { from: trader } + args.closer, + { from: args.closer } ); console.log(" done (closing)."); + await bucketLender.checkInvariants(); } async function runAliceBot(expectThrow = false) { @@ -208,7 +219,8 @@ async function setUpPosition(accounts, openThePosition = true) { ); await Promise.all([ - issueTokenToAccountInAmountAndApproveProxy(heldToken, accounts[0], deposit), + issueTokenToAccountInAmountAndApproveProxy(owedToken, TRUSTED_PARTY, OT.times(1000)), + issueTokenToAccountInAmountAndApproveProxy(heldToken, TRUSTED_PARTY, deposit), doDeposit(lender1, OT.times(2)), doDeposit(lender2, OT.times(3)), issueTokenToAccountInAmountAndApproveProxy(heldToken, trader, OT.times(1000)), @@ -249,15 +261,17 @@ async function setUpPosition(accounts, openThePosition = true) { MAX_DURATION, INTEREST_RATE, INTEREST_PERIOD - ] + ], + { from: TRUSTED_PARTY } ); + await bucketLender.checkInvariants(); } contract('BucketLender', accounts => { // ============ Before/After ============ - beforeEach('Set up contracts', async () => { + beforeEach('set up contracts', async () => { [ margin, heldToken, @@ -545,7 +559,7 @@ contract('BucketLender', accounts => { describe('#receiveLoanOwnership', () => { const ogPrincipal = OT.times(2); - const ogDeposit = OT.times(2); + const ogDeposit = OT.times(6); it('succeeds under normal conditions', async () => { await setUpPosition(accounts); @@ -945,19 +959,88 @@ contract('BucketLender', accounts => { }); it('succeeds after full-close', async () => { - //TODO + await wait(1); + await doIncrease(OT.times(4)); + await wait(60 * 60 * 24 * 2); + await doClose(OT.times(4)); + await doClose(BIGNUMBERS.ONES_255, { closer: TRUSTED_PARTY }); + + const isClosed = await margin.isPositionClosed.call(POSITION_ID); + expect(isClosed).to.be.true; + + const {owedWithdrawn, heldWithdrawn, remainingWeight} = await doWithdraw(lender1, 0); + expect(owedWithdrawn).to.be.bignumber.gt(OT.times(2)); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(0); }); it('succeeds after force-closing', async () => { - //TODO + await doIncrease(OT.times(4)); + await wait(MAX_DURATION.toNumber()); + await doClose(OT.times(2)); + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + + const ratio = 3; + let result, weight, value; + + weight = await bucketLender.weightForBucketForAccount.call(0, lender1); + result = await doWithdraw(lender1, 0); + expect(result.owedWithdrawn).to.be.bignumber.gt(0); + expect(result.heldWithdrawn).to.be.bignumber.gt(0); + expect(result.remainingWeight).to.be.bignumber.eq(0); + value = result.owedWithdrawn.plus(result.heldWithdrawn.div(ratio)); + expect(value).to.be.bignumber.gt(weight); + + weight = await bucketLender.weightForBucketForAccount.call(0, lender2); + result = await doWithdraw(lender2, 0); + expect(result.owedWithdrawn).to.be.bignumber.gt(0); + expect(result.heldWithdrawn).to.be.bignumber.gt(0); + expect(result.remainingWeight).to.be.bignumber.eq(0); + value = result.owedWithdrawn.plus(result.heldWithdrawn.div(ratio)); + expect(value).to.be.bignumber.gt(weight); + + weight = await bucketLender.weightForBucketForAccount.call(0, TRUSTED_PARTY); + result = await doWithdraw(TRUSTED_PARTY, 0); + expect(result.owedWithdrawn).to.be.bignumber.gt(0); + expect(result.heldWithdrawn).to.be.bignumber.gt(0); + expect(result.remainingWeight).to.be.bignumber.eq(0); + value = result.owedWithdrawn.plus(result.heldWithdrawn.div(ratio)); + expect(value).to.be.bignumber.gt(weight); }); - it('fails for withdrawing from the current bucket', async () => { - //TODO + it('fails for not-enough available amount', async () => { + await doIncrease(OT.times(4)); + await wait(60 * 60 * 24 * 4); + + await doWithdraw(lender1, 0, { throws: true }); + + const weight = OT.div(2); + const {owedWithdrawn, heldWithdrawn, remainingWeight} = + await doWithdraw(lender1, 0, { weight: weight }); + expect(owedWithdrawn).to.be.bignumber.gte(weight); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(OT.times(3).div(2)); }); - it('fails for no available amount', async () => { - //TODO + it('succeeds but returns no tokens for the current/critical bucket', async () => { + let bucket; + await wait(1); + + bucket = await doDeposit(lender1, OT); + expect(bucket).to.be.bignumber.eq(1); + + await doIncrease(OT.times(5).plus(1)); + + bucket = await doDeposit(lender1, OT); + expect(bucket).to.be.bignumber.eq(1); + await bucketLender.rebalanceBuckets(); + + const weight = await bucketLender.weightForBucketForAccount.call(1, lender1); + const {owedWithdrawn, heldWithdrawn, remainingWeight} = await doWithdraw(lender1, 1); + + expect(owedWithdrawn).to.be.bignumber.eq(0); + expect(heldWithdrawn).to.be.bignumber.eq(0); + expect(remainingWeight).to.be.bignumber.eq(weight); }); it('fails to withdraw to the zero address', async () => { @@ -985,11 +1068,114 @@ contract('BucketLender', accounts => { }); describe('#rebalanceBuckets', () => { - it('TODO', async () => { - //TODO + async function getAmounts() { + const [ + principalTotal, + principal0, + principal1, + availableTotal, + available0, + available1, + ] = await Promise.all([ + bucketLender.principalTotal.call(), + bucketLender.principalForBucket.call(0), + bucketLender.principalForBucket.call(1), + bucketLender.availableTotal.call(), + bucketLender.availableForBucket.call(0), + bucketLender.availableForBucket.call(1), + ]); + + expect(principalTotal).to.be.bignumber.eq(principal0.plus(principal1)); + expect(availableTotal).to.be.bignumber.eq(available0.plus(available1)); + + return { + principalTotal, + principal0, + principal1, + availableTotal, + available0, + available1, + }; + } + + it('succeeds after close', async () => { + let result; + const startingPrincipal = await bucketLender.principalTotal.call(); + await wait(1); + + await doDeposit(lender1, OT.times(5)); + result = await getAmounts(); + expect(result.principal0).to.be.bignumber.eq(startingPrincipal); + expect(result.principal1).to.be.bignumber.eq(0); + expect(result.available0).to.be.bignumber.eq(OT.times(5)); + expect(result.available1).to.be.bignumber.eq(OT.times(5)); + + await doIncrease(OT.times(8)); + result = await getAmounts(); + expect(result.principal0).to.be.bignumber.eq(startingPrincipal.plus(OT.times(5))); + expect(result.principal1).to.be.bignumber.eq(OT.times(3)); + expect(result.available0).to.be.bignumber.eq(0); + expect(result.available1).to.be.bignumber.eq(OT.times(2)); + + await wait(60 * 60 * 24 * 2); + + await doClose(OT.times(5)); + result = await getAmounts(); + expect(result.principal0).to.be.bignumber.eq(startingPrincipal.plus(OT.times(5))); + expect(result.principal1).to.be.bignumber.eq(OT.times(3)); + expect(result.available0).to.be.bignumber.eq(0); + expect(result.available1).to.be.bignumber.eq(OT.times(2)); + + await bucketLender.rebalanceBuckets(); + result = await getAmounts(); + expect(result.principal0).to.be.bignumber.eq(startingPrincipal.plus(OT.times(3))); + expect(result.principal1).to.be.bignumber.eq(0); + expect(result.available0).to.be.bignumber.gte(OT.times(2)); + expect(result.available1).to.be.bignumber.gte(OT.times(5)); }); - //TODO: add more tests + it('does nothing after the position is force-closed', async () => { + await doDeposit(lender1, OT.times(5)); + await doIncrease(OT.times(8)); + await wait(MAX_DURATION.toNumber()); + await doClose(OT.times(5)); + + const result0 = await getAmounts(); + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + const result1 = await getAmounts(); + await bucketLender.rebalanceBuckets(); + const result2 = await getAmounts(); + + expect(result0.principal0).to.be.bignumber.not.eq(result1.principal0); + expect(result0.principal1).to.be.bignumber.not.eq(result1.principal1); + expect(result0.available0).to.be.bignumber.not.eq(result1.available0); + expect(result0.available1).to.be.bignumber.not.eq(result1.available1); + + expect(result1.principal0).to.be.bignumber.eq(result2.principal0); + expect(result1.principal1).to.be.bignumber.eq(result2.principal1); + expect(result1.available0).to.be.bignumber.eq(result2.available0); + expect(result1.available1).to.be.bignumber.eq(result2.available1); + }); + + it('does nothing when the position has not been closed since the last rebalance', async () => { + await wait(1); + await doDeposit(lender1, OT.times(5)); + await wait(60 * 60 * 24); + await doIncrease(OT.times(5)); + await wait(60 * 60 * 24); + await doClose(OT.times(2)); + await wait(60 * 60 * 24); + await doIncrease(OT.times(5)); + + const result1 = await getAmounts(); + await bucketLender.rebalanceBuckets(); + const result2 = await getAmounts(); + + expect(result1.principal0).to.be.bignumber.eq(result2.principal0); + expect(result1.principal1).to.be.bignumber.eq(result2.principal1); + expect(result1.available0).to.be.bignumber.eq(result2.available0); + expect(result1.available1).to.be.bignumber.eq(result2.available1); + }); }); // ============ Integration Tests ============ @@ -1019,7 +1205,6 @@ contract('BucketLender', accounts => { await bucketLender.rebalanceBuckets(); await runAliceBot(); - await expectThrow(doDeposit(uselessLender, 0)); await doDeposit(uselessLender, OT.times(3)); await runAliceBot(); @@ -1086,7 +1271,6 @@ contract('BucketLender', accounts => { // Force-recover collateral console.log(" force-recovering collateral..."); - await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); console.log(" done."); await bucketLender.checkInvariants(); @@ -1261,7 +1445,6 @@ contract('BucketLender', accounts => { // Force-recover collateral console.log(" force-recovering collateral..."); - await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); console.log(" done."); await bucketLender.checkInvariants(); @@ -1382,7 +1565,6 @@ contract('BucketLender', accounts => { await doClose(OT); await runAliceBot(); - await expectThrow(doDeposit(uselessLender, 0)); await doDeposit(uselessLender, OT.times(3)); await runAliceBot(); @@ -1459,7 +1641,6 @@ contract('BucketLender', accounts => { // Force-recover collateral console.log(" force-recovering collateral..."); - await expectThrow(margin.forceRecoverCollateral(POSITION_ID, TRUSTED_PARTY)); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); console.log(" done."); await bucketLender.checkInvariants(); From aef6239748ab9b7e5a139069c20c36f11e8669cb Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Fri, 29 Jun 2018 11:18:20 -0700 Subject: [PATCH 07/21] remove extra console.log statements --- .../external/bucketlender/TestBucketLender.js | 48 +------------------ 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index d1a7dffe..9a2c3a6f 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -53,11 +53,8 @@ function gcd(a, b) { // grants tokens to a lender and has them deposit them into the bucket lender async function doDeposit(account, amount) { await bucketLender.checkInvariants(); - console.log(" depositing..."); await issueAndSetAllowance(owedToken, account, amount, bucketLender.address); - console.log(" ..."); const tx = await transact(bucketLender.deposit, account, amount, { from: account }); - console.log(" done (depositing)."); await bucketLender.checkInvariants(); return tx.result; } @@ -122,9 +119,7 @@ async function doIncrease(amount, args) { return; } - console.log(" increasing..."); await callIncreasePosition(margin, incrTx); - console.log(" done (increasing)."); await bucketLender.checkInvariants(); } @@ -133,41 +128,32 @@ async function doClose(amount, args) { args.closer = args.closer || trader; await bucketLender.checkInvariants(); - console.log(" closing..."); await margin.closePositionDirectly( POSITION_ID, amount, args.closer, { from: args.closer } ); - console.log(" done (closing)."); await bucketLender.checkInvariants(); } async function runAliceBot(expectThrow = false) { const aliceAmount = OT; - console.log(" runnning alice bot..."); - console.log(" checking invariants..."); await bucketLender.checkInvariants(); - console.log(" depositing..."); await issueAndSetAllowance(owedToken, alice, aliceAmount, bucketLender.address); if (expectThrow) { await expectThrow(bucketLender.deposit(alice, aliceAmount, { from: alice })); - console.log(" done (alice bot)."); return; } const bucket = await transact(bucketLender.deposit, alice, aliceAmount, { from: alice }); - console.log(" withdrawing (bucket " + bucket.result.toString() + ")..."); const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(alice, bucket.result); expect(owedWithdrawn).to.be.bignumber.lte(aliceAmount); expect(owedWithdrawn.plus(1)).to.bignumber.gte(aliceAmount); expect(heldWithdrawn).to.be.bignumber.eq(0); expect(remainingWeight).to.be.bignumber.eq(0); - console.log(" checking invariants..."); await bucketLender.checkInvariants(); - console.log(" done (alice bot)."); } async function giveAPositionTo(contract, accounts) { @@ -1270,9 +1256,7 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // Force-recover collateral - console.log(" force-recovering collateral..."); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); - console.log(" done."); await bucketLender.checkInvariants(); // can't deposit after position closed @@ -1282,25 +1266,17 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // do all remaining withdrawals - console.log(" doing all remaining withdrawals..."); for(let a = 0; a < 10; a++) { let act = accounts[a]; for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); - console.log(" owed: " + owedWithdrawn.toString()); - console.log(" held: " + heldWithdrawn.toString()); - console.log(" remw: " + remainingWeight.toString()); - console.log(" done."); } } } - console.log(" done."); // check constants - console.log(" checking constants..."); const [ c_wasForceClosed, c_criticalBucket, @@ -1351,7 +1327,6 @@ contract('BucketLender', accounts => { expect(isClosed).to.be.true; expect(bucketLenderOwedToken).to.be.bignumber.eq(0); expect(bucketLenderHeldToken).to.be.bignumber.eq(0); - console.log(" done."); await bucketLender.checkInvariants(); }); @@ -1444,9 +1419,7 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // Force-recover collateral - console.log(" force-recovering collateral..."); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); - console.log(" done."); await bucketLender.checkInvariants(); // can't deposit after position closed @@ -1456,25 +1429,17 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // do all remaining withdrawals - console.log(" doing all remaining withdrawals..."); for(let a = 0; a < 10; a++) { let act = accounts[a]; for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); - console.log(" owed: " + owedWithdrawn.toString()); - console.log(" held: " + heldWithdrawn.toString()); - console.log(" remw: " + remainingWeight.toString()); - console.log(" done."); } } } - console.log(" done."); // check constants - console.log(" checking constants..."); const [ c_wasForceClosed, c_criticalBucket, @@ -1525,7 +1490,6 @@ contract('BucketLender', accounts => { expect(isClosed).to.be.true; expect(bucketLenderOwedToken).to.be.bignumber.eq(0); expect(bucketLenderHeldToken).to.be.bignumber.eq(0); - console.log(" done."); await bucketLender.checkInvariants(); }); @@ -1640,9 +1604,8 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // Force-recover collateral - console.log(" force-recovering collateral..."); await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); - console.log(" done."); + consolelog(" done."); await bucketLender.checkInvariants(); // can't deposit after position closed @@ -1652,25 +1615,17 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); // do all remaining withdrawals - console.log(" doing all remaining withdrawals..."); for(let a = 0; a < 10; a++) { let act = accounts[a]; for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - console.log(" withdrawing (bucket " + b + ") (account " + a + ")..."); const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); - console.log(" owed: " + owedWithdrawn.toString()); - console.log(" held: " + heldWithdrawn.toString()); - console.log(" remw: " + remainingWeight.toString()); - console.log(" done."); } } } - console.log(" done."); // check constants - console.log(" checking constants..."); const [ c_wasForceClosed, c_criticalBucket, @@ -1721,7 +1676,6 @@ contract('BucketLender', accounts => { expect(isClosed).to.be.true; expect(bucketLenderOwedToken).to.be.bignumber.eq(0); expect(bucketLenderHeldToken).to.be.bignumber.eq(0); - console.log(" done."); await bucketLender.checkInvariants(); }); }); From 569189655bcdb6f6619c7b881d71b0f876682076 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Fri, 29 Jun 2018 13:29:03 -0700 Subject: [PATCH 08/21] more changes --- .../external/BucketLender/BucketLender.sol | 118 ++++++++++-------- .../EthWrapperForBucketLender.sol | 11 +- .../external/bucketlender/TestBucketLender.js | 1 - .../TestEthWrapperForBucketLender.js | 2 +- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index baa98073..c2c77887 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -99,6 +99,7 @@ contract BucketLender is ReentrancyGuard { using SafeMath for uint256; + using TokenInteract for address; // ============ Events ============ @@ -243,8 +244,7 @@ contract BucketLender is } // Set maximum allowance on proxy - TokenInteract.approve( - OWED_TOKEN, + OWED_TOKEN.approve( Margin(DYDX_MARGIN).getProxyAddress(), MathHelpers.maxUint256() ); @@ -377,11 +377,16 @@ contract BucketLender is { MarginCommon.Position memory position = MarginHelper.getPosition(DYDX_MARGIN, POSITION_ID); uint256 initialPrincipal = position.principal; + uint256 minHeldToken = MathHelpers.getPartialAmount( + uint256(MIN_HELD_TOKEN_NUMERATOR), + uint256(MIN_HELD_TOKEN_DENOMINATOR), + initialPrincipal + ); - assert(principalTotal == 0); assert(initialPrincipal > 0); + assert(principalTotal == 0); + assert(from != address(this)); // position must be opened without lending from this position - // lenders should have certain guarantees about how the position is collateralized require( position.owedToken == OWED_TOKEN, "BucketLender#receiveLoanOwnership: Position owedToken mismatch" @@ -390,18 +395,10 @@ contract BucketLender is position.heldToken == HELD_TOKEN, "BucketLender#receiveLoanOwnership: Position heldToken mismatch" ); - - // require enough heldToken - uint256 minStartingHeldToken = MathHelpers.getPartialAmount( - uint256(MIN_HELD_TOKEN_NUMERATOR), - uint256(MIN_HELD_TOKEN_DENOMINATOR), - initialPrincipal + require( + Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= minHeldToken, + "BucketLender#receiveLoanOwnership: Not enough heldToken as collateral" ); - require(Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= minStartingHeldToken); - - // assert that the position was opened without using funds from this position - // (i.e. that it was opened using openWithoutCounterparty()) - assert(from != address(this)); // set relevant constants principalForBucket[0] = initialPrincipal; @@ -595,8 +592,7 @@ contract BucketLender is rebalanceBucketsInternal(); - TokenInteract.transferFrom( - OWED_TOKEN, + OWED_TOKEN.transferFrom( msg.sender, address(this), amount @@ -623,7 +619,7 @@ contract BucketLender is ); // update state - updateAvailable(bucket, amount, true); + increaseAvailable(bucket, amount); weightForBucketForAccount[bucket][beneficiary] = weightForBucketForAccount[bucket][beneficiary].add(weightToAdd); weightForBucket[bucket] = weightForBucket[bucket].add(weightToAdd); @@ -695,7 +691,7 @@ contract BucketLender is uint256 maxHeldToken = 0; if (wasForceClosed) { - maxHeldToken = TokenInteract.balanceOf(HELD_TOKEN, address(this)); + maxHeldToken = HELD_TOKEN.balanceOf(address(this)); } for (uint256 i = 0; i < buckets.length; i++) { @@ -717,8 +713,8 @@ contract BucketLender is } // Transfer share of owedToken - TokenInteract.transfer(OWED_TOKEN, beneficiary, totalOwedToken); - TokenInteract.transfer(HELD_TOKEN, beneficiary, totalHeldToken); + OWED_TOKEN.transfer(beneficiary, totalOwedToken); + HELD_TOKEN.transfer(beneficiary, totalHeldToken); return (totalOwedToken, totalHeldToken); } @@ -788,8 +784,8 @@ contract BucketLender is availableToAdd ); - updateAvailable(bucket, availableTemp, true); - updatePrincipal(bucket, principalTemp, false); + increaseAvailable(bucket, availableTemp); + decreasePrincipal(bucket, principalTemp); principalToSub = principalToSub.sub(principalTemp); availableToAdd = availableToAdd.sub(availableTemp); @@ -848,8 +844,8 @@ contract BucketLender is principalToAdd ); - updateAvailable(bucket, availableTemp, false); - updatePrincipal(bucket, principalTemp, true); + decreaseAvailable(bucket, availableTemp); + increasePrincipal(bucket, principalTemp); principalToAdd = principalToAdd.sub(principalTemp); availableToSub = availableToSub.sub(availableTemp); @@ -956,7 +952,7 @@ contract BucketLender is ); // update amounts - updateAvailable(bucket, owedTokenToWithdraw, false); + decreaseAvailable(bucket, owedTokenToWithdraw); return owedTokenToWithdraw; } @@ -1000,7 +996,7 @@ contract BucketLender is maxHeldToken ); - updatePrincipal(bucket, principalForBucketForAccount, false); + decreasePrincipal(bucket, principalForBucketForAccount); return heldTokenToWithdraw; } @@ -1024,51 +1020,71 @@ contract BucketLender is } /** - * Changes the available owedToken amount. This changes both the variable to track the total + * Increases the available owedToken amount. This changes both the variable to track the total * amount as well as the variable to track a particular bucket. * * @param bucket The bucket number * @param amount The amount to change the available amount by - * @param increase True if positive change, false if negative change */ - function updateAvailable( + function increaseAvailable( uint256 bucket, - uint256 amount, - bool increase + uint256 amount ) private { - if (increase) { - availableTotal = availableTotal.add(amount); - availableForBucket[bucket] = availableForBucket[bucket].add(amount); - } else { - availableTotal = availableTotal.sub(amount); - availableForBucket[bucket] = availableForBucket[bucket].sub(amount); - } + availableTotal = availableTotal.add(amount); + availableForBucket[bucket] = availableForBucket[bucket].add(amount); + } + + /** + * Decreases the available owedToken amount. This changes both the variable to track the total + * amount as well as the variable to track a particular bucket. + * + * @param bucket The bucket number + * @param amount The amount to change the available amount by + */ + function decreaseAvailable( + uint256 bucket, + uint256 amount + ) + private + { + availableTotal = availableTotal.sub(amount); + availableForBucket[bucket] = availableForBucket[bucket].sub(amount); } /** - * Changes the principal amount. This changes both the variable to track the total + * Increases the principal amount. This changes both the variable to track the total * amount as well as the variable to track a particular bucket. * * @param bucket The bucket number * @param amount The amount to change the principal amount by - * @param increase True if positive change, false if negative change */ - function updatePrincipal( + function increasePrincipal( uint256 bucket, - uint256 amount, - bool increase + uint256 amount ) private { - if (increase) { - principalTotal = principalTotal.add(amount); - principalForBucket[bucket] = principalForBucket[bucket].add(amount); - } else { - principalTotal = principalTotal.sub(amount); - principalForBucket[bucket] = principalForBucket[bucket].sub(amount); - } + principalTotal = principalTotal.add(amount); + principalForBucket[bucket] = principalForBucket[bucket].add(amount); + } + + /** + * Decreases the principal amount. This changes both the variable to track the total + * amount as well as the variable to track a particular bucket. + * + * @param bucket The bucket number + * @param amount The amount to change the principal amount by + */ + function decreasePrincipal( + uint256 bucket, + uint256 amount + ) + private + { + principalTotal = principalTotal.sub(amount); + principalForBucket[bucket] = principalForBucket[bucket].sub(amount); } // ============ Getter Functions ============ diff --git a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol index a623e78a..887d5a26 100644 --- a/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol +++ b/contracts/margin/external/BucketLender/EthWrapperForBucketLender.sol @@ -33,6 +33,8 @@ import { TokenInteract } from "../../../lib/TokenInteract.sol"; */ contract EthWrapperForBucketLender { + using TokenInteract for address; + // ============ Constants ============ // Address of the WETH token @@ -80,10 +82,13 @@ contract EthWrapperForBucketLender // wrap the eth WETH9(WETH).deposit.value(amount)(); - assert(TokenInteract.balanceOf(WETH, address(this)) >= amount); + assert(WETH.balanceOf(address(this)) >= amount); - // approve for "unlimited amount". WETH9 leaves this value as-is when doing transferFrom - TokenInteract.approve(WETH, bucketLender, MathHelpers.maxUint256()); + // ensure enough allowance + if (WETH.allowance(address(this), bucketLender) == 0) { + // approve for "unlimited amount". WETH9 leaves this value as-is when doing transferFrom + WETH.approve(bucketLender, MathHelpers.maxUint256()); + } // deposit the tokens return BucketLender(bucketLender).deposit(beneficiary, amount); diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index 9a2c3a6f..683bddbb 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -1605,7 +1605,6 @@ contract('BucketLender', accounts => { // Force-recover collateral await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); - consolelog(" done."); await bucketLender.checkInvariants(); // can't deposit after position closed diff --git a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js index 77587b8f..402ee559 100644 --- a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js +++ b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js @@ -10,7 +10,7 @@ const BucketLender = artifacts.require("BucketLender"); const EthWrapperForBucketLender = artifacts.require("EthWrapperForBucketLender"); const { transact } = require('../../../helpers/ContractHelper'); -const { BIGNUMBERS, BYTES32 } = require('../../../helpers/Constants'); +const { BIGNUMBERS } = require('../../../helpers/Constants'); const { expectThrow } = require('../../../helpers/ExpectHelper'); let heldToken, weth, bucketLender, ethWrapper; From 2654dae23a70b99a69cb90fb1cbfb72f479f2dc2 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Fri, 29 Jun 2018 16:42:33 -0700 Subject: [PATCH 09/21] add events --- .../external/BucketLender/BucketLender.sol | 201 +++++++++--------- contracts/testing/TestBucketLender.sol | 16 +- .../external/bucketlender/TestBucketLender.js | 37 +++- .../TestEthWrapperForBucketLender.js | 36 ++-- 4 files changed, 152 insertions(+), 138 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index c2c77887..048ce08b 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -118,6 +118,34 @@ contract BucketLender is uint256 heldTokenWithdrawn ); + event PrincipalIncreased( + uint256 principalTotal, + uint256 bucketNumber, + uint256 principalForBucket, + uint256 amount + ); + + event PrincipalDecreased( + uint256 principalTotal, + uint256 bucketNumber, + uint256 principalForBucket, + uint256 amount + ); + + event AvailableIncreased( + uint256 availableTotal, + uint256 bucketNumber, + uint256 availableForBucket, + uint256 amount + ); + + event AvailableDecreased( + uint256 availableTotal, + uint256 bucketNumber, + uint256 availableForBucket, + uint256 amount + ); + // ============ State Variables ============ /** @@ -214,13 +242,7 @@ contract BucketLender is bytes32 positionId, address heldToken, address owedToken, - uint32 bucketTime, - uint32 interestRate, - uint32 interestPeriod, - uint32 maxDuration, - uint32 callTimelimit, - uint32 minHeldTokenNumerator, - uint32 minHeldTokenDenominator, + uint32[7] parameters, address[] trustedMarginCallers ) public @@ -230,14 +252,13 @@ contract BucketLender is HELD_TOKEN = heldToken; OWED_TOKEN = owedToken; - BUCKET_TIME = bucketTime; - INTEREST_RATE = interestRate; - INTEREST_PERIOD = interestPeriod; - MAX_DURATION = maxDuration; - CALL_TIMELIMIT = callTimelimit; - - MIN_HELD_TOKEN_NUMERATOR = minHeldTokenNumerator; - MIN_HELD_TOKEN_DENOMINATOR = minHeldTokenDenominator; + BUCKET_TIME = parameters[0]; + INTEREST_RATE = parameters[1]; + INTEREST_PERIOD = parameters[2]; + MAX_DURATION = parameters[3]; + CALL_TIMELIMIT = parameters[4]; + MIN_HELD_TOKEN_NUMERATOR = parameters[5]; + MIN_HELD_TOKEN_DENOMINATOR = parameters[6]; for (uint256 i = 0; i < trustedMarginCallers.length; i++) { TRUSTED_MARGIN_CALLERS[trustedMarginCallers[i]] = true; @@ -267,37 +288,11 @@ contract BucketLender is * will be generated off-chain. The "loan owner" address will own the loan-side of the resulting * position. * - * @param addresses Array of addresses: - * - * [0] = owedToken - * [1] = heldToken - * [2] = loan payer - * [3] = loan owner - * [4] = loan taker - * [5] = loan positionOwner - * [6] = loan fee recipient - * [7] = loan lender fee token - * [8] = loan taker fee token - * - * @param values256 Values corresponding to: - * - * [0] = loan maximum amount - * [1] = loan minimum amount - * [2] = loan minimum heldToken - * [3] = loan lender fee - * [4] = loan taker fee - * [5] = loan expiration timestamp (in seconds) - * [6] = loan salt - * - * @param values32 Values corresponding to: - * - * [0] = loan call time limit (in seconds) - * [1] = loan maxDuration (in seconds) - * [2] = loan interest rate (annual nominal percentage times 10**6) - * [3] = loan interest update period (in seconds) - * + * @param addresses Loan offering addresses + * @param values256 Loan offering uint256s + * @param values32 Loan offering uint32s * @param positionId Unique ID of the position - * @param signature Arbitrary bytes; may or may not be an ECDSA signature + * @param signature Arbitrary bytes * @return This address to accept, a different address to ask that contract */ function verifyLoanOffering( @@ -619,7 +614,7 @@ contract BucketLender is ); // update state - increaseAvailable(bucket, amount); + updateAvailable(bucket, amount, true); weightForBucketForAccount[bucket][beneficiary] = weightForBucketForAccount[bucket][beneficiary].add(weightToAdd); weightForBucket[bucket] = weightForBucket[bucket].add(weightToAdd); @@ -784,8 +779,8 @@ contract BucketLender is availableToAdd ); - increaseAvailable(bucket, availableTemp); - decreasePrincipal(bucket, principalTemp); + updateAvailable(bucket, availableTemp, true); + updatePrincipal(bucket, principalTemp, false); principalToSub = principalToSub.sub(principalTemp); availableToAdd = availableToAdd.sub(availableTemp); @@ -844,8 +839,8 @@ contract BucketLender is principalToAdd ); - decreaseAvailable(bucket, availableTemp); - increasePrincipal(bucket, principalTemp); + updateAvailable(bucket, availableTemp, false); + updatePrincipal(bucket, principalTemp, true); principalToAdd = principalToAdd.sub(principalTemp); availableToSub = availableToSub.sub(availableTemp); @@ -941,10 +936,6 @@ contract BucketLender is availableForBucket[bucket].add(getBucketOwedAmount(bucket)) ); - if (owedTokenToWithdraw == 0) { - return 0; - } - // check that there is enough token to give back require( owedTokenToWithdraw <= availableForBucket[bucket], @@ -952,7 +943,7 @@ contract BucketLender is ); // update amounts - decreaseAvailable(bucket, owedTokenToWithdraw); + updateAvailable(bucket, owedTokenToWithdraw, false); return owedTokenToWithdraw; } @@ -986,17 +977,13 @@ contract BucketLender is principalForBucket[bucket] ); - if (principalForBucketForAccount == 0) { - return 0; - } - uint256 heldTokenToWithdraw = MathHelpers.getPartialAmount( principalForBucketForAccount, principalTotal, maxHeldToken ); - decreasePrincipal(bucket, principalForBucketForAccount); + updatePrincipal(bucket, principalForBucketForAccount, false); return heldTokenToWithdraw; } @@ -1014,77 +1001,83 @@ contract BucketLender is private { // don't spend the gas to sstore unless we need to change the value - if (criticalBucket != bucket) { - criticalBucket = bucket; + if (criticalBucket == bucket) { + return; } - } - /** - * Increases the available owedToken amount. This changes both the variable to track the total - * amount as well as the variable to track a particular bucket. - * - * @param bucket The bucket number - * @param amount The amount to change the available amount by - */ - function increaseAvailable( - uint256 bucket, - uint256 amount - ) - private - { - availableTotal = availableTotal.add(amount); - availableForBucket[bucket] = availableForBucket[bucket].add(amount); + criticalBucket = bucket; } /** - * Decreases the available owedToken amount. This changes both the variable to track the total + * Changes the available owedToken amount. This changes both the variable to track the total * amount as well as the variable to track a particular bucket. * * @param bucket The bucket number * @param amount The amount to change the available amount by + * @param increase True if positive change, false if negative change */ - function decreaseAvailable( + function updateAvailable( uint256 bucket, - uint256 amount + uint256 amount, + bool increase ) private { - availableTotal = availableTotal.sub(amount); - availableForBucket[bucket] = availableForBucket[bucket].sub(amount); - } + if (amount == 0) { + return; + } - /** - * Increases the principal amount. This changes both the variable to track the total - * amount as well as the variable to track a particular bucket. - * - * @param bucket The bucket number - * @param amount The amount to change the principal amount by - */ - function increasePrincipal( - uint256 bucket, - uint256 amount - ) - private - { - principalTotal = principalTotal.add(amount); - principalForBucket[bucket] = principalForBucket[bucket].add(amount); + uint256 newTotal; + uint256 newForBucket; + + if (increase) { + newTotal = availableTotal.add(amount); + newForBucket = availableForBucket[bucket].add(amount); + emit AvailableIncreased(newTotal, bucket, newForBucket, amount); + } else { + newTotal = availableTotal.sub(amount); + newForBucket = availableForBucket[bucket].sub(amount); + emit AvailableDecreased(newTotal, bucket, newForBucket, amount); + } + + availableTotal = newTotal; + availableForBucket[bucket] = newForBucket; } /** - * Decreases the principal amount. This changes both the variable to track the total + * Changes the principal amount. This changes both the variable to track the total * amount as well as the variable to track a particular bucket. * * @param bucket The bucket number * @param amount The amount to change the principal amount by + * @param increase True if positive change, false if negative change */ - function decreasePrincipal( + function updatePrincipal( uint256 bucket, - uint256 amount + uint256 amount, + bool increase ) private { - principalTotal = principalTotal.sub(amount); - principalForBucket[bucket] = principalForBucket[bucket].sub(amount); + if (amount == 0) { + return; + } + + uint256 newTotal; + uint256 newForBucket; + + if (increase) { + newTotal = principalTotal.add(amount); + newForBucket = principalForBucket[bucket].add(amount); + emit PrincipalIncreased(newTotal, bucket, newForBucket, amount); + } else { + newTotal = principalTotal.sub(amount); + newForBucket = principalForBucket[bucket].sub(amount); + emit PrincipalDecreased(newTotal, bucket, newForBucket, amount); + } + + principalTotal = newTotal; + principalForBucket[bucket] = newForBucket; } // ============ Getter Functions ============ diff --git a/contracts/testing/TestBucketLender.sol b/contracts/testing/TestBucketLender.sol index ca3028ce..cda56014 100644 --- a/contracts/testing/TestBucketLender.sol +++ b/contracts/testing/TestBucketLender.sol @@ -33,13 +33,7 @@ contract TestBucketLender is BucketLender { bytes32 positionId, address heldToken, address owedToken, - uint32 bucketTime, - uint32 interestRate, - uint32 interestPeriod, - uint32 maxDuration, - uint32 callTimelimit, - uint32 minHeldTokenNumerator, - uint32 minHeldTokenDenominator, + uint32[7] parameters, address[] trustedMarginCallers ) public @@ -48,13 +42,7 @@ contract TestBucketLender is BucketLender { positionId, heldToken, owedToken, - bucketTime, - interestRate, - interestPeriod, - maxDuration, - callTimelimit, - minHeldTokenNumerator, - minHeldTokenDenominator, + parameters, trustedMarginCallers ) { diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index 683bddbb..ee0989a5 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -194,13 +194,15 @@ async function setUpPosition(accounts, openThePosition = true) { POSITION_ID, heldToken.address, owedToken.address, - BUCKET_TIME, - INTEREST_RATE, - INTEREST_PERIOD, - MAX_DURATION, - CALL_TIMELIMIT, - deposit.div(g), // MIN_HELD_TOKEN_NUMERATOR, - principal.div(g), // MIN_HELD_TOKEN_DENOMINATOR, + [ + BUCKET_TIME, + INTEREST_RATE, + INTEREST_PERIOD, + MAX_DURATION, + CALL_TIMELIMIT, + deposit.div(g), // MIN_HELD_TOKEN_NUMERATOR, + principal.div(g), // MIN_HELD_TOKEN_DENOMINATOR, + ], [TRUSTED_PARTY] // trusted margin-callers ); @@ -937,6 +939,27 @@ contract('BucketLender', accounts => { } }); + it('Withdraw succeeds even when principal amount for lender is zero', async () => { + await doDeposit(lender1, 1); + await doDeposit(lender2, OT); + + await wait(60 * 24 * 24 * 1 + 1); + + await doIncrease(OT.times(5)); + + await wait(MAX_DURATION.toNumber()); + await margin.forceRecoverCollateral(POSITION_ID, bucketLender.address); + + const result1 = await doWithdraw(lender1, 1); + expect(result1.owedWithdrawn).to.be.bignumber.eq(0); + expect(result1.heldWithdrawn).to.be.bignumber.eq(0); + expect(result1.remainingWeight).to.be.bignumber.eq(0); + const result2 = await doWithdraw(lender2, 1); + expect(result2.owedWithdrawn).to.be.bignumber.gt(0); + expect(result2.heldWithdrawn).to.be.bignumber.gt(0); + expect(result2.remainingWeight).to.be.bignumber.eq(0); + }); + it('succeeds but returns no tokens for random buckets', async () => { const {owedWithdrawn, heldWithdrawn, remainingWeight} = await doWithdraw(lender1, 1000); expect(owedWithdrawn).to.be.bignumber.eq(0); diff --git a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js index 402ee559..f481d11d 100644 --- a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js +++ b/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js @@ -49,13 +49,15 @@ contract('BucketLender', accounts => { POSITION_ID, heldToken.address, weth.address, - BUCKET_TIME, - INTEREST_RATE, - INTEREST_PERIOD, - MAX_DURATION, - CALL_TIMELIMIT, - DEPOSIT, // MIN_HELD_TOKEN_NUMERATOR, - PRINCIPAL, // MIN_HELD_TOKEN_DENOMINATOR, + [ + BUCKET_TIME, + INTEREST_RATE, + INTEREST_PERIOD, + MAX_DURATION, + CALL_TIMELIMIT, + DEPOSIT, // MIN_HELD_TOKEN_NUMERATOR, + PRINCIPAL, // MIN_HELD_TOKEN_DENOMINATOR, + ], [] // trusted margin-callers ) ]); @@ -77,18 +79,26 @@ contract('BucketLender', accounts => { }); describe('#depositEth', () => { - it('succeeds for normal case', async () => { + it('succeeds when depositing multiple times', async () => { const sender = accounts[1]; const beneficiary = accounts[2]; - const result = await transact( + let result; + + result = await transact( ethWrapper.depositEth, bucketLender.address, beneficiary, { from: sender, value: value } ); + expect(result.result).to.be.bignumber.eq(0); // expect bucket 0 - // expect bucket 0 - expect(result.result).to.be.bignumber.eq(0); + result = await transact( + ethWrapper.depositEth, + bucketLender.address, + beneficiary, + { from: sender, value: value } + ); + expect(result.result).to.be.bignumber.eq(0); // expect bucket 0 const [ weight1, @@ -97,8 +107,8 @@ contract('BucketLender', accounts => { bucketLender.weightForBucket.call(0), bucketLender.weightForBucketForAccount.call(0, beneficiary), ]); - expect(weight1).to.be.bignumber.eq(value); - expect(weight2).to.be.bignumber.eq(value); + expect(weight1).to.be.bignumber.eq(value.times(2)); + expect(weight2).to.be.bignumber.eq(value.times(2)); }); it('fails for zero amount', async () => { From 9dd6e387fe9c3ec8f4be6b6828daf4ffa5973d78 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 16 Jul 2018 10:21:28 -0700 Subject: [PATCH 10/21] make suggested changes --- .../external/BucketLender/BucketLender.sol | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 048ce08b..e0203533 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -23,6 +23,7 @@ import { ReentrancyGuard } from "zeppelin-solidity/contracts/ReentrancyGuard.sol import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { HasNoEther } from "zeppelin-solidity/contracts/ownership/HasNoEther.sol"; +import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; import { Margin } from "../../Margin.sol"; import { MathHelpers } from "../../../lib/MathHelpers.sol"; import { TokenInteract } from "../../../lib/TokenInteract.sol"; @@ -89,6 +90,7 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; */ contract BucketLender is HasNoEther, + Ownable, OnlyMargin, LoanOwner, IncreaseLoanDelegator, @@ -568,6 +570,9 @@ contract BucketLender is nonReentrant returns (uint256) { + Margin margin = Margin(DYDX_MARGIN); + bytes32 positionId = POSITION_ID; + require( beneficiary != address(0), "BucketLender#deposit: Beneficiary cannot be the zero address" @@ -577,11 +582,11 @@ contract BucketLender is "BucketLender#deposit: Cannot deposit zero tokens" ); require( - !Margin(DYDX_MARGIN).isPositionClosed(POSITION_ID), + !margin.isPositionClosed(positionId), "BucketLender#deposit: Cannot deposit after the position is closed" ); require( - !Margin(DYDX_MARGIN).isPositionCalled(POSITION_ID), + !margin.isPositionCalled(positionId), "BucketLender#deposit: Cannot deposit while the position is margin-called" ); @@ -674,10 +679,7 @@ contract BucketLender is // decide if some bucket is unable to be withdrawn from (is locked) // the zero value represents no-lock uint256 lockedBucket = 0; - if ( - Margin(DYDX_MARGIN).containsPosition(POSITION_ID) && - criticalBucket == getCurrentBucket() - ) { + if (criticalBucket == getCurrentBucket()) { lockedBucket = criticalBucket; } @@ -988,6 +990,30 @@ contract BucketLender is return heldTokenToWithdraw; } + function withdrawExcessToken( + address token, + address to + ) + external + onlyOwner + returns (uint256) + { + if (token == OWED_TOKEN) { + uint256 amount = token.balanceOf(address(this)); + amount = amount.sub(availableTotal); + token.transfer(to, amount); + } else { + if (token == HELD_TOKEN) { + require( + principalTotal == 0, + "BucketLender#withdrawExcessToken: Withdrawing heldToken when principal is zero" + ); + } + uint256 amount = token.balanceOf(address(this)); + token.transfer(to, amount); + } + } + // ============ Setter Functions ============ /** From bbd8040d64853d7bee8d0e27729c33fece6298ac Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Tue, 17 Jul 2018 17:02:31 -0700 Subject: [PATCH 11/21] move function, add comment --- .../external/BucketLender/BucketLender.sol | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index e0203533..671f0fe7 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -716,6 +716,41 @@ contract BucketLender is return (totalOwedToken, totalHeldToken); } + /** + * Allows the owner to withdraw any excess tokens sent to the vault by unconventional means, + * including (but not limited-to) token airdrops. Any tokens moved to this contract by calling + * deposit() will be accounted for and will not be withdrawable by this function. + * + * @param token ERC20 token address + * @param to Address to transfer tokens to + * @return Amount of tokens withdrawn + */ + function withdrawExcessToken( + address token, + address to + ) + external + onlyOwner + returns (uint256) + { + rebalanceBucketsInternal(); + + if (token == OWED_TOKEN) { + uint256 amount = token.balanceOf(address(this)); + amount = amount.sub(availableTotal); + token.transfer(to, amount); + } else { + if (token == HELD_TOKEN) { + require( + principalTotal == 0, + "BucketLender#withdrawExcessToken: Withdrawing heldToken when principal is zero" + ); + } + uint256 amount = token.balanceOf(address(this)); + token.transfer(to, amount); + } + } + // ============ Helper Functions ============ /** @@ -990,30 +1025,6 @@ contract BucketLender is return heldTokenToWithdraw; } - function withdrawExcessToken( - address token, - address to - ) - external - onlyOwner - returns (uint256) - { - if (token == OWED_TOKEN) { - uint256 amount = token.balanceOf(address(this)); - amount = amount.sub(availableTotal); - token.transfer(to, amount); - } else { - if (token == HELD_TOKEN) { - require( - principalTotal == 0, - "BucketLender#withdrawExcessToken: Withdrawing heldToken when principal is zero" - ); - } - uint256 amount = token.balanceOf(address(this)); - token.transfer(to, amount); - } - } - // ============ Setter Functions ============ /** From ab6d83a2fa3b7453e749eb72b2c7a09a5ee6d68d Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 23 Jul 2018 13:53:04 -0700 Subject: [PATCH 12/21] remove extra 'Ownable' (is included in 'HasNoEther') --- contracts/margin/external/BucketLender/BucketLender.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 671f0fe7..5e90429c 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -23,7 +23,6 @@ import { ReentrancyGuard } from "zeppelin-solidity/contracts/ReentrancyGuard.sol import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { HasNoEther } from "zeppelin-solidity/contracts/ownership/HasNoEther.sol"; -import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; import { Margin } from "../../Margin.sol"; import { MathHelpers } from "../../../lib/MathHelpers.sol"; import { TokenInteract } from "../../../lib/TokenInteract.sol"; @@ -90,7 +89,6 @@ import { MarginHelper } from "../lib/MarginHelper.sol"; */ contract BucketLender is HasNoEther, - Ownable, OnlyMargin, LoanOwner, IncreaseLoanDelegator, From b870ea31beca58796fca3cc088fa8a474859767f Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 23 Jul 2018 14:43:44 -0700 Subject: [PATCH 13/21] revert change to withdraw() --- contracts/margin/external/BucketLender/BucketLender.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 5e90429c..6626226f 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -677,7 +677,10 @@ contract BucketLender is // decide if some bucket is unable to be withdrawn from (is locked) // the zero value represents no-lock uint256 lockedBucket = 0; - if (criticalBucket == getCurrentBucket()) { + if ( + Margin(DYDX_MARGIN).containsPosition(POSITION_ID) && + criticalBucket == getCurrentBucket() + ) { lockedBucket = criticalBucket; } From 3cae0faf5bb30d87675de216a5cd569547495afd Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 23 Jul 2018 14:52:34 -0700 Subject: [PATCH 14/21] remove linting errors --- .../external/BucketLender/BucketLender.sol | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 6626226f..9ef6bdba 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -325,24 +325,66 @@ contract BucketLender is assert(loanOffering.heldToken == HELD_TOKEN); assert(loanOffering.payer == address(this)); assert(loanOffering.owner == address(this)); - require(loanOffering.taker == address(0)); - require(loanOffering.feeRecipient == address(0)); - require(loanOffering.positionOwner == address(0)); - require(loanOffering.lenderFeeToken == address(0)); - require(loanOffering.takerFeeToken == address(0)); + require( + loanOffering.taker == address(0), + "BucketLender#verifyLoanOffering: loanOffering.taker is non-zero" + ); + require( + loanOffering.feeRecipient == address(0), + "BucketLender#verifyLoanOffering: loanOffering.feeRecipient is non-zero" + ); + require( + loanOffering.positionOwner == address(0), + "BucketLender#verifyLoanOffering: loanOffering.positionOwner is non-zero" + ); + require( + loanOffering.lenderFeeToken == address(0), + "BucketLender#verifyLoanOffering: loanOffering.lenderFeeToken is non-zero" + ); + require( + loanOffering.takerFeeToken == address(0), + "BucketLender#verifyLoanOffering: loanOffering.takerFeeToken is non-zero" + ); // CHECK VALUES256 - require(loanOffering.rates.maxAmount == MathHelpers.maxUint256()); - require(loanOffering.rates.minAmount == 0); - require(loanOffering.rates.minHeldToken == 0); - require(loanOffering.rates.lenderFee == 0); - require(loanOffering.rates.takerFee == 0); - require(loanOffering.expirationTimestamp == MathHelpers.maxUint256()); - require(loanOffering.salt == 0); + require( + loanOffering.rates.maxAmount == MathHelpers.maxUint256(), + "BucketLender#verifyLoanOffering: loanOffering.maxAmount is incorrect" + ); + require( + loanOffering.rates.minAmount == 0, + "BucketLender#verifyLoanOffering: loanOffering.minAmount is non-zero" + ); + require( + loanOffering.rates.minHeldToken == 0, + "BucketLender#verifyLoanOffering: loanOffering.minHeldToken is non-zero" + ); + require( + loanOffering.rates.lenderFee == 0, + "BucketLender#verifyLoanOffering: loanOffering.lenderFee is non-zero" + ); + require( + loanOffering.rates.takerFee == 0, + "BucketLender#verifyLoanOffering: loanOffering.takerFee is non-zero" + ); + require( + loanOffering.expirationTimestamp == MathHelpers.maxUint256(), + "BucketLender#verifyLoanOffering: expirationTimestamp is incorrect" + ); + require( + loanOffering.salt == 0, + "BucketLender#verifyLoanOffering: loanOffering.salt is non-zero" + ); // CHECK VALUES32 - require(loanOffering.callTimeLimit == CALL_TIMELIMIT); - require(loanOffering.maxDuration == MAX_DURATION); + require( + loanOffering.callTimeLimit == CALL_TIMELIMIT, + "BucketLender#verifyLoanOffering: loanOffering.callTimelimit is incorrect" + ); + require( + loanOffering.maxDuration == MAX_DURATION, + "BucketLender#verifyLoanOffering: loanOffering.maxDuration is incorrect" + ); assert(loanOffering.rates.interestRate == INTEREST_RATE); assert(loanOffering.rates.interestPeriod == INTEREST_PERIOD); @@ -1071,11 +1113,11 @@ contract BucketLender is if (increase) { newTotal = availableTotal.add(amount); newForBucket = availableForBucket[bucket].add(amount); - emit AvailableIncreased(newTotal, bucket, newForBucket, amount); + emit AvailableIncreased(newTotal, bucket, newForBucket, amount); // solium-disable-line } else { newTotal = availableTotal.sub(amount); newForBucket = availableForBucket[bucket].sub(amount); - emit AvailableDecreased(newTotal, bucket, newForBucket, amount); + emit AvailableDecreased(newTotal, bucket, newForBucket, amount); // solium-disable-line } availableTotal = newTotal; @@ -1107,11 +1149,11 @@ contract BucketLender is if (increase) { newTotal = principalTotal.add(amount); newForBucket = principalForBucket[bucket].add(amount); - emit PrincipalIncreased(newTotal, bucket, newForBucket, amount); + emit PrincipalIncreased(newTotal, bucket, newForBucket, amount); // solium-disable-line } else { newTotal = principalTotal.sub(amount); newForBucket = principalForBucket[bucket].sub(amount); - emit PrincipalDecreased(newTotal, bucket, newForBucket, amount); + emit PrincipalDecreased(newTotal, bucket, newForBucket, amount); // solium-disable-line } principalTotal = newTotal; From 6fa2ca5c6b063423a40543764c31a25150a1bc53 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 25 Jul 2018 11:36:19 -0700 Subject: [PATCH 15/21] change tests to not use round numbers --- .../external/BucketLender/BucketLender.sol | 2 +- .../external/bucketlender/TestBucketLender.js | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 9ef6bdba..c622eeff 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -266,7 +266,7 @@ contract BucketLender is // Set maximum allowance on proxy OWED_TOKEN.approve( - Margin(DYDX_MARGIN).getProxyAddress(), + Margin(margin).getProxyAddress(), MathHelpers.maxUint256() ); } diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index ee0989a5..f1f1db7b 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -27,7 +27,7 @@ const { } = require('../../../helpers/MarginHelper'); const { wait } = require('@digix/tempo')(web3); -let OT = new BigNumber('1e18'); +let OT = new BigNumber('1234567898765543211'); const web3Instance = new Web3(web3.currentProvider); @@ -187,7 +187,6 @@ async function setUpPosition(accounts, openThePosition = true) { const principal = OT.times(2); const deposit = OT.times(6); - const g = gcd(principal.toNumber(), deposit.toNumber()); bucketLender = await TestBucketLender.new( Margin.address, @@ -200,8 +199,8 @@ async function setUpPosition(accounts, openThePosition = true) { INTEREST_PERIOD, MAX_DURATION, CALL_TIMELIMIT, - deposit.div(g), // MIN_HELD_TOKEN_NUMERATOR, - principal.div(g), // MIN_HELD_TOKEN_DENOMINATOR, + 3, + 1 ], [TRUSTED_PARTY] // trusted margin-callers ); @@ -1023,12 +1022,13 @@ contract('BucketLender', accounts => { await doWithdraw(lender1, 0, { throws: true }); - const weight = OT.div(2); + const weightToWithdraw = OT.div(2).floor(); + const startingWeight = await bucketLender.weightForBucketForAccount.call(0, lender1); const {owedWithdrawn, heldWithdrawn, remainingWeight} = - await doWithdraw(lender1, 0, { weight: weight }); - expect(owedWithdrawn).to.be.bignumber.gte(weight); + await doWithdraw(lender1, 0, { weight: weightToWithdraw }); + expect(owedWithdrawn).to.be.bignumber.gte(weightToWithdraw); expect(heldWithdrawn).to.be.bignumber.eq(0); - expect(remainingWeight).to.be.bignumber.eq(OT.times(3).div(2)); + expect(remainingWeight).to.be.bignumber.eq(startingWeight.minus(weightToWithdraw)); }); it('succeeds but returns no tokens for the current/critical bucket', async () => { @@ -1294,7 +1294,7 @@ contract('BucketLender', accounts => { for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + await doWithdraw(act, b); } } } @@ -1457,7 +1457,7 @@ contract('BucketLender', accounts => { for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + await doWithdraw(act, b); } } } @@ -1642,7 +1642,7 @@ contract('BucketLender', accounts => { for(let b = 0; b < 20; b++) { const hasWeight = await bucketLender.weightForBucketForAccount.call(b, act); if (!hasWeight.isZero()) { - const { owedWithdrawn, heldWithdrawn, remainingWeight } = await doWithdraw(act, b); + await doWithdraw(act, b); } } } From ba81917b6a356604b40a0573fe7e95f1b3984035 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Fri, 27 Jul 2018 15:46:23 -0700 Subject: [PATCH 16/21] add BucketLenderFactory --- .../BucketLender/BucketLenderFactory.sol | 107 ++++++++++++++++ .../bucketlender/TestBucketLenderFactory.js | 116 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 contracts/margin/external/BucketLender/BucketLenderFactory.sol create mode 100644 test/margin/external/bucketlender/TestBucketLenderFactory.js diff --git a/contracts/margin/external/BucketLender/BucketLenderFactory.sol b/contracts/margin/external/BucketLender/BucketLenderFactory.sol new file mode 100644 index 00000000..77389428 --- /dev/null +++ b/contracts/margin/external/BucketLender/BucketLenderFactory.sol @@ -0,0 +1,107 @@ +/* + + Copyright 2018 dYdX Trading Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental "v0.5.0"; + +import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; +import { BucketLender } from "./BucketLender.sol"; + + +/** + * @title BucketLenderFactory + * @author dYdX + * + * Contract that allows anyone to deploy a BucketLender contract by sending a transaction. + */ +contract BucketLenderFactory { + + // ============ Events ============ + + event BucketLenderCreated( + address indexed creator, + address at, + bytes32 positionId + ); + + // ============ State Variables ============ + + // Address of the Margin contract for the dYdX Margin Trading Protocol + address public DYDX_MARGIN; + + // ============ Constructor ============ + + constructor( + address margin + ) + public + { + DYDX_MARGIN = margin; + } + + // ============ Public Functions ============ + + /** + * Deploy a new BucketLender contract to the blockchain + * + * @param positionId Unique ID of the position + * @param heldToken Address of the token held in the position as collateral + * @param owedToken Address of the token being lent by the BucketLender + * @param parameters Values corresponding to: + * + * [0] = number of seconds per bucket + * [1] = interest rate + * [2] = interest period + * [3] = maximum loan duration + * [4] = margin-call timelimit + * [5] = numerator of minimum heldToken-per-owedToken + * [6] = denominator of minimum heldToken-per-owedToken + * + * @param marginCallers Accounts that are permitted to margin-call positions (or cancel the margin call) + * @return The address of the new BucketLender contract + */ + function createBucketLender( + bytes32 positionId, + address heldToken, + address owedToken, + uint32[7] parameters, + address[] marginCallers + ) + external + returns (address) + { + address newBucketLender = new BucketLender( + DYDX_MARGIN, + positionId, + heldToken, + owedToken, + parameters, + marginCallers + ); + + Ownable(newBucketLender).transferOwnership(msg.sender); + + emit BucketLenderCreated( + msg.sender, + newBucketLender, + positionId + ); + + return newBucketLender; + } +} diff --git a/test/margin/external/bucketlender/TestBucketLenderFactory.js b/test/margin/external/bucketlender/TestBucketLenderFactory.js new file mode 100644 index 00000000..926cb003 --- /dev/null +++ b/test/margin/external/bucketlender/TestBucketLenderFactory.js @@ -0,0 +1,116 @@ +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-bignumber')()); +const BigNumber = require('bignumber.js'); + +const Margin = artifacts.require("Margin"); +const HeldToken = artifacts.require("TokenA"); +const OwedToken = artifacts.require("TokenB"); +const BucketLender = artifacts.require("BucketLender"); +const BucketLenderFactory = artifacts.require("BucketLenderFactory"); + +const { transact } = require('../../../helpers/ContractHelper'); +const { ADDRESSES, BYTES32 } = require('../../../helpers/Constants'); + +contract('BucketLenderFactory', () => { + + // ============ Before/After ============ + + beforeEach('set up contracts', async () => { + }); + + // ============ Constructor ============ + + describe('Constructor', () => { + it('sets constants correctly', async () => { + const marginAddress = ADDRESSES.TEST[0]; + const factory = await BucketLenderFactory.new(marginAddress); + const dydxMargin = await factory.DYDX_MARGIN.call(); + expect(dydxMargin).to.be.eq(marginAddress); + }); + }); + + // ============ Functions ============ + + describe('createBucketLender', () => { + it('succeeds', async () => { + const positionId = BYTES32.TEST[0]; + const bucketTime = new BigNumber(123); + const interestRate = new BigNumber(456); + const interestPeriod = new BigNumber(789); + const maxDuration = new BigNumber(101112); + const callTimelimit = new BigNumber(131415); + const numerator = new BigNumber(161718); + const denominator = new BigNumber(192021); + const marginCaller = ADDRESSES.TEST[0]; + const notMarginCaller = ADDRESSES.TEST[1]; + + const factory = await BucketLenderFactory.new(Margin.address); + + const newBucketLender = await transact( + factory.createBucketLender, + positionId, + HeldToken.address, + OwedToken.address, + [ + bucketTime, + interestRate, + interestPeriod, + maxDuration, + callTimelimit, + numerator, + denominator + ], + [ + marginCaller + ] + ); + + const bucketLender = await BucketLender.at(newBucketLender.result); + + const [ + bl_margin, + bl_positionId, + bl_heldToken, + bl_owedToken, + bl_bucketTime, + bl_interestRate, + bl_interestPeriod, + bl_maxDuration, + bl_callTimelimit, + bl_numerator, + bl_denominator, + bl_marginCallerOkay, + bl_notMarginCallerNotOkay, + ] = await Promise.all([ + bucketLender.DYDX_MARGIN.call(), + bucketLender.POSITION_ID.call(), + bucketLender.HELD_TOKEN.call(), + bucketLender.OWED_TOKEN.call(), + bucketLender.BUCKET_TIME.call(), + bucketLender.INTEREST_RATE.call(), + bucketLender.INTEREST_PERIOD.call(), + bucketLender.MAX_DURATION.call(), + bucketLender.CALL_TIMELIMIT.call(), + bucketLender.MIN_HELD_TOKEN_NUMERATOR.call(), + bucketLender.MIN_HELD_TOKEN_DENOMINATOR.call(), + bucketLender.TRUSTED_MARGIN_CALLERS.call(marginCaller), + bucketLender.TRUSTED_MARGIN_CALLERS.call(notMarginCaller), + ]); + + expect(bl_margin).to.be.eq(Margin.address); + expect(bl_positionId).to.be.eq(positionId); + expect(bl_heldToken).to.be.eq(HeldToken.address); + expect(bl_owedToken).to.be.eq(OwedToken.address); + expect(bl_bucketTime).to.be.bignumber.eq(bucketTime); + expect(bl_interestRate).to.be.bignumber.eq(interestRate); + expect(bl_interestPeriod).to.be.bignumber.eq(interestPeriod); + expect(bl_maxDuration).to.be.bignumber.eq(maxDuration); + expect(bl_callTimelimit).to.be.bignumber.eq(callTimelimit); + expect(bl_numerator).to.be.bignumber.eq(numerator); + expect(bl_denominator).to.be.bignumber.eq(denominator); + expect(bl_marginCallerOkay).to.be.eq(true); + expect(bl_notMarginCallerNotOkay).to.be.eq(false); + }); + }); +}); From a434b6bcda3b3b2b98f5104b1fc175a6448ae0c7 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 1 Aug 2018 10:58:17 -0700 Subject: [PATCH 17/21] add tests, fix withdrawExcess --- .../external/BucketLender/BucketLender.sol | 21 ++- .../external/bucketlender/TestBucketLender.js | 123 +++++++++++++++++- 2 files changed, 126 insertions(+), 18 deletions(-) diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index c622eeff..2c579e59 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -778,20 +778,19 @@ contract BucketLender is { rebalanceBucketsInternal(); + uint256 amount = token.balanceOf(address(this)); + if (token == OWED_TOKEN) { - uint256 amount = token.balanceOf(address(this)); amount = amount.sub(availableTotal); - token.transfer(to, amount); - } else { - if (token == HELD_TOKEN) { - require( - principalTotal == 0, - "BucketLender#withdrawExcessToken: Withdrawing heldToken when principal is zero" - ); - } - uint256 amount = token.balanceOf(address(this)); - token.transfer(to, amount); + } else if (token == HELD_TOKEN) { + require( + !wasForceClosed, + "BucketLender#withdrawExcessToken: heldToken cannot be withdrawn if force-closed" + ); } + + token.transfer(to, amount); + return amount; } // ============ Helper Functions ============ diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index f1f1db7b..9664f9c2 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -7,6 +7,7 @@ const BigNumber = require('bignumber.js'); const Margin = artifacts.require("Margin"); const HeldToken = artifacts.require("TokenA"); const OwedToken = artifacts.require("TokenB"); +const TestToken = artifacts.require("TokenC"); const TestBucketLender = artifacts.require("TestBucketLender"); const TestLoanOwner = artifacts.require("TestLoanOwner"); const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); @@ -43,13 +44,6 @@ let margin, heldToken, owedToken; let bucketLender; let TRUSTED_PARTY, lender1, lender2, uselessLender, trader, alice; -function gcd(a, b) { - if (!b) { - return a; - } - return gcd(b, a % b); -} - // grants tokens to a lender and has them deposit them into the bucket lender async function doDeposit(account, amount) { await bucketLender.checkInvariants(); @@ -679,6 +673,29 @@ contract('BucketLender', accounts => { }); it('prevents lending while the position is margin-called', async () => { + const principal = OT.times(2); + const deposit = OT.times(6); + await setUpPosition(accounts, false); + await margin.openWithoutCounterparty( + [ + trader, + owedToken.address, + heldToken.address, + bucketLender.address + ], + [ + principal, + deposit, + NONCE + ], + [ + CALL_TIMELIMIT, + MAX_DURATION, + INTEREST_RATE, + INTEREST_PERIOD + ], + { from: TRUSTED_PARTY } + ); await margin.marginCall(POSITION_ID, 0); let tx = createIncreaseTx(trader, OT); await expectThrow(callIncreasePosition(margin, tx)); @@ -899,6 +916,98 @@ contract('BucketLender', accounts => { }); }); + describe('#withdrawExcessToken', () => { + const reciever = accounts[9]; + const amount = new BigNumber("1123498756213"); + + async function doWithdrawExtra(token, toExpect) { + const [bl0, rc0] = await Promise.all([ + token.balanceOf.call(bucketLender.address), + token.balanceOf.call(reciever) + ]); + const moved = await transact(bucketLender.withdrawExcessToken, token.address, reciever); + const [bl1, rc1] = await Promise.all([ + token.balanceOf.call(bucketLender.address), + token.balanceOf.call(reciever) + ]); + expect(moved.result).to.be.bignumber.eq(toExpect); + expect(bl0.sub(bl1)).to.be.bignumber.eq(toExpect); + expect(rc1.sub(rc0)).to.be.bignumber.eq(toExpect); + } + + it('succeeds after rebalance for owedToken', async () => { + await owedToken.issueTo(bucketLender.address, amount); + await doDeposit(uselessLender, amount); + await doWithdrawExtra(owedToken, amount); + + await owedToken.issueTo(bucketLender.address, amount); + await doDeposit(uselessLender, amount); + await doWithdrawExtra(owedToken, amount); + + await doIncrease(amount); + await owedToken.issueTo(bucketLender.address, amount); + await doWithdrawExtra(owedToken, amount); + + await owedToken.issueTo(bucketLender.address, amount); + await doIncrease(amount); + await doWithdrawExtra(owedToken, amount); + + await owedToken.issueTo(bucketLender.address, amount); + await doClose(amount); + await bucketLender.rebalanceBuckets(); + await doWithdrawExtra(owedToken, amount); + + await owedToken.issueTo(bucketLender.address, amount); + await doClose(amount); + await owedToken.issueTo(bucketLender.address, amount); + await bucketLender.rebalanceBuckets(); + await doWithdrawExtra(owedToken, amount.times(2)); + + await owedToken.issueTo(bucketLender.address, amount); + await doWithdrawExtra(owedToken, amount); + }); + + it('succeeds before rebalance for owedToken', async () => { + await doIncrease(amount); + await owedToken.issueTo(bucketLender.address, amount); + await doClose(amount); + await owedToken.issueTo(bucketLender.address, amount); + await doWithdrawExtra(owedToken, amount.times(2)); + }); + + it('succeeds for heldToken', async () => { + // succeeds while open + await heldToken.issueTo(bucketLender.address, amount); + await doWithdrawExtra(heldToken, amount); + + // succeeds after close + await heldToken.issueTo(bucketLender.address, amount); + await doClose(BIGNUMBERS.ONES_255, { closer: TRUSTED_PARTY }); + await doWithdrawExtra(heldToken, amount); + }); + + it('fails after force-close for heldToken', async () => { + await heldToken.issueTo(bucketLender.address, amount); + + // force-close it + await margin.marginCall(POSITION_ID, 0, { from: TRUSTED_PARTY }); + await wait(MAX_DURATION.toNumber()); + await margin.forceRecoverCollateral( + POSITION_ID, + bucketLender.address, + { from: TRUSTED_PARTY } + ); + + await expectThrow(bucketLender.withdrawExcessToken(heldToken.address, reciever)); + }); + + it('succeeds for random token', async () => { + const randomToken = await TestToken.new(); + await randomToken.issueTo(bucketLender.address, amount); + await doWithdrawExtra(randomToken, amount); + }); + }); + describe('#withdraw', () => { it('succeeds in withdrawing from bucket 0', async () => { await doWithdraw(lender1, 0); From 12b2c24aec0c2189f9e13084455b18b87053d7a4 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Fri, 3 Aug 2018 14:59:29 -0700 Subject: [PATCH 18/21] make changes --- contracts/lib/MathHelpers.sol | 14 ++ .../external/BucketLender/BucketLender.sol | 27 ++- .../BucketLender/BucketLenderFactory.sol | 22 +- test/helpers/Constants.js | 5 +- test/margin/TestFractionMath.js | 2 +- test/margin/TestMathHelpers.js | 2 +- .../external/TestZeroExExchangeWrapper.js | 2 +- .../external/bucketlender/TestBucketLender.js | 190 +++++++++--------- 8 files changed, 153 insertions(+), 111 deletions(-) diff --git a/contracts/lib/MathHelpers.sol b/contracts/lib/MathHelpers.sol index 5d95da19..3cc13033 100644 --- a/contracts/lib/MathHelpers.sol +++ b/contracts/lib/MathHelpers.sol @@ -107,6 +107,20 @@ library MathHelpers { return 2 ** 256 - 1; } + /** + * Calculates and returns the maximum value for a uint256 in solidity + * + * @return The maximum value for uint256 + */ + function maxUint32( + ) + internal + pure + returns (uint32) + { + return 2 ** 32 - 1; + } + /** * Returns the number of bits in a uint256. That is, the lowest number, x, such that n >> x == 0 * diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 2c579e59..6ead9d81 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -104,14 +104,14 @@ contract BucketLender is // ============ Events ============ event Deposit( - address beneficiary, + address indexed beneficiary, uint256 bucket, uint256 amount, uint256 weight ); event Withdraw( - address withdrawer, + address indexed withdrawer, uint256 bucket, uint256 weight, uint256 owedTokenWithdrawn, @@ -155,6 +155,7 @@ contract BucketLender is */ // Available Amount for each bucket mapping(uint256 => uint256) public availableForBucket; + // Total Available Amount uint256 public availableTotal; @@ -165,6 +166,7 @@ contract BucketLender is */ // Outstanding Principal for each bucket mapping(uint256 => uint256) public principalForBucket; + // Total Outstanding Principal uint256 public principalTotal; @@ -177,6 +179,7 @@ contract BucketLender is */ // Weight for each account in each bucket mapping(uint256 => mapping(address => uint256)) public weightForBucketForAccount; + // Total Weight for each bucket mapping(uint256 => uint256) public weightForBucket; @@ -378,11 +381,11 @@ contract BucketLender is // CHECK VALUES32 require( - loanOffering.callTimeLimit == CALL_TIMELIMIT, + loanOffering.callTimeLimit == MathHelpers.maxUint32(), "BucketLender#verifyLoanOffering: loanOffering.callTimelimit is incorrect" ); require( - loanOffering.maxDuration == MAX_DURATION, + loanOffering.maxDuration == MathHelpers.maxUint32(), "BucketLender#verifyLoanOffering: loanOffering.maxDuration is incorrect" ); assert(loanOffering.rates.interestRate == INTEREST_RATE); @@ -432,6 +435,22 @@ contract BucketLender is position.heldToken == HELD_TOKEN, "BucketLender#receiveLoanOwnership: Position heldToken mismatch" ); + require( + position.maxDuration == MAX_DURATION, + "BucketLender#receiveLoanOwnership: Position maxDuration mismatch" + ); + require( + position.callTimeLimit == CALL_TIMELIMIT, + "BucketLender#receiveLoanOwnership: Position callTimeLimit mismatch" + ); + require( + position.interestRate == INTEREST_RATE, + "BucketLender#receiveLoanOwnership: Position interestRate mismatch" + ); + require( + position.interestPeriod == INTEREST_PERIOD, + "BucketLender#receiveLoanOwnership: Position interestPeriod mismatch" + ); require( Margin(DYDX_MARGIN).getPositionBalance(POSITION_ID) >= minHeldToken, "BucketLender#receiveLoanOwnership: Not enough heldToken as collateral" diff --git a/contracts/margin/external/BucketLender/BucketLenderFactory.sol b/contracts/margin/external/BucketLender/BucketLenderFactory.sol index 77389428..eff85be0 100644 --- a/contracts/margin/external/BucketLender/BucketLenderFactory.sol +++ b/contracts/margin/external/BucketLender/BucketLenderFactory.sol @@ -35,8 +35,8 @@ contract BucketLenderFactory { event BucketLenderCreated( address indexed creator, - address at, - bytes32 positionId + bytes32 indexed positionId, + address at ); // ============ State Variables ============ @@ -64,13 +64,13 @@ contract BucketLenderFactory { * @param owedToken Address of the token being lent by the BucketLender * @param parameters Values corresponding to: * - * [0] = number of seconds per bucket - * [1] = interest rate - * [2] = interest period - * [3] = maximum loan duration - * [4] = margin-call timelimit - * [5] = numerator of minimum heldToken-per-owedToken - * [6] = denominator of minimum heldToken-per-owedToken + * [0] = number of seconds per bucket + * [1] = interest rate + * [2] = interest period + * [3] = maximum loan duration + * [4] = margin-call timelimit + * [5] = numerator of minimum heldToken-per-owedToken + * [6] = denominator of minimum heldToken-per-owedToken * * @param marginCallers Accounts that are permitted to margin-call positions (or cancel the margin call) * @return The address of the new BucketLender contract @@ -98,8 +98,8 @@ contract BucketLenderFactory { emit BucketLenderCreated( msg.sender, - newBucketLender, - positionId + positionId, + newBucketLender ); return newBucketLender; diff --git a/test/helpers/Constants.js b/test/helpers/Constants.js index 75f88979..5032210d 100644 --- a/test/helpers/Constants.js +++ b/test/helpers/Constants.js @@ -30,8 +30,9 @@ module.exports = { ZERO: new BigNumber(0), ONE_DAY_IN_SECONDS: new BigNumber(60 * 60 * 24), ONE_YEAR_IN_SECONDS: new BigNumber(60 * 60 * 24 * 365), - ONES_127: new BigNumber("340282366920938463463374607431768211455"), // 2**128-1 - ONES_255: new BigNumber( + MAX_UINT32: new BigNumber("4294967295"), // 2**32-1 + MAX_UINT128: new BigNumber("340282366920938463463374607431768211455"), // 2**128-1 + MAX_UINT256: new BigNumber( "115792089237316195423570985008687907853269984665640564039457584007913129639935"), // 2**256-1 }, BYTES32: { diff --git a/test/margin/TestFractionMath.js b/test/margin/TestFractionMath.js index 49f4d45b..d3fef972 100644 --- a/test/margin/TestFractionMath.js +++ b/test/margin/TestFractionMath.js @@ -6,7 +6,7 @@ const TestFractionMath = artifacts.require("TestFractionMath"); const { BIGNUMBERS } = require('../helpers/Constants'); const { expectAssertFailure } = require('../helpers/ExpectHelper'); -const bn = BIGNUMBERS.ONES_127; +const bn = BIGNUMBERS.MAX_UINT128; contract('FractionMath', function(_accounts) { let contract; diff --git a/test/margin/TestMathHelpers.js b/test/margin/TestMathHelpers.js index 49c18da6..595d18b7 100644 --- a/test/margin/TestMathHelpers.js +++ b/test/margin/TestMathHelpers.js @@ -105,7 +105,7 @@ contract('InterestHelper', function(_accounts) { describe('#maxUint256', () => { it('gives the expected value', async () => { const result = await contract.maxUint256.call(); - expect(result).to.be.bignumber.equal(BIGNUMBERS.ONES_255); + expect(result).to.be.bignumber.equal(BIGNUMBERS.MAX_UINT256); }); }); diff --git a/test/margin/external/TestZeroExExchangeWrapper.js b/test/margin/external/TestZeroExExchangeWrapper.js index c077dc5c..6eb266c4 100644 --- a/test/margin/external/TestZeroExExchangeWrapper.js +++ b/test/margin/external/TestZeroExExchangeWrapper.js @@ -50,7 +50,7 @@ describe('ZeroExExchangeWrapper', () => { expect(ZERO_EX_EXCHANGE).to.eq(ZeroExExchange.address); expect(ZERO_EX_PROXY).to.eq(ZeroExProxy.address); expect(ZRX).to.eq(FeeToken.address); - expect(zrxProxyAllowance).to.be.bignumber.eq(BIGNUMBERS.ONES_255); + expect(zrxProxyAllowance).to.be.bignumber.eq(BIGNUMBERS.MAX_UINT256); }); }); }); diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index 9664f9c2..f010f7a5 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -58,7 +58,7 @@ async function doWithdraw(account, bucket, args) { args = args || {}; args.beneficiary = args.beneficiary || account; args.throws = args.throws || false; - args.weight = args.weight || BIGNUMBERS.ONES_255; + args.weight = args.weight || BIGNUMBERS.MAX_UINT256; await bucketLender.checkInvariants(); if (args.throws) { @@ -390,20 +390,20 @@ contract('BucketLender', accounts => { OpenDirectlyExchangeWrapper.address, ], [ - BIGNUMBERS.ONES_255, + BIGNUMBERS.MAX_UINT256, BIGNUMBERS.ZERO, BIGNUMBERS.ZERO, BIGNUMBERS.ZERO, BIGNUMBERS.ZERO, - BIGNUMBERS.ONES_255, + BIGNUMBERS.MAX_UINT256, BIGNUMBERS.ZERO, OT, OT.times(3), NONCE ], [ - CALL_TIMELIMIT, - MAX_DURATION, + BIGNUMBERS.MAX_UINT32, + BIGNUMBERS.MAX_UINT32, INTEREST_RATE, INTEREST_PERIOD ], @@ -463,6 +463,7 @@ contract('BucketLender', accounts => { }); it('prevents different values', async () => { + await wait(2); let incrTx; // works once @@ -470,70 +471,70 @@ contract('BucketLender', accounts => { await callIncreasePosition(margin, incrTx); // maxAmount - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.rates.maxAmount = OT.times(1000); await expectThrow( callIncreasePosition(margin, incrTx) ); // minAmount - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.rates.minAmount = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // minHeldToken - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.rates.minHeldToken = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // lenderFee - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.rates.lenderFee = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // takerFee - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.rates.takerFee = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // expirationTimestamp - incrTx = createIncreaseTx(trader, OT); - incrTx.loanOffering.expirationTimestamp = BIGNUMBERS.ONES_255.minus(1); + incrTx = createIncreaseTx(trader, OT) + incrTx.loanOffering.expirationTimestamp = BIGNUMBERS.MAX_UINT256.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // salt - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) incrTx.loanOffering.salt = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); - // callTimeLimit - incrTx = createIncreaseTx(trader, OT); - incrTx.loanOffering.callTimeLimit = CALL_TIMELIMIT.plus(1); + // maxDuration + incrTx = createIncreaseTx(trader, OT) + incrTx.loanOffering.maxDuration = BIGNUMBERS.MAX_UINT32.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); - // maxDuration - incrTx = createIncreaseTx(trader, OT); - incrTx.loanOffering.maxDuration = MAX_DURATION.plus(1); + // callTimeLimit + incrTx = createIncreaseTx(trader, OT) + incrTx.loanOffering.callTimeLimit = BIGNUMBERS.MAX_UINT32.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // works again - incrTx = createIncreaseTx(trader, OT); + incrTx = createIncreaseTx(trader, OT) await callIncreasePosition(margin, incrTx); }); }); @@ -542,6 +543,45 @@ contract('BucketLender', accounts => { const ogPrincipal = OT.times(2); const ogDeposit = OT.times(6); + async function openWithoutCounterpartyForBucketLender(args) { + args = args || {}; + args.owner = args.owner || ERC20ShortCreator.address; + args.owedToken = args.owedToken || owedToken.address; + args.heldToken = args.heldToken || heldToken.address; + args.lender = args.lender || bucketLender.address; + args.principal = args.principal || ogPrincipal; + args.deposit = args.deposit || ogDeposit; + args.nonce = args.nonce || NONCE; + args.callTimeLimit = args.callTimeLimit || CALL_TIMELIMIT; + args.maxDuration = args.maxDuration || MAX_DURATION; + args.interestRate = args.interestRate || INTEREST_RATE; + args.interestPeriod = args.interestPeriod || INTEREST_PERIOD; + const promise = margin.openWithoutCounterparty( + [ + args.owner, + args.owedToken, + args.heldToken, + args.lender + ], + [ + args.principal, + args.deposit, + args.nonce + ], + [ + args.callTimeLimit, + args.maxDuration, + args.interestRate, + args.interestPeriod + ] + ); + if (args.throws) { + await expectThrow(promise); + } else { + await Promise.all([promise]); + } + } + it('succeeds under normal conditions', async () => { await setUpPosition(accounts); const owner = await margin.getPositionLender.call(POSITION_ID); @@ -552,77 +592,45 @@ contract('BucketLender', accounts => { await setUpPosition(accounts, false); const badToken = await HeldToken.new(); await issueTokenToAccountInAmountAndApproveProxy(badToken, accounts[0], OT.times(1000)), - await expectThrow( - margin.openWithoutCounterparty( - [ - ERC20ShortCreator.address, - owedToken.address, - badToken.address, - bucketLender.address - ], - [ - ogPrincipal, - ogDeposit, - NONCE - ], - [ - CALL_TIMELIMIT, - MAX_DURATION, - INTEREST_RATE, - INTEREST_PERIOD - ] - ) - ); + await openWithoutCounterpartyForBucketLender({ throws: true, heldToken: badToken.address }); }); it('fails for the wrong owedToken', async () => { await setUpPosition(accounts, false); const badToken = await OwedToken.new(); - await expectThrow( - margin.openWithoutCounterparty( - [ - ERC20ShortCreator.address, - badToken.address, - heldToken.address, - bucketLender.address - ], - [ - ogPrincipal, - ogDeposit, - NONCE - ], - [ - CALL_TIMELIMIT, - MAX_DURATION, - INTEREST_RATE, - INTEREST_PERIOD - ] - ) + await openWithoutCounterpartyForBucketLender({ throws: true, owedToken: badToken.address }); + }); + + it('fails for the wrong maxDuration', async () => { + await setUpPosition(accounts, false); + const maxDuration = BIGNUMBERS.MAX_UINT32.minus(2); + await openWithoutCounterpartyForBucketLender({ throws: true, maxDuration: maxDuration }); + }); + + it('fails for the wrong callTimeLimit', async () => { + await setUpPosition(accounts, false); + const callTimeLimit = BIGNUMBERS.MAX_UINT32.minus(2); + await openWithoutCounterpartyForBucketLender({ throws: true, callTimeLimit: callTimeLimit }); + }); + + it('fails for the wrong interestRate', async () => { + await setUpPosition(accounts, false); + const interestRate = INTEREST_RATE.add(1); + await openWithoutCounterpartyForBucketLender({ throws: true, interestRate: interestRate }); + }); + + it('fails for the wrong interestPeriod', async () => { + await setUpPosition(accounts, false); + const interestPeriod = INTEREST_RATE.add(1); + await openWithoutCounterpartyForBucketLender( + { throws: true, interestPeriod: interestPeriod } ); }); it('fails for insufficient collateral', async () => { await setUpPosition(accounts, false); - await expectThrow( - margin.openWithoutCounterparty( - [ - ERC20ShortCreator.address, - owedToken.address, - heldToken.address, - bucketLender.address - ], - [ - ogPrincipal.plus(1), - ogDeposit, - NONCE - ], - [ - CALL_TIMELIMIT, - MAX_DURATION, - INTEREST_RATE, - INTEREST_PERIOD - ] - ) + await openWithoutCounterpartyForBucketLender( + { throws: true, principal: ogPrincipal.plus(1) } ); }); @@ -906,7 +914,7 @@ contract('BucketLender', accounts => { await issueTokenToAccountInAmountAndApproveProxy(owedToken, TRUSTED_PARTY, OT.times(1000)); await margin.closePositionDirectly( POSITION_ID, - BIGNUMBERS.ONES_255, + BIGNUMBERS.MAX_UINT256, TRUSTED_PARTY, { from: TRUSTED_PARTY } ); @@ -982,7 +990,7 @@ contract('BucketLender', accounts => { // succeeds after close await heldToken.issueTo(bucketLender.address, amount); - await doClose(BIGNUMBERS.ONES_255, { closer: TRUSTED_PARTY }); + await doClose(BIGNUMBERS.MAX_UINT256, { closer: TRUSTED_PARTY }); await doWithdrawExtra(heldToken, amount); }); @@ -1080,7 +1088,7 @@ contract('BucketLender', accounts => { await doIncrease(OT.times(4)); await wait(60 * 60 * 24 * 2); await doClose(OT.times(4)); - await doClose(BIGNUMBERS.ONES_255, { closer: TRUSTED_PARTY }); + await doClose(BIGNUMBERS.MAX_UINT256, { closer: TRUSTED_PARTY }); const isClosed = await margin.isPositionClosed.call(POSITION_ID); expect(isClosed).to.be.true; @@ -1169,7 +1177,7 @@ contract('BucketLender', accounts => { await expectThrow( bucketLender.withdraw( [0], - [BIGNUMBERS.ONES_255, BIGNUMBERS.ONES_255], + [BIGNUMBERS.MAX_UINT256, BIGNUMBERS.MAX_UINT256], lender1, { from: lender1 } ) @@ -1177,7 +1185,7 @@ contract('BucketLender', accounts => { await expectThrow( bucketLender.withdraw( [0, 1], - [BIGNUMBERS.ONES_255], + [BIGNUMBERS.MAX_UINT256], lender1, { from: lender1 } ) @@ -1382,7 +1390,7 @@ contract('BucketLender', accounts => { await bucketLender.checkInvariants(); - await doClose(BIGNUMBERS.ONES_255); + await doClose(BIGNUMBERS.MAX_UINT256); await wait(60 * 60 * 24 * 1); await bucketLender.checkInvariants(); @@ -1831,7 +1839,7 @@ function createIncreaseTx(trader, principal) { lenderFeeTokenAddress: ADDRESSES.ZERO, takerFeeTokenAddress: ADDRESSES.ZERO, rates: { - maxAmount: BIGNUMBERS.ONES_255, + maxAmount: BIGNUMBERS.MAX_UINT256, minAmount: BIGNUMBERS.ZERO, minHeldToken: BIGNUMBERS.ZERO, lenderFee: BIGNUMBERS.ZERO, @@ -1839,9 +1847,9 @@ function createIncreaseTx(trader, principal) { interestRate: INTEREST_RATE, interestPeriod: INTEREST_PERIOD }, - expirationTimestamp: BIGNUMBERS.ONES_255, - callTimeLimit: CALL_TIMELIMIT.toNumber(), - maxDuration: MAX_DURATION.toNumber(), + expirationTimestamp: BIGNUMBERS.MAX_UINT256, + callTimeLimit: BIGNUMBERS.MAX_UINT32, + maxDuration: BIGNUMBERS.MAX_UINT32, salt: 0, signature: BYTES.EMPTY } From e8d165c1606fcaedcdc759303deda21161ea8715 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Mon, 6 Aug 2018 18:32:48 -0700 Subject: [PATCH 19/21] add back semicolons --- .../external/bucketlender/TestBucketLender.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/margin/external/bucketlender/TestBucketLender.js index f010f7a5..8d14c40d 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/margin/external/bucketlender/TestBucketLender.js @@ -471,70 +471,70 @@ contract('BucketLender', accounts => { await callIncreasePosition(margin, incrTx); // maxAmount - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.rates.maxAmount = OT.times(1000); await expectThrow( callIncreasePosition(margin, incrTx) ); // minAmount - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.rates.minAmount = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // minHeldToken - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.rates.minHeldToken = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // lenderFee - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.rates.lenderFee = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // takerFee - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.rates.takerFee = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // expirationTimestamp - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.expirationTimestamp = BIGNUMBERS.MAX_UINT256.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // salt - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.salt = new BigNumber(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // maxDuration - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.maxDuration = BIGNUMBERS.MAX_UINT32.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // callTimeLimit - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); incrTx.loanOffering.callTimeLimit = BIGNUMBERS.MAX_UINT32.minus(1); await expectThrow( callIncreasePosition(margin, incrTx) ); // works again - incrTx = createIncreaseTx(trader, OT) + incrTx = createIncreaseTx(trader, OT); await callIncreasePosition(margin, incrTx); }); }); From d5a0a82e565fbbede9b41a41381e1f68d34d6c6e Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 8 Aug 2018 13:29:53 -0700 Subject: [PATCH 20/21] move test files --- .../margin/external/bucketlender/TestBucketLender.js | 12 ++++++------ .../external/bucketlender/TestBucketLenderFactory.js | 4 ++-- .../bucketlender/TestEthWrapperForBucketLender.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) rename test/{ => tests}/margin/external/bucketlender/TestBucketLender.js (99%) rename test/{ => tests}/margin/external/bucketlender/TestBucketLenderFactory.js (96%) rename test/{ => tests}/margin/external/bucketlender/TestEthWrapperForBucketLender.js (94%) diff --git a/test/margin/external/bucketlender/TestBucketLender.js b/test/tests/margin/external/bucketlender/TestBucketLender.js similarity index 99% rename from test/margin/external/bucketlender/TestBucketLender.js rename to test/tests/margin/external/bucketlender/TestBucketLender.js index 8d14c40d..d05c5a83 100644 --- a/test/margin/external/bucketlender/TestBucketLender.js +++ b/test/tests/margin/external/bucketlender/TestBucketLender.js @@ -14,18 +14,18 @@ const TestMarginCallDelegator = artifacts.require("TestMarginCallDelegator"); const ERC20ShortCreator = artifacts.require("ERC20ShortCreator"); const OpenDirectlyExchangeWrapper = artifacts.require("OpenDirectlyExchangeWrapper"); -const { transact } = require('../../../helpers/ContractHelper'); -const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../../helpers/Constants'); -const { expectThrow } = require('../../../helpers/ExpectHelper'); -const { issueAndSetAllowance } = require('../../../helpers/TokenHelper'); -const { signLoanOffering } = require('../../../helpers/LoanHelper'); +const { transact } = require('../../../../helpers/ContractHelper'); +const { ADDRESSES, BIGNUMBERS, BYTES, ORDER_TYPE } = require('../../../../helpers/Constants'); +const { expectThrow } = require('../../../../helpers/ExpectHelper'); +const { issueAndSetAllowance } = require('../../../../helpers/TokenHelper'); +const { signLoanOffering } = require('../../../../helpers/LoanHelper'); const { issueTokenToAccountInAmountAndApproveProxy, issueTokensAndSetAllowances, callIncreasePosition, callOpenPosition, createOpenTx, -} = require('../../../helpers/MarginHelper'); +} = require('../../../../helpers/MarginHelper'); const { wait } = require('@digix/tempo')(web3); let OT = new BigNumber('1234567898765543211'); diff --git a/test/margin/external/bucketlender/TestBucketLenderFactory.js b/test/tests/margin/external/bucketlender/TestBucketLenderFactory.js similarity index 96% rename from test/margin/external/bucketlender/TestBucketLenderFactory.js rename to test/tests/margin/external/bucketlender/TestBucketLenderFactory.js index 926cb003..c4218d23 100644 --- a/test/margin/external/bucketlender/TestBucketLenderFactory.js +++ b/test/tests/margin/external/bucketlender/TestBucketLenderFactory.js @@ -9,8 +9,8 @@ const OwedToken = artifacts.require("TokenB"); const BucketLender = artifacts.require("BucketLender"); const BucketLenderFactory = artifacts.require("BucketLenderFactory"); -const { transact } = require('../../../helpers/ContractHelper'); -const { ADDRESSES, BYTES32 } = require('../../../helpers/Constants'); +const { transact } = require('../../../../helpers/ContractHelper'); +const { ADDRESSES, BYTES32 } = require('../../../../helpers/Constants'); contract('BucketLenderFactory', () => { diff --git a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js b/test/tests/margin/external/bucketlender/TestEthWrapperForBucketLender.js similarity index 94% rename from test/margin/external/bucketlender/TestEthWrapperForBucketLender.js rename to test/tests/margin/external/bucketlender/TestEthWrapperForBucketLender.js index f481d11d..fdbd9baf 100644 --- a/test/margin/external/bucketlender/TestEthWrapperForBucketLender.js +++ b/test/tests/margin/external/bucketlender/TestEthWrapperForBucketLender.js @@ -9,9 +9,9 @@ const WETH9 = artifacts.require("WETH9"); const BucketLender = artifacts.require("BucketLender"); const EthWrapperForBucketLender = artifacts.require("EthWrapperForBucketLender"); -const { transact } = require('../../../helpers/ContractHelper'); -const { BIGNUMBERS } = require('../../../helpers/Constants'); -const { expectThrow } = require('../../../helpers/ExpectHelper'); +const { transact } = require('../../../../helpers/ContractHelper'); +const { BIGNUMBERS } = require('../../../../helpers/Constants'); +const { expectThrow } = require('../../../../helpers/ExpectHelper'); let heldToken, weth, bucketLender, ethWrapper; const value = new BigNumber('1e10'); From 07494ef6493f87177afc5f4cce7081581f916837 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Wed, 8 Aug 2018 13:47:18 -0700 Subject: [PATCH 21/21] split into LoanOfferingParser --- .../external/BucketLender/BucketLender.sol | 4 +- .../external/lib/LoanOfferingParser.sol | 101 ++++++++++++++++++ .../interfaces/LoanOfferingVerifier.sol | 73 ------------- .../external/bucketlender/TestBucketLender.js | 16 ++- 4 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 contracts/margin/external/lib/LoanOfferingParser.sol diff --git a/contracts/margin/external/BucketLender/BucketLender.sol b/contracts/margin/external/BucketLender/BucketLender.sol index 6ead9d81..e679a032 100644 --- a/contracts/margin/external/BucketLender/BucketLender.sol +++ b/contracts/margin/external/BucketLender/BucketLender.sol @@ -35,6 +35,7 @@ import { ForceRecoverCollateralDelegator } from "../../interfaces/lender/ForceRe import { IncreaseLoanDelegator } from "../../interfaces/lender/IncreaseLoanDelegator.sol"; import { LoanOwner } from "../../interfaces/lender/LoanOwner.sol"; import { MarginCallDelegator } from "../../interfaces/lender/MarginCallDelegator.sol"; +import { LoanOfferingParser } from "../lib/LoanOfferingParser.sol"; import { MarginHelper } from "../lib/MarginHelper.sol"; @@ -95,6 +96,7 @@ contract BucketLender is MarginCallDelegator, CancelMarginCallDelegator, ForceRecoverCollateralDelegator, + LoanOfferingParser, LoanOfferingVerifier, ReentrancyGuard { @@ -269,7 +271,7 @@ contract BucketLender is // Set maximum allowance on proxy OWED_TOKEN.approve( - Margin(margin).getProxyAddress(), + Margin(margin).getTokenProxyAddress(), MathHelpers.maxUint256() ); } diff --git a/contracts/margin/external/lib/LoanOfferingParser.sol b/contracts/margin/external/lib/LoanOfferingParser.sol new file mode 100644 index 00000000..ca4d3872 --- /dev/null +++ b/contracts/margin/external/lib/LoanOfferingParser.sol @@ -0,0 +1,101 @@ +/* + + Copyright 2018 dYdX Trading Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental "v0.5.0"; + +import { MarginCommon } from "../../impl/MarginCommon.sol"; + + +/** + * @title LoanOfferingParser + * @author dYdX + * + * Contract for LoanOfferingVerifiers to parse arguments + */ +contract LoanOfferingParser { + + // ============ Parsing Functions ============ + + function parseLoanOffering( + address[9] addresses, + uint256[7] values256, + uint32[4] values32, + bytes signature + ) + internal + pure + returns (MarginCommon.LoanOffering memory) + { + MarginCommon.LoanOffering memory loanOffering; + + fillLoanOfferingAddresses(loanOffering, addresses); + fillLoanOfferingValues256(loanOffering, values256); + fillLoanOfferingValues32(loanOffering, values32); + loanOffering.signature = signature; + + return loanOffering; + } + + function fillLoanOfferingAddresses( + MarginCommon.LoanOffering memory loanOffering, + address[9] addresses + ) + private + pure + { + loanOffering.owedToken = addresses[0]; + loanOffering.heldToken = addresses[1]; + loanOffering.payer = addresses[2]; + loanOffering.owner = addresses[3]; + loanOffering.taker = addresses[4]; + loanOffering.positionOwner = addresses[5]; + loanOffering.feeRecipient = addresses[6]; + loanOffering.lenderFeeToken = addresses[7]; + loanOffering.takerFeeToken = addresses[8]; + } + + function fillLoanOfferingValues256( + MarginCommon.LoanOffering memory loanOffering, + uint256[7] values256 + ) + private + pure + { + loanOffering.rates.maxAmount = values256[0]; + loanOffering.rates.minAmount = values256[1]; + loanOffering.rates.minHeldToken = values256[2]; + loanOffering.rates.lenderFee = values256[3]; + loanOffering.rates.takerFee = values256[4]; + loanOffering.expirationTimestamp = values256[5]; + loanOffering.salt = values256[6]; + } + + function fillLoanOfferingValues32( + MarginCommon.LoanOffering memory loanOffering, + uint32[4] values32 + ) + private + pure + { + loanOffering.callTimeLimit = values32[0]; + loanOffering.maxDuration = values32[1]; + loanOffering.rates.interestRate = values32[2]; + loanOffering.rates.interestPeriod = values32[3]; + } +} diff --git a/contracts/margin/interfaces/LoanOfferingVerifier.sol b/contracts/margin/interfaces/LoanOfferingVerifier.sol index 17f1dee1..d197014d 100644 --- a/contracts/margin/interfaces/LoanOfferingVerifier.sol +++ b/contracts/margin/interfaces/LoanOfferingVerifier.sol @@ -19,8 +19,6 @@ pragma solidity 0.4.24; pragma experimental "v0.5.0"; -import { MarginCommon } from "../impl/MarginCommon.sol"; - /** * @title LoanOfferingVerifier @@ -34,8 +32,6 @@ import { MarginCommon } from "../impl/MarginCommon.sol"; */ interface LoanOfferingVerifier { - // ============ Margin-Only State-Changing Functions ============ - /** * Function a smart contract must implement to be able to consent to a loan. The loan offering * will be generated off-chain. The "loan owner" address will own the loan-side of the resulting @@ -88,73 +84,4 @@ interface LoanOfferingVerifier { external /* onlyMargin */ returns (address); - - // ============ Parsing Functions ============ - - function parseLoanOffering( - address[9] addresses, - uint256[7] values256, - uint32[4] values32, - bytes signature - ) - internal - pure - returns (MarginCommon.LoanOffering memory) - { - MarginCommon.LoanOffering memory loanOffering; - - fillLoanOfferingAddresses(loanOffering, addresses); - fillLoanOfferingValues256(loanOffering, values256); - fillLoanOfferingValues32(loanOffering, values32); - loanOffering.signature = signature; - - return loanOffering; - } - - function fillLoanOfferingAddresses( - MarginCommon.LoanOffering memory loanOffering, - address[9] addresses - ) - private - pure - { - loanOffering.owedToken = addresses[0]; - loanOffering.heldToken = addresses[1]; - loanOffering.payer = addresses[2]; - loanOffering.owner = addresses[3]; - loanOffering.taker = addresses[4]; - loanOffering.positionOwner = addresses[5]; - loanOffering.feeRecipient = addresses[6]; - loanOffering.lenderFeeToken = addresses[7]; - loanOffering.takerFeeToken = addresses[8]; - } - - function fillLoanOfferingValues256( - MarginCommon.LoanOffering memory loanOffering, - uint256[7] values256 - ) - private - pure - { - loanOffering.rates.maxAmount = values256[0]; - loanOffering.rates.minAmount = values256[1]; - loanOffering.rates.minHeldToken = values256[2]; - loanOffering.rates.lenderFee = values256[3]; - loanOffering.rates.takerFee = values256[4]; - loanOffering.expirationTimestamp = values256[5]; - loanOffering.salt = values256[6]; - } - - function fillLoanOfferingValues32( - MarginCommon.LoanOffering memory loanOffering, - uint32[4] values32 - ) - private - pure - { - loanOffering.callTimeLimit = values32[0]; - loanOffering.maxDuration = values32[1]; - loanOffering.rates.interestRate = values32[2]; - loanOffering.rates.interestPeriod = values32[3]; - } } diff --git a/test/tests/margin/external/bucketlender/TestBucketLender.js b/test/tests/margin/external/bucketlender/TestBucketLender.js index d05c5a83..40eac0f7 100644 --- a/test/tests/margin/external/bucketlender/TestBucketLender.js +++ b/test/tests/margin/external/bucketlender/TestBucketLender.js @@ -351,16 +351,6 @@ contract('BucketLender', accounts => { }); }); - // ============ Complicated case ============ - - describe('Alice Bot GOOOO', () => { - it('runs alice bot several times', async () => { - await runAliceBot(); - await runAliceBot(); - await runAliceBot(); - }); - }); - // ============ Margin-Only State-Changing Functions ============ describe('#verifyLoanOffering', () => { @@ -1017,6 +1007,12 @@ contract('BucketLender', accounts => { }); describe('#withdraw', () => { + it('Multiple deposit and withdraw', async () => { + await runAliceBot(); + await runAliceBot(); + await runAliceBot(); + }); + it('succeeds in withdrawing from bucket 0', async () => { await doWithdraw(lender1, 0); await doWithdraw(lender1, 0, { weight: 0 });