Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Here are some important concepts to understand so you know how the auto-roller w
* `AutoRollerFactory` - on-chain factory for easy deployment of new auto-roller instances.

* `RollerPeriphery` - the slippage protected LP management interface to all AutoRollers.
* `OwnableAdapter` - Sense adapter owned by a unique auto-roller. Through the auto-roller, series rollers can sponsor new series via `AutoRoller.roll` and settle series via `AutoRoller.settle`.
* `OwnableAdapter` - Sense adapter owned by a unique auto-roller. Through the auto-roller, series rollers can sponsor new series via `AutoRoller.roll` and settle series via `AutoRoller.settle`. This adapter must **override** `getMaturityBounds` so only the roller can sponsor preventing a potential DoS attack.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* `OwnableAdapter` - Sense adapter owned by a unique auto-roller. Through the auto-roller, series rollers can sponsor new series via `AutoRoller.roll` and settle series via `AutoRoller.settle`. This adapter must **override** `getMaturityBounds` so only the roller can sponsor preventing a potential DoS attack.
* `OwnableAdapter` - Sense adapter owned by a unique auto-roller. Through the auto-roller, series rollers can sponsor new series via `AutoRoller.roll` and settle series via `AutoRoller.settle`. This adapter must **override** `getMaturityBounds` so only the roller can sponsor, preventing a potential DoS attack.


