-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsecurity_token.py
295 lines (270 loc) · 13.6 KB
/
security_token.py
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
from pyteal import *
# Code reused for setting the totalSupply in the mint, burn and clear state program
def update_total_supply():
return App.globalPut(Bytes("totalSupply"), App.globalGet(Bytes("cap")) - App.globalGet(Bytes("reserve")))
def approval_program():
on_creation = Seq([
App.globalPut(Bytes("totalSupply"), Int(0)),
App.globalPut(Bytes("cap"), Btoi(Txn.application_args[0])),
App.globalPut(Bytes("reserve"), Btoi(Txn.application_args[0])),
App.globalPut(Bytes("paused"), Int(0)),
App.globalPut(Bytes("decimals"), Btoi(Txn.application_args[1])),
App.globalPut(Bytes("symbol"), Txn.application_args[2]),
App.globalPut(Bytes("name"), Txn.application_args[3]),
App.localPut(Int(0), Bytes("transferGroup"), Int(1)),
App.localPut(Int(0), Bytes("balance"), Int(0)),
App.localPut(Int(0), Bytes("roles"), Int(15)),
Return(Int(1))
])
local_permissions = App.localGet(Int(0), Bytes("roles"))
is_wallets_admin = BitwiseAnd(local_permissions, Int(1))
is_transfer_rules_admin = BitwiseAnd(local_permissions, Int(2))
is_reserve_admin = BitwiseAnd(local_permissions, Int(4))
is_contract_admin = BitwiseAnd(local_permissions, Int(8))
# when an account opts-in set the accounts local variables
# balance of 0
# transfer group 1
register = Seq([
App.localPut(Int(0), Bytes("balance"), Int(0)),
App.localPut(Int(0), Bytes("maxBalance"), Int(0)),
App.localPut(Int(0), Bytes("lockUntil"), Int(0)),
App.localPut(Int(0), Bytes("transferGroup"), Int(1)),
Return(Int(1))
])
# pause all transfers
# goal app call --app-id uint --from account --app-arg 'str:pause' --app-arg "int:${0 for false and 1 for true}"
# the sender must be a contract admin
new_pause_value = Btoi(Txn.application_args[1])
pause = Seq([
App.globalPut(Bytes("paused"), new_pause_value),
Return(is_contract_admin)
])
# Set Permissions
# goal app call --app-id uint --from admin --app-account targetAddress --app-arg "str:grantRoles" --app-arg "int:${role-uint}"
#
# set contract permissions for Txn.accounts[1]
# Txn.application_args[1] should be a 4-bit permissions integer
# permssions can only be set by a contract admin
#
# Permissions are set with an integer where each of the first 4 bits represents a role:
# Int(0) | 0000 | No admin role
# Int(1) | 0001 | Wallets
# Int(2) | 0010 | Transfer Rules
# Int(4) | 0100 | Reserve
# Int(8) | 1000 | Contract Admin
# Int(10) | 1010 | Contract Admin + Transfer Rules
#
# Roles can be combined using the bitmask for the binary representation of the permissions
# then converted into the corresponding integer for the bitmask.
# For example permission integer 15 grants all roles:
# Int(15) | 1111 | Contract Admin + Reserve + Transfer Rules + Wallets
#
# Here is the full list of permissions integers and their corresponding bitmask
# Role Int| Bits | Roles
# Int(0) | 0000 | No admin role
# Int(1) | 0001 | Wallets
# Int(2) | 0010 | Transfer Rules
# Int(3) | 0011 | Transfer Rules + Wallets
# Int(4) | 0100 | Reserve
# Int(5) | 0101 | Reserve + Wallets
# Int(6) | 0110 | Reserve + Transfer Rules
# Int(7) | 0111 | Reserve + Transfer Rules + Wallets
# Int(8) | 1000 | Contract Admin
# Int(9) | 1001 | Contract Admin + Wallets
# Int(10) | 1010 | Contract Admin + Transfer Rules
# Int(11) | 1011 | Contract Admin + Transfer Rules + Wallets
# Int(12) | 1100 | Contract Admin + Reserve
# Int(13) | 1101 | Contract Admin + Reserve + Wallets
# Int(14) | 1110 | Contract Admin + Reserve + Transfer Rules
# Int(15) | 1111 | Contract Admin + Reserve + Transfer Rules + Wallets
#
# WARNING: contract admin permission can only be revoked by other contract admins
# to avoid removing all contract admins.
roles = Btoi(Txn.application_args[1])
grant_roles = Seq([
Assert(And(
is_contract_admin,
roles <= Int(15)
)),
If(
Eq(Txn.sender(), Txn.accounts[1]),
Assert(BitwiseAnd(roles, Int(8)))
),
App.localPut(Int(1), Bytes("roles"), roles),
Return(Int(1))
])
# setAddressPermissions
# set address permissions for target Txn.accounts[1]:
# arg 1) freeze
# arg 2) maxBalance in the smallest token unit
# arg 3) lockUntil a UNIX timestamp
# arg 4) transfer group
#
# sender must be wallets admin
freeze_value = Btoi(Txn.application_args[1])
max_balance_value = Btoi(Txn.application_args[2])
lock_until_value = Btoi(Txn.application_args[3])
transfer_group_value = Btoi(Txn.application_args[4])
set_address_permissions = Seq([
Assert(And(
is_wallets_admin,
Txn.accounts.length() == Int(1)
)),
App.localPut(Int(1), Bytes("frozen"), freeze_value),
App.localPut(Int(1), Bytes("maxBalance"), max_balance_value),
App.localPut(Int(1), Bytes("lockUntil"), lock_until_value),
App.localPut(Int(1), Bytes("transferGroup"), transfer_group_value),
Return(Int(1))
])
def getRuleKey(sendGroup, receiveGroup):
return Concat(Bytes("rule"), Itob(sendGroup), Itob(receiveGroup))
# setTransferRule
# goal app call --app-id $APP_ID --from $FROM --app-arg 'str:setTransferRule' --app-arg "int:$FROM_GROUP_ID" \
# --app-arg "int:$TO_GROUP_ID" --app-arg "int:$LOCK_UNTIL_UNIX_TIMESTAMP"
#
# set a lockUntil time for transfers between a transfer from-group and a to-group
# each account belongs to 1 and only 1 group
# by default transfers between groups are not allowed between groups
# only at transfer rules admin can set transfer rules
lock_transfer_key = getRuleKey(Btoi(Txn.application_args[1]), Btoi(Txn.application_args[2]))
lock_transfer_until = Btoi(Txn.application_args[3])
set_transfer_rules = Seq([
Assert(is_transfer_rules_admin,),
App.globalPut(lock_transfer_key, lock_transfer_until),
Return(Int(1))
])
# mint
# goal app call --app-id uint --from address --app-account targetAddr --app-arg 'str:mint' --app-arg "int:${amount}"
# move assets from the reserve to Txn.accounts[1]
# the from address must have the asset admin role
mint_amount = Btoi(Txn.application_args[1])
receiver_max_balance = App.localGet(Int(1), Bytes("maxBalance"))
mint = Seq([
Assert(And(
is_reserve_admin,
Txn.accounts.length() == Int(1),
mint_amount <= App.globalGet(Bytes("reserve"))
)),
Assert(
Or(
receiver_max_balance == Int(0),
receiver_max_balance >= App.localGet(Int(1), Bytes("balance")) + mint_amount
),
),
App.globalPut(Bytes("reserve"), App.globalGet(Bytes("reserve")) - mint_amount),
App.localPut(Int(1), Bytes("balance"), App.localGet(Int(1), Bytes("balance")) + mint_amount),
update_total_supply(),
Return(Int(1))
])
# burn
# goal app call --app-id uint --from address --app-account targetAddr --app-arg 'str:burn' --app-arg "int:${amount}"
# burn moves assets from Txn.accounts[1] to the reserve
# the from address must have the assets admin role
burn_amount = Btoi(Txn.application_args[1])
burn = Seq([
Assert(And(
is_reserve_admin,
Txn.accounts.length() == Int(1),
burn_amount <= App.localGet(Int(1), Bytes("balance"))
)),
App.globalPut(Bytes("reserve"), App.globalGet(Bytes("reserve")) + burn_amount),
App.localPut(Int(1), Bytes("balance"), App.localGet(Int(1), Bytes("balance")) - burn_amount),
update_total_supply(),
Return(Int(1))
])
# accepts sender and receiver indices in current Txn.accounts[]
# returns true if all checks are successful and transfer is allowed
def isTransferAllowed(sender_idx, receiver_idx, amount):
return Not(Or(
Lt(App.localGet(sender_idx, Bytes("balance")), amount), # check sender balance
App.globalGet(Bytes("paused")), # can't transfer when the contract is paused
App.localGet(sender_idx, Bytes("frozen")), # sender account can't be frozen
App.localGet(receiver_idx, Bytes("frozen")), # receiver account can't be frozen
App.localGet(sender_idx, Bytes("lockUntil")) >= Global.latest_timestamp(), # sender account can't be locked
# check that a transfer rule exists and allows the transfer from the sender to the receiver at the current time
App.globalGet(getRuleKey(App.localGet(sender_idx, Bytes("transferGroup")), App.localGet(receiver_idx, Bytes("transferGroup")))) < Int(1),
App.globalGet(getRuleKey(App.localGet(sender_idx, Bytes("transferGroup")), App.localGet(receiver_idx, Bytes("transferGroup")))) >= Global.latest_timestamp(),
# check that max balance is not exceeded
And(
receiver_max_balance > Int(0),
App.localGet(receiver_idx, Bytes("maxBalance")) < App.localGet(receiver_idx, Bytes("balance")) + amount
)
))
# detect
# transaction succeeds if transfer is possible from the sender Txn.accounts[1] to the receiver Txn.accounts[2]
# goal app call --app-id uint --from address --app-account senderAddr --app-account receiverAddr --app-arg 'str:detect' --app-arg "int:amount"
# checks are made to see if the sender account is frozen or locked
# checks are made to see if there is a transfer rule allowing transfer between sender and receiver transfer groups
# the transfer must occur after the transfer group lockUntil date
transfer_amount = Btoi(Txn.application_args[1])
detect = Seq([
Assert(isTransferAllowed(Int(1), Int(2), transfer_amount)),
Return(Int(1))
])
# transfer
# transfers assets from the sender to the receiver Txn.accounts[1]
# goal app call --app-id uint --from address --app-account receiverAddr --app-arg 'str:transfer' --app-arg "int:amount"
# checks are made to see if the sender account is frozen or locked
# checks are made to see if there is a transfer rule allowing transfer between sender and receiver transfer groups
# the transfer must occur after the transfer group lockUntil date
transfer_amount = Btoi(Txn.application_args[1])
transfer = Seq([
Assert(And(
Txn.accounts.length() == Int(1),
isTransferAllowed(Int(0), Int(1), transfer_amount)
)),
App.localPut(Int(0), Bytes("balance"), App.localGet(Int(0), Bytes("balance")) - transfer_amount),
App.localPut(Int(1), Bytes("balance"), App.localGet(Int(1), Bytes("balance")) + transfer_amount),
Return(Int(1))
])
program = Cond(
[Txn.application_id() == Int(0), on_creation],
# goal app delete --app-id uint --from address
# WARNING: to preserve critical global application state
# calling this Algorand required app function will fail with "transaction rejected by ApprovalProgram"
[Txn.on_completion() == OnComplete.DeleteApplication, Return(Int(0))],
[Txn.on_completion() == OnComplete.UpdateApplication, Return(is_contract_admin)],
# goal app closeout --app-id uint --from address
# WARNING: to keep from deleting critical token balances held in the addresses local storage
# do not allow the app to be closed out
# calling this will fail with "transaction rejected by ApprovalProgram"
[Txn.on_completion() == OnComplete.CloseOut, Return(Int(0))],
[Txn.on_completion() == OnComplete.OptIn, register],
[Txn.application_args[0] == Bytes("pause"), pause],
[Txn.application_args[0] == Bytes("grantRoles"), grant_roles],
[Txn.application_args[0] == Bytes("setTransferRule"), set_transfer_rules],
[Txn.application_args[0] == Bytes("setAddressPermissions"), set_address_permissions],
[Txn.application_args[0] == Bytes("mint"), mint],
[Txn.application_args[0] == Bytes("burn"), burn],
[Txn.application_args[0] == Bytes("transfer"), transfer],
[Txn.application_args[0] == Bytes("detect"), detect],
)
return program
# All Algorand Apps can be cleared from an address that has opted in to the app by the account holder.
# For example, one way to clear an addresses app state is using the goal command:
# goal app clear --app-id uint --from address
#
# The clear state program handles clearing the app from an addresses local storage, returns tokens to the reserve,
# updates the totalSupply, and opts out the address from the app.
#
# WARNING: Calling this will return the tokens held by the address to the reserve and the account will no longer have
# the balance of tokens even if the account opts back in to the account. It is recommended that you never call clear
# state to avoid loosing your balance. It is implemented because it is required functionality for all Algorand Apps.
def clear_state_program():
program = Seq([
# To preserve cap integrity, balances are returned to the reserve when the clear state is executed.
App.globalPut(
Bytes("reserve"),
App.globalGet(Bytes("reserve")) + App.localGet(Int(0), Bytes("balance"))
),
update_total_supply(),
Return(Int(1))
])
return program
if __name__ == "__main__":
with open('security_token_approval.teal', 'w') as f:
compiled = compileTeal(approval_program(), Mode.Application)
f.write(compiled)
with open('security_token_clear_state.teal', 'w') as f:
compiled = compileTeal(clear_state_program(), Mode.Application)
f.write(compiled)