1import smartpy as sp
2
3
4@sp.module
5def main():
6 class BakingSwap(sp.Contract):
7 """A contract that takes tez deposits and pays interest.
8
9 The deposited funds cannot leave the contract, but the administrator can
10 delegate them for baking.
11
12 In more detail:
13
14 - The administrator funds the contract with collateral.
15
16 - The administrator publishes an offer: a rate (in basis points) and a
17 duration (in days).
18
19 - For each deposit the amount to be paid out and the due date are recorded.
20 The corresponding amount of collateral is locked up.
21
22 - At maturity the deposit plus interest can be withdrawn.
23 """
24
25 def __init__(self, admin, initialRate, initialDuration):
26 """Constructor
27
28 Args:
29 admin (sp.address): admin of the contract.
30 initialRate (sp.nat): Basis points to compute the interest.
31 initialDuration (sp.nat): Number of days before a deposit can be
32 withdrawn.
33 """
34 self.data.admin = admin
35 self.data.collateral = sp.mutez(0)
36 self.data.ledger = {}
37 self.data.rate = initialRate
38 self.data.duration = initialDuration
39
40 # Admin-only entrypoints
41
42 @sp.entrypoint
43 def delegate(self, public_key_hash):
44 """Admin-only. Delegate the contract's balance to the admin.
45
46 Args:
47 public_key_hash (sp.key_hash): public key hash of the admin.
48 """
49 assert sp.sender == self.data.admin
50 assert sp.amount == sp.mutez(0)
51 assert sp.sender == sp.to_address(sp.implicit_account(public_key_hash))
52 sp.set_delegate(sp.Some(public_key_hash))
53
54 @sp.entrypoint
55 def collateralize(self):
56 """Admin-only. Provide tez as collateral for interest to be paid."""
57 assert sp.sender == self.data.admin
58 self.data.collateral += sp.amount
59
60 @sp.entrypoint
61 def uncollateralize(self, amount, receiver):
62 """Admin-only. Withdraw collateral.
63
64 Transfer `amount` mutez to admin if it doesn't exceed collateral.
65
66 Args:
67
68 amount (sp.mutez): Amount to be removed from the collateral.
69 """
70 assert sp.sender == self.data.admin
71 # Explicitly fails for insufficient collateral.
72 assert amount <= self.data.collateral, "insufficient collateral"
73 self.data.collateral -= amount
74 sp.send(receiver, amount)
75
76 @sp.entrypoint
77 def set_offer(self, rate, duration):
78 """Admin-only. Set the current offer: interest rate (in basis points)
79 and duration.
80
81 Args:
82 rate (sp.nat): Basis points to compute the interest.
83 duration (sp.nat): Number of days before a deposit can be withdrawn.
84 """
85 assert sp.sender == self.data.admin
86 assert sp.amount == sp.mutez(0)
87 self.data.rate = rate
88 self.data.duration = duration
89
90 # Permissionless entrypoints
91
92 @sp.entrypoint
93 def deposit(self, rate, duration):
94 """Deposit tez. The current offer has to be repeated in the parameters.
95
96 Args:
97 rate (sp.nat): Basis points to compute the interest.
98 duration (sp.nat): Number of days before a deposit can be withdrawn.
99 """
100 assert self.data.rate >= rate
101 assert self.data.duration <= duration
102 assert not self.data.ledger.contains(sp.sender)
103
104 # Compute interest to be paid.
105 interest = sp.split_tokens(sp.amount, self.data.rate, 10_000)
106 self.data.collateral -= interest
107
108 # Record the payment to be made.
109 self.data.ledger[sp.sender] = sp.record(
110 amount=sp.amount + interest,
111 due=sp.add_days(sp.now, self.data.duration),
112 )
113
114 @sp.entrypoint
115 def withdraw(self, receiver):
116 """Withdraw tez at maturity."""
117 assert sp.amount == sp.mutez(0)
118 entry = self.data.ledger.get(sp.sender, error="NoDeposit")
119 assert sp.now >= entry.due
120 sp.send(receiver, entry.amount)
121 del self.data.ledger[sp.sender]
122
123
124@sp.module
125def testing():
126 class Receiver(sp.Contract):
127 @sp.entrypoint
128 def default(self):
129 pass
130
131
132if "main" in __name__:
133 admin = sp.test_account("Admin")
134 non_admin = sp.test_account("non_admin")
135 voting_powers = {
136 admin.public_key_hash: 0,
137 }
138
139 @sp.add_test()
140 def basic_scenario():
141 scenario = sp.test_scenario("Baking swap basic scenario", main)
142 scenario.h1("Baking Swap")
143 c = main.BakingSwap(admin.address, 700, 365)
144 scenario += c
145
146 c.delegate(admin.public_key_hash, _sender=admin, _voting_powers=voting_powers)
147
148 @sp.add_test()
149 def test():
150 sc = sp.test_scenario("Full", [main, testing])
151 sc.h1("Full test")
152 sc.h2("Origination")
153 c = main.BakingSwap(admin.address, 0, 10000)
154 sc += c
155 sc.h2("Delegator")
156 delegator = testing.Receiver()
157 sc += delegator
158 sc.h2("Admin receiver")
159 admin_receiver = testing.Receiver()
160 sc += admin_receiver
161
162 sc.h2("delegate")
163 c.delegate(admin.public_key_hash, _sender=admin, _voting_powers=voting_powers)
164 sc.verify(c.baker == sp.Some(admin.public_key_hash))
165 sc.h3("Failures")
166 c.delegate(
167 admin.public_key_hash,
168 _sender=non_admin,
169 _voting_powers=voting_powers,
170 _valid=False,
171 _exception="Assert failure: sp.sender == self.data.admin",
172 )
173 c.delegate(
174 admin.public_key_hash,
175 _sender=admin,
176 _amount=sp.mutez(1),
177 _voting_powers=voting_powers,
178 _valid=False,
179 _exception="Assert failure: sp.amount == sp.tez(0)",
180 )
181 c.delegate(
182 non_admin.public_key_hash,
183 _sender=admin,
184 _voting_powers=voting_powers,
185 _valid=False,
186 _exception="Assert failure: sp.sender == sp.to_address(sp.implicit_account(params))",
187 )
188
189 sc.h2("collateralize")
190 c.collateralize(_sender=admin, _amount=sp.tez(500))
191 sc.verify(c.data.collateral == sp.tez(500))
192 sc.h3("Failures")
193 c.collateralize(
194 _sender=non_admin,
195 _amount=sp.tez(500),
196 _valid=False,
197 _exception="Assert failure: sp.sender == self.data.admin",
198 )
199
200 sc.h2("set_offer")
201 c.set_offer(rate=1000, duration=365, _sender=admin)
202 sc.h3("Failures")
203 c.set_offer(
204 rate=1000,
205 duration=365,
206 _sender=non_admin,
207 _valid=False,
208 _exception="Assert failure: sp.sender == self.data.admin",
209 )
210 c.set_offer(
211 rate=1000,
212 duration=365,
213 _sender=admin,
214 _amount=sp.mutez(1),
215 _valid=False,
216 _exception="Assert failure: sp.amount == sp.tez(0)",
217 )
218
219 sc.h2("deposit")
220 c.deposit(
221 rate=1000, duration=365, _sender=delegator.address, _amount=sp.tez(100)
222 )
223 sc.verify(c.data.collateral == sp.tez(490))
224 sc.verify(
225 c.data.ledger[delegator.address]
226 == sp.record(amount=sp.tez(110), due=sp.timestamp(365 * 24 * 3600))
227 )
228 sc.h3("Failures")
229 c.deposit(
230 rate=1001,
231 duration=365,
232 _sender=delegator.address,
233 _amount=sp.tez(100),
234 _valid=False,
235 _exception="Assert failure: self.data.rate >= params.rate",
236 )
237 c.deposit(
238 rate=1000,
239 duration=364,
240 _sender=delegator.address,
241 _amount=sp.tez(100),
242 _valid=False,
243 _exception="Assert failure: self.data.duration <= params.duration",
244 )
245 c.deposit(
246 rate=1000,
247 duration=365,
248 _sender=delegator.address,
249 _amount=sp.tez(100),
250 _valid=False,
251 _exception="Assert failure: not (self.data.ledger.contains(sp.sender))",
252 )
253
254 sc.h2("uncollateralize")
255 sc.h3("Failures")
256 c.uncollateralize(
257 amount=sp.tez(500),
258 receiver=admin_receiver.address,
259 _sender=admin,
260 _valid=False,
261 _exception="insufficient collateral",
262 )
263 c.uncollateralize(
264 amount=sp.tez(490),
265 receiver=admin_receiver.address,
266 _sender=non_admin,
267 _valid=False,
268 _exception="Assert failure: sp.sender == self.data.admin",
269 )
270 sc.h3("Valid")
271 c.uncollateralize(
272 amount=sp.tez(490), receiver=admin_receiver.address, _sender=admin
273 )
274 sc.verify(c.data.collateral == sp.tez(0))
275 sc.verify(admin_receiver.balance == sp.tez(490))
276
277 sc.h2("withdraw")
278 sc.h3("Failures")
279 c.withdraw(
280 delegator.address,
281 _sender=delegator.address,
282 _amount=sp.mutez(1),
283 _now=sp.timestamp(365 * 24 * 3600),
284 _valid=False,
285 _exception="Assert failure: sp.amount == sp.tez(0)",
286 )
287 c.withdraw(
288 delegator.address,
289 _sender=delegator.address,
290 _now=sp.timestamp(365 * 24 * 3600 - 1),
291 _valid=False,
292 _exception="Assert failure: sp.now >= entry.due",
293 )
294 sc.h3("Valid")
295 c.withdraw(
296 delegator.address,
297 _sender=delegator.address,
298 _now=sp.timestamp(365 * 24 * 3600),
299 )
300 sc.verify(delegator.balance == sp.tez(110))
301 sc.verify(~c.data.ledger.contains(delegator.address))
302 sc.h3("Failures")
303 c.withdraw(
304 delegator.address,
305 _sender=delegator.address,
306 _valid=False,
307 _now=sp.timestamp(365 * 24 * 3600),
308 _exception="NoDeposit",
309 )
310
311 # @sp.add_test(name="Mutation")
312 # def test():
313 # s = sp.test_scenario()
314 # with s.mutation_test() as mt:
315 # mt.add_scenario("Full")