forked from FilippoBurresi/FWBD_Tendering
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Tendering.sol
427 lines (370 loc) · 20.1 KB
/
Tendering.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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
pragma solidity >0.4.13 <0.7.0;
pragma experimental ABIEncoderV2;
import "./strings.sol"; // needed for splitting text later
import "./SafeMath.sol";
import "./PA.sol"; // needed for access control
/// @title A smart contract for tendering procedures
/// @notice The contract takes into account all the four parts that define a tendering:
/// - the request part, where an allowed public administration issues a request for tender with all information;
/// - the bidding part, where each allowed firm can send a bid in response to a request for tender;
/// - the evaluation part, where the scores are computed and the winner is assigned;
/// - the publishing part, where people can access the results of the tendering in question.
contract TenderingSmartContract is PA {
using SafeMath for uint;
using strings for *;
address owner;
// a Public Administration can publish a tender only if authorized
mapping (address => bool) private allowedInstitution;
struct BiddingOffer {
address contractor; // bidder address
bytes32 hashOffer; // hash of the offer sent
bool valid; // a bid becomes valid when the actual offer is verified to coincide with the hash sent
string description; // each bid has to indicate the price, time of realization and environment friendliness (expressed with a score that goes from 1 (highest) to 4 (lowest))
string separator; // each offer is a string where the 3 aforementioned elements are separated by a random separator
string[] NewDescription; // the offer written without separator: a list of the 3 elements([prince,time,environment])
}
struct Tender {
uint256 tender_id;
uint256 bidOpeningDate; // date from which hashes can be sent
uint256 bidSubmissionClosingDateData; // date by which to send the unencrypted bid
uint256 bidSubmissionClosingDateHash; // date by which to send the hash
uint[] evaluation_weights; // array of lenght=3 that stores the weights used for evaluation, i.e. weighted average
mapping(address => BiddingOffer) bids; // from bidding address to its bidding offer
mapping(address => uint) addressToScore; // from bidding address to its score
mapping(address => bool) AlreadyBid; // checking each bidder = 1 bid per tender
address tenderingInstitution;
address winningContractor; //the winning bidder
string tenderName;
string description; // tender description
}
uint[] private tenderList; // list of tender keys so we can enumarate them
uint private tenderKeys; // lenght of tenderList
mapping (uint => Tender) private tenders; // from tender_id to the tender characteristics
// needed for the evaluation part
mapping(uint => address[]) private _participants; // from tender id => list of participants
mapping(uint => uint[]) private _scores; // from tender id => list of scores
/**
* a modifier to check that an encrypted bid is sent between the bid opening date and the first
* deadline, i.e. the date by which to send the hash of an offer.
*/
modifier inTimeHash (uint256 _tenderKey) {
require(
(now >= tenders[_tenderKey].bidOpeningDate) && (now <= tenders[_tenderKey].bidSubmissionClosingDateHash),
"hash sent before the opening date or after the hash closing date!"
);
_;
}
/**
* a modifier to ensure that each bidder can partecipate into a specific tender
* only once.
*/
modifier AlreadyPlacedBid(uint256 _tenderKey) {
require(
(tenders[_tenderKey].AlreadyBid[msg.sender] != true),
"Bid already placed for this tender!"
);
_;
}
/**
* a modifier to check that each unencrypted bid is sent after the first
* deadline and before the second one.
*/
modifier inTimeData (uint256 _tenderKey) {
require(
(now >= tenders[_tenderKey].bidSubmissionClosingDateHash) && (now < tenders[_tenderKey].bidSubmissionClosingDateData),
"data sent before the hash closing date or after the data closing date."
);
_;
}
/**
* a modifier to check that the date by which to send the unencrypted offer
* has alredy passed.
*/
modifier afterDeadline (uint256 _tenderKey) {
require(tenders[_tenderKey].bidSubmissionClosingDateData < now);
_;
}
/**
* @notice CreateTender function can only be called by an authorized PA to create a new tender
* @param _daysUntilClosingDateHash _daysUntilClosingDateData indicate the days between
* the bid opening date and the first and second deadlines.
* For simplicity, we are going to express them as seconds.
* @param w1, w2, w3 are the weights - to be assigned to price, time, environment -
* that will be used to evaluate the scores.
* Note: they are expressed as int (not as float)
*
* Requirements:
* - the sum of the weights has to = 100
* - _daysUntilClosingDateData has to be greater than _daysUntilClosingDateHash
*/
function CreateTender(string calldata _tenderName, string calldata _description,uint256 _daysUntilClosingDateData, uint256 _daysUntilClosingDateHash,
uint w1, uint w2, uint w3) external onlyPA{
uint sum = w1.add(w2.add(w3));
require(sum == 100, 'sum must be 100');
require(_daysUntilClosingDateData > _daysUntilClosingDateHash);
// the value of tenderKeys specifies the id the created tender
Tender storage c = tenders[tenderKeys];
c.tender_id = tenderKeys;
c.tenderName = _tenderName;
c.description = _description;
// the parameters of the function are used to set the characteristics of the tender
c.bidOpeningDate = now;
c.bidSubmissionClosingDateHash= now + (_daysUntilClosingDateHash* 1 seconds);
c.bidSubmissionClosingDateData = now + (_daysUntilClosingDateData* 1 seconds);
c.tenderingInstitution = msg.sender;
// the chosen weights are memorized
c.evaluation_weights.push(w1);
c.evaluation_weights.push(w2);
c.evaluation_weights.push(w3);
// tenderKeys keeps memory of how many tenders have been created so far
tenderKeys ++;
}
/**
* @notice placeBid can only be called by an authorized firm to place a bid.
* This function has to be called before the first hash deadline.
* @param _tenderKey is the id of the tender for which placing a bid
* @param _hashOffer is the encrypted offer
*/
function placeBid (uint256 _tenderKey, bytes32 _hashOffer) external onlyFirm inTimeHash(_tenderKey) AlreadyPlacedBid(_tenderKey) {
Tender storage c = tenders[_tenderKey];
/* once a firm has placed a bid for a specific tender,
it cannot partecipate into that tender anymore */
c.AlreadyBid[msg.sender] = true;
// a new offer is created. All the elements a BiddingOffer type is made of are inserted
c.bids[msg.sender] = BiddingOffer(msg.sender,_hashOffer,false,"","", new string[](0));
}
/**
* @notice concludeBid can only be called by an authorized firm to conclude a bid,
* by sending the unencrypted offer before the second (data) deadline.
* @param _tenderKey is the id of the tender the bid refers to.
* @param _description is a string containing the price, the realization time and the environment score.
* @param _separator indicates the separator that arises between these three elements.
*
* Requirements:
* - the firm concluding the Bid has to match the firm that has placed that Bid.
* - The hash sent at the beginning has to match the hash of the unencrypted bid.
*/
function concludeBid(uint256 _tenderKey, string calldata _description, string calldata _separator) external onlyFirm inTimeData(_tenderKey) {
/* assert that the contractor who is trying to conclude the Bid
is the one who has placed the bid in the first palce */
require(tenders[_tenderKey].bids[msg.sender].contractor == msg.sender);
// check that the hash corresponds
require(keccak256(abi.encodePacked(_description)) == tenders[_tenderKey].bids[msg.sender].hashOffer);
Tender storage c = tenders[_tenderKey];
// finally conclude the bid by submitting the description
c.bids[msg.sender].description = _description;
// memorizing the separator used in each bid
c.bids[msg.sender].separator = _separator;
c.bids[msg.sender].valid = true;
_participants[_tenderKey].push(msg.sender); // only firms with valid bids are considered
}
/**
* @notice this function is based on the String & slice utility library.
* It performs the splitting of a string according to a specified separator
* @return the list of strings obtained after the splitting
*/
function SMT(string memory _phrase,string memory _separator ) private pure returns(string[] memory) {
strings.slice memory s = _phrase.toSlice();
strings.slice memory delim = _separator.toSlice();
string[] memory parts = new string[](s.count(delim));
for (uint i = 0; i < parts.length; i++) {
parts[i] = s.split(delim).toString();
}
return (parts);
}
/**
* @notice this function converts a string to an integer type
* @return _ret : the initial parameter parsed as uint
*/
function parseInt(string memory _value) private pure returns (uint _ret) {
bytes memory _bytesValue = bytes(_value);
uint j = 1;
for(uint i = _bytesValue.length-1; i >= 0 && i < _bytesValue.length; i--) {
assert(uint8(_bytesValue[i]) >= 48 && uint8(_bytesValue[i]) <= 57);
_ret += (uint8(_bytesValue[i]) - 48)*j;
j*=10;
}
}
/**
* @notice this function is called by the PA after the tender is closed to
* split each bidding offer.
* The starting point is an offer presented as a string of type price////time////environment_score////.
* Each string offer is then splitted and
* converted into a list made of the three separated elements: [price,time,environment_score]
* This step is necessary to then evaluate all the offers and compute the scores.
*/
function splitDescription(uint256 _tenderKey) private onlyPA afterDeadline(_tenderKey) {
for (uint i=0; i < _participants[_tenderKey].length; i++){
// looking at the sepator used in each string offer
string memory separatorToUse = tenders[_tenderKey].bids[_participants[_tenderKey][i]].separator;
// update the description
string memory descriptionAtTheMoment = tenders[_tenderKey].bids[_participants[_tenderKey][i]].description;
tenders[_tenderKey].bids[_participants[_tenderKey][i]].NewDescription = SMT(descriptionAtTheMoment,separatorToUse);
}
}
/** @notice this function makes some changes on the measures the three elements characterizing
* each offer are expressed with.
* This is necessary for the computations of the weighted averages (i.e. the scores).
* The reason lies on the fact that the price is probably expressed on the scale of thousands,
* while the time on that of tens/hundreds and the environment score is just a number between
* 1 and 4 (where 1 = highest attention to the environment).
* Without any arrangement, a small change in the price will always predominate even in the case
* of a very small weight assigned to this variable.
* Therefore, adjusting the measures is fundamental to make things fair and to
* make each variable as important as the related weight requires.
* @param _thingToLook is the variable of reference
* according to which rescaling the other variable (in our case the price)
* @param _thingToAdjust is the variable whose scale of size needs to be changed
*/
function adjust_measures(uint _thingToLook, uint _thingToAdjust) private pure returns(uint) {
uint n_times;
uint _thingNew = _thingToLook;
while (_thingNew / (10) != 0) {
_thingNew = _thingNew / 10;
n_times ++;
}
return ( _thingToAdjust.mul(10 ** n_times));
}
/**
* @notice compute_scores function is called by the PA after the tender is closed to evaluate
* all the offers.
* In the evaluation phase, the weights chosen at the beginning are used
* and, before computing the weighted averages, the adjust_measures function is called
* in order to make the time and the environment_score comparable with the price
*/
function compute_scores(uint _tenderKey) external onlyPA afterDeadline(_tenderKey) {
require(msg.sender == tenders[_tenderKey].tenderingInstitution);
// weight associated to price
uint w1 = tenders[_tenderKey].evaluation_weights[0];
// weight associated to timing
uint w2 = tenders[_tenderKey].evaluation_weights[1];
/* weight associated to environmental safeguard level,
i.e. four categories: 1 [highest] to 4 [lowest] */
uint w3 = tenders[_tenderKey].evaluation_weights[2];
splitDescription(_tenderKey);
for (uint i = 0; i < _participants[_tenderKey].length; i++){
address target_address = _participants[_tenderKey][i];
BiddingOffer memory to_store= tenders[_tenderKey].bids[target_address];
if (to_store.valid == true){
//to make timing and envir comparable with price
// e.g. if price=10000 and env=2 then envir_adj = 20000
uint price = parseInt(to_store.NewDescription[0]);
uint timing = adjust_measures(price, parseInt(to_store.NewDescription[1]));
uint environment = adjust_measures(price, parseInt(to_store.NewDescription[2]));
uint score = w1.mul(price);
score = score.add(w2.mul(timing));
score = score.add(w3.mul(environment));
// all the scores are saved in the _scores mapping
_scores[_tenderKey].push(score);
// each bidder address is assigned to its own score
tenders[_tenderKey].addressToScore[to_store.contractor] = score;
}
}
}
/**
* @notice assign_winner function is called by the PA after the tender is closed to compare
* the scores previously computed and to decree the winner,
* i.e. the bidder with the lowest score .
* In case of the same score between two bidders,
* the firm which sent the bidding first is preferred.
*/
function assign_winner(uint _tenderKey) external onlyPA afterDeadline(_tenderKey) {
require(msg.sender == tenders[_tenderKey].tenderingInstitution);
uint winning_score = _scores[_tenderKey][0];
uint winning_index;
for (uint i = 1; i < _participants[_tenderKey].length; i++){
uint score = _scores[_tenderKey][i];
if (score < winning_score){
winning_score = score;
winning_index = i;
}
}
tenders[_tenderKey].winningContractor = _participants[_tenderKey][winning_index];
}
/**
* @notice displayWinner function can be called by any citizen after the tender is closed.
* @return the address of the winner and its score.
*/
function displayWinner(uint _tenderKey) external view afterDeadline(_tenderKey) returns (address, uint) {
return (tenders[_tenderKey].winningContractor, tenders[_tenderKey].addressToScore[tenders[_tenderKey].winningContractor]);
/*
Alternatively, we can simply return tenders[_tenderKey].winningContractor
Check which option consumes less gas. And, if tenders[_tenderKey].winningContractor consumes less,
we must eliminate the mapping tenderIdToWinner at the beginning of the smart contract
*/
}
/**
* @notice getResultsLenght function can be called after the tender is closed.
* @return the number of participants to a specific tender.
*/
function getResultsLenght(uint _tenderKey) external view afterDeadline(_tenderKey) returns(uint) {
return _participants[_tenderKey].length;
}
/**
* @notice getResultsValue function can be called after the tender is closed.
* @param _index speficies the bidder id we are interested in.
* @return the address of the contractor, its score and wheter it has won the tender or not.
*/
function getResultsValue(uint _tenderKey, uint _index) external view afterDeadline(_tenderKey) returns (address,uint, bool) {
bool is_winner;
if (tenders[_tenderKey].winningContractor == _participants[_tenderKey][_index]) {
is_winner = true;
} else {
is_winner = false;
}
return (_participants[_tenderKey][_index], _scores[_tenderKey][_index], is_winner);
}
/**
* @notice getBidDetails function can be called by any citizen after the tender is closed.
* @param _index speficies the bidder id we are interested in.
* @return the name of the firm contractor, its offer ([price, time, envir]),
* the sepator used in presenting the string offer initially, the reached score and
* whether it has won the tender or not.
*/
function getBidDetails(uint _tenderKey, address _index) external view afterDeadline(_tenderKey) returns (address, string[] memory, string memory, uint, bool) {
address name_contractor = tenders[_tenderKey].bids[_index].contractor;
string[] memory text_description = tenders[_tenderKey].bids[_index].NewDescription;
string memory sep = tenders[_tenderKey].bids[_index].separator; // thus, one can check if the score was correct by using the separator and the description
bool is_winner;
if (tenders[_tenderKey].winningContractor == _index) {
is_winner = true;
} else {
is_winner = false;
}
// the index is an address so I needed to create this mapping inside Tender to retrieve the score by the given address
uint score = tenders[_tenderKey].addressToScore[_index];
return (name_contractor, text_description, sep, score, is_winner);
}
/**
* @notice this function returns the total number of created tenders
* @return tenderKeys
*/
function getTendersLength() external view returns(uint) {
return (tenderKeys);
}
/**
* @notice this function allows to visualize the status of a speficic tender.
* A tender is pending if the first deadline has already passed
* (meaning that new hashed offers cannot be accepted anymore)
* @return the tender_id we are interested in and its pending status.
*/
function isPending(uint _tenderKey) external view returns(uint, bool) {
bool pending_status;
if (tenders[_tenderKey].bidSubmissionClosingDateHash > now) {
pending_status = true;
} else {
pending_status = false;
}
return (_tenderKey, pending_status);
}
/**
* @notice this function allows to visualize some information related to a specific tender.
* It returns the id of the tender we are interested in, its name, its description,
* the weights chosen to evaluate the offers with, the number of participants
* and the final winner.
*/
function see_TenderDetails(uint _tenderKey) external view returns (uint tender_id, string memory tenderName,string memory description,
uint[] memory evaluation_weights, uint firms, address winningContractor){
return (tenders[_tenderKey].tender_id, tenders[_tenderKey].tenderName, tenders[_tenderKey].description, tenders[_tenderKey].evaluation_weights, _participants[_tenderKey].length, tenders[_tenderKey].winningContractor);
}
}