### Phases
The auto-roller has an **active** and a **cooldown** phase. During the active phase, there is a specific Space pool that the auto-roller is managing, and the liquidity it holds is in the form of Space LP shares and YTs (e.g. sY-wstETH). In contrast, the cooldown phase is in-between active phases when the auto-roller has settled one series but not yet sponsored a new one and deposited liquidity into the new pool (also at the very beginning of the contract’s lifecycle before it has entered the first pool).
Expand Down
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# interfaces are automatically ignored by forge coverage
ignore:
- "**/**.t*.sol" # ignore test files (ending in t*.sol)
- "**/**.s.sol" # ignore script files (ending in s.sol)
10 changes: 5 additions & 5 deletions src/AutoRoller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ contract AutoRoller is ERC4626 {

DividerLike internal immutable divider;
BalancerVault internal immutable balancerVault;
OwnedAdapterLike internal immutable adapter;
OwnedAdapterLike internal immutable adapter; // This adapter must **override** `getMaturityBounds` so only the roller can sponsor preventing a potential DoS attack.

uint256 internal immutable ifee; // Cached issuance fee.
uint256 internal immutable minSwapAmount; // Min number of PTs that can be swapped out when exiting.
Expand Down Expand Up @@ -200,9 +200,9 @@ contract AutoRoller is ERC4626 {
uint256 targetBal = _asset.balanceOf(address(this));

// Get the reserve balances that would imply the given targetedRate in the Space pool,
// assuming that we we're going to deposit the amount of Target currently in this contract.
// assuming that we're going to deposit the amount of Target currently in this contract.
// In other words, this function simulating the reserve balances that would result from the actions:
// 1) Use the some Target to issue PTs/YTs
// 1) Use some Target to issue PTs/YTs
// 2) Deposit some amount of Target
// 3) Swap PTs into the pool to initialize the targeted rate
// 4) Deposit the rest of the PTs and Target in this contract (which remain in the exact ratio the pool expects)
Expand Down Expand Up @@ -272,7 +272,7 @@ contract AutoRoller is ERC4626 {
}

/// @notice Settle the active Series, transfer stake and ifees to the settler, and enter a cooldown phase.
/// @dev Because the auto-roller is the series sponsor from the Divider's perspective, this.settle is the only entrypoint for athe lastRoller to settle during the series' sponsor window.
/// @dev Because the auto-roller is the series sponsor from the Divider's perspective, this.settle is the only entrypoint for the lastRoller to settle during the series' sponsor window.
/// More info on the series lifecylce: https://docs.sense.finance/docs/series-lifecycle-detail/#phase-3-settling.
function settle() public {
if(msg.sender != lastRoller) revert InvalidSettler();
Expand Down Expand Up @@ -369,7 +369,7 @@ contract AutoRoller is ERC4626 {

balances[1 - _pti] = targetToJoin;

if (assets - targetToJoin > 0) { // Assumption: this is false if Space has only Target liquidity.
if (assets - targetToJoin > 0) { // Assumption: this is false if Space has only Target liquidity. TODO: can this ever happen?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps not if we don't allow the pool to be initialized at 0%!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But is this something we should keep just in case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think so, not much harm in being safe here besides bloat, but i'll think about it as I continue audit resolutions

balances[_pti] = divider.issue(address(adapter), maturity, assets - targetToJoin);
}

Expand Down
227 changes: 226 additions & 1 deletion src/test/AutoRoller.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,42 @@ contract AutoRollerTest is DSTestPlus {

// Auth

function testUpdateAdminParams() public {
vm.record();

// 1. Impersonate owner and try to update admin params
vm.startPrank(address(this));

autoRoller.setParam("SPACE_FACTORY", address(0xbabe));

autoRoller.setParam("PERIPHERY", address(0xbabe));

vm.expectRevert();
autoRoller.setParam("UNKNOWN", address(0xbabe));

autoRoller.setParam("OWNER", address(0xbabe));

vm.stopPrank();

// 2. Impersonate new owner try to update rest of admin params
vm.startPrank(address(0xbabe));

autoRoller.setParam("MAX_RATE", 1337);

autoRoller.setParam("TARGET_DURATION", 1337);

autoRoller.setParam("COOLDOWN", 1337);

vm.expectRevert();
autoRoller.setParam("UNKNOWN", 1337);

vm.stopPrank();

(, bytes32[] memory writes) = vm.accesses(address(autoRoller));
// Check that no storage slots were written to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Check that no storage slots were written to
// Check that the expected number of storage slots were written to

assertEq(writes.length, 6);
}

function testFuzzUpdateAdminParams(address lad) public {
vm.record();
vm.assume(lad != address(this)); // For any address other than the testing contract
Expand All @@ -167,6 +203,9 @@ contract AutoRollerTest is DSTestPlus {
vm.expectRevert();
autoRoller.setParam("OWNER", address(0xbabe));

vm.expectRevert();
autoRoller.setParam("UNKNOWN", address(0xbabe));

vm.expectRevert();
autoRoller.setParam("MAX_RATE", 1337);

Expand All @@ -176,6 +215,9 @@ contract AutoRollerTest is DSTestPlus {
vm.expectRevert();
autoRoller.setParam("COOLDOWN", 1337);

vm.expectRevert();
autoRoller.setParam("UNKNOWN", 1337);

(, bytes32[] memory writes) = vm.accesses(address(autoRoller));
// Check that no storage slots were written to
assertEq(writes.length, 0);
Expand Down Expand Up @@ -936,12 +978,195 @@ contract AutoRollerTest is DSTestPlus {
utils.getNewTargetedRate(0, address(mockAdapter), maturity, space);

vm.warp(maturity);

// Scale goes down
mockAdapter.setScale(0.9e18);
autoRoller.settle();

// Targeted rate is 0 if scale has gone down.
mockAdapter.setScale(0.9e18);
uint256 targetedRate = utils.getNewTargetedRate(0, address(mockAdapter), maturity, space);
assertEq(targetedRate, 0);

vm.warp(maturity + autoRoller.cooldown());

// Roll into new series
autoRoller.roll();

// Scale goes up
mockAdapter.setScale(1.9e18);

maturity = autoRoller.maturity();
vm.warp(maturity);

autoRoller.settle();

// Targeted rate is not 0 TODO: maybe assert against the rate that should be
targetedRate = utils.getNewTargetedRate(0, address(mockAdapter), maturity, space);
assertGt(targetedRate, 0);
}

// OTHER TESTS (LINES NOT COVERED ACCORDING CODECOV) TODO: re-order these tests?

function testAfterDepositWithOnlyTargetLiquidity() public {
// The idea is making a test that covers line case on the `afterDeposit` function
// where `if (assets - targetToJoin > 0)` is false.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is true, because even if we have someone try to buy all of the PTs out of the pool, I don't think it's possible to get it down to true 0 after initialization

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

perfect, I'll remove all this

// I think can never happen since `onSponsorWindowOpened`, when calling `_space.getEQReserves`
// we are doing `targetedRate < 0.01e18 ? 0.01e18 : targetedRate` which means that we are always
// getting a `eqPTReserves` > 0 so we will be issuing PTs.
}

function testPreviewWithdrawInsufficentLiquidity(uint256 amount) public {
// 1. Roll into the first Series.
autoRoller.roll();

// 2. Make a deposit.
autoRoller.deposit(1e18, address(this));

// 3. Expect revert because of InsufficientLiquidity.
vm.expectRevert(abi.encodeWithSelector(AutoRoller.InsufficientLiquidity.selector));
autoRoller.previewWithdraw(100e18);
}

function testMaxWithdrawDuringCooldown(uint256 amount) public {
// 1. Make a deposit durring cooldown.
autoRoller.deposit(1e18, address(this));

// 2. Assert max withdraw is same as deposit.
uint256 maxWithdraw = autoRoller.maxWithdraw(address(this));
assertEq(maxWithdraw, 1e18);
}

function testRedeemWithYTsExcess() public {
// 1. Deposit during the initial cooldown phase.
autoRoller.deposit(0.2e18, address(this));
assertEq(autoRoller.balanceOf(address(this)), 0.2e18);

// 3. Roll the Target into the first Series.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 3. Roll the Target into the first Series.
// 2. Roll the Target into the first Series.

autoRoller.roll();

// 4. Deposit during the first active phase.
autoRoller.deposit(0.3e18, address(this));
assertRelApproxEq(autoRoller.balanceOf(address(this)), 0.5e18, 0.0001e18 /* 0.01% */);

// 5. Redeem all shares while still in the active phase.
uint256 targetBalPre = target.balanceOf(address(this));
vm.expectCall(address(periphery), abi.encodeWithSelector(periphery.swapYTsForTarget.selector));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice check here 👌

autoRoller.redeem(autoRoller.balanceOf(address(this)), address(this), address(this));
uint256 targetBalPost = target.balanceOf(address(this));
// assertRelApproxEq(targetBalPost - targetBalPre, 0.5e18, 0.0001e18 /* 0.01% */); // TODO: what should it be the expected value?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should expect it to be pretty close to "total deposited amount - firstDeposit" (since firstDeposit is kept locked in the contract forever, similar to the min bpt for our space pool). What were you seeing here for this val?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm yeah, i was seeing something similar (I forgot to consider the firstDeposit) but now, considering the firstDeposit:

  • I have deposited in total 0.5e18 and I'm asserting that 0.5e18 - 0.01e18 (total deposited - firstDeposit) should almost equal to the target I received back which is 499642339889372330, shouldn't that be lower? Or how do I establish which is a good delta in this case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ahh yea, after you roll the math starts to get a little more complicated I think. You can always do a previewRedeem here to see what it should be out, or you can walk through the trace to see what it's doing. I might do the latter today or tomorrow just out of curiosity


// Check that the Target dust leftover is small
assertLt(target.balanceOf(address(autoRoller)), 1e9);
}

function testMaxRedeemDuringCooldown(uint256 amount) public {
// 1. Make a deposit during cooldown.
uint256 sharesOut = autoRoller.deposit(0.1e18, address(this));

// 2. Assert max withdraw is same sharesOut.
assertEq(autoRoller.maxRedeem(address(this)), sharesOut);

// 4. Roll into the first Series.
autoRoller.roll();

// 3. Make another deposit during cooldown.
sharesOut += autoRoller.deposit(0.1e18, address(this));

vm.warp(autoRoller.maturity());

// 4. Increase scale and settle the first Series.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did we want to manually increase scale somewhere here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no no, it's not needed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not? 😅

autoRoller.settle();

// 5. Assert max withdraw is same total shares.
assertEq(autoRoller.maxRedeem(address(this)), sharesOut);
}

function testMaxRedeemWithPTsExcess(uint256 amount) public {
// 1. Make a deposit durring cooldown.
uint256 sharesOut = autoRoller.deposit(1e18, address(this));

// 2. Roll into the first Series.
autoRoller.roll();

// 3. Swap PTs in to generate excess of PTs.
target.mint(address(this), 1e18);
target.approve(address(divider), 1e18);
divider.issue(address(mockAdapter), autoRoller.maturity(), 1e18);
ERC20 pt = ERC20(divider.pt(address(mockAdapter), autoRoller.maturity()));
Space space = Space(spaceFactory.pools(address(mockAdapter), autoRoller.maturity()));
pt.approve(address(balancerVault), 1e18);

// 4. Swap PTs in to generate excess of PTs and that the number of PTs we can sell
// is less than the diff between YTs and PTs.
_swap(
BalancerVault.SingleSwap({
poolId: space.getPoolId(),
kind: BalancerVault.SwapKind.GIVEN_IN,
assetIn: address(pt),
assetOut: address(autoRoller.asset()),
amount: 0.001e18, // TODO: how do I calculate a number such that it would make the maxPTSale < diff? This number works though
userData: hex""
})
);

// 5. Assert max redeem is same as sharesOut.
assertEq(autoRoller.maxRedeem(address(this)), sharesOut);

// 6. Swap PTs in to generate excess of PTs and that the number of PTs we can sell
// is more than the diff between YTs and PTs.
_swap(
BalancerVault.SingleSwap({
poolId: space.getPoolId(),
kind: BalancerVault.SwapKind.GIVEN_IN,
assetIn: address(pt),
assetOut: address(autoRoller.asset()),
amount: 0.01e18, // TODO: how do I calculate a number such that it would make the maxPTSale >= diff? This number works though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, the only case where this isn't true IIRC is if order we'd have to swap here such that the pool becomes very close to the maxRate value (time shifted) so that the user can't safely make the PT in swap needed to withdraw their position without exceeding the max rate

Note that you can check the rate "(pt reserves + total supply / (target reserve * scale))"

userData: hex""
})
);

// 6. Assert max redeem is same as TODO.
sharesOut = autoRoller.maxRedeem(address(this));
// assertEq(sharesOut, TODO);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

think we could do assertEq(autoRoller.maxRedeem(address(this)), sharesOut); again here perhaps?

@fedealconada fedealconada Nov 11, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh, it works :), i would have sworn i tried that and was not working 🤔 and that;s why i was confused


// 7. TODO: add missing scenario: `if (ptReserves >= diff)`.
}

function testEjectWithPTsExcess() public {
// 1. Deposit during the initial cooldown phase.
autoRoller.deposit(1e18, address(this));

// 2. Roll into the first Series.
autoRoller.roll();

// 3. Swap PTs in to generate excess of PTs.
target.mint(address(this), 1e18);
target.approve(address(divider), 1e18);
divider.issue(address(mockAdapter), autoRoller.maturity(), 1e18);

ERC20 pt = ERC20(divider.pt(address(mockAdapter), autoRoller.maturity()));
Space space = Space(spaceFactory.pools(address(mockAdapter), autoRoller.maturity()));

pt.approve(address(balancerVault), 1e18);
_swap(
BalancerVault.SingleSwap({
poolId: space.getPoolId(),
kind: BalancerVault.SwapKind.GIVEN_IN,
assetIn: address(pt),
assetOut: address(autoRoller.asset()),
amount: 0.001e18,
userData: hex""
})
);

// 4. Eject everything.
uint256 ptBalBefore = pt.balanceOf(address(this));
( , uint256 excessBal, bool isExcessPTs) = autoRoller.eject(autoRoller.balanceOf(address(this)), address(this), address(this));

// Expect just a little PT excess.
assertBoolEq(isExcessPTs, true);
assertLt(excessBal, 1e16);
assertEq(excessBal, pt.balanceOf(address(this)) - ptBalBefore);
}

// function testRedeemPreviewReversion() public {
Expand Down