Skip to content

Commit 1cac572

Browse files
authored
Merge pull request #69 from benesjan/compound
Compound
2 parents febdc59 + 5d85ce7 commit 1cac572

File tree

4 files changed

+375
-0
lines changed

4 files changed

+375
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
// Copyright 2022 Spilsbury Holdings Ltd
3+
pragma solidity >=0.8.4;
4+
5+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import {IDefiBridge} from "../../interfaces/IDefiBridge.sol";
8+
import {AztecTypes} from "../../aztec/AztecTypes.sol";
9+
import {IRollupProcessor} from "../../interfaces/IRollupProcessor.sol";
10+
import {ICERC20} from "./interfaces/ICERC20.sol";
11+
import {ICETH} from "./interfaces/ICETH.sol";
12+
13+
/**
14+
* @title Aztec Connect Bridge for Compound protocol
15+
* @notice You can use this contract to mint or redeem cTokens.
16+
* @dev Implementation of the IDefiBridge interface for cTokens.
17+
*/
18+
contract CompoundBridge is IDefiBridge {
19+
using SafeERC20 for IERC20;
20+
21+
error InvalidCaller();
22+
error IncorrectInputAsset();
23+
error IncorrectOutputAsset();
24+
error IncorrectAuxData();
25+
error AsyncModeDisabled();
26+
27+
address public immutable ROLLUP_PROCESSOR;
28+
29+
/**
30+
* @notice Set the address of RollupProcessor.sol
31+
* @param _rollupProcessor Address of RollupProcessor.sol
32+
*/
33+
constructor(address _rollupProcessor) {
34+
ROLLUP_PROCESSOR = _rollupProcessor;
35+
}
36+
37+
receive() external payable {}
38+
39+
/**
40+
* @notice Function which mints and burns cTokens in an exchange for the underlying asset.
41+
* @dev This method can only be called from RollupProcessor.sol. If auxData is 0 the mint flow is executed,
42+
* if 1 redeem flow.
43+
*
44+
* @param inputAssetA - ETH/ERC20 (Mint), cToken ERC20 (Redeem)
45+
* @param outputAssetA - cToken (Mint), ETH/ERC20 (Redeem)
46+
* @param totalInputValue - the amount of ERC20 token/ETH to deposit (Mint), the amount of cToken to burn (Redeem)
47+
* @param interactionNonce - interaction nonce as defined in RollupProcessor.sol
48+
* @param auxData - 0 (Mint), 1 (Redeem)
49+
* @return outputValueA - the amount of cToken (Mint) or ETH/ERC20 (Redeem) transferred to RollupProcessor.sol
50+
*/
51+
function convert(
52+
AztecTypes.AztecAsset calldata inputAssetA,
53+
AztecTypes.AztecAsset calldata,
54+
AztecTypes.AztecAsset calldata outputAssetA,
55+
AztecTypes.AztecAsset calldata,
56+
uint256 totalInputValue,
57+
uint256 interactionNonce,
58+
uint64 auxData,
59+
address
60+
)
61+
external
62+
payable
63+
override
64+
returns (
65+
uint256 outputValueA,
66+
uint256,
67+
bool
68+
)
69+
{
70+
if (msg.sender != ROLLUP_PROCESSOR) revert InvalidCaller();
71+
72+
if (auxData == 0) {
73+
// Mint
74+
if (outputAssetA.assetType != AztecTypes.AztecAssetType.ERC20) revert IncorrectOutputAsset();
75+
76+
if (inputAssetA.assetType == AztecTypes.AztecAssetType.ETH) {
77+
ICETH cToken = ICETH(outputAssetA.erc20Address);
78+
cToken.mint{value: msg.value}();
79+
outputValueA = cToken.balanceOf(address(this));
80+
cToken.approve(ROLLUP_PROCESSOR, outputValueA);
81+
} else if (inputAssetA.assetType == AztecTypes.AztecAssetType.ERC20) {
82+
IERC20 tokenIn = IERC20(inputAssetA.erc20Address);
83+
ICERC20 tokenOut = ICERC20(outputAssetA.erc20Address);
84+
// Using safeIncreaseAllowance(...) instead of approve(...) here because tokenIn can be Tether
85+
tokenIn.safeIncreaseAllowance(address(tokenOut), totalInputValue);
86+
tokenOut.mint(totalInputValue);
87+
outputValueA = tokenOut.balanceOf(address(this));
88+
tokenOut.approve(ROLLUP_PROCESSOR, outputValueA);
89+
} else {
90+
revert IncorrectInputAsset();
91+
}
92+
} else if (auxData == 1) {
93+
// Redeem
94+
if (inputAssetA.assetType != AztecTypes.AztecAssetType.ERC20) revert IncorrectInputAsset();
95+
96+
if (outputAssetA.assetType == AztecTypes.AztecAssetType.ETH) {
97+
// Redeem cETH case
98+
ICETH cToken = ICETH(inputAssetA.erc20Address);
99+
cToken.redeem(totalInputValue);
100+
outputValueA = address(this).balance;
101+
IRollupProcessor(ROLLUP_PROCESSOR).receiveEthFromBridge{value: outputValueA}(interactionNonce);
102+
} else if (outputAssetA.assetType == AztecTypes.AztecAssetType.ERC20) {
103+
ICERC20 tokenIn = ICERC20(inputAssetA.erc20Address);
104+
IERC20 tokenOut = IERC20(outputAssetA.erc20Address);
105+
tokenIn.redeem(totalInputValue);
106+
outputValueA = tokenOut.balanceOf(address(this));
107+
// Using safeIncreaseAllowance(...) instead of approve(...) here because tokenOut can be Tether
108+
tokenOut.safeIncreaseAllowance(ROLLUP_PROCESSOR, outputValueA);
109+
} else {
110+
revert IncorrectOutputAsset();
111+
}
112+
} else {
113+
revert IncorrectAuxData();
114+
}
115+
}
116+
117+
function finalise(
118+
AztecTypes.AztecAsset calldata,
119+
AztecTypes.AztecAsset calldata,
120+
AztecTypes.AztecAsset calldata,
121+
AztecTypes.AztecAsset calldata,
122+
uint256,
123+
uint64
124+
)
125+
external
126+
payable
127+
returns (
128+
uint256,
129+
uint256,
130+
bool
131+
)
132+
{
133+
revert AsyncModeDisabled();
134+
}
135+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pragma solidity >=0.8.4;
2+
3+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
4+
5+
interface ICERC20 is IERC20 {
6+
function accrueInterest() external;
7+
8+
function balanceOfUnderlying(address) external view returns (uint256);
9+
10+
function exchangeRateStored() external view returns (uint256);
11+
12+
function mint(uint256) external returns (uint256);
13+
14+
function redeem(uint256) external returns (uint256);
15+
16+
function redeemUnderlying(uint256) external returns (uint256);
17+
18+
function underlying() external returns (address);
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pragma solidity >=0.8.4;
2+
3+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
4+
5+
interface ICETH is IERC20 {
6+
function exchangeRateStored() external view returns (uint256);
7+
8+
function mint() external payable;
9+
10+
function redeem(uint256) external returns (uint256);
11+
12+
function redeemUnderlying(uint256) external returns (uint256);
13+
}

src/test/compound/Compound.t.sol

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
// Copyright 2022 Spilsbury Holdings Ltd
3+
pragma solidity >=0.8.4;
4+
5+
import {Test} from "forge-std/Test.sol";
6+
import {AztecTypes} from "./../../aztec/AztecTypes.sol";
7+
import {DefiBridgeProxy} from "./../../aztec/DefiBridgeProxy.sol";
8+
import {RollupProcessor} from "./../../aztec/RollupProcessor.sol";
9+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10+
import {ICERC20} from "./../../bridges/compound/interfaces/ICERC20.sol";
11+
import {CompoundBridge} from "./../../bridges/compound/CompoundBridge.sol";
12+
13+
contract CompoundTest is Test {
14+
// solhint-disable-next-line
15+
address public constant cETH = 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5;
16+
17+
address[] public cTokens = [
18+
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c, // cAAve
19+
0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E, // cBAT
20+
0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4, // cCOMP
21+
0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643, // cDAI
22+
0xFAce851a4921ce59e912d19329929CE6da6EB0c7, // cLINK
23+
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b, // cMKR
24+
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7, // cSUSHI
25+
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86, // cTUSD
26+
0x35A18000230DA775CAc24873d00Ff85BccdeD550, // cUNI
27+
0x39AA39c021dfbaE8faC545936693aC917d5E7563, // cUSDC
28+
0x041171993284df560249B57358F931D9eB7b925D, // cUSDP
29+
0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9, // cUSDT
30+
0xccF4429DB6322D5C611ee964527D42E5d685DD6a, // cWBTC2
31+
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946, // cYFI
32+
0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407 // cZRX
33+
];
34+
35+
DefiBridgeProxy internal defiBridgeProxy;
36+
RollupProcessor internal rollupProcessor;
37+
38+
CompoundBridge internal compoundBridge;
39+
40+
function setUp() public {
41+
defiBridgeProxy = new DefiBridgeProxy();
42+
rollupProcessor = new RollupProcessor(address(defiBridgeProxy));
43+
compoundBridge = new CompoundBridge(address(rollupProcessor));
44+
}
45+
46+
function testETHDepositAndWithdrawal(uint88 depositAmount) public {
47+
vm.assume(depositAmount > 1e9);
48+
vm.deal(address(rollupProcessor), depositAmount);
49+
50+
AztecTypes.AztecAsset memory empty;
51+
AztecTypes.AztecAsset memory depositInputAssetA = AztecTypes.AztecAsset({
52+
id: 1,
53+
erc20Address: address(0x0000000000000000000000000000000000000000),
54+
assetType: AztecTypes.AztecAssetType.ETH
55+
});
56+
AztecTypes.AztecAsset memory depositOutputAssetA = AztecTypes.AztecAsset({
57+
id: 2,
58+
erc20Address: cETH,
59+
assetType: AztecTypes.AztecAssetType.ERC20
60+
});
61+
62+
// cETH minting
63+
(uint256 outputValueA, , ) = rollupProcessor.convert(
64+
address(compoundBridge),
65+
depositInputAssetA,
66+
empty,
67+
depositOutputAssetA,
68+
empty,
69+
depositAmount,
70+
1,
71+
0
72+
);
73+
74+
assertGt(outputValueA, 0, "cETH received is zero");
75+
76+
uint256 redeemAmount = outputValueA;
77+
AztecTypes.AztecAsset memory redeemInputAssetA = depositOutputAssetA;
78+
AztecTypes.AztecAsset memory redeemOutputAssetA = depositInputAssetA;
79+
80+
// withdrawing ETH (cETH burning)
81+
(outputValueA, , ) = rollupProcessor.convert(
82+
address(compoundBridge),
83+
redeemInputAssetA,
84+
empty,
85+
redeemOutputAssetA,
86+
empty,
87+
redeemAmount,
88+
1,
89+
1
90+
);
91+
92+
// ETH withdrawn should be approximately equal to ETH deposited
93+
// --> the amounts are not the same due to rounding errors in Compound
94+
assertLt(
95+
depositAmount - outputValueA,
96+
1e10,
97+
"amount of ETH withdrawn is not similar to the amount of ETH deposited"
98+
);
99+
}
100+
101+
function testERC20DepositAndWithdrawal(uint88 depositAmount) public {
102+
// Note: if Foundry implements parametrized tests remove this for loop,
103+
// stop calling setup() from _depositAndWithdrawERC20 and use the native
104+
// functionality
105+
106+
// For the tests to pass a return value of cTokens has to be >0.
107+
// Since the ratio of token/cToken is very skewed plenty of the times
108+
// I set relatively high minimum value.
109+
vm.assume(depositAmount > 1e14);
110+
for (uint256 i; i < cTokens.length; ++i) {
111+
_depositAndWithdrawERC20(cTokens[i], depositAmount);
112+
}
113+
}
114+
115+
function _depositAndWithdrawERC20(address cToken, uint256 depositAmount) private {
116+
setUp();
117+
address underlyingToken = ICERC20(cToken).underlying();
118+
119+
deal(underlyingToken, address(rollupProcessor), depositAmount);
120+
121+
AztecTypes.AztecAsset memory empty;
122+
AztecTypes.AztecAsset memory depositInputAssetA = AztecTypes.AztecAsset({
123+
id: 1,
124+
erc20Address: underlyingToken,
125+
assetType: AztecTypes.AztecAssetType.ERC20
126+
});
127+
AztecTypes.AztecAsset memory depositOutputAssetA = AztecTypes.AztecAsset({
128+
id: 2,
129+
erc20Address: cToken,
130+
assetType: AztecTypes.AztecAssetType.ERC20
131+
});
132+
133+
// cToken minting
134+
(uint256 outputValueA, , ) = rollupProcessor.convert(
135+
address(compoundBridge),
136+
depositInputAssetA,
137+
empty,
138+
depositOutputAssetA,
139+
empty,
140+
depositAmount,
141+
0,
142+
0
143+
);
144+
145+
assertGt(outputValueA, 0, "cToken received is zero");
146+
147+
uint256 redeemAmount = outputValueA;
148+
AztecTypes.AztecAsset memory redeemInputAssetA = depositOutputAssetA;
149+
AztecTypes.AztecAsset memory redeemOutputAssetA = depositInputAssetA;
150+
151+
// withdrawing underlying (cToken burning)
152+
(outputValueA, , ) = rollupProcessor.convert(
153+
address(compoundBridge),
154+
redeemInputAssetA,
155+
empty,
156+
redeemOutputAssetA,
157+
empty,
158+
redeemAmount,
159+
1,
160+
1
161+
);
162+
163+
// token withdrawn should be approximately equal to token deposited
164+
// --> the amounts are not exactly the same due to rounding errors in Compound
165+
assertLt(
166+
depositAmount - outputValueA,
167+
1e10,
168+
"amount of underlying Token withdrawn is not similar to the amount of cToken deposited"
169+
);
170+
}
171+
172+
function testInvalidCaller() public {
173+
AztecTypes.AztecAsset memory empty;
174+
175+
vm.expectRevert(CompoundBridge.InvalidCaller.selector);
176+
compoundBridge.convert(empty, empty, empty, empty, 0, 0, 0, address(0));
177+
}
178+
179+
function testIncorrectInputAsset() public {
180+
AztecTypes.AztecAsset memory empty;
181+
182+
AztecTypes.AztecAsset memory ethAsset = AztecTypes.AztecAsset({
183+
id: 1,
184+
erc20Address: address(0x0000000000000000000000000000000000000000),
185+
assetType: AztecTypes.AztecAssetType.ETH
186+
});
187+
188+
vm.prank(address(rollupProcessor));
189+
vm.expectRevert(CompoundBridge.IncorrectInputAsset.selector);
190+
compoundBridge.convert(ethAsset, empty, empty, empty, 0, 0, 1, address(0));
191+
}
192+
193+
function testIncorrectOutputAsset() public {
194+
AztecTypes.AztecAsset memory empty;
195+
196+
vm.prank(address(rollupProcessor));
197+
vm.expectRevert(CompoundBridge.IncorrectOutputAsset.selector);
198+
compoundBridge.convert(empty, empty, empty, empty, 0, 0, 0, address(0));
199+
}
200+
201+
function testIncorrectAuxData() public {
202+
AztecTypes.AztecAsset memory empty;
203+
204+
vm.prank(address(rollupProcessor));
205+
vm.expectRevert(CompoundBridge.IncorrectAuxData.selector);
206+
compoundBridge.convert(empty, empty, empty, empty, 0, 0, 2, address(0));
207+
}
208+
}

0 commit comments

Comments
 (0)