diff --git a/src/CompoundJoin.sol b/src/CompoundJoin.sol
new file mode 100644
index 0000000..8a5ae4c
--- /dev/null
+++ b/src/CompoundJoin.sol
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2021 Dai Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity 0.6.12;
+
+import "./CropJoin.sol";
+
+interface CTokenLike is ERC20 {
+ function admin() external returns (address);
+ function pendingAdmin() external returns (address);
+ function comptroller() external returns (address);
+ function interestRateModel() external returns (address);
+ function initialExchangeRateMantissa() external returns (uint256);
+ function reserveFactorMantissa() external returns (uint256);
+ function accrualBlockNumber() external returns (uint256);
+ function borrowIndex() external returns (uint256);
+ function totalBorrows() external returns (uint256);
+ function totalReserves() external returns (uint256);
+ function totalSupply() external returns (uint256);
+ function accountTokens(address) external returns (uint256);
+ function transferAllowances(address,address) external returns (uint256);
+
+ function balanceOfUnderlying(address owner) external returns (uint256);
+ function underlying() external view returns (address);
+ function getAccountSnapshot(address account) external view returns (uint256, uint256, uint256, uint256);
+ function borrowRatePerBlock() external view returns (uint256);
+ function supplyRatePerBlock() external view returns (uint256);
+ function totalBorrowsCurrent() external returns (uint256);
+ function borrowBalanceCurrent(address account) external returns (uint256);
+ function borrowBalanceStored(address account) external view returns (uint256);
+ function exchangeRateCurrent() external returns (uint256);
+ function exchangeRateStored() external view returns (uint256);
+ function getCash() external view returns (uint256);
+ function accrueInterest() external returns (uint256);
+ function seize(address liquidator, address borrower, uint256 seizeTokens) external returns (uint256);
+
+ function mint(uint256 mintAmount) external returns (uint256);
+ function redeem(uint256 redeemTokens) external returns (uint256);
+ function redeemUnderlying(uint256 redeemAmount) external returns (uint256);
+ function borrow(uint256 borrowAmount) external returns (uint256);
+ function repayBorrow(uint256 repayAmount) external returns (uint256);
+ function repayBorrowBehalf(address borrower, uint256 repayAmount) external returns (uint256);
+ function liquidateBorrow(address borrower, uint256 repayAmount, CTokenLike cTokenCollateral) external returns (uint256);
+}
+
+interface ComptrollerLike {
+ function accountAssets(address, uint256) external view returns (address);
+ function enterMarkets(address[] calldata cTokens) external returns (uint256[] memory);
+ function claimComp(address[] calldata holders, address[] calldata cTokens, bool borrowers, bool suppliers) external;
+ function compAccrued(address) external returns (uint256);
+ function compBorrowerIndex(address,address) external returns (uint256);
+ function compSupplierIndex(address,address) external returns (uint256);
+ function seizeAllowed(
+ address cTokenCollateral,
+ address cTokenBorrowed,
+ address liquidator,
+ address borrower,
+ uint256 seizeTokens) external returns (uint256);
+ function getAccountLiquidity(address) external returns (uint256,uint256,uint256);
+ function markets(address) external returns (bool,uint256,bool);
+}
+
+contract CompoundJoinImp is CropJoinImp {
+ CTokenLike immutable public cgem;
+ ComptrollerLike immutable public comptroller;
+ uint256 public minf = 0; // minimum target collateral factor [wad]
+ uint256 public maxf = 0; // maximum target collateral factor [wad]
+ uint256 public dust = 0; // value (in gems) below which to stop looping
+
+ // --- Events ---
+ event File(bytes32 indexed what, address data);
+ event File(bytes32 indexed what, uint256 data);
+
+ /**
+ @param vat_ MCD_VAT DSS core accounting module
+ @param ilk_ Collateral type
+ @param gem_ The collateral LP token address
+ @param comp_ The COMP token contract address.
+ @param cgem_ The cToken which the underlying token is the gem.
+ @param comptroller_ The Compound Comptroller address.
+ */
+ constructor(
+ address vat_,
+ bytes32 ilk_,
+ address gem_,
+ address comp_,
+ address cgem_,
+ address comptroller_
+ )
+ public
+ CropJoinImp(vat_, ilk_, gem_, comp_)
+ {
+ // Sanity checks
+ require(CTokenLike(cgem_).comptroller() == comptroller_, "CompoundJoin/comptroller-mismatch");
+ require(CTokenLike(cgem_).underlying() == gem_, "CompoundJoin/underlying-mismatch");
+
+ cgem = CTokenLike(cgem_);
+ comptroller = ComptrollerLike(comptroller_);
+ }
+
+ function init() external {
+ ERC20(gem).approve(address(cgem), type(uint256).max);
+
+ address[] memory ctokens = new address[](1);
+ ctokens[0] = address(cgem);
+ uint256[] memory errors = new uint256[](1);
+ errors = comptroller.enterMarkets(ctokens);
+ require(errors[0] == 0);
+ }
+
+ // --- Math ---
+ function zsub(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ return sub(x, min(x, y));
+ }
+ function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
+ return x <= y ? x : y;
+ }
+
+ // --- Administration ---
+ function file(bytes32 what, uint256 data) external auth {
+ if (what == "minf") require((minf = data) <= WAD, "CompoundJoin/bad-value");
+ else if (what == "maxf") require((maxf = data) <= WAD, "CompoundJoin/bad-value");
+ else if (what == "dust") dust = data;
+ else revert("CompoundJoin/file-unrecognized-param");
+ emit File(what, data);
+ }
+
+ function nav() public override returns (uint256) {
+ return mul(
+ add(
+ gem.balanceOf(address(this)),
+ sub(
+ cgem.balanceOfUnderlying(address(this)),
+ cgem.borrowBalanceCurrent(address(this))
+ )
+ ),
+ to18ConversionFactor
+ );
+ }
+
+ function crop() internal override returns (uint256) {
+ address[] memory ctokens = new address[](1);
+ address[] memory users = new address[](1);
+ ctokens[0] = address(cgem);
+ users [0] = address(this);
+
+ comptroller.claimComp(users, ctokens, true, true);
+
+ return super.crop();
+ }
+
+ function join(address urn, address usr, uint256 val) public override {
+ super.join(urn, usr, val);
+ require(cgem.mint(val) == 0);
+ }
+
+ function exit(address urn, address usr, uint256 val) public override {
+ require(cgem.redeemUnderlying(val) == 0);
+ super.exit(urn, usr, val);
+ }
+
+ function flee(address urn, address usr) public override {
+ uint256 wad = vat.gem(ilk, urn);
+ uint256 val = wmul(wmul(wad, nps()), toGemConversionFactor);
+ require(cgem.redeemUnderlying(val) == 0);
+ super.flee(urn, usr);
+ }
+
+ // --- Recursive Leverage Controls (Used by Keeper) ---
+
+ // borrow_: how much underlying to borrow (dec decimals)
+ // loops_: how many times to repeat a max borrow loop before the
+ // specified borrow/mint
+ // loan_: how much underlying to lend to the contract for this
+ // transaction
+ function wind(uint256 borrow_, uint256 loops_, uint256 loan_) external {
+ require(cgem.accrueInterest() == 0);
+ if (loan_ > 0) {
+ require(gem.transferFrom(msg.sender, address(this), loan_));
+ }
+ uint256 gems = gem.balanceOf(address(this));
+ if (gems > 0) {
+ require(cgem.mint(gems) == 0);
+ }
+ (,uint256 cf,) = comptroller.markets(address(cgem));
+
+ for (uint256 i=0; i < loops_; i++) {
+ uint256 s = cgem.balanceOfUnderlying(address(this));
+ uint256 b = cgem.borrowBalanceStored(address(this));
+ // math overflow if
+ // - b / (s + L) > cf [insufficient loan to unwind]
+ // - minf > 1e18 [bad configuration]
+ // - minf < u [can't wind over minf]
+ uint256 x1 = sub(wmul(s, cf), b);
+ uint256 x2 = wdiv(sub(wmul(sub(s, loan_), minf), b),
+ sub(1e18, minf));
+ uint256 max_borrow = min(x1, x2);
+ if (max_borrow < dust) break;
+ require(cgem.borrow(max_borrow) == 0);
+ require(cgem.mint(max_borrow) == 0);
+ }
+ if (borrow_ > 0) {
+ require(cgem.borrow(borrow_) == 0);
+ require(cgem.mint(borrow_) == 0);
+ }
+ if (loan_ > 0) {
+ require(cgem.redeemUnderlying(loan_) == 0);
+ require(gem.transfer(msg.sender, loan_));
+ }
+
+ uint256 u = wdiv(cgem.borrowBalanceStored(address(this)),
+ cgem.balanceOfUnderlying(address(this)));
+ require(u < maxf, "bad-wind");
+ }
+ // repay_: how much underlying to repay (dec decimals)
+ // loops_: how many times to repeat a max repay loop before the
+ // specified redeem/repay
+ // exit_: how much underlying to remove following unwind
+ // loan_: how much underlying to lend to the contract for this
+ // transaction
+ function unwind(uint256 repay_, uint256 loops_, uint256 exit_, uint256 loan_) external {
+ require(cgem.accrueInterest() == 0);
+ uint256 u = wdiv(cgem.borrowBalanceStored(address(this)),
+ cgem.balanceOfUnderlying(address(this)));
+ if (loan_ > 0) {
+ require(gem.transferFrom(msg.sender, address(this), loan_));
+ }
+ require(cgem.mint(gem.balanceOf(address(this))) == 0, "failed-mint");
+ (,uint256 cf,) = comptroller.markets(address(cgem));
+
+ for (uint256 i=0; i < loops_; i++) {
+ uint256 s = cgem.balanceOfUnderlying(address(this));
+ uint256 b = cgem.borrowBalanceStored(address(this));
+ // math overflow if
+ // - [insufficient loan to unwind]
+ // - [insufficient loan for exit]
+ // - [bad configuration]
+ uint256 x1 = wdiv(sub(wmul(s, cf), b), cf);
+ uint256 x2 = wdiv(zsub(add(b, wmul(exit_, maxf)),
+ wmul(sub(s, loan_), maxf)),
+ sub(1e18, maxf));
+ uint256 max_repay = min(x1, x2);
+ if (max_repay < dust) break;
+ require(cgem.redeemUnderlying(max_repay) == 0, "failed-redeem");
+ require(cgem.repayBorrow(max_repay) == 0, "failed-repay");
+ }
+ if (repay_ > 0) {
+ require(cgem.redeemUnderlying(repay_) == 0, "failed-redeem");
+ require(cgem.repayBorrow(repay_) == 0, "failed-repay");
+ }
+ if (exit_ > 0 || loan_ > 0) {
+ require(cgem.redeemUnderlying(add(exit_, loan_)) == 0, "failed-redeem");
+ }
+ if (loan_ > 0) {
+ require(gem.transfer(msg.sender, loan_), "failed-transfer");
+ }
+ //if (exit_ > 0) {
+ // exit(exit_);
+ //}
+
+ uint256 nb = cgem.balanceOfUnderlying(address(this));
+ uint256 u_ = nb > 0 ? wdiv(cgem.borrowBalanceStored(address(this)), nb) : 0;
+ bool ramping = u < minf && u_ > u && u_ < maxf;
+ bool damping = u > maxf && u_ < u && u_ > minf;
+ bool tamping = u_ >= minf && u_ <= maxf;
+ require(ramping || damping || tamping, "bad-unwind");
+ }
+}
diff --git a/src/CropJoin.sol b/src/CropJoin.sol
index af4abac..c3c7c13 100644
--- a/src/CropJoin.sol
+++ b/src/CropJoin.sol
@@ -130,47 +130,47 @@ contract CropJoinImp {
bonus = ERC20(bonus_);
}
- function add(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x, "ds-math-add-overflow");
}
- function sub(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x - y) <= x, "ds-math-sub-underflow");
}
- function mul(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow");
}
function divup(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(x, sub(y, 1)) / y;
}
uint256 constant WAD = 10 ** 18;
- function wmul(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, y) / WAD;
}
- function wdiv(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, WAD) / y;
}
- function wdivup(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function wdivup(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = divup(mul(x, WAD), y);
}
uint256 constant RAY = 10 ** 27;
- function rmul(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, y) / RAY;
}
- function rmulup(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function rmulup(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = divup(mul(x, y), RAY);
}
- function rdiv(uint256 x, uint256 y) public pure returns (uint256 z) {
+ function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = mul(x, RAY) / y;
}
// Net Asset Valuation [wad]
- function nav() public virtual view returns (uint256) {
+ function nav() public virtual returns (uint256) {
uint256 _nav = gem.balanceOf(address(this));
return mul(_nav, to18ConversionFactor);
}
// Net Assets per Share [wad]
- function nps() public view returns (uint256) {
+ function nps() public returns (uint256) {
if (total == 0) return WAD;
else return wdiv(nav(), total);
}
diff --git a/src/SushiJoinV1.sol b/src/SushiJoinV1.sol
index bac8750..2b4ff75 100644
--- a/src/SushiJoinV1.sol
+++ b/src/SushiJoinV1.sol
@@ -94,7 +94,7 @@ contract SushiJoinImp is CropJoinImp {
}
// Ignore gems that have been directly transferred
- function nav() public override view returns (uint256) {
+ function nav() public override returns (uint256) {
return total;
}
diff --git a/src/SushiJoinV2.sol b/src/SushiJoinV2.sol
index 9a4ac35..37a2ce5 100644
--- a/src/SushiJoinV2.sol
+++ b/src/SushiJoinV2.sol
@@ -104,7 +104,7 @@ contract SushiJoinImp is CropJoinImp {
}
// Ignore gems that have been directly transferred
- function nav() public override view returns (uint256) {
+ function nav() public override returns (uint256) {
return total;
}
diff --git a/src/test/CompoundJoin-integration.t.sol b/src/test/CompoundJoin-integration.t.sol
new file mode 100644
index 0000000..89acfe6
--- /dev/null
+++ b/src/test/CompoundJoin-integration.t.sol
@@ -0,0 +1,999 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2021 Dai Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pragma solidity 0.6.12;
+
+import "./TestBase.sol";
+import {CompoundJoinImp,ERC20,CTokenLike,ComptrollerLike,CropJoin} from "../CompoundJoin.sol";
+import {CropManager,CropManagerImp} from "../CropManager.sol";
+
+interface VatLike {
+ function wards(address) external view returns (uint256);
+ function rely(address) external;
+ function hope(address) external;
+ function gem(bytes32, address) external view returns (uint256);
+ function flux(bytes32, address, address, uint256) external;
+}
+
+contract ComptrollerStorage {
+ struct Market {
+ bool isListed;
+ uint collateralFactorMantissa;
+ mapping(address => bool) accountMembership;
+ bool isComped;
+ }
+ mapping(address => Market) public markets;
+}
+
+contract Usr {
+
+ Hevm hevm;
+ VatLike vat;
+ CompoundJoinImp adapter;
+ CropManagerImp manager;
+ ERC20 gem;
+
+ constructor(Hevm hevm_, CompoundJoinImp join_, CropManagerImp manager_, ERC20 gem_) public {
+ hevm = hevm_;
+ adapter = join_;
+ manager = manager_;
+ gem = gem_;
+
+ vat = VatLike(address(adapter.vat()));
+
+ gem.approve(address(manager), uint(-1));
+
+ manager.getOrCreateProxy(address(this));
+ }
+
+ function join(address usr, uint wad) public {
+ manager.join(address(adapter), usr, wad);
+ }
+ function join(uint wad) public {
+ manager.join(address(adapter), address(this), wad);
+ }
+ function exit(address usr, uint wad) public {
+ manager.exit(address(adapter), usr, wad);
+ }
+ function exit(uint wad) public {
+ manager.exit(address(adapter), address(this), wad);
+ }
+ function proxy() public view returns (address) {
+ return manager.proxy(address(this));
+ }
+ function crops() public view returns (uint256) {
+ return adapter.crops(proxy());
+ }
+ function stake() public view returns (uint256) {
+ return adapter.stake(proxy());
+ }
+ function gems() public view returns (uint256) {
+ return adapter.vat().gem(adapter.ilk(), proxy());
+ }
+ function urn() public view returns (uint256, uint256) {
+ return adapter.vat().urns(adapter.ilk(), proxy());
+ }
+ function bonus() public view returns (uint256) {
+ return adapter.bonus().balanceOf(address(this));
+ }
+ function balance() public view returns (uint256) {
+ return adapter.gem().balanceOf(address(this));
+ }
+ function reap() public {
+ manager.join(address(adapter), address(this), 0);
+ }
+ function flee() public {
+ manager.flee(address(adapter));
+ }
+ function flux(address src, address dst, uint256 wad) public {
+ manager.flux(address(adapter), src, dst, wad);
+ }
+ function giveTokens(ERC20 token, uint256 amount) public {
+ // Edge case - balance is already set for some reason
+ if (token.balanceOf(address(this)) == amount) return;
+
+ for (uint256 i = 0; i < 200; i++) {
+ // Scan the storage for the balance storage slot
+ bytes32 prevValue = hevm.load(
+ address(token),
+ keccak256(abi.encode(address(this), uint256(i)))
+ );
+ hevm.store(
+ address(token),
+ keccak256(abi.encode(address(this), uint256(i))),
+ bytes32(amount)
+ );
+ if (token.balanceOf(address(this)) == amount) {
+ // Found it
+ return;
+ } else {
+ // Keep going after restoring the original value
+ hevm.store(
+ address(token),
+ keccak256(abi.encode(address(this), uint256(i))),
+ prevValue
+ );
+ }
+ }
+ }
+ function hope(address usr) public {
+ vat.hope(usr);
+ }
+
+}
+
+contract Troll {
+ Token comp;
+ constructor(address comp_) public {
+ comp = Token(comp_);
+ }
+ mapping (address => uint) public compAccrued;
+ function reward(address usr, uint wad) public {
+ compAccrued[usr] += wad;
+ }
+ function claimComp(address[] memory, address[] memory, bool, bool) public {
+ comp.mint(msg.sender, compAccrued[msg.sender]);
+ compAccrued[msg.sender] = 0;
+ }
+ function claimComp() public {
+ comp.mint(msg.sender, compAccrued[msg.sender]);
+ compAccrued[msg.sender] = 0;
+ }
+ function enterMarkets(address[] memory ctokens) public returns (uint[] memory) {
+ comp; ctokens;
+ uint[] memory err = new uint[](1);
+ err[0] = 0;
+ return err;
+ }
+ function compBorrowerIndex(address c, address b) public returns (uint) {}
+ function mintAllowed(address ctoken, address minter, uint256 mintAmount) public returns (uint) {}
+ function getBlockNumber() public view returns (uint) {
+ return block.number;
+ }
+ function getAccountLiquidity(address) external returns (uint,uint,uint) {}
+ function liquidateBorrowAllowed(
+ address cTokenBorrowed,
+ address cTokenCollateral,
+ address liquidator,
+ address borrower,
+ uint repayAmount) external returns (uint) {}
+}
+
+// Here we run some tests against the real Compound on mainnet
+contract CompoundIntegrationTest is TestBase {
+
+ Token usdc;
+ CTokenLike cusdc;
+ Token comp;
+ Troll troll;
+ VatLike vat;
+ CompoundJoinImp adapter;
+ CropManagerImp manager;
+ address self;
+ bytes32 ilk = "USDC-C";
+
+ function setUp() public {
+ self = address(this);
+
+ vat = VatLike(0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B);
+ usdc = Token(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
+ cusdc = CTokenLike(0x39AA39c021dfbaE8faC545936693aC917d5E7563);
+ comp = Token(0xc00e94Cb662C3520282E6f5717214004A7f26888);
+ troll = Troll(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B);
+
+ // Give this contract admin access on the vat
+ giveAuthAccess(address(vat), address(this));
+
+ CropJoin baseJoin = new CropJoin();
+ baseJoin.setImplementation(address(new CompoundJoinImp(
+ address(vat),
+ ilk,
+ address(usdc),
+ address(comp),
+ address(cusdc),
+ address(troll)
+ )));
+ adapter = CompoundJoinImp(address(baseJoin));
+ CropManager base = new CropManager();
+ base.setImplementation(address(new CropManagerImp(address(vat))));
+ manager = CropManagerImp(address(base));
+ CropJoin(address(adapter)).rely(address(manager));
+ vat.rely(address(adapter));
+ adapter.init();
+ adapter.file("minf", 0.674e18);
+ adapter.file("maxf", 0.675e18);
+ adapter.file("dust", 1e6);
+ CropJoin(address(adapter)).deny(address(this)); // Only access should be through manager
+
+ // give ourselves some usdc
+ giveTokens(address(usdc), 1000 * 1e6);
+
+ hevm.roll(block.number + 10);
+
+ usdc.approve(address(manager), uint(-1));
+ }
+
+
+ function init_user() internal returns (Usr a, Usr b) {
+ return init_user(200 * 1e6);
+ }
+ function init_user(uint256 cash) internal returns (Usr a, Usr b) {
+ a = new Usr(hevm, adapter, manager, ERC20(address(usdc)));
+ b = new Usr(hevm, adapter, manager, ERC20(address(usdc)));
+
+ giveTokens(address(usdc), usdc.balanceOf(address(this)) + 2 * cash);
+
+ usdc.transfer(address(a), cash);
+ usdc.transfer(address(b), cash);
+ }
+
+ function can_exit(uint val) public returns (bool) {
+ bytes memory call = abi.encodeWithSignature
+ ("exit(uint256)", val);
+ return can_call(address(adapter), call);
+ }
+ function can_wind(uint borrow, uint n, uint loan) public returns (bool) {
+ bytes memory call = abi.encodeWithSignature
+ ("wind(uint256,uint256,uint256)", borrow, n, loan);
+ return can_call(address(adapter), call);
+ }
+ function can_unwind(uint repay, uint n, uint exit_, uint loan_) public returns (bool) {
+ bytes memory call = abi.encodeWithSignature
+ ("unwind(uint256,uint256,uint256,uint256)", repay, n, exit_, loan_);
+ return can_call(address(adapter), call);
+ }
+ function can_unwind_exit(uint val) public returns (bool) {
+ return can_unwind_exit(val, 0);
+ }
+ function can_unwind_exit(uint val, uint loan) public returns (bool) {
+ return can_unwind(0, 1, val, loan);
+ }
+ function can_unwind(uint repay, uint n) public returns (bool) {
+ return can_unwind(repay, n, 0, 0);
+ }
+
+ function get_s() internal returns (uint256 cf) {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ return CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter));
+ }
+ function get_b() internal returns (uint256 cf) {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ return CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter));
+ }
+ function get_cf() internal returns (uint256 cf) {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ cf = wdiv(CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter)),
+ CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter)));
+ }
+
+ function test_underlying() public {
+ assertEq(CTokenLike(address(cusdc)).underlying(), address(usdc));
+ }
+
+ function test_double_init() public {
+ address cgem = ComptrollerLike(address(troll)).accountAssets(address(adapter), 0);
+ assertTrue(cgem != address(0));
+ adapter.init(); // Should be idempotent
+ assertEq(ComptrollerLike(address(troll)).accountAssets(address(adapter), 0), cgem);
+ }
+
+ function reward(uint256 tic) internal {
+ log_named_uint("=== tic ==>", tic);
+ // accrue ~tic day of rewards
+ hevm.warp(block.timestamp + tic);
+ // unneeded?
+ hevm.roll(block.number + tic / 15);
+ }
+
+ function test_reward_unwound() public {
+ (Usr a,) = init_user();
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ a.join(100 * 1e6);
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ adapter.wind(0, 0, 0);
+
+ reward(1 days);
+
+ a.join(0);
+ assertGt(comp.balanceOf(address(a)), 0 ether);
+ }
+
+ function test_reward_wound() public {
+ (Usr a,) = init_user();
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ a.join(100 * 1e6);
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ adapter.wind(50 * 10**6, 0, 0);
+
+ reward(1 days);
+
+ a.join(0);
+ assertGt(comp.balanceOf(address(a)), 0 ether);
+
+ assertLt(get_cf(), adapter.maxf());
+ assertLt(get_cf(), adapter.minf());
+ }
+
+ function test_reward_wound_fully() public {
+ (Usr a,) = init_user();
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ a.join(100 * 1e6);
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ adapter.wind(0, 5, 0);
+
+ reward(1 days);
+
+ a.join(0);
+ assertGt(comp.balanceOf(address(a)), 0 ether);
+
+ assertLt(get_cf(), adapter.maxf(), "cf < maxf");
+ assertGt(get_cf(), adapter.minf(), "cf > minf");
+ }
+
+ function test_wind_unwind() public {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ (Usr a,) = init_user();
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ a.join(100 * 1e6);
+ assertEq(comp.balanceOf(address(a)), 0 ether);
+
+ adapter.wind(0, 5, 0);
+
+ reward(1 days);
+
+ assertLt(get_cf(), adapter.maxf(), "under target");
+ assertGt(get_cf(), adapter.minf(), "over minimum");
+
+ log_named_uint("cf", get_cf());
+ reward(1000 days);
+ log_named_uint("cf", get_cf());
+
+ assertGt(get_cf(), adapter.maxf(), "over target after interest");
+
+ // unwind is used for deleveraging our position. Here we have
+ // gone over the target due to accumulated interest, so we
+ // unwind to bring us back under the target leverage.
+ assertTrue( can_unwind(0, 1), "able to unwind if over target");
+ adapter.unwind(0, 1, 0, 0);
+
+ assertLt(get_cf(), 0.676e18, "near target post unwind");
+ assertGt(get_cf(), 0.674e18, "over minimum post unwind");
+ }
+
+ function test_unwind_multiple() public {
+ manager.join(address(adapter), address(this), 100e6);
+ adapter.wind(0, 5, 0);
+
+ set_cf(0.72e18);
+ adapter.unwind(0, 1, 0, 0);
+ log_named_uint("cf", get_cf());
+ adapter.unwind(0, 1, 0, 0);
+ log_named_uint("cf", get_cf());
+ adapter.unwind(0, 1, 0, 0);
+ log_named_uint("cf", get_cf());
+ adapter.unwind(0, 1, 0, 0);
+ log_named_uint("cf", get_cf());
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+
+ set_cf(0.72e18);
+ adapter.unwind(0, 8, 0, 0);
+ log_named_uint("cf", get_cf());
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+ }
+
+ // if utilisation ends up over the limit we will need to use a loan
+ // to unwind
+ function test_unwind_over_limit() public {
+ // we need a loan of
+ // L / s0 >= (u - cf) / (cf * (1 - u) * (1 - cf))
+ manager.join(address(adapter), address(this), 100e6);
+ adapter.wind(0, 5, 0);
+ set_cf(0.77e18);
+ log_named_uint("cf", get_cf());
+
+ uint cf = 0.75e18;
+ uint u = get_cf();
+ uint Lmin = wmul(100e6, wdiv(sub(u, cf), wmul(wmul(cf, 1e18 - u), 1e18 - cf)));
+ log_named_uint("s", get_s());
+ log_named_uint("b", get_s());
+ log_named_uint("L", Lmin);
+
+ assertTrue(!can_unwind(0, 1, 0, 0), "can't unwind without a loan");
+ assertTrue(!can_unwind(0, 1, 0, Lmin - 1e2), "can't unwind without enough loan");
+ assertTrue( can_unwind(0, 1, 0, Lmin), "can unwind with sufficient loan");
+ }
+
+ function test_unwind_under_limit() public {
+ manager.join(address(adapter), address(this), 100e6);
+ adapter.wind(0, 5, 0);
+ set_cf(0.673e18);
+ log_named_uint("cf", get_cf());
+ assertTrue(!can_unwind(0, 1, 0, 0));
+ // todo: minimum exit amount
+ }
+
+ function test_flash_wind_necessary_loan() public {
+ // given nav s0, we can calculate the minimum loan L needed to
+ // effect a wind up to a given u',
+ //
+ // L/s0 >= (u'/cf - 1 + u' - u*u'/cf) / [(1 - u') * (1 - u)]
+ //
+ // e.g. for u=0, u'=0.675, L/s0 ~= 1.77
+ //
+ // we can also write the maximum u' for a given L,
+ //
+ // u' <= (1 + (1 - u) * L / s0) / (1 + (1 - u) * (L / s0 + 1 / cf))
+ //
+ // and the borrow to directly achieve a given u'
+ //
+ // x = s0 (1 / (1 - u') - 1 / (1 - u))
+ //
+ // e.g. for u=0, u'=0.675, x/s0 ~= 2.0769
+ //
+ // here we test the u' that we achieve with given L
+
+ (Usr a,) = init_user();
+ a.join(100 * 1e6);
+
+ assertTrue(!can_wind(207.69 * 1e6, 0, 176 * 1e6), "insufficient loan");
+ assertTrue( can_wind(207.69 * 1e6, 0, 177 * 1e6), "sufficient loan");
+
+ adapter.wind(207.69 * 1e6, 0, 177 * 1e6);
+ log_named_uint("cf", get_cf());
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+ }
+
+ function test_flash_wind_sufficient_loan() public {
+ // we can also have wind determine the maximum borrow itself
+ (Usr a,) = init_user();
+ a.giveTokens(ERC20(address(usdc)), 900e6);
+
+ a.join(100 * 1e6);
+ adapter.wind(0, 1, 200 * 1e6);
+ log_named_uint("cf", get_cf());
+ assertGt(get_cf(), 0.673e18);
+ assertLt(get_cf(), 0.675e18);
+
+ return;
+ a.join(100 * 1e6);
+ logs("200");
+ adapter.wind(0, 1, 200 * 1e6);
+ log_named_uint("cf", get_cf());
+
+ a.join(100 * 1e6);
+ logs("100");
+ adapter.wind(0, 1, 100 * 1e6);
+ log_named_uint("cf", get_cf());
+
+ a.join(100 * 1e6);
+ logs("100");
+ adapter.wind(0, 1, 100 * 1e6);
+ log_named_uint("cf", get_cf());
+
+ a.join(100 * 1e6);
+ logs("150");
+ adapter.wind(0, 1, 150 * 1e6);
+ log_named_uint("cf", get_cf());
+
+ a.join(100 * 1e6);
+ logs("175");
+ adapter.wind(0, 1, 175 * 1e6);
+ log_named_uint("cf", get_cf());
+
+ assertGt(get_cf(), 0.673e18);
+ assertLt(get_cf(), 0.675e18);
+ }
+ // compare gas costs of a flash loan wind and a iteration wind
+ function test_wind_gas_flash() public {
+ (Usr a,) = init_user();
+
+ a.join(100 * 1e6);
+ uint256 gas_before = gasleft();
+ adapter.wind(0, 1, 200 * 1e6);
+ uint256 gas_after = gasleft();
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ assertGt(get_cf(), 0.673e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("gas", gas_before - gas_after);
+ }
+ function test_wind_gas_iteration() public {
+ (Usr a,) = init_user();
+
+ a.join(100 * 1e6);
+ uint256 gas_before = gasleft();
+ adapter.wind(0, 5, 0);
+ uint256 gas_after = gasleft();
+
+ assertGt(get_cf(), 0.673e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ log_named_uint("gas", gas_before - gas_after);
+ }
+ function test_wind_gas_partial_loan() public {
+ (Usr a,) = init_user();
+
+ a.join(100 * 1e6);
+ uint256 gas_before = gasleft();
+ adapter.wind(0, 3, 50e6);
+ uint256 gas_after = gasleft();
+
+ assertGt(get_cf(), 0.673e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ log_named_uint("gas", gas_before - gas_after);
+ }
+
+ function set_cf(uint256 cf) internal {
+ uint256 nav = adapter.nav();
+
+ // desired supply and borrow in terms of underlying
+ uint256 x = cusdc.exchangeRateCurrent();
+ uint256 s = (nav * 1e18 / (1e18 - cf)) / 1e12;
+ uint256 b = s * cf / 1e18 - 1;
+
+ log_named_uint("nav ", nav);
+ log_named_uint("new s", s);
+ log_named_uint("new b", b);
+ log_named_uint("set u", cf);
+
+ //set_usdc(address(adapter), 0);
+ // cusdc.accountTokens
+ hevm.store(
+ address(cusdc),
+ keccak256(abi.encode(address(adapter), uint256(15))),
+ bytes32((s * 1e18) / x)
+ );
+ // cusdc.accountBorrows.principal
+ hevm.store(
+ address(cusdc),
+ keccak256(abi.encode(address(adapter), uint256(17))),
+ bytes32(b)
+ );
+ // cusdc.accountBorrows.interestIndex
+ hevm.store(
+ address(cusdc),
+ bytes32(uint(keccak256(abi.encode(address(adapter), uint256(17)))) + 1),
+ bytes32(cusdc.borrowIndex())
+ );
+
+ log_named_uint("new u", get_cf());
+ log_named_uint("nav ", adapter.nav());
+ }
+
+ // simple test of `cage` where we set the target leverage to zero
+ // and then seek to withdraw all of the collateral
+ function test_cage_single_user() public {
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ adapter.wind(0, 5, 0);
+ set_cf(0.6745e18);
+
+ // log("unwind 1");
+ // adapter.unwind(0, 6, 0, 0);
+
+ set_cf(0.675e18);
+
+ // this causes a sub overflow unless we use zsub
+ // adapter.tune(0.673e18, 0);
+
+ adapter.file("minf", 0);
+ adapter.file("maxf", 0);
+
+ assertEq(usdc.balanceOf(address(this)), 900 * 1e6);
+ adapter.unwind(0, 6, 100 * 1e6, 0);
+ assertEq(usdc.balanceOf(address(this)), 1000 * 1e6);
+ }
+ // test of `cage` with two users, where the adapter is unwound
+ // by a third party and the two users then exit separately
+ function test_cage_multi_user() public {
+ cage_multi_user(60 * 1e6, 40 * 1e6, 200 * 1e6);
+ }
+ // the same test but fuzzing over various ranges:
+ // - uint32 is up to $4.5k
+ // - uint40 is up to $1.1m
+ // - uint48 is up to $280m, but we cap it at $50m due to liquidity
+ function test_cage_multi_user_small(uint32 a_join, uint32 b_join) public {
+ if (a_join < 100e6 || b_join < 100e6) return;
+ cage_multi_user(a_join, b_join, uint32(-1));
+ }
+ function test_cage_multi_user_medium(uint40 a_join, uint40 b_join) public {
+ if (a_join < uint32(-1) || b_join < uint32(-1)) return;
+ cage_multi_user(a_join, b_join, uint40(-1));
+ }
+ function test_cage_multi_user_large(uint48 a_join, uint48 b_join) public {
+ if (a_join < uint40(-1) || b_join < uint40(-1)) return;
+ if (a_join > 50e6 * 1e6 || b_join > 50e6 * 1e6) return;
+ cage_multi_user(a_join, b_join, uint48(-1));
+ }
+
+ function cage_multi_user(uint a_join, uint b_join, uint cash) public {
+ // this would truncate to whole usdc amounts, but there don't
+ // seem to be any failures for that
+ // a_join = a_join / 1e6 * 1e6;
+ // b_join = b_join / 1e6 * 1e6;
+
+ log_named_decimal_uint("a_join", a_join, 6);
+ log_named_decimal_uint("b_join", b_join, 6);
+ (Usr a, Usr b) = init_user(cash);
+ assertEq(usdc.balanceOf(address(a)), cash);
+ assertEq(usdc.balanceOf(address(b)), cash);
+ a.join(a_join);
+ b.join(b_join);
+
+ assertEq(usdc.balanceOf(address(a)), cash - a_join);
+ assertEq(usdc.balanceOf(address(b)), cash - b_join);
+
+ adapter.wind(0, 1, 0);
+ reward(30 days);
+ adapter.file("minf", 0);
+ adapter.file("maxf", 0);
+
+ adapter.unwind(0, 10, 0, 0);
+ assertEq(cusdc.balanceOfUnderlying(address(adapter)), 0);
+
+ a.exit(a_join);
+ b.exit(b_join);
+
+ assertEq(usdc.balanceOf(address(a)), cash);
+ assertEq(usdc.balanceOf(address(b)), cash);
+ }
+ // wind / unwind make the underlying unavailable as it is deposited
+ // into the ctoken. In order to exit we will have to free up some
+ // underlying.
+ function wound_unwind_exit(bool loan) public {
+ manager.join(address(adapter), address(this), 100 * 1e6);
+
+ assertEq(comp.balanceOf(self), 0 ether, "no initial rewards");
+
+ set_cf(0.675e18);
+
+ assertTrue(get_cf() < adapter.maxf(), "cf under target");
+ assertTrue(get_cf() > adapter.minf(), "cf over minimum");
+
+ // we can't exit as there is no available usdc
+ assertTrue(!can_exit(10 * 1e6), "cannot 10% exit initially");
+
+ // however we can exit with unwind
+ assertTrue( can_unwind_exit(14.7 * 1e6), "ok exit with 14.7%");
+ assertTrue(!can_unwind_exit(14.9 * 1e6), "no exit with 14.9%");
+
+ if (loan) {
+ // with a loan we can exit an extra (L * (1 - u) / u) ~= 0.481L
+ assertTrue( can_unwind_exit(19.5 * 1e6, 10 * 1e6), "ok loan exit");
+ assertTrue(!can_unwind_exit(19.7 * 1e6, 10 * 1e6), "no loan exit");
+
+ log_named_uint("s ", CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter)));
+ log_named_uint("b ", CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter)));
+ log_named_uint("u ", get_cf());
+
+ uint prev = usdc.balanceOf(address(this));
+ //adapter.unwind(0, 1, 10 * 1e6, 10 * 1e6);
+ assertEq(usdc.balanceOf(address(this)) - prev, 10 * 1e6);
+
+ log_named_uint("s'", CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter)));
+ log_named_uint("b'", CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter)));
+ log_named_uint("u'", get_cf());
+
+ } else {
+ log_named_uint("s ", CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter)));
+ log_named_uint("b ", CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter)));
+ log_named_uint("u ", get_cf());
+
+ uint prev = usdc.balanceOf(address(this));
+ //adapter.unwind(0, 1, 10 * 1e6, 0);
+ assertEq(usdc.balanceOf(address(this)) - prev, 10 * 1e6);
+
+ log_named_uint("s'", CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter)));
+ log_named_uint("b'", CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter)));
+ log_named_uint("u'", get_cf());
+ }
+ }
+ function test_unwind_exit() public {
+ wound_unwind_exit(false);
+ }
+ function test_unwind_exit_with_loan() public {
+ wound_unwind_exit(true);
+ }
+ function test_unwind_full_exit() public {
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ set_cf(0.675e18);
+
+ // we can unwind in a single cycle using a loan
+ //adapter.unwind(0, 1, 100e6 - 1e4, 177 * 1e6);
+
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ set_cf(0.675e18);
+
+ // or we can unwind by iteration without a loan
+ //adapter.unwind(0, 6, 100e6 - 1e4, 0);
+ }
+ function test_unwind_gas_flash() public {
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ set_cf(0.675e18);
+ uint gas_before = gasleft();
+ adapter.unwind(0, 1, 100e6 - 1e4, 177e6);
+ uint gas_after = gasleft();
+
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ log_named_uint("gas", gas_before - gas_after);
+ }
+ function test_unwind_gas_iteration() public {
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ set_cf(0.675e18);
+ uint gas_before = gasleft();
+ adapter.unwind(0, 5, 100e6 - 1e4, 0);
+ uint gas_after = gasleft();
+
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ log_named_uint("gas", gas_before - gas_after);
+ }
+ function test_unwind_gas_shallow() public {
+ // we can withdraw a fraction of the pool without loans or
+ // iterations
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ set_cf(0.675e18);
+ uint gas_before = gasleft();
+ adapter.unwind(0, 1, 14e6, 0);
+ uint gas_after = gasleft();
+
+ assertGt(get_cf(), 0.674e18);
+ assertLt(get_cf(), 0.675e18);
+ log_named_uint("s ", get_s());
+ log_named_uint("b ", get_b());
+ log_named_uint("s + b", get_s() + get_b());
+ log_named_uint("cf", get_cf());
+ log_named_uint("gas", gas_before - gas_after);
+ }
+
+ // The nav of the adapter will drop over time, due to interest
+ // accrual, check that this is well behaved.
+ function test_nav_drop_with_interest() public {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ (Usr a,) = init_user();
+
+ manager.join(address(adapter), address(this), 600 * 1e6);
+
+ assertEq(usdc.balanceOf(address(a)), 200 * 1e6);
+ a.join(100 * 1e6);
+ assertEq(usdc.balanceOf(address(a)), 100 * 1e6);
+ assertEq(adapter.nps(), 1 ether, "initial nps is 1");
+
+ log_named_uint("nps before wind ", adapter.nps());
+ adapter.wind(0, 5, 0);
+
+ assertGt(get_cf(), 0.673e18, "near minimum");
+ assertLt(get_cf(), 0.675e18, "under target");
+
+ log_named_uint("nps before interest", adapter.nps());
+ reward(100 days);
+ assertLt(adapter.nps(), 1e18, "nps falls after interest");
+ log_named_uint("nps after interest ", adapter.nps());
+
+ assertEq(usdc.balanceOf(address(a)), 100 * 1e6, "usdc before exit");
+ assertEq(adapter.stake(address(a)), 100 ether, "balance before exit");
+
+ uint max_usdc = mul(adapter.nps(), adapter.stake(address(a))) / 1e30;
+ logs("===");
+ log_named_uint("max usdc ", max_usdc);
+ log_named_uint("adapter.balance", adapter.stake(address(a)));
+ log_named_uint("vat.gem ", vat.gem(adapter.ilk(), address(a)));
+ log_named_uint("usdc ", usdc.balanceOf(address(adapter)));
+ log_named_uint("cf", get_cf());
+ logs("exit ===");
+ //a.unwind_exit(max_usdc);
+ log_named_uint("nps after exit ", adapter.nps());
+ log_named_uint("adapter.balance", adapter.stake(address(a)));
+ log_named_uint("adapter.balance", adapter.stake(address(a)) / 1e12);
+ log_named_uint("vat.gem ", vat.gem(adapter.ilk(), address(a)));
+ log_named_uint("usdc ", usdc.balanceOf(address(adapter)));
+ log_named_uint("cf", get_cf());
+ assertLt(usdc.balanceOf(address(a)), 200 * 1e6, "less usdc after");
+ assertGt(usdc.balanceOf(address(a)), 199 * 1e6, "less usdc after");
+
+ assertLt(adapter.stake(address(a)), 1e18/1e6, "zero balance after full exit");
+ }
+ function test_nav_drop_with_liquidation() public {
+ require(CTokenLike(address(cusdc)).accrueInterest() == 0);
+ enable_seize(address(this));
+
+ (Usr a,) = init_user();
+
+ manager.join(address(adapter), address(this), 600 * 1e6);
+
+ assertEq(usdc.balanceOf(address(a)), 200 * 1e6);
+ a.join(100 * 1e6);
+ assertEq(usdc.balanceOf(address(a)), 100 * 1e6);
+
+ logs("wind===");
+ adapter.wind(0, 5, 0);
+
+ assertGt(get_cf(), 0.673e18, "near minimum");
+ assertLt(get_cf(), 0.675e18, "under target");
+
+ uint liquidity; uint shortfall; uint supp; uint borr;
+ supp = CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter));
+ borr = CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter));
+ (, liquidity, shortfall) =
+ troll.getAccountLiquidity(address(adapter));
+ log_named_uint("cf ", get_cf());
+ log_named_uint("s ", supp);
+ log_named_uint("b ", borr);
+ log_named_uint("liquidity", liquidity);
+ log_named_uint("shortfall", shortfall);
+
+ uint nps_before = adapter.nps();
+ logs("time...===");
+ reward(5000 days);
+ assertLt(adapter.nps(), nps_before, "nps falls after interest");
+
+ supp = CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter));
+ borr = CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter));
+ (, liquidity, shortfall) =
+ troll.getAccountLiquidity(address(adapter));
+ log_named_uint("cf' ", get_cf());
+ log_named_uint("s' ", supp);
+ log_named_uint("b' ", borr);
+ log_named_uint("liquidity", liquidity);
+ log_named_uint("shortfall", shortfall);
+
+ cusdc.approve(address(cusdc), uint(-1));
+ usdc.approve(address(cusdc), uint(-1));
+ log_named_uint("allowance", cusdc.allowance(address(this), address(cusdc)));
+ giveTokens(address(usdc), 1000e6);
+ log_named_uint("usdc ", usdc.balanceOf(address(this)));
+ log_named_uint("cusdc", cusdc.balanceOf(address(this)));
+ require(cusdc.mint(100e6) == 0);
+ logs("mint===");
+ log_named_uint("usdc ", usdc.balanceOf(address(this)));
+ log_named_uint("cusdc", cusdc.balanceOf(address(this)));
+ logs("liquidate===");
+ return;
+ // liquidation is not possible for cusdc-cusdc pairs, as it is
+ // blocked by a re-entrancy guard????
+ uint repay = 20; // units of underlying
+ assertTrue(!can_call( address(cusdc)
+ , abi.encodeWithSignature(
+ "liquidateBorrow(address,uint256,address)",
+ address(adapter), repay, CTokenLike(address(cusdc)))),
+ "can't perform liquidation");
+ cusdc.liquidateBorrow(address(adapter), repay, CTokenLike(address(cusdc)));
+
+ supp = CTokenLike(address(cusdc)).balanceOfUnderlying(address(adapter));
+ borr = CTokenLike(address(cusdc)).borrowBalanceStored(address(adapter));
+ (, liquidity, shortfall) =
+ troll.getAccountLiquidity(address(adapter));
+ log_named_uint("cf' ", get_cf());
+ log_named_uint("s' ", supp);
+ log_named_uint("b' ", borr);
+ log_named_uint("liquidity", liquidity);
+ log_named_uint("shortfall", shortfall);
+
+ // check how long it would take for us to get to 100% utilisation
+ reward(30 * 365 days);
+ log_named_uint("cf' ", get_cf());
+ assertGt(get_cf(), 1e18, "cf > 1");
+ }
+
+ // allow the test contract to seize collateral from a borrower
+ // (normally only cTokens can do this). This allows us to mock
+ // liquidations.
+ function enable_seize(address usr) internal {
+ hevm.store(
+ address(troll),
+ keccak256(abi.encode(usr, uint256(9))),
+ bytes32(uint256(1))
+ );
+ }
+ // comptroller expects this to be available if we're pretending to
+ // be a cToken
+ function comptroller() external returns (address) {
+ return address(troll);
+ }
+ function test_enable_seize() public {
+ ComptrollerStorage stroll = ComptrollerStorage(address(troll));
+ bool isListed;
+ (isListed,,) = stroll.markets(address(this));
+ assertTrue(!isListed);
+
+ enable_seize(address(this));
+
+ (isListed,,) = stroll.markets(address(this));
+ assertTrue(isListed);
+ }
+ function test_can_seize() public {
+ enable_seize(address(this));
+
+ manager.join(address(adapter), address(this), 100 * 1e6);
+ adapter.wind(0, 4, 0);
+
+ uint seize = 100 * 1e8;
+
+ uint cusdc_before = cusdc.balanceOf(address(adapter));
+ assertEq(cusdc.balanceOf(address(this)), 0, "no cusdc before");
+
+ uint s = CTokenLike(address(cusdc)).seize(address(this), address(adapter), seize);
+ assertEq(s, 0, "seize successful");
+
+ uint cusdc_after = cusdc.balanceOf(address(adapter));
+ assertEq(cusdc.balanceOf(address(this)), seize, "cusdc after");
+ assertEq(cusdc_before - cusdc_after, seize, "join supply decreased");
+ }
+ function test_nav_drop_with_seizure() public {
+ enable_seize(address(this));
+
+ (Usr a,) = init_user();
+
+ manager.join(address(adapter), address(this), 600 * 1e6);
+ a.join(100 * 1e6);
+ log_named_uint("nps", adapter.nps());
+ log_named_uint("usdc ", usdc.balanceOf(address(adapter)));
+ log_named_uint("cusdc", cusdc.balanceOf(address(adapter)));
+
+ logs("wind===");
+ adapter.wind(0, 5, 0);
+ log_named_uint("nps", adapter.nps());
+ log_named_uint("cf", get_cf());
+ log_named_uint("adapter usdc ", usdc.balanceOf(address(adapter)));
+ log_named_uint("adapter cusdc", cusdc.balanceOf(address(adapter)));
+ log_named_uint("adapter nav ", adapter.nav());
+ log_named_uint("a max usdc ", mul(adapter.stake(address(a)), adapter.nps()) / 1e18);
+
+ assertGt(get_cf(), 0.673e18, "near minimum");
+ assertLt(get_cf(), 0.675e18, "under target");
+
+ logs("seize===");
+ uint seize = 350 * 1e6 * 1e18 / cusdc.exchangeRateCurrent();
+ log_named_uint("seize", seize);
+ uint s = CTokenLike(address(cusdc)).seize(address(this), address(adapter), seize);
+ assertEq(s, 0, "seize successful");
+ log_named_uint("nps", adapter.nps());
+ log_named_uint("cf", get_cf());
+ log_named_uint("adapter usdc ", usdc.balanceOf(address(adapter)));
+ log_named_uint("adapter cusdc", cusdc.balanceOf(address(adapter)));
+ log_named_uint("adapter nav ", adapter.nav());
+ log_named_uint("a max usdc ", mul(adapter.stake(address(a)), adapter.nps()) / 1e18);
+
+ assertLt(adapter.nav(), 350 * 1e18, "nav is halved");
+ }
+}
diff --git a/src/test/TestBase.sol b/src/test/TestBase.sol
index 4973f13..08e8a68 100644
--- a/src/test/TestBase.sol
+++ b/src/test/TestBase.sol
@@ -208,4 +208,25 @@ contract TestBase is DSTest {
}
}
+ function try_call(address addr, bytes calldata data) external returns (bool) {
+ bytes memory _data = data;
+ assembly {
+ let ok := call(gas(), addr, 0, add(_data, 0x20), mload(_data), 0, 0)
+ let free := mload(0x40)
+ mstore(free, ok)
+ mstore(0x40, add(free, 32))
+ revert(free, 32)
+ }
+ }
+ function can_call(address addr, bytes memory data) internal returns (bool) {
+ (bool ok, bytes memory success) = address(this).call(
+ abi.encodeWithSignature(
+ "try_call(address,bytes)"
+ , addr
+ , data
+ ));
+ ok = abi.decode(success, (bool));
+ if (ok) return true;
+ }
+
}