Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

GEN-1630: increase test coverage #12

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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.

### 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)
8 changes: 4 additions & 4 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
223 changes: 220 additions & 3 deletions src/test/AutoRoller.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ contract AutoRollerTest is DSTestPlus {
vm.stopPrank();

// Mint Target
target.mint(address(this), 2e18);
target.approve(address(autoRoller), 2e18);
target.mint(address(this), 10e18);
target.approve(address(autoRoller), 10e18);

// Mint Stake
stake.mint(address(this), 1e18);
Expand All @@ -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 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,187 @@ 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 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(2e18, address(this));
assertEq(autoRoller.balanceOf(address(this)), 2e18);

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

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

// 4. 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
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));
uint256 scalingFactor = 10**(18 - autoRoller.decimals());
uint256 firstDeposit = (0.01e18 - 1) / scalingFactor + 1;
// assertRelApproxEq(targetBalPost - targetBalPre, 5e18 - firstDeposit, 0.0001e18 /* 0.01% */); // TODO: what should it be the expected value?

// 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);

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

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

vm.warp(autoRoller.maturity());
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. Issue 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()));

// 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.
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, // 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 more 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
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""
})
);

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

// 6. 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