-
Notifications
You must be signed in to change notification settings - Fork 125
/
Copy pathEthlanceStructs.sol
236 lines (202 loc) · 9.72 KB
/
EthlanceStructs.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
// import "@ganache/console.log/console.sol";
library EthlanceStructs {
enum TokenType {
ETH,
ERC20,
ERC721,
ERC1155
}
struct TokenContract {
TokenType tokenType;
address tokenAddress;
}
struct Token {
TokenContract tokenContract;
uint tokenId;
}
struct TokenValue {
Token token;
uint value;
}
struct Deposit {
address depositor;
// This (tokenValue) reflects the amount depositor has added - withdrawn
// It excludes (doesn't get updated for) the amounts paid out as invoices
TokenValue tokenValue;
}
function makeTokenValue(uint value, TokenType tokenType) public pure returns(TokenValue[] memory) {
EthlanceStructs.TokenValue[] memory single = new EthlanceStructs.TokenValue[](1);
single[0].value = value;
single[0].token.tokenContract.tokenAddress = address(0);
single[0].token.tokenContract.tokenType = tokenType;
return single;
}
function min(uint a, uint b) pure internal returns(uint) {
return a <= b ? a : b;
}
// Normally the contributors (job creator and those who have added funds) can withdraw all their funds
// at any point. This is not the case when there have already been payouts and thus the funds kept in
// this Job contract are less.
// In such case these users will be eligible up to what they've contributed limited to what's left in Job
//
// This method can be used to receive array of TokenValue-s with max amounts to be used
// for subsequent withdrawFunds call
function maxWithdrawableAmounts(address contributor,
bytes32[] memory depositIds,
mapping(bytes32 => Deposit) storage deposits
) public view returns(EthlanceStructs.TokenValue[] memory) {
EthlanceStructs.TokenValue[] memory withdrawables = new EthlanceStructs.TokenValue[](depositIds.length);
uint withdrawablesCount = 0;
for(uint i = 0; i < depositIds.length; i++) {
Deposit memory deposit = deposits[depositIds[i]];
if(deposit.depositor == contributor) {
TokenValue memory tv = deposit.tokenValue;
uint jobTokenBalance = tokenValueBalance(address(this), tv);
if (jobTokenBalance == 0) { break; } // Nothing to do if 0 tokens left of the kind
uint valueToWithdraw = min(jobTokenBalance, tv.value);
if (valueToWithdraw == 0) { break; } // Nothing to do if could withdraw 0
tv.value = valueToWithdraw;
withdrawables[withdrawablesCount] = tv;
withdrawablesCount += 1;
}
}
// Return only the ones that matched contributor (can't dynamically allocate in-memory array, need to reconstruct)
TokenValue[] memory compactWithdrawables = new TokenValue[](withdrawablesCount);
for(uint i = 0; i < withdrawablesCount; i++) {
compactWithdrawables[i] = withdrawables[i];
}
return compactWithdrawables;
}
function tokenValuesEqual(EthlanceStructs.TokenValue memory first, EthlanceStructs.TokenValue memory second) public pure returns (bool) {
bytes32 firstHash = keccak256(abi.encodePacked(first.value, first.token.tokenId, first.token.tokenContract.tokenAddress));
bytes32 secondHash = keccak256(abi.encodePacked(second.value, second.token.tokenId, second.token.tokenContract.tokenAddress));
return firstHash == secondHash;
}
function tokenValueBalance(address owner, TokenValue memory tokenValue) view public returns(uint) {
TokenType tokenType = tokenValue.token.tokenContract.tokenType;
if (tokenType == TokenType.ETH) {
return address(owner).balance;
} else if (tokenType == TokenType.ERC20) {
IERC20 token = IERC20(tokenValue.token.tokenContract.tokenAddress);
return token.balanceOf(owner);
} else if (tokenType == TokenType.ERC721) {
IERC721 token = IERC721(tokenValue.token.tokenContract.tokenAddress);
bool isOwner = token.ownerOf(tokenValue.token.tokenId) == owner;
return isOwner ? 1 : 0;
} else if (tokenType == TokenType.ERC1155) {
IERC1155 token = IERC1155(tokenValue.token.tokenContract.tokenAddress);
return token.balanceOf(owner, tokenValue.token.tokenId);
} else {
revert UnsupportedTokenType(tokenType);
}
}
error UnsupportedTokenType(TokenType);
function transferTokenValue(TokenValue memory tokenValue, address from, address to) public {
TokenType tokenType = tokenValue.token.tokenContract.tokenType;
if (tokenType == TokenType.ETH) {
transferETH(tokenValue, payable(to));
} else if (tokenType == TokenType.ERC20) {
transferERC20(tokenValue, from, to);
} else if (tokenType == TokenType.ERC721) {
transferERC721(tokenValue, from, to);
} else if (tokenType == TokenType.ERC1155) {
transferERC1155(tokenValue, from, to);
} else {
revert UnsupportedTokenType(tokenType);
}
}
function transferETH(TokenValue memory tokenValue, address payable to) public {
to.call{value: tokenValue.value}("");
}
function transferERC20(TokenValue memory tokenValue, address from, address to) public {
uint offeredAmount = tokenValue.value;
IERC20 offeredToken = IERC20(tokenValue.token.tokenContract.tokenAddress);
bool isAlreadyOwner = from == address(this);
if (isAlreadyOwner) {
require(offeredToken.balanceOf(address(this)) >= offeredAmount, "Insufficient ERC20 tokens");
offeredToken.transfer(to, offeredAmount);
} else {
uint allowedToTransfer = offeredToken.allowance(from, to);
require(allowedToTransfer >= offeredAmount, "Insufficient amount of ERC20 allowed. Approve more.");
offeredToken.transferFrom(from, to, offeredAmount);
}
}
function transferERC721(TokenValue memory tokenValue, address from, address to) public {
uint tokenId = tokenValue.token.tokenId;
IERC721 offeredToken = IERC721(tokenValue.token.tokenContract.tokenAddress);
offeredToken.safeTransferFrom(from, to, tokenId);
}
function transferERC1155(TokenValue memory tokenValue, address from, address to) public {
uint tokenId = tokenValue.token.tokenId;
uint tokenAmount = tokenValue.value;
IERC1155 offeredToken = IERC1155(tokenValue.token.tokenContract.tokenAddress);
offeredToken.safeTransferFrom(from, to, tokenId, tokenAmount, "");
}
function transferToJob(address initialOwner, address ethlance, address jobProxy, TokenValue[] memory _offeredValues) public {
for(uint i = 0; i < _offeredValues.length; i++) {
EthlanceStructs.TokenValue memory offer = _offeredValues[i];
TokenType tokenType = offer.token.tokenContract.tokenType;
if (tokenType == TokenType.ETH) {
transferETH(offer, payable(jobProxy));
} else if (tokenType == TokenType.ERC20) {
transferERC20(offer, initialOwner, ethlance);
transferERC20(offer, ethlance, jobProxy);
} else if (tokenType == TokenType.ERC721) {
transferERC721ToJob(initialOwner, ethlance, jobProxy, offer);
} else if (tokenType == TokenType.ERC1155) {
transferERC1155ToJob(initialOwner, ethlance, jobProxy, offer);
} else {
revert UnsupportedTokenType(tokenType);
}
}
}
// Attempts to transfer tokens from initialOwner end up in job address
// Works both when job is created through `Ethlance#createJob` and `onERC721Received`
//
// 1. In first case the token owner would have to approve the transfer first and
// the process requires 2 transactions
// 2. In the second case, user sends the transaction with extra data to Token contract
// which will call back onERC721Received on Ethlance which will end up creating job and
// completing the process (at the end of which the tokens belong to the Job created)
function transferERC721ToJob(address initialOwner, address ethlance, address job, TokenValue memory offer) public {
uint tokenId = offer.token.tokenId;
IERC721 offeredToken = IERC721(offer.token.tokenContract.tokenAddress);
bool needToTransferToEthlanceFirst = offeredToken.ownerOf(tokenId) == initialOwner && offeredToken.getApproved(tokenId) == ethlance;
if (needToTransferToEthlanceFirst) { transferERC721(offer, initialOwner, ethlance); }
transferERC721(offer, ethlance, job);
}
function transferERC1155ToJob(address initialOwner, address ethlance, address job, TokenValue memory offer) public {
uint tokenId = offer.token.tokenId;
uint tokenAmount = offer.value;
IERC1155 offeredToken = IERC1155(offer.token.tokenContract.tokenAddress);
bool needToTransferToEthlanceFirst = offeredToken.balanceOf(initialOwner, tokenId) >= tokenAmount && offeredToken.isApprovedForAll(initialOwner, ethlance);
if (needToTransferToEthlanceFirst) { transferERC1155(offer, initialOwner, ethlance); }
transferERC1155(offer, ethlance, job);
}
function toString(address account) public pure returns(string memory) {
return toString(abi.encodePacked(account));
}
function toString(uint256 value) public pure returns(string memory) {
return toString(abi.encodePacked(value));
}
function toString(bytes32 value) public pure returns(string memory) {
return toString(abi.encodePacked(value));
}
function toString(bytes memory data) public pure returns(string memory) {
bytes memory alphabet = "0123456789abcdef";
bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < data.length; i++) {
str[2+i*2] = alphabet[uint(uint8(data[i] >> 4))];
str[3+i*2] = alphabet[uint(uint8(data[i] & 0x0f))];
}
return string(str);
}
}