templates.baking_swap

  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")