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

[Under Audit] Add Bucket-Lender #357

Merged
merged 22 commits into from
Aug 8, 2018
Merged

[Under Audit] Add Bucket-Lender #357

merged 22 commits into from
Aug 8, 2018

Conversation

BrendanChou
Copy link

@BrendanChou BrendanChou commented Jun 7, 2018

fixes #231

@BrendanChou BrendanChou force-pushed the bc_bucket2 branch 8 times, most recently from 8d3b67c to bf5322d Compare June 7, 2018 01:04
@coveralls
Copy link

coveralls commented Jun 7, 2018

Pull Request Test Coverage Report for Build 6110

  • 257 of 257 (100.0%) changed or added relevant lines in 7 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 100.0%

Totals Coverage Status
Change from base Build 6092: 0.0%
Covered Lines: 1186
Relevant Lines: 1186

💛 - Coveralls

@BrendanChou BrendanChou force-pushed the bc_bucket2 branch 7 times, most recently from 9779dd6 to 6792c1c Compare June 9, 2018 00:26
// Available token to lend
PerBucket public available;

function available2(uint256 bucket) external view returns(uint256) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after returns returns(uint256) -> returns (uint256)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait actually what are these functions even lol?


// ============ Structs ============

struct PerBucket {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very unclear what these are from the naming

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both the struct and variable names

}

// Current allocated principal for each bucket
PerBucket public principal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

principal is a really confusing name for this. I'd expect that to just be a number

}

// Bucket accounting for which accounts have deposited into that bucket
mapping(uint256 => PerAccount) public weight;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weight? I'm also pretty confused what this is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe bucketWeights? Or just bucketAmounts? Also the comment saying "Bucket accounting for..." makes it sound like this is itself a bucket, which I believe it is not...? This is like how much each account has deposited into each bucket?

// ============ Constants ============

// Address of the token being lent
address public OWED_TOKEN;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All caps is not the style we used for other constants like this. See SharedLoan.sol:

// Address of the position's owedToken. Cached for convenience and lower-cost withdrawals
    address public owedToken;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of indifferent on which is better, but it should all be the same, and I'm more inclined to just going with what we had before and not changing it


// ============ Structs ============

