-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEscrow.sol
More file actions
281 lines (224 loc) · 8.38 KB
/
Escrow.sol
File metadata and controls
281 lines (224 loc) · 8.38 KB
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
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
pragma solidity ^0.6.5;
interface Exchange {
function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold);
}
interface DaiToken {
function balanceOf(address tokenOwner) external view returns (uint256);
function permit(
address holder,
address spender,
uint256 nonce,
uint256 expiry,
bool allowed,
uint8 v,
bytes32 r,
bytes32 s
) external;
function pull(address usr, uint wad) external;
function push(address usr, uint wad) external;
function approve(address usr, uint wad) external returns (bool);
}
/// @author Vypo Mouse
/// @title DaiEscrowTimeouts
/// @notice Holds Dai tokens in escrow until the buyer and seller agree to
/// release them. A relayer handles paying the transaction fees, and is
/// reimbursed when the funds are released.
/// @dev First construct the contract, specifying the buyer and seller addresses.
/// Then `initialize` the contract with signatures for Dai's `permit`. This
/// transfers Dai from the buyer to the escrow contract.
///
/// When the seller has completed their responsibilities, the relayer
/// calls `submit` on their behalf. If the seller does not complete their
/// tasks within ~30 days, anyone may call `submitPastDue` and refund the
/// buyer.
///
/// Once `submit` has been called, the buyer has ~30 days to call `review`.
/// If the buyer does not call `review`, anyone may call `reviewPastDue` to
/// release the funds to the seller.
///
/// When `review` is called, the buyer may choose to approve the submission
/// or not approve it. If the submission is approved, the funds are released
/// to the seller. If the buyer does not approve, the funds are locked
/// forever.
contract Escrow {
enum Status {
AwaitingWad,
AwaitingSubmission,
AwaitingReview,
Complete,
Locked
}
// keccak256("Review(bool _approve)")
bytes32 public constant REVIEW_TYPEHASH = 0xfa5e0016fb62b8dffda8fd95249d438edcffd3689b40ac3b4281d4cf710609ae;
// keccak256("Submit(bytes32 _submission)")
bytes32 public constant SUBMIT_TYPEHASH = 0x62b607caa4d4e7fcbd31bf4c033cd30888b536567fadc83710fdf15f8d5cfc9e;
// Mainnet //
// DaiToken constant DAI = DaiToken(0x6B175474E89094C44Da98b954EedeAC495271d0F);
// Exchange constant UNISWAP = Exchange(0x2a1530C4C41db0B0b2bB646CB5Eb1A67b7158667);
// Kovan //
DaiToken constant DAI = DaiToken(0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa);
Exchange constant UNISWAP = Exchange(0x613639E23E91fd54d50eAfd6925AF2Ed6701A46b);
uint constant TIMEOUT = 30 days;
uint256 constant MAX_DAI_FOR_RELAYER = 5 ether;
bytes32 public immutable domain_separator;
address payable public immutable relayer;
address immutable public seller;
address immutable public buyer;
uint immutable public wad;
uint public initialized;
uint public submitted;
uint public relayer_owed;
Status public status;
modifier relayedGasCtor(uint _base) {
uint at_start = gasleft();
_;
uint at_end = gasleft();
relayer_owed += tx.gasprice * (_base + (at_start - at_end));
}
modifier relayedGas(uint _base) {
uint at_start = gasleft();
_;
uint at_end = gasleft();
if (tx.origin == relayer) {
relayer_owed += tx.gasprice * (_base + (at_start - at_end));
}
}
modifier onlyWhen(Status _status) {
require(status == _status, "Fn not presently valid");
_;
}
constructor(
address _seller,
address _buyer,
uint _wad
) public relayedGasCtor(1039528) {
require(_seller != address(0), "invalid seller");
require(_buyer != address(0), "invalid buyer");
uint8 chain_id;
assembly {
chain_id := chainid()
}
domain_separator = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("escrow")),
keccak256(bytes("1")),
chain_id,
address(this)
));
wad = _wad;
relayer = tx.origin;
seller = _seller;
buyer = _buyer;
status = Status.AwaitingWad;
}
function initialize(
uint256 nonce,
uint256 expiry,
uint8 v_allow,
bytes32 r_allow,
bytes32 s_allow,
uint8 v_deny,
bytes32 r_deny,
bytes32 s_deny
) external onlyWhen(Status.AwaitingWad) relayedGas(0) {
status = Status.AwaitingSubmission;
initialized = block.timestamp;
// Unlock buyer's Dai balance to transfer `wad` to this contract.
DAI.permit(buyer, address(this), nonce, expiry, true, v_allow, r_allow, s_allow);
// Transfer Dai from `buyer` to this contract.
DAI.pull(buyer, wad);
// Relock Dai balance of `buyer`.
DAI.permit(buyer, address(this), nonce + 1, expiry, false, v_deny, r_deny, s_deny);
}
/// @notice Signal that the seller has taken too long. Pays outstanding fees
/// to `relayer` and transfers remaining Dai to `buyer`.
function submitPastDue() external onlyWhen(Status.AwaitingSubmission) {
require(block.timestamp >= (initialized + TIMEOUT), "not past due");
// TODO: Track gas for `relayer_owed`
resolve(buyer);
}
/// @notice Signal that the buyer has taken too long. Pays outstanding fees
/// to `relayer` and transfers remaining Dai to `seller`.
function reviewPastDue() external onlyWhen(Status.AwaitingReview) {
require(block.timestamp >= (submitted + TIMEOUT), "not past due");
assert(submitted != 0);
// TODO: Track gas for `relayer_owed`
resolve(seller);
}
function submit(
bytes32 _submission,
uint8 _v,
bytes32 _r,
bytes32 _s
) external onlyWhen(Status.AwaitingSubmission) relayedGas(0) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
domain_separator,
keccak256(abi.encode(SUBMIT_TYPEHASH, _submission))
));
require(seller == ecrecover(digest, _v, _r, _s), "invalid-permit");
status = Status.AwaitingReview;
submitted = block.timestamp;
}
function review(
bool _approve,
uint8 _v,
bytes32 _r,
bytes32 _s
) external onlyWhen(Status.AwaitingReview) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
domain_separator,
keccak256(abi.encode(REVIEW_TYPEHASH, _approve))
));
require(buyer == ecrecover(digest, _v, _r, _s), "invalid-permit");
// TODO: Track gas for `relayer_owed`
if (_approve) {
resolve(seller);
} else {
resolve(address(0));
}
}
function forfeit() external {
require(msg.sender == relayer, "relayer only");
relayer_owed = 0;
}
function resolve(address dai_target) private {
bool locked = dai_target == address(0);
if (locked) {
status = Status.Locked;
} else {
status = Status.Complete;
}
if (relayer_owed > 0) {
uint owed = relayer_owed;
relayer_owed = 0;
bool approved = DAI.approve(address(UNISWAP), uint(-1));
assert(approved);
UNISWAP.tokenToEthTransferOutput(
owed,
MAX_DAI_FOR_RELAYER,
block.timestamp,
relayer
);
}
if (!locked) {
DAI.push(dai_target, DAI.balanceOf(address(this)));
}
}
function cancel() external {
require(status == Status.AwaitingWad || status == Status.Complete, "wrong status");
require(msg.sender == relayer, "relayer only");
initialized = 0;
submitted = 0;
selfdestruct(relayer);
}
function die() external {
// XXX: FOR TESTING ONLY
DAI.push(relayer, DAI.balanceOf(address(this)));
selfdestruct(relayer);
}
}