Skip to content

Commit 31e51ff

Browse files
ckoopmannFlattestWhiteChristian Koopmann
authored
Exchange issuance zero ex (#85)
* Implement exchange issuance via 0x Api / Exchange Router Co-authored-by: Richard Guan <[email protected]> Co-authored-by: Christian Koopmann <[email protected]>
1 parent 5d0175f commit 31e51ff

22 files changed

+3504
-425
lines changed

contracts/exchangeIssuance/ExchangeIssuanceZeroEx.sol

Lines changed: 525 additions & 0 deletions
Large diffs are not rendered by default.

contracts/interfaces/IBasicIssuanceModule.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface IBasicIssuanceModule {
1919
function getRequiredComponentUnitsForIssue(
2020
ISetToken _setToken,
2121
uint256 _quantity
22-
) external returns(address[] memory, uint256[] memory);
22+
) external view returns(address[] memory, uint256[] memory);
2323
function issue(ISetToken _setToken, uint256 _quantity, address _to) external;
2424
function redeem(ISetToken _token, uint256 _quantity, address _to) external;
25-
}
25+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2020 Set Labs Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
SPDX-License-Identifier: Apache License, Version 2.0
13+
*/
14+
pragma solidity >=0.6.10;
15+
16+
import { ISetToken } from "./ISetToken.sol";
17+
18+
interface IDebtIssuanceModule {
19+
function getRequiredComponentIssuanceUnits(
20+
ISetToken _setToken,
21+
uint256 _quantity
22+
) external view returns (address[] memory, uint256[] memory, uint256[] memory);
23+
function getRequiredComponentRedemptionUnits(
24+
ISetToken _setToken,
25+
uint256 _quantity
26+
) external view returns (address[] memory, uint256[] memory, uint256[] memory);
27+
function issue(ISetToken _setToken, uint256 _quantity, address _to) external;
28+
function redeem(ISetToken _token, uint256 _quantity, address _to) external;
29+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// SPDX-License-Identifier: Apache License, Version 2.0
2+
pragma solidity 0.6.10;
3+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
4+
5+
6+
7+
contract ZeroExExchangeProxyMock {
8+
9+
// Originally I also wanted to test Custom error handling,
10+
// but refrained from doing so, since the necessary upgrade of ethers lead to a lot of typescript issues.
11+
// TODO: Add Custom error handling test when ethers.js is upgraded to a compatible version
12+
enum ErrorType {
13+
None,
14+
RevertMessage,
15+
CustomError
16+
}
17+
18+
// Mappings to control amount of buy / sell token transfered
19+
mapping(address => uint256) public buyAmountMultipliers;
20+
mapping(address => uint256) public sellAmountMultipliers;
21+
mapping(address => ErrorType) public errorMapping;
22+
23+
24+
string public constant testRevertMessage = "test revert message";
25+
26+
// Method mocking the UniswapFeature of the zeroEx setup in tests
27+
// Returns the `minBuyAmount` of target token to the caller, which needs to be deposited into this contract beforehand
28+
// Original Implementation: https://github.com/0xProject/protocol/blob/development/contracts/zero-ex/contracts/src/features/UniswapFeature.sol#L99
29+
function sellToUniswap(
30+
IERC20[] calldata tokens,
31+
uint256 sellAmount,
32+
uint256 minBuyAmount,
33+
bool // isSushi
34+
)
35+
external
36+
payable
37+
returns (uint256 buyAmount)
38+
{
39+
require(tokens.length > 1, "UniswapFeature/InvalidTokensLength");
40+
IERC20 sellToken = tokens[0];
41+
IERC20 buyToken = tokens[tokens.length - 1];
42+
43+
44+
_throwErrorIfNeeded(sellToken);
45+
46+
uint256 multipliedSellAmount = getSellAmount(sellToken, sellAmount);
47+
sellToken.transferFrom(msg.sender, address(this), multipliedSellAmount);
48+
49+
buyAmount = getBuyAmount(buyToken, minBuyAmount);
50+
buyToken.transfer(msg.sender, buyAmount);
51+
}
52+
53+
function _throwErrorIfNeeded(IERC20 sellToken) internal
54+
{
55+
if (errorMapping[address(sellToken)] == ErrorType.RevertMessage) {
56+
revert(testRevertMessage);
57+
}
58+
}
59+
60+
function getBuyAmount(
61+
IERC20 buyToken,
62+
uint256 minBuyAmount
63+
) public view returns (uint256 buyAmount) {
64+
uint256 buyMultiplier = buyAmountMultipliers[address(buyToken)];
65+
if (buyMultiplier == 0) {
66+
buyAmount = minBuyAmount;
67+
}
68+
else{
69+
buyAmount = (minBuyAmount * buyMultiplier) / 10**18;
70+
}
71+
}
72+
73+
// Function to adjust the amount of buy token that will be returned
74+
// Set to 0 to disable / i.e. always return exact minBuyAmount
75+
function setBuyMultiplier(
76+
IERC20 buyToken,
77+
uint256 multiplier
78+
) public {
79+
buyAmountMultipliers[address(buyToken)] = multiplier;
80+
}
81+
82+
function getSellAmount(
83+
IERC20 sellToken,
84+
uint256 inputSellAmount
85+
) public view returns (uint256 sellAmount) {
86+
uint256 sellMultiplier = sellAmountMultipliers[address(sellToken)];
87+
if (sellMultiplier == 0) {
88+
sellAmount = inputSellAmount;
89+
}
90+
else{
91+
sellAmount = (inputSellAmount * sellMultiplier) / 10**18;
92+
}
93+
}
94+
95+
// Function to adjust the amount of sell token that will be returned
96+
// Set to 0 to disable / i.e. always return exact minSellAmount
97+
function setSellMultiplier(
98+
IERC20 sellToken,
99+
uint256 multiplier
100+
) public {
101+
sellAmountMultipliers[address(sellToken)] = multiplier;
102+
}
103+
104+
function setErrorMapping(
105+
address sellToken,
106+
ErrorType errorType
107+
) public {
108+
errorMapping[sellToken] = errorType;
109+
}
110+
}

external/contracts/set/DebtIssuanceModule.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,4 @@ contract DebtIssuanceModule is ModuleBase, ReentrancyGuard {
690690
}
691691
}
692692
}
693-
}
693+
}

