diff --git a/src/CurveGaugeJoin.sol b/src/CurveGaugeJoin.sol
new file mode 100644
index 0000000..e7f7406
--- /dev/null
+++ b/src/CurveGaugeJoin.sol
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2022 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 LiquidityGaugeLike {
+ function crv_token() external view returns (address);
+ function lp_token() external view returns (address);
+ function minter() external view returns (address);
+ function deposit(uint256) external;
+ function withdraw(uint256) external;
+}
+
+interface LiquidityGaugeMinterLike {
+ function mint_for(address,address) external;
+}
+
+// Join adapter for the Curve gauge contract
+contract CurveGaugeJoinImp is CropJoinImp {
+
+ LiquidityGaugeLike immutable public pool;
+ LiquidityGaugeMinterLike immutable public minter;
+
+ /**
+ @param vat_ MCD_VAT DSS core accounting module
+ @param ilk_ Collateral type
+ @param gem_ The collateral LP token address
+ @param bonus_ The rewards token contract address.
+ @param pool_ The staking rewards pool.
+ */
+ constructor(
+ address vat_,
+ bytes32 ilk_,
+ address gem_,
+ address bonus_,
+ address pool_
+ )
+ public
+ CropJoinImp(vat_, ilk_, gem_, bonus_)
+ {
+ // Sanity checks
+ require(LiquidityGaugeLike(pool_).crv_token() == bonus_, "CurveGaugeJoin/bonus-mismatch");
+ require(LiquidityGaugeLike(pool_).lp_token() == gem_, "CurveGaugeJoin/gem-mismatch");
+
+ pool = LiquidityGaugeLike(pool_);
+ minter = LiquidityGaugeMinterLike(LiquidityGaugeLike(pool_).minter());
+ }
+
+ function init() external {
+ gem.approve(address(pool), type(uint256).max);
+ }
+
+ function nav() public override view returns (uint256) {
+ return total;
+ }
+
+ function crop() internal override returns (uint256) {
+ if (live == 1) {
+ minter.mint_for(address(pool), address(this));
+ }
+ return super.crop();
+ }
+
+ function join(address urn, address usr, uint256 val) public override {
+ super.join(urn, usr, val);
+ if (val > 0) pool.deposit(val);
+ }
+
+ function exit(address urn, address usr, uint256 val) public override {
+ if (live == 1) {
+ if (val > 0) pool.withdraw(val);
+ }
+ super.exit(urn, usr, val);
+ }
+
+ function flee(address urn, address usr, uint256 val) public override {
+ if (live == 1) {
+ if (val > 0) pool.withdraw(val);
+ }
+ super.flee(urn, usr, val);
+ }
+ function cage() override public auth {
+ require(live == 1, "CurveGaugeJoin/not-live");
+
+ if (total > 0) pool.withdraw(total);
+ live = 0;
+ }
+ function uncage() external auth {
+ require(live == 0, "CurveGaugeJoin/live");
+
+ if (total > 0) pool.deposit(total);
+ live = 1;
+ }
+}
diff --git a/src/test/CurveGaugeJoin-integration.t.sol b/src/test/CurveGaugeJoin-integration.t.sol
new file mode 100644
index 0000000..c7e1cb4
--- /dev/null
+++ b/src/test/CurveGaugeJoin-integration.t.sol
@@ -0,0 +1,384 @@
+// 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 {ERC20, CropJoin, CurveGaugeJoinImp} from "../CurveGaugeJoin.sol";
+import {Cropper,CropperImp} from "../Cropper.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;
+}
+
+interface LiquidityGaugeLike {
+ function crv_token() external view returns (address);
+ function lp_token() external view returns (address);
+ function minter() external view returns (address);
+ function deposit(uint256) external;
+ function withdraw(uint256) external;
+ function balanceOf(address) external view returns (uint256);
+}
+
+contract Usr {
+
+ Hevm hevm;
+ VatLike vat;
+ CurveGaugeJoinImp adapter;
+ CropperImp cropper;
+ ERC20 gem;
+
+ constructor(Hevm hevm_, CurveGaugeJoinImp join_, CropperImp cropper_, ERC20 gem_) public {
+ hevm = hevm_;
+ adapter = join_;
+ cropper = cropper_;
+ gem = gem_;
+
+ vat = VatLike(address(adapter.vat()));
+
+ gem.approve(address(cropper), uint(-1));
+
+ cropper.getOrCreateProxy(address(this));
+ }
+
+ function join(address usr, uint wad) public {
+ cropper.join(address(adapter), usr, wad);
+ }
+ function join(uint wad) public {
+ cropper.join(address(adapter), address(this), wad);
+ }
+ function exit(address usr, uint wad) public {
+ cropper.exit(address(adapter), usr, wad);
+ }
+ function exit(uint wad) public {
+ cropper.exit(address(adapter), address(this), wad);
+ }
+ function proxy() public view returns (address) {
+ return cropper.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 tokens() public view returns (uint256) {
+ return adapter.gem().balanceOf(address(this));
+ }
+ function bonus() public view returns (uint256) {
+ return adapter.bonus().balanceOf(address(this));
+ }
+ function urn() public view returns (uint256, uint256) {
+ return adapter.vat().urns(adapter.ilk(), proxy());
+ }
+ function reap() public {
+ cropper.join(address(adapter), address(this), 0);
+ }
+ function flee(uint256 amt) public {
+ cropper.flee(address(adapter), address(this), amt);
+ }
+ function giveTokens(ERC20 token, uint256 amount) external {
+ // Edge case - balance is already set for some reason
+ if (token.balanceOf(address(this)) == amount) return;
+
+ // Solidity-style
+ for (uint256 i = 0; i < 20; 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
+ );
+ }
+ }
+
+ // Vyper-style
+ for (uint256 i = 0; i < 20; i++) {
+ // Scan the storage for the balance storage slot
+ bytes32 prevValue = hevm.load(
+ address(token),
+ keccak256(abi.encode(uint256(i), address(this)))
+ );
+ hevm.store(
+ address(token),
+ keccak256(abi.encode(uint256(i), address(this))),
+ 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(uint256(i), address(this))),
+ prevValue
+ );
+ }
+ }
+ }
+
+}
+
+// Mainnet tests against gauge CRV rewards for Curve pools
+contract CurveGaugeIntegrationTest is TestBase {
+
+ ERC20 gem;
+ ERC20 bonus;
+ LiquidityGaugeLike pool;
+ VatLike vat;
+ bytes32 ilk = "3CRV-A";
+ CurveGaugeJoinImp join;
+ CropperImp cropper;
+
+ Usr user1;
+ Usr user2;
+ Usr user3;
+
+ function setUp() public {
+ vat = VatLike(0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B);
+ gem = ERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); // 3pool
+ bonus = ERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); // CRV token
+ pool = LiquidityGaugeLike(0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A);
+
+ // Give this contract admin access on the vat
+ giveAuthAccess(address(vat), address(this));
+
+ CropJoin baseJoin = new CropJoin();
+ baseJoin.setImplementation(address(new CurveGaugeJoinImp(address(vat), ilk, address(gem), address(bonus), address(pool))));
+ join = CurveGaugeJoinImp(address(baseJoin));
+ join.init();
+ Cropper baseCropper = new Cropper();
+ baseCropper.setImplementation(address(new CropperImp(address(vat))));
+ cropper = CropperImp(address(baseCropper));
+ baseJoin.rely(address(cropper));
+ baseJoin.deny(address(this)); // Only access should be through cropper
+ vat.rely(address(baseJoin));
+ assertEq(address(join.pool()), address(pool));
+ user1 = new Usr(hevm, join, cropper, gem);
+ user2 = new Usr(hevm, join, cropper, gem);
+ user3 = new Usr(hevm, join, cropper, gem);
+
+ assertTrue(user1.proxy() != address(0));
+ assertTrue(user2.proxy() != address(0));
+ assertTrue(user3.proxy() != address(0));
+
+ user1.giveTokens(gem, 100 ether);
+ assertEq(gem.balanceOf(address(user1)), 100 ether);
+ user2.giveTokens(gem, 100 ether);
+ user3.giveTokens(gem, 100 ether);
+ }
+
+ function test_join() public {
+ user1.join(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ }
+
+ function test_join_rewards() public {
+ user1.join(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(bonus.balanceOf(address(join)), 0 ether);
+ assertEq(user1.bonus(), 0 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+ user1.reap();
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(bonus.balanceOf(address(join)), 0 ether);
+ assertGt(user1.bonus(), 0 ether);
+ }
+
+ function test_join_exit() public {
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+
+ user1.exit(5 ether);
+
+ assertEq(pool.balanceOf(address(join)), 5 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 5 ether);
+ }
+
+ function test_join_exit_rewards() public {
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+ assertEq(user1.bonus(), 0 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+ user1.exit(5 ether);
+
+ assertEq(pool.balanceOf(address(join)), 5 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 5 ether);
+ assertGt(user1.bonus(), 0 ether);
+ }
+
+ function test_join_exit_none() public {
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+
+ user1.exit(0 ether);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+ }
+
+ function test_flee() public {
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+
+ user1.flee(10 ether); // Should exit without rewards
+
+ assertEq(pool.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal);
+ assertEq(user1.bonus(), 0 ether);
+ }
+
+ function test_cage() public {
+ // Re-auth the join to cage it
+ giveAuthAccess(address(join), address(this));
+
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+ assertEq(CropJoin(address(join)).live(), 1);
+
+ join.cage();
+
+ assertEq(CropJoin(address(join)).live(), 0);
+ assertEq(pool.balanceOf(address(join)), 0 ether);
+ assertEq(gem.balanceOf(address(join)), 10 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+
+ // Can get all my gems
+ user1.exit(10 ether);
+
+ assertEq(pool.balanceOf(address(join)), 0 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal);
+ }
+
+ function test_cage_uncage() public {
+ // Re-auth the join to cage it
+ giveAuthAccess(address(join), address(this));
+
+ uint256 origBal = user1.tokens();
+
+ user1.join(10 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+ assertEq(CropJoin(address(join)).live(), 1);
+
+ join.cage();
+
+ assertEq(CropJoin(address(join)).live(), 0);
+ assertEq(pool.balanceOf(address(join)), 0 ether);
+ assertEq(gem.balanceOf(address(join)), 10 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+
+ join.uncage();
+
+ assertEq(CropJoin(address(join)).live(), 1);
+ assertEq(pool.balanceOf(address(join)), 10 ether);
+ assertEq(gem.balanceOf(address(join)), 0 ether);
+ assertEq(user1.tokens(), origBal - 10 ether);
+ }
+
+ function testFail_uncage() public {
+ // Re-auth the join to cage it
+ giveAuthAccess(address(join), address(this));
+
+ user1.join(10 ether);
+
+ // Acquire some rewards
+ hevm.warp(now + 100 days);
+
+ join.uncage(); // This will fail
+ }
+
+ function testFail_cage_join() public {
+ // Re-auth the join to cage it
+ giveAuthAccess(address(join), address(this));
+
+ join.cage();
+
+ user1.join(10 ether); // This will fail
+ }
+
+}
diff --git a/src/test/SushiJoinV1-integration.t.sol b/src/test/SushiJoinV1-integration.t.sol
index 66d9458..6d98257 100644
--- a/src/test/SushiJoinV1-integration.t.sol
+++ b/src/test/SushiJoinV1-integration.t.sol
@@ -175,7 +175,8 @@ contract Usr {
}
// Mainnet tests against SushiSwap
-contract SushiV1IntegrationTest is TestBase {
+// NOTE: Set to abstract to disable tests
+abstract contract SushiV1IntegrationTest is TestBase {
SushiLPLike pair;
ERC20 sushi;
diff --git a/src/test/SushiJoinV2-integration.t.sol b/src/test/SushiJoinV2-integration.t.sol
index 50ac448..34f9b0b 100644
--- a/src/test/SushiJoinV2-integration.t.sol
+++ b/src/test/SushiJoinV2-integration.t.sol
@@ -183,7 +183,8 @@ contract Usr {
}
// Mainnet tests against SushiSwap
-contract SushiIntegrationTest is TestBase {
+// NOTE: Set to abstract to disable tests
+abstract contract SushiIntegrationTest is TestBase {
SushiLPLike pair;
ERC20 sushi;