Skip to content

Commit da2424a

Browse files
pblivin0xcgewecke
andauthored
feat(BatchTradeExtension): Add BatchTradeExtension [SIM-239] (#30)
* add BatchTradeExtension Co-authored-by: cgewecke <[email protected]>
1 parent 70aa470 commit da2424a

File tree

12 files changed

+1372
-7
lines changed

12 files changed

+1372
-7
lines changed

.circleci/config.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,21 @@ jobs:
6060
- run:
6161
name: Set Up Environment Variables
6262
command: cp .env.default .env
63+
- run:
64+
name: Create shared coverage outputs folder
65+
command: mkdir -p /tmp/forked_coverage
6366
- run:
6467
name: Hardhat Test
65-
command: yarn test:fork
68+
command: yarn test:fork:coverage
69+
- run:
70+
name: Save coverage
71+
command: |
72+
cp coverage.json /tmp/forked_coverage/forked_cov.json
73+
chmod -R 777 /tmp/forked_coverage/forked_cov.json
74+
- persist_to_workspace:
75+
root: /tmp/forked_coverage
76+
paths:
77+
- forked_cov.json
6678

6779
coverage:
6880
docker:
@@ -109,18 +121,22 @@ jobs:
109121
steps:
110122
- attach_workspace:
111123
at: /tmp/coverage
124+
- attach_workspace:
125+
at: /tmp/forked_coverage
112126
- restore_cache:
113127
key: compiled-env-{{ .Environment.CIRCLE_SHA1 }}
114128
- run:
115129
name: Combine coverage reports
116130
command: |
117131
cp -R /tmp/coverage/* .
132+
cp -R /tmp/forked_coverage/* .
118133
npx istanbul-combine-updated -r lcov \
119134
cov_0.json \
120135
cov_1.json \
121136
cov_2.json \
122137
cov_3.json \
123-
cov_4.json
138+
cov_4.json \
139+
forked_cov.json
124140
- run:
125141
name: Upload coverage
126142
command: |
@@ -143,3 +159,4 @@ workflows:
143159
- report_coverage:
144160
requires:
145161
- coverage
162+
- test_forked_network
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
Copyright 2022 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache License, Version 2.0
17+
*/
18+
19+
pragma solidity 0.6.10;
20+
pragma experimental "ABIEncoderV2";
21+
22+
import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol";
23+
import { ITradeModule } from "@setprotocol/set-protocol-v2/contracts/interfaces/ITradeModule.sol";
24+
25+
import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol";
26+
import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol";
27+
import { IManagerCore } from "../interfaces/IManagerCore.sol";
28+
29+
/**
30+
* @title BatchTradeExtension
31+
* @author Set Protocol
32+
*
33+
* Smart contract global extension which provides DelegatedManager privileged operator(s) the ability to execute a
34+
* batch of trade on a DEX and the owner the ability to restrict operator(s) permissions with an asset whitelist.
35+
*/
36+
contract BatchTradeExtension is BaseGlobalExtension {
37+
38+
/* ============ Structs ============ */
39+
40+
struct TradeInfo {
41+
string exchangeName; // Human readable name of the exchange in the integrations registry
42+
address sendToken; // Address of the token to be sent to the exchange
43+
uint256 sendQuantity; // Units of token in SetToken sent to the exchange
44+
address receiveToken; // Address of the token that will be received from the exchange
45+
uint256 minReceiveQuantity; // Min units of token in SetToken to be received from the exchange
46+
bytes data; // Arbitrary bytes to be used to construct trade call data
47+
}
48+
49+
/* ============ Events ============ */
50+
51+
event BatchTradeExtensionInitialized(
52+
address indexed _setToken,
53+
address indexed _delegatedManager
54+
);
55+
56+
event StringTradeFailed(
57+
address indexed _setToken, // SetToken which failed trade targeted
58+
uint256 _index, // Index of trade that failed in _trades parameter of batchTrade call
59+
string _reason // String reason for the failure
60+
);
61+
62+
event BytesTradeFailed(
63+
address indexed _setToken, // SetToken which failed trade targeted
64+
uint256 _index, // Index of trade that failed in _trades parameter of batchTrade call
65+
bytes _reason // Custom error bytes encoding reason for failure
66+
);
67+
68+
/* ============ State Variables ============ */
69+
70+
// Instance of TradeModule
71+
ITradeModule public immutable tradeModule;
72+
73+
/* ============ Modifiers ============ */
74+
75+
/**
76+
* Throws if any assets are not allowed to be held by the Set
77+
*/
78+
modifier onlyAllowedAssets(ISetToken _setToken, TradeInfo[] memory _trades) {
79+
for(uint256 i = 0; i < _trades.length; i++) {
80+
require(_manager(_setToken).isAllowedAsset(_trades[i].receiveToken), "Must be allowed asset");
81+
}
82+
_;
83+
}
84+
85+
/* ============ Constructor ============ */
86+
87+
constructor(
88+
IManagerCore _managerCore,
89+
ITradeModule _tradeModule
90+
)
91+
public
92+
BaseGlobalExtension(_managerCore)
93+
{
94+
tradeModule = _tradeModule;
95+
}
96+
97+
/* ============ External Functions ============ */
98+
99+
/**
100+
* ONLY OWNER: Initializes TradeModule on the SetToken associated with the DelegatedManager.
101+
*
102+
* @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for
103+
*/
104+
function initializeModule(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) {
105+
require(_delegatedManager.isInitializedExtension(address(this)), "Extension must be initialized");
106+
107+
_initializeModule(_delegatedManager.setToken(), _delegatedManager);
108+
}
109+
110+
/**
111+
* ONLY OWNER: Initializes BatchTradeExtension to the DelegatedManager.
112+
*
113+
* @param _delegatedManager Instance of the DelegatedManager to initialize
114+
*/
115+
function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) {
116+
require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending");
117+
118+
ISetToken setToken = _delegatedManager.setToken();
119+
120+
_initializeExtension(setToken, _delegatedManager);
121+
122+
emit BatchTradeExtensionInitialized(address(setToken), address(_delegatedManager));
123+
}
124+
125+
/**
126+
* ONLY OWNER: Initializes TradeExtension to the DelegatedManager and TradeModule to the SetToken
127+
*
128+
* @param _delegatedManager Instance of the DelegatedManager to initialize
129+
*/
130+
function initializeModuleAndExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager){
131+
require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending");
132+
133+
ISetToken setToken = _delegatedManager.setToken();
134+
135+
_initializeExtension(setToken, _delegatedManager);
136+
_initializeModule(setToken, _delegatedManager);
137+
138+
emit BatchTradeExtensionInitialized(address(setToken), address(_delegatedManager));
139+
}
140+
141+
/**
142+
* ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the BatchTradeExtension
143+
*/
144+
function removeExtension() external override {
145+
IDelegatedManager delegatedManager = IDelegatedManager(msg.sender);
146+
ISetToken setToken = delegatedManager.setToken();
147+
148+
_removeExtension(setToken, delegatedManager);
149+
}
150+
151+
/**
152+
* ONLY OPERATOR: Executes a batch of trades on a supported DEX. If any individual trades fail, events are emitted.
153+
* @dev Although the SetToken units are passed in for the send and receive quantities, the total quantity
154+
* sent and received is the quantity of SetToken units multiplied by the SetToken totalSupply.
155+
*
156+
* @param _setToken Instance of the SetToken to trade
157+
* @param _trades Struct of information for individual trades
158+
*/
159+
function batchTrade(
160+
ISetToken _setToken,
161+
TradeInfo[] memory _trades
162+
)
163+
external
164+
onlyOperator(_setToken)
165+
onlyAllowedAssets(_setToken, _trades)
166+
{
167+
for(uint256 i = 0; i < _trades.length; i++) {
168+
bytes memory callData = abi.encodeWithSignature(
169+
"trade(address,string,address,uint256,address,uint256,bytes)",
170+
_setToken,
171+
_trades[i].exchangeName,
172+
_trades[i].sendToken,
173+
_trades[i].sendQuantity,
174+
_trades[i].receiveToken,
175+
_trades[i].minReceiveQuantity,
176+
_trades[i].data
177+
);
178+
179+
// ZeroEx (for example) throws custom errors which slip through OpenZeppelin's
180+
// functionCallWithValue error management and surface here as `bytes`. These should be
181+
// decode-able off-chain given enough context about protocol targeted by the adapter.
182+
try _manager(_setToken).interactManager(address(tradeModule), callData) {}
183+
catch Error(string memory _err) {
184+
emit StringTradeFailed(address(_setToken), i, _err);
185+
} catch (bytes memory _err) {
186+
emit BytesTradeFailed(address(_setToken), i, _err);
187+
}
188+
}
189+
}
190+
191+
/* ============ Internal Functions ============ */
192+
193+
/**
194+
* Internal function to initialize TradeModule on the SetToken associated with the DelegatedManager.
195+
*
196+
* @param _setToken Instance of the SetToken corresponding to the DelegatedManager
197+
* @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for
198+
*/
199+
function _initializeModule(ISetToken _setToken, IDelegatedManager _delegatedManager) internal {
200+
bytes memory callData = abi.encodeWithSignature("initialize(address)", _setToken);
201+
_invokeManager(_delegatedManager, address(tradeModule), callData);
202+
}
203+
}

