-
Notifications
You must be signed in to change notification settings - Fork 665
/
Copy pathlib.rs
343 lines (327 loc) · 14.4 KB
/
lib.rs
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
use crate::{
error::Error,
types::{ExecuteResponse, ExecutionContext},
};
use error::{UnsupportedAction, UserError};
use near_contract_standards::storage_management::StorageBalance;
use near_sdk::{
borsh::{BorshDeserialize, BorshSerialize},
env,
json_types::U64,
near_bindgen, AccountId, Allowance, Gas, GasWeight, NearToken, Promise, PromiseOrValue,
PromiseResult,
};
use types::{EthEmulationKind, TransactionKind};
pub mod error;
pub mod eth_emulation;
pub mod ethabi_utils;
pub mod internal;
pub mod near_action;
pub mod types;
#[cfg(test)]
mod tests;
const MICRO_NEAR: u128 = 10_u128.pow(18);
const ADDRESS_REGISTRAR_ACCOUNT_ID: &str = std::include_str!("ADDRESS_REGISTRAR_ACCOUNT_ID");
/// This storage deposit value is the one used by the standard NEP-141 implementation,
/// which essentially all tokens use. Therefore we hard-code it here instead of doing
/// the extra on-chain call to `storage_balance_bounds`. This also prevents malicious
/// token contracts with very high `storage_balance_bounds` from taking lots of $NEAR
/// from eth-wallet-contract users.
const NEP_141_STORAGE_DEPOSIT_AMOUNT: NearToken = NearToken::from_yoctonear(1_250 * MICRO_NEAR);
const NEP_141_STORAGE_DEPOSIT_GAS: Gas = Gas::from_tgas(5);
const NEP_141_STORAGE_BALANCE_OF_GAS: Gas = Gas::from_tgas(5);
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
#[borsh(crate = "near_sdk::borsh")]
pub struct WalletContract {
pub nonce: u64,
}
#[near_bindgen]
impl WalletContract {
/// Return the nonce value currently stored in the contract.
/// Following the Ethereum protocol, only transactions with nonce equal
/// to the current value will be accepted.
/// Additionally, the Ethereum protocol requires the nonce of an account increment
/// by 1 each time a transaction with the correct nonce and a valid signature
/// is submitted (even if that transaction eventually fails). In this way, each
/// nonce value can only be used once (hence the name "nonce") and thus transaction
/// replay is prevented.
pub fn get_nonce(&self) -> U64 {
U64(self.nonce)
}
/// This is the main entry point into this contract. It accepts an RLP-encoded
/// Ethereum transaction signed by the private key associated with the address
/// for the account where this contract is deployed. RLP is a binary format,
/// so the argument is actually passed as a base64-encoded string.
/// The Ethereum transaction represents a Near action the owner of the address
/// wants to perform. This method decodes that action from the Ethereum transaction
/// and crates a promise to perform that action.
/// Actions on Near are sent to a particular account ID where they are supposed to
/// be executed (for example, a `FunctionCall` action is sent to the contract
/// which will execute the method). In the Ethereum transaction only the address
/// of the target can be specified because it does not have a notion of named accounts
/// like Near has. The `target` field of this method gives the actual account ID
/// that the action will be sent to. The `target` must itself be an eth-implicit
/// account and match the `to` address of the Ethereum transaction; or `target`
/// must hash to the address given in the `to` field of the Ethereum transaction.
/// The output of this function is an `ExecuteResponse` which gives the output
/// of the Near action or an error message if there was a problem during the execution.
#[payable]
pub fn rlp_execute(
&mut self,
target: AccountId,
tx_bytes_b64: String,
) -> PromiseOrValue<ExecuteResponse> {
let current_account_id = env::current_account_id();
let predecessor_account_id = env::predecessor_account_id();
let result = inner_rlp_execute(
current_account_id.clone(),
predecessor_account_id,
target,
tx_bytes_b64,
&mut self.nonce,
);
match result {
Ok(promise) => PromiseOrValue::Promise(promise),
Err(Error::Relayer(_)) if env::signer_account_id() == current_account_id => {
let promise = create_ban_relayer_promise(current_account_id);
PromiseOrValue::Promise(promise)
}
Err(e) => PromiseOrValue::Value(e.into()),
}
}
/// Callback after checking if an address is contained in the registrar.
/// This check happens when the target is another eth implicit account to
/// confirm that the relayer really did check for a named account with that address.
#[private]
pub fn address_check_callback(
&mut self,
target: AccountId,
action: near_action::Action,
) -> PromiseOrValue<ExecuteResponse> {
let maybe_account_id: Option<AccountId> = match env::promise_result(0) {
PromiseResult::Failed => {
return PromiseOrValue::Value(ExecuteResponse {
success: false,
success_value: None,
error: Some("Call to Address Registrar contract failed".into()),
});
}
PromiseResult::Successful(value) => serde_json::from_slice(&value)
.unwrap_or_else(|_| env::panic_str("Unexpected response from account registrar")),
};
let current_account_id = env::current_account_id();
let promise = if maybe_account_id.is_some() {
if env::signer_account_id() == current_account_id {
create_ban_relayer_promise(current_account_id)
} else {
return PromiseOrValue::Value(ExecuteResponse {
success: false,
success_value: None,
error: Some("Invalid target: target is address corresponding to existing named account_id".into()),
});
}
} else {
let ext = WalletContract::ext(current_account_id).with_unused_gas_weight(1);
match action_to_promise(target, action).map(|p| p.then(ext.rlp_execute_callback())) {
Ok(p) => p,
Err(e) => {
return PromiseOrValue::Value(e.into());
}
}
};
PromiseOrValue::Promise(promise)
}
#[private]
pub fn nep_141_storage_balance_callback(
&mut self,
token_id: AccountId,
receiver_id: AccountId,
action: near_action::Action,
) -> PromiseOrValue<ExecuteResponse> {
let maybe_storage_balance: Option<StorageBalance> = match env::promise_result(0) {
PromiseResult::Failed => {
return PromiseOrValue::Value(ExecuteResponse {
success: false,
success_value: None,
error: Some(format!("Call to NEP-141 {token_id}::storage_balance_of failed")),
});
}
PromiseResult::Successful(value) => {
serde_json::from_slice(&value).unwrap_or_else(|_| {
env::panic_str("Unexpected response from NEP-141 storage_balance_of")
})
}
};
let current_account_id = env::current_account_id();
let ext = WalletContract::ext(current_account_id).with_unused_gas_weight(1);
let promise = match maybe_storage_balance {
Some(_) => {
// receiver_id is registered so we can send the transfer
// without additional actions. Note: in the standard NEP-141
// implementation it is impossible to have `Some` storage balance,
// but have it be insufficient to transact.
match action_to_promise(token_id, action)
.map(|p| p.then(ext.rlp_execute_callback()))
{
Ok(p) => p,
Err(e) => {
return PromiseOrValue::Value(e.into());
}
}
}
None => {
// receiver_id is not registered so we must call `storage_deposit` first.
let storage_deposit_args =
format!(r#"{{"account_id": "{receiver_id}"}}"#).into_bytes();
let transfer_function_call = match action {
near_action::Action::FunctionCall(x) => x,
_ => {
env::panic_str("Expected function call action to perform NEP-141 transfer")
}
};
Promise::new(token_id)
.function_call(
"storage_deposit".into(),
storage_deposit_args,
NEP_141_STORAGE_DEPOSIT_AMOUNT,
NEP_141_STORAGE_DEPOSIT_GAS,
)
.function_call(
transfer_function_call.method_name,
transfer_function_call.args,
transfer_function_call.deposit,
transfer_function_call.gas,
)
.then(ext.rlp_execute_callback())
}
};
PromiseOrValue::Promise(promise)
}
#[private]
pub fn rlp_execute_callback(&mut self) -> ExecuteResponse {
let n = env::promise_results_count();
let mut success_value = None;
for i in 0..n {
match env::promise_result(i) {
PromiseResult::Failed => {
return ExecuteResponse {
success: false,
success_value: None,
error: Some("Failed Near promise".into()),
};
}
PromiseResult::Successful(value) => success_value = Some(value),
}
}
ExecuteResponse { success: true, success_value, error: None }
}
#[private]
pub fn ban_relayer(&mut self) -> ExecuteResponse {
ExecuteResponse {
success: false,
success_value: None,
error: Some("Error: faulty relayer".into()),
}
}
}
fn inner_rlp_execute(
current_account_id: AccountId,
predecessor_account_id: AccountId,
target: AccountId,
tx_bytes_b64: String,
nonce: &mut u64,
) -> Result<Promise, Error> {
if *nonce == u64::MAX {
return Err(Error::AccountNonceExhausted);
}
let context = ExecutionContext::new(
current_account_id.clone(),
predecessor_account_id,
env::attached_deposit(),
)?;
let (action, transaction_kind) =
internal::parse_rlp_tx_to_action(&tx_bytes_b64, &target, &context, nonce)?;
let promise = match transaction_kind {
TransactionKind::EthEmulation(EthEmulationKind::EOABaseTokenTransfer {
address_check: Some(address),
}) => {
let ext = WalletContract::ext(current_account_id).with_unused_gas_weight(1);
let address_registrar = {
let account_id = ADDRESS_REGISTRAR_ACCOUNT_ID
.trim()
.parse()
.unwrap_or_else(|_| env::panic_str("Invalid address registrar"));
ext_registrar::ext(account_id).with_static_gas(Gas::from_tgas(5))
};
let address = format!("0x{}", hex::encode(address));
address_registrar.lookup(address).then(ext.address_check_callback(target, action))
}
TransactionKind::EthEmulation(EthEmulationKind::ERC20Transfer { receiver_id }) => {
// In the case of the emulated ERC-20 transfer, the receiving account
// might not be registered with the NEP-141 contract (per the NEP-145)
// storage standard. Therefore we must create a multi-step promise where
// first we check if the receiver is registered and then if not call
// `storage_deposit` in addition to `ft_transfer`.
let token_id = target;
let ext: WalletContractExt =
WalletContract::ext(current_account_id).with_unused_gas_weight(1);
let storage_balance_args =
format!(r#"{{"account_id": "{}"}}"#, receiver_id.as_str()).into_bytes();
Promise::new(token_id.clone())
.function_call(
"storage_balance_of".into(),
storage_balance_args,
NearToken::from_yoctonear(0),
NEP_141_STORAGE_BALANCE_OF_GAS,
)
.then(ext.nep_141_storage_balance_callback(token_id, receiver_id, action))
}
_ => {
let ext = WalletContract::ext(current_account_id).with_unused_gas_weight(1);
action_to_promise(target, action)?.then(ext.rlp_execute_callback())
}
};
Ok(promise)
}
fn action_to_promise(target: AccountId, action: near_action::Action) -> Result<Promise, Error> {
match action {
near_action::Action::FunctionCall(action) => Ok(Promise::new(target).function_call(
action.method_name,
action.args,
action.deposit,
action.gas,
)),
near_action::Action::Transfer(action) => Ok(Promise::new(target).transfer(action.deposit)),
near_action::Action::AddKey(action) => match action.access_key.permission {
near_action::AccessKeyPermission::FullAccess => {
Err(Error::User(UserError::UnsupportedAction(UnsupportedAction::AddFullAccessKey)))
}
near_action::AccessKeyPermission::FunctionCall(access) => Ok(Promise::new(target)
.add_access_key_allowance_with_nonce(
action.public_key,
access.allowance.and_then(Allowance::limited).unwrap_or(Allowance::Unlimited),
access.receiver_id,
access.method_names.join(","),
action.access_key.nonce,
)),
},
near_action::Action::DeleteKey(action) => {
Ok(Promise::new(target).delete_key(action.public_key))
}
}
}
fn create_ban_relayer_promise(current_account_id: AccountId) -> Promise {
let pk = env::signer_account_pk();
Promise::new(current_account_id).delete_key(pk).function_call_weight(
"ban_relayer".into(),
Vec::new(),
NearToken::from_yoctonear(0),
Gas::from_tgas(1),
GasWeight(1),
)
}
#[near_sdk::ext_contract(ext_registrar)]
trait AddressRegistrar {
fn lookup(&self, address: String) -> Option<AccountId>;
}