-
Notifications
You must be signed in to change notification settings - Fork 11.3k
/
Copy pathCrossChainAirdrop.move
142 lines (124 loc) · 5.51 KB
/
CrossChainAirdrop.move
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
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
/// Allow a trusted oracle to mint a copy of NFT from a different chain. There can
/// only be one copy for each unique pair of contract_address and token_id. We only
/// support a single chain(Ethereum) right now, but this can be extended to other
/// chains by adding a chain_id field.
module Sui::CrossChainAirdrop {
use Std::Vector;
use Sui::ERC721Metadata::{Self, ERC721Metadata, TokenID};
use Sui::ID::{VersionedID};
use Sui::NFT;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};
/// The oracle manages one `PerContractAirdropInfo` for each Ethereum contract
struct CrossChainAirdropOracle has key {
id: VersionedID,
// TODO: replace this with SparseSet for O(1) on-chain uniqueness check
managed_contracts: vector<PerContractAirdropInfo>,
}
/// The address of the source contract
struct SourceContractAddress has store, copy {
address: vector<u8>,
}
/// Contains the Airdrop info for one contract address on Ethereum
struct PerContractAirdropInfo has store {
/// A single contract address on Ethereum
source_contract_address: SourceContractAddress,
/// The Ethereum token ids whose Airdrop has been claimed. These
/// are stored to prevent the same NFT from being claimed twice
// TODO: replace u64 with u256 once the latter is supported
// <https://github.com/MystenLabs/fastnft/issues/618>
// TODO: replace this with SparseSet for O(1) on-chain uniqueness check
claimed_source_token_ids: vector<TokenID>
}
/// The Sui representation of the original ERC721 NFT on Eth
struct ERC721 has store {
/// The address of the source contract, e.g, the Ethereum contract address
source_contract_address: SourceContractAddress,
/// The metadata associated with this NFT
metadata: ERC721Metadata,
}
/// Address of the Oracle
// TODO: Change this to something else before testnet launch
const ORACLE_ADDRESS: address = @0xCEF1A51D2AA1226E54A1ACB85CFC58A051125A49;
// Error codes
/// Trying to claim a token that has already been claimed
const ETOKEN_ID_CLAIMED: u64 = 0;
/// Create the `Orcacle` capability and hand it off to the oracle
fun init(ctx: &mut TxContext) {
let oracle = oracle_address();
Transfer::transfer(
CrossChainAirdropOracle {
id: TxContext::new_id(ctx),
managed_contracts: Vector::empty(),
},
oracle
)
}
/// Called by the oracle to mint the airdrop NFT and transfer to the recipient
public fun claim(
oracle: &mut CrossChainAirdropOracle,
recipient: address,
source_contract_address: vector<u8>,
source_token_id: u64,
name: vector<u8>,
token_uri: vector<u8>,
ctx: &mut TxContext,
) {
let contract = get_or_create_contract(oracle, &source_contract_address);
let token_id = ERC721Metadata::new_token_id(source_token_id);
// NOTE: this is where the globally uniqueness check happens
assert!(!is_token_claimed(contract, &token_id), ETOKEN_ID_CLAIMED);
let nft = NFT::mint(
ERC721 {
source_contract_address: SourceContractAddress { address: source_contract_address },
metadata: ERC721Metadata::new(token_id, name, token_uri),
},
ctx
);
Vector::push_back(&mut contract.claimed_source_token_ids, token_id);
Transfer::transfer(nft, recipient)
}
fun get_or_create_contract(oracle: &mut CrossChainAirdropOracle, source_contract_address: &vector<u8>): &mut PerContractAirdropInfo {
let index = 0;
// TODO: replace this with SparseSet so that the on-chain uniqueness check can be O(1)
while (index < Vector::length(&oracle.managed_contracts)) {
let info = Vector::borrow_mut(&mut oracle.managed_contracts, index);
if (&info.source_contract_address.address == source_contract_address) {
return info
};
index = index + 1;
};
create_contract(oracle, source_contract_address)
}
fun create_contract(oracle: &mut CrossChainAirdropOracle, source_contract_address: &vector<u8>): &mut PerContractAirdropInfo {
let info = PerContractAirdropInfo {
source_contract_address: SourceContractAddress { address: *source_contract_address },
claimed_source_token_ids: Vector::empty()
};
Vector::push_back(&mut oracle.managed_contracts, info);
let idx = Vector::length(&oracle.managed_contracts) - 1;
Vector::borrow_mut(&mut oracle.managed_contracts, idx)
}
fun is_token_claimed(contract: &PerContractAirdropInfo, source_token_id: &TokenID): bool {
// TODO: replace this with SparseSet so that the on-chain uniqueness check can be O(1)
let index = 0;
while (index < Vector::length(&contract.claimed_source_token_ids)) {
let claimed_id = Vector::borrow(&contract.claimed_source_token_ids, index);
if (claimed_id == source_token_id) {
return true
};
index = index + 1;
};
false
}
public fun oracle_address(): address {
ORACLE_ADDRESS
}
#[test_only]
/// Wrapper of module initializer for testing
public fun test_init(ctx: &mut TxContext) {
init(ctx)
}
}