hardhat.config.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import "./tasks";
1111

1212
const forkingConfig = {
1313
url: "https://eth-mainnet.alchemyapi.io/v2/" + process.env.ALCHEMY_TOKEN,
14-
blockNumber: 11649166,
14+
blockNumber: process.env.LATESTBLOCK ? undefined : 11649166,
1515
};
1616

17+
const INTEGRATIONTEST_TIMEOUT = 600000;
18+
1719
const mochaConfig = {
1820
grep: "@forked-network",
19-
invert: (process.env.FORK) ? false : true,
20-
timeout: (process.env.FORK) ? 50000 : 40000,
21+
invert: process.env.FORK ? false : true,
22+
timeout: process.env.INTEGRATIONTEST ? INTEGRATIONTEST_TIMEOUT : process.env.FORK ? 50000 : 40000,
2123
} as Mocha.MochaOptions;
2224

2325
const config: HardhatUserConfig = {
@@ -32,15 +34,19 @@ const config: HardhatUserConfig = {
3234
},
3335
networks: {
3436
hardhat: {
35-
forking: (process.env.FORK) ? forkingConfig : undefined,
37+
forking: process.env.FORK ? forkingConfig : undefined,
3638
accounts: getHardhatPrivateKeys(),
3739
gas: 12000000,
3840
blockGasLimit: 12000000,
41+
// @ts-ignore
42+
timeout: INTEGRATIONTEST_TIMEOUT,
43+
initialBaseFeePerGas: 0,
3944
},
4045
localhost: {
4146
url: "http://127.0.0.1:8545",
4247
gas: 12000000,
4348
blockGasLimit: 12000000,
49+
timeout: INTEGRATIONTEST_TIMEOUT,
4450
},
4551
kovan: {
4652
url: "https://kovan.infura.io/v3/" + process.env.INFURA_TOKEN,
@@ -61,7 +67,7 @@ const config: HardhatUserConfig = {
6167
};
6268

6369
function getHardhatPrivateKeys() {
64-
return privateKeys.map(key => {
70+
return privateKeys.map((key) => {
6571
const TEN_MILLION_ETH = "10000000000000000000000000";
6672
return {
6773
privateKey: key,

scripts/calculateEIGasCosts.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { ethers } from "hardhat";
2+
import { BigNumber } from "ethers";
3+
import axios from "axios";
4+
import qs from "qs";
5+
6+
const MAX_RETRIES = 40;
7+
const RETRY_STATUSES = [503, 429];
8+
const API_QUOTE_URL = "https://api.0x.org/swap/v1/quote";
9+
10+
export const sleep = (ms: number) => {
11+
return new Promise((resolve) => setTimeout(resolve, ms));
12+
};
13+
14+
async function getQuote(params: any, retryCount: number = 0): Promise<any> {
15+
try {
16+
const url = `${API_QUOTE_URL}?${qs.stringify(params)}`;
17+
const response = await axios(url);
18+
return response.data;
19+
} catch (error) {
20+
if (RETRY_STATUSES.includes(error.response?.status) && retryCount < MAX_RETRIES) {
21+
await sleep(1000);
22+
return await getQuote(params, retryCount + 1);
23+
} else {
24+
throw error;
25+
}
26+
}
27+
}
28+
29+
async function getQuotes(
30+
positions: any[],
31+
inputToken: string,
32+
setAmount: number,
33+
wethStage: boolean,
34+
) {
35+
const componentSwapInputToken = wethStage ? "WETH" : inputToken;
36+
const componentInputTokenAddress = TOKEN_ADDRESSES[componentSwapInputToken];
37+
const quotes = await getPositionQuotes(positions, componentInputTokenAddress, setAmount);
38+
if (wethStage) {
39+
const wethBuyAmount = quotes.reduce(
40+
(sum: BigNumber | number, quote: any) => BigNumber.from(quote.sellAmount).add(sum),
41+
0,
42+
);
43+
const wethQuote = await getQuote({
44+
buyToken: TOKEN_ADDRESSES["WETH"],
45+
sellToken: TOKEN_ADDRESSES[inputToken],
46+
buyAmount: wethBuyAmount.toString(),
47+
});
48+
quotes.push(wethQuote);
49+
}
50+
return quotes;
51+
}
52+
53+
async function getPositionQuotes(
54+
positions: any[],
55+
inputTokenAddress: string,
56+
setAmount: number,
57+
): Promise<any[]> {
58+
const promises = positions.map((position: any) => {
59+
if (
60+
ethers.utils.getAddress(position.component) === ethers.utils.getAddress(inputTokenAddress)
61+
) {
62+
console.log("No swap needed");
63+
return Promise.resolve({ gas: "0", sellAmount: position.unit.mul(setAmount).toString() });
64+
} else {
65+
const params = {
66+
buyToken: position.component,
67+
sellToken: inputTokenAddress,
68+
buyAmount: position.unit.mul(setAmount).toString(),
69+
};
70+
return getQuote(params);
71+
}
72+
});
73+
return await Promise.all(promises);
74+
}
75+
76+
type GasCostRow = {
77+
setToken: string;
78+
inputToken: string;
79+
setAmount: number;
80+
wethStage: boolean;
81+
gas: number;
82+
};
83+
84+
const TOKEN_ADDRESSES: Record<string, string> = {
85+
DPI: "0x1494ca1f11d487c2bbe4543e90080aeba4ba3c2b",
86+
DAI: "0x6b175474e89094c44da98b954eedeac495271d0f",
87+
UNI: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
88+
WETH: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
89+
};
90+
91+
async function calculateTotalGas(
92+
setToken: string,
93+
inputToken: string,
94+
setAmount: number,
95+
wethStage: boolean,
96+
): Promise<GasCostRow> {
97+
const setAddress = TOKEN_ADDRESSES[setToken];
98+
const setContract = await ethers.getContractAt("ISetToken", setAddress);
99+
const positions = await setContract.getPositions();
100+
const positionQuotes = await getQuotes(positions, inputToken, setAmount, wethStage);
101+
const gas = positionQuotes.reduce((sum: number, quote: any) => sum + parseInt(quote.gas), 0);
102+
return { setToken, inputToken, setAmount, wethStage, gas };
103+
}
104+
105+
//@ts-ignore
106+
const f = (a, b) => [].concat(...a.map((d) => b.map((e) => [].concat(d, e))));
107+
//@ts-ignore
108+
const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);
109+
110+
async function main() {
111+
const setTokens = ["DPI"];
112+
const inputTokens = ["DAI"];
113+
const setAmounts = [100, 1000, 10000];
114+
const wethStage = [false, true];
115+
const scenarios = cartesian(setTokens, inputTokens, setAmounts, wethStage);
116+
const promises = scenarios.map((params: [string, string, number, boolean]) =>
117+
calculateTotalGas(params[0], params[1], params[2], params[3]),
118+
);
119+
const results = await Promise.all(promises);
120+
console.table(results);
121+
}
122+
main()
123+
.then(() => process.exit(0))
124+
.catch((error) => {
125+
console.error(error);
126+
process.exit(1);
127+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ethers } from "hardhat";
2+
3+
async function main() {
4+
const ExchangeIssuanceZeroEx = await ethers.getContractFactory("ExchangeIssuanceZeroEx");
5+
// Mainnet addresses
6+
const wethAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
7+
const controllerAddress = "0xa4c8d221d8bb851f83aadd0223a8900a6921a349";
8+
const issuanceModuleAddress = "0xd8EF3cACe8b4907117a45B0b125c68560532F94D";
9+
const zeroExProxyAddress = "0xDef1C0ded9bec7F1a1670819833240f027b25EfF";
10+
11+
const dpiAddress = "0x1494ca1f11d487c2bbe4543e90080aeba4ba3c2b";
12+
13+
const exchangeIssuanceZeroEx = await ExchangeIssuanceZeroEx.deploy(
14+
wethAddress,
15+
controllerAddress,
16+
issuanceModuleAddress,
17+
zeroExProxyAddress,
18+
);
19+
console.log("Exchange Issuacne deployed to", exchangeIssuanceZeroEx.address);
20+
await exchangeIssuanceZeroEx.approveSetToken(dpiAddress);
21+
console.log("Approved dpi token");
22+
}
23+
main()
24+
.then(() => process.exit(0))
25+
.catch((error) => {
26+
console.error(error);
27+
process.exit(1);
28+
});

0 commit comments

Comments
 (0)