Skip to content
This repository was archived by the owner on Jan 18, 2023. It is now read-only.

Commit 1c6c5fc

Browse files
authored
Merge pull request #15 from SetProtocol/0.2.0-partial-redeem
Partial Redeem Implementation and Happy Path Tests
2 parents edb9a0c + 0649de7 commit 1c6c5fc

File tree

5 files changed

+409
-194
lines changed

5 files changed

+409
-194
lines changed

contracts/SetToken.sol

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ contract SetToken is StandardToken, DetailedERC20("", "", 18), Set {
1919
uint256 public totalSupply;
2020
address[] public components;
2121
uint[] public units;
22+
mapping(address => bool) internal isComponent;
23+
24+
25+
struct PartialRedeemStatus {
26+
uint unredeemedBalance;
27+
bool isRedeemed;
28+
}
29+
30+
// Mapping of token address -> user address -> partialRedeemStatus
31+
mapping(address => mapping(address => PartialRedeemStatus)) public unredeemedComponents;
32+
33+
event LogPartialRedemption(
34+
address indexed _sender,
35+
uint indexed _quantity
36+
);
37+
38+
modifier hasSufficientBalance(uint quantity) {
39+
// Check that the sender has sufficient components
40+
// Since the component length is defined ahead of time, this is not
41+
// an unbounded loop
42+
require(balances[msg.sender] >= quantity);
43+
_;
44+
}
45+
46+
modifier preventRedeemReEntrancy(address sender, uint quantity) {
47+
// To prevent re-entrancy attacks, decrement the user's Set balance
48+
balances[sender] = balances[sender].sub(quantity);
49+
50+
// Decrement the total token supply
51+
totalSupply = totalSupply.sub(quantity);
52+
_;
53+
}
2254

2355
/**
2456
* @dev Constructor Function for the issuance of an {Set} token
@@ -43,6 +75,9 @@ contract SetToken is StandardToken, DetailedERC20("", "", 18), Set {
4375
// Check that all addresses are non-zero
4476
address currentComponent = _components[i];
4577
require(currentComponent != address(0));
78+
79+
// add component to isComponent mapping
80+
isComponent[currentComponent] = true;
4681
}
4782

4883
// As looping operations are expensive, checking for duplicates will be
@@ -99,20 +134,14 @@ contract SetToken is StandardToken, DetailedERC20("", "", 18), Set {
99134
*
100135
* The ERC20 components do not need to be approved to call this function
101136
*
102-
* @param quantity uint The quantity of components desired to redeem in Wei
137+
* @param quantity uint The quantity of Sets desired to redeem in Wei
103138
*/
104-
function redeem(uint quantity) public returns (bool success) {
105-
// Check that the sender has sufficient components
106-
// Since the component length is defined ahead of time, this is not
107-
// an unbounded loop
108-
require(balances[msg.sender] >= quantity);
109-
110-
// To prevent re-entrancy attacks, decrement the user's Set balance
111-
balances[msg.sender] = balances[msg.sender].sub(quantity);
112-
113-
// Decrement the total token supply
114-
totalSupply = totalSupply.sub(quantity);
115-
139+
function redeem(uint quantity)
140+
public
141+
hasSufficientBalance(quantity)
142+
preventRedeemReEntrancy(msg.sender, quantity)
143+
returns (bool success)
144+
{
116145
for (uint i = 0; i < components.length; i++) {
117146
address currentComponent = components[i];
118147
uint currentUnits = units[i];
@@ -135,6 +164,96 @@ contract SetToken is StandardToken, DetailedERC20("", "", 18), Set {
135164
return true;
136165
}
137166

167+
/**
168+
* @dev Function to withdraw a portion of the component tokens of a Set
169+
*
170+
* This function should be used in the event that a component token has been
171+
* paused for transfer temporarily or permanently. This allows users a
172+
* method to withdraw tokens in the event that one token has been frozen
173+
*
174+
* @param quantity uint The quantity of Sets desired to redeem in Wei
175+
* @param excludedComponents address[] The list of tokens to exclude
176+
*/
177+
function partialRedeem(uint quantity, address[] excludedComponents)
178+
public
179+
hasSufficientBalance(quantity)
180+
preventRedeemReEntrancy(msg.sender, quantity)
181+
returns (bool success)
182+
{
183+
// Excluded tokens should be less than the number of components
184+
// Otherwise, use the normal redeem function
185+
require(excludedComponents.length < components.length);
186+
require(excludedComponents.length > 0);
187+
188+
for (uint i = 0; i < components.length; i++) {
189+
bool isExcluded = false;
190+
191+
// Transfer value is defined as the currentUnits (in GWei)
192+
// multiplied by quantity in Wei divided by the units of gWei.
193+
// We do this to allow fractional units to be defined
194+
uint transferValue = units[i].fxpMul(quantity, 10**9);
195+
196+
// Protect against the case that the gWei divisor results in a value that is
197+
// 0 and the user is able to generate Sets without sending a balance
198+
assert(transferValue > 0);
199+
200+
// This is unideal to do a doubly nested loop, but the number of excludedComponents
201+
// should generally be a small number
202+
for (uint j = 0; j < excludedComponents.length; j++) {
203+
address currentExcluded = excludedComponents[j];
204+
205+
// Check that excluded token is indeed a component in this contract
206+
assert(isComponent[currentExcluded]);
207+
208+
// If the token is excluded, add to the user's unredeemed component value
209+
if (components[i] == currentExcluded) {
210+
// Ensures there are no duplicates
211+
bool currentIsRedeemed = unredeemedComponents[components[i]][msg.sender].isRedeemed;
212+
assert(currentIsRedeemed == false);
213+
214+
unredeemedComponents[components[i]][msg.sender].unredeemedBalance += transferValue;
215+
216+
// Mark redeemed to ensure no duplicates
217+
unredeemedComponents[components[i]][msg.sender].isRedeemed = true;
218+
219+
isExcluded = true;
220+
221+
}
222+
}
223+
224+
if (isExcluded == false) {
225+
// The transaction will fail if any of the components fail to transfer
226+
assert(ERC20(components[i]).transfer(msg.sender, transferValue));
227+
}
228+
}
229+
230+
// Mark all excluded components not redeemed
231+
for (uint k = 0; k < excludedComponents.length; k++) {
232+
address currentExcludedToUnredeem = excludedComponents[k];
233+
unredeemedComponents[currentExcludedToUnredeem][msg.sender].isRedeemed = false;
234+
}
235+
236+
LogPartialRedemption(msg.sender, quantity);
237+
238+
return true;
239+
}
240+
241+
function redeemExcluded(uint quantity, address excludedComponent)
242+
public
243+
returns (bool success)
244+
{
245+
// Check there is enough balance
246+
uint remainingBalance = unredeemedComponents[excludedComponent][msg.sender].unredeemedBalance;
247+
require(remainingBalance >= quantity);
248+
249+
// To prevent re-entrancy attacks, decrement the user's Set balance
250+
unredeemedComponents[excludedComponent][msg.sender].unredeemedBalance = remainingBalance.sub(quantity);
251+
252+
assert(ERC20(excludedComponent).transfer(msg.sender, quantity));
253+
254+
return true;
255+
}
256+
138257
function componentCount() public view returns(uint componentsLength) {
139258
return components.length;
140259
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"deploy:development": "bash scripts/deploy_development.sh",
2222
"dist": "bash scripts/prepare_dist.sh",
2323
"clean": "rm -rf build",
24+
"compile": "truffle compile",
2425
"test": "truffle compile --all && yarn run generate-typings && yarn run transpile && truffle test transpiled/test/*.js",
2526
"transpile": "tsc",
2627
"generate-typings": "abi-gen --abis './build/contracts/*.json' --out './types/generated' --template './types/contract_templates/contract.mustache' --partials './types/contract_templates/partials/*.mustache' && yarn run rename-generated-abi",

0 commit comments

Comments
 (0)