contracts/mocks/TradeAdapterMock.sol

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// SPDX-License-Identifier: Apache License, Version 2.0
22
pragma solidity 0.6.10;
3-
pragma experimental ABIEncoderV2;
43

54
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
65

@@ -25,13 +24,29 @@ contract TradeAdapterMock {
2524
address _destinationToken,
2625
address _destinationAddress,
2726
uint256 _sourceQuantity,
28-
uint256 /* _minDestinationQuantity */
27+
uint256 _minDestinationQuantity
2928
)
3029
external
3130
{
3231
uint256 destinationBalance = ERC20(_destinationToken).balanceOf(address(this));
3332
require(ERC20(_sourceToken).transferFrom(_destinationAddress, address(this), _sourceQuantity), "ERC20 TransferFrom failed");
34-
require(ERC20(_destinationToken).transfer(_destinationAddress, destinationBalance), "ERC20 transfer failed");
33+
if (_minDestinationQuantity == 1) { // byte revert case, min nonzero uint256 minimum receive quantity
34+
bytes memory data = abi.encodeWithSelector(
35+
bytes4(keccak256("trade(address,address,address,uint256,uint256)")),
36+
_sourceToken,
37+
_destinationToken,
38+
_destinationAddress,
39+
_sourceQuantity,
40+
_minDestinationQuantity
41+
);
42+
assembly { revert(add(data, 32), mload(data)) }
43+
}
44+
if (destinationBalance >= _minDestinationQuantity) { // normal case
45+
require(ERC20(_destinationToken).transfer(_destinationAddress, destinationBalance), "ERC20 transfer failed");
46+
}
47+
else { // string revert case, minimum destination quantity not in exchange
48+
revert("Insufficient funds in exchange");
49+
}
3550
}
3651

3752
/* ============ Adapter Functions ============ */

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"prepare": "yarn build",
3434
"prepublishOnly": "yarn clean && yarn build:npm",
3535
"test": "npx hardhat test --network localhost",
36+
"test:fork:coverage": "FORK=true COVERAGE=true npx hardhat coverage",
3637
"test:fork": "FORK=true npx hardhat test",
3738
"test:fork:fast": "NO_COMPILE=true TS_NODE_TRANSPILE_ONLY=1 FORK=true npx hardhat test --no-compile",
3839
"test:clean": "yarn clean && yarn build && yarn test",

0 commit comments

Comments
 (0)