struct LoanOffering {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different than the LoanOffering in MarginCommon which is confusing. It's nice this doesn't use the dumb rates thing, but we should make them both the same


// find highest bucket with outstanding principal
uint256 bucket = getBucketNumber();
while (principalForBkt[bucket] == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you just store a pointer to this rather than finding it each time? That seems more gas efficient (assuming bucket levels are crossed infrequently)

}

// (available up / principal down) starting at the highest bucket
uint256 p_total = principalRemoved;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't this naming convention anywhere else, so it's kinda weird here

* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't exactly right because you can touch multiple buckets

while (p_total > 0) {
uint256 p_i = Math.min256(p_total, principalForBkt[bucket]);
if (p_i == 0) {
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, if p_i is 0 won't this infinite loop? did you mean to break instead?

@BrendanChou BrendanChou force-pushed the bc_bucket2 branch 9 times, most recently from 07c2632 to 6c5efee Compare June 14, 2018 00:21
Copy link
Member

@AntonioJuliano AntonioJuliano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got through like half of this

* - 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't they just straight up not exist?

*
* - Token Withdrawals:
* - Can be from any bucket with available amount
* - Decrease the bucket's AA (if it has enough, otherwise throw)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the stuff in parenthesis seems to be satisfied by your first point

* 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's confusing that the name of the variable doesn't match either of the things in the comment (Outstanding Principal / OP)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise for the available stuff too


/**
* The critical bucket is:
* - Greater-than-or-equal-to The highest bucket with OP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've decided that I find the AA/OP abbreviations to be confusing haha. it's not too much more to just say outstanding principal / available amount, or the variable versions outstandingPrincipal/availableAmount

It's not a huge deal though

signature
);

// CHECK POSITIONID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: POSITION_ID

// CHECK ADDRESSES
require(loanOffering.owedToken == OWED_TOKEN);
require(loanOffering.heldToken == HELD_TOKEN);
require(loanOffering.payer == address(this));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be an assert too right?

* 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't unused

);

// CHECK POSITIONID
require(positionId == POSITION_ID);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use your onlyPosition modifier

* @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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not unused (just check all these comments / not sure if you've updated them all yet)

"BucketLender#marginCallOnBehalfOf: Margin-caller must be trusted"
);
require(
depositAmount == 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put in the comment of Margin#marginCall that making depositAmount 0 makes it so the position owner can't cancel it by depositing. It took some digging for me to confirm that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

@AntonioJuliano AntonioJuliano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more comments, still not done


uint256 marginPrincipal = getCurrentPrincipalFromMargin();

accountForClose(principalTotal.sub(marginPrincipal));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So principalTotal will always be >= marginPrincipal? I think that's right, because we would know if any increases happened

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct yeah

* Helper function to make sure allowance is set on the dYdX proxy. Callable by anyone.
*/
function setMaximumAllowanceOnProxy()
public
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this public?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do something like how we do ensure allowance in the 0x exchange wrapper?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but that also requires an external call and a state read every time. If we really are trying to enforce the same loanhash every time, then we shouldn't need to do this more than once (in the constructor) because the loanFills amount in the base protocol would overflow at the exact same time.

I really don't feel like it's a problem to set it to max int once. You would need a token with 18 decimal places and then a supply of like 10^80. I realize that the same tokens can be lent multiple times, but honestly I think you would run out of ether to lend that many tokens of anything.

Consider a gas cost of 1 gwei and that lending costs at least 1000 gas (a huge underestimate). Then a transaction costs 10^-6 eth. Since there is 10^8 eth in existence, then you could make 10^14 loans if you had all the eth in existence. Since 2**256 is around 10^77, then there would need to be at least 10^60 supply of a coin which is unheard of.

Copy link
Member

@AntonioJuliano AntonioJuliano Jun 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think it'd be more likely with a token with like 64 decimal places or something. The convention of 18 decimal places is just a convention. I know most tokens follow this, but could be a confusing edge case if it starts throwing errors for specific weird tokens

Also, does anything actually enforce the same loan hash each time?

// loop over buckets in reverse order starting with the critical bucket
for (
uint256 bucket = criticalBucket;
principalToSub > 0 && bucket <= criticalBucket;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would bucket ever be greater than criticalBucket? Aren't we always just subtracting from bucket?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below. This is the underflow protection

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, negative for-loops with unsigned ints are very annoying. Safemath could be used, but would need to add an if-statement at the bottom of the for loop

if (bucket == 0) {
    break;
}

for (
uint256 bucket = criticalBucket;
principalToSub > 0 && bucket <= criticalBucket;
bucket--
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should assert that bucket doesn't underflow, so maybe use safemath. I know it shouldn't because we should have always accounted for more principal than is paid back, but it's a good assert to have

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bucket doesn't underflow because of the bucket <= criticalBucket part of it

if (principalTemp == 0) {
continue;
}
uint256 availableTemp = MathHelpers.getPartialAmount(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it's possible we get rounding error on this right? I guess the last time we add available amount it will use the rest of it, so if anything this would slightly skew the payout towards earlier buckets

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct

event BucketLenderCreated(
address indexed creator,
address at,
bytes32 positionId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should index the positionId this will likely be a thing people search by

* @param owedToken Address of the token being lent by the BucketLender
* @param parameters Values corresponding to:
*
* [0] = number of seconds per bucket
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: to be consistent with Margin align these with 2 spaces after the *

// ============ Events ============

event Deposit(
address beneficiary,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make indexed

);

event Withdraw(
address withdrawer,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexed

ReentrancyGuard
{
using SafeMath for uint256;
using TokenInteract for address;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is cool, do we want to do this for other contracts too?

*/
// Available Amount for each bucket
mapping(uint256 => uint256) public availableForBucket;
// Total Available Amount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: newline before this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for others

* These tokens are also available to be withdrawn by the accounts that have weight in the
* bucket.
*/
// Available Amount for each bucket
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in the multiline comment above, it's weird they're separate. i.e.:

/**
 * Available Amount for 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.
 */

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for others

}

// Set maximum allowance on proxy
OWED_TOKEN.approve(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forget if I've asked this: what happens if we eat through all this allowance? Do we refresh it anywhere?


// CHECK VALUES32
require(
loanOffering.callTimeLimit == CALL_TIMELIMIT,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change these to maximum value as discussed

"BucketLender#verifyLoanOffering: loanOffering.callTimelimit is incorrect"
);
require(
loanOffering.maxDuration == MAX_DURATION,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to maximum value as discussed

loanOffering.maxDuration == MAX_DURATION,
"BucketLender#verifyLoanOffering: loanOffering.maxDuration is incorrect"
);
assert(loanOffering.rates.interestRate == INTEREST_RATE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this up top with the other asserts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also just want to confirm this is set by the base protocol on increase - I believe it is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


assert(initialPrincipal > 0);
assert(principalTotal == 0);
assert(from != address(this)); // position must be opened without lending from this position
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just want to confirm this should be assert not require

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes due to the first line in verifyLoanOffering()

AntonioJuliano
AntonioJuliano previously approved these changes Aug 7, 2018
loanOffering.maxDuration == MathHelpers.maxUint32(),
"BucketLender#verifyLoanOffering: loanOffering.maxDuration is incorrect"
);
assert(loanOffering.rates.interestRate == INTEREST_RATE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move these up with the other asserts

@BrendanChou BrendanChou merged commit db1b6c7 into master Aug 8, 2018
@BrendanChou BrendanChou deleted the bc_bucket2 branch August 8, 2018 22:41
@@ -107,6 +107,20 @@ library MathHelpers {
return 2 ** 256 - 1;
}

/**
* Calculates and returns the maximum value for a uint256 in solidity

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit -- Comment needs updating to uint32.

/**
* Calculates and returns the maximum value for a uint256 in solidity
*
* @return The maximum value for uint256

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit -- Comment needs updating to uint32.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Create an On-Chain Shared Lender
4 participants