| 1 | [ |
| 2 | "autonomous agent", |
| 3 | { |
| 4 | "doc_url": "https://ostable.org/stablecoin.json", |
| 5 | "init": "{ |
| 6 | $decimals = params.decimals OTHERWISE 2; |
| 7 | |
| 8 | $max_loan_value_in_underlying = params.max_loan_value_in_underlying OTHERWISE 10000; |
| 9 | $overcollateralization_ratio = params.overcollateralization_ratio OTHERWISE 1.5; |
| 10 | $liquidation_ratio = params.liquidation_ratio OTHERWISE 1.3; |
| 11 | $auction_period = params.auction_period OTHERWISE 3600; |
| 12 | $auction_min_increment = params.auction_min_increment OTHERWISE 1; |
| 13 | $oracle = params.oracle OTHERWISE 'F4KHJUCLJKY4JV7M5F754LAJX4EB7M4N'; |
| 14 | if (params.feed_name AND !params.ma_feed_name OR !params.feed_name AND !params.ma_feed_name) |
| 15 | bounce("both or none of feed names must be specified"); |
| 16 | $feed_name = params.feed_name OTHERWISE 'GBYTE_USD'; |
| 17 | $ma_feed_name = params.ma_feed_name OTHERWISE 'GBYTE_USD_MA'; |
| 18 | $max_volatility = params.max_volatility OTHERWISE 5; |
| 19 | $expiry_ts = parse_date(params.expiry_date); |
| 20 | $asset = var['asset']; |
| 21 | $expiry_exchange_rate = var['expiry_exchange_rate']; |
| 22 | $expired = !!$expiry_exchange_rate; |
| 23 | if ($expired) |
| 24 | $insolvent = (balance[base]/1e9 * $expiry_exchange_rate < var['circulating_supply']/10^$decimals); |
| 25 | }", |
| 26 | "messages": { |
| 27 | "cases": [ |
| 28 | { |
| 29 | "if": "{ trigger.data.define AND !$asset }", |
| 30 | "messages": [ |
| 31 | { |
| 32 | "app": "asset", |
| 33 | "payload": { |
| 34 | "is_private": false, |
| 35 | "is_transferrable": true, |
| 36 | "auto_destroy": false, |
| 37 | "fixed_denominations": false, |
| 38 | "issued_by_definer_only": true, |
| 39 | "cosigned_by_definer": false, |
| 40 | "spender_attested": false |
| 41 | } |
| 42 | }, |
| 43 | { |
| 44 | "app": "state", |
| 45 | "state": "{ |
| 46 | var['asset'] = response_unit; |
| 47 | response['asset'] = response_unit; |
| 48 | }" |
| 49 | } |
| 50 | ] |
| 51 | }, |
| 52 | { |
| 53 | "if": "{trigger.data.repay AND trigger.data.id AND $asset AND trigger.output[[asset=$asset]] > 0}", |
| 54 | "init": "{ |
| 55 | $id = trigger.data.id; |
| 56 | if (var[$id || '_owner'] != trigger.address) |
| 57 | bounce('you are not the owner'); |
| 58 | if (var[$id || '_repaid']) |
| 59 | bounce('already repaid'); |
| 60 | if (var[$id || '_winner']) |
| 61 | bounce("the loan is currently on auction"); |
| 62 | $amount = var[$id || '_amount']; |
| 63 | if (trigger.output[[asset=$asset]] < $amount) |
| 64 | bounce('you sent less than the loan amount'); |
| 65 | $change = trigger.output[[asset=$asset]] - $amount; |
| 66 | $exchange_rate = $expired ? $expiry_exchange_rate : data_feed[[oracles=$oracle, feed_name=$ma_feed_name]]; |
| 67 | $collateral = var[$id || '_collateral']; |
| 68 | $due_in_bytes = $amount/10^$decimals / $exchange_rate * 1e9; |
| 69 | $min_collateral = ceil($due_in_bytes * $liquidation_ratio); |
| 70 | if ($collateral < $min_collateral) |
| 71 | bounce("the loan is not sufficiently collateralized"); |
| 72 | }", |
| 73 | "messages": [ |
| 74 | { |
| 75 | "app": "payment", |
| 76 | "payload": { |
| 77 | "asset": "base", |
| 78 | "outputs": [ |
| 79 | { |
| 80 | "address": "{trigger.address}", |
| 81 | "amount": "{ var[$id || '_collateral'] }" |
| 82 | } |
| 83 | ] |
| 84 | } |
| 85 | }, |
| 86 | { |
| 87 | "app": "payment", |
| 88 | "payload": { |
| 89 | "asset": "{$asset}", |
| 90 | "outputs": [ |
| 91 | { |
| 92 | "address": "{trigger.address}", |
| 93 | "amount": "{ $change }" |
| 94 | } |
| 95 | ] |
| 96 | } |
| 97 | }, |
| 98 | { |
| 99 | "app": "state", |
| 100 | "state": "{ |
| 101 | var[$id || '_repaid'] = 1; |
| 102 | var['circulating_supply'] -= trigger.output[[asset=$asset]]; |
| 103 | }" |
| 104 | } |
| 105 | ] |
| 106 | }, |
| 107 | { |
| 108 | "if": "{trigger.data.add_collateral AND trigger.data.id AND $asset AND trigger.output[[asset=base]] >= 1e5}", |
| 109 | "init": "{ |
| 110 | $id = trigger.data.id; |
| 111 | if (var[$id || '_owner'] != trigger.address) |
| 112 | bounce('you are not the owner'); |
| 113 | if (var[$id || '_repaid']) |
| 114 | bounce('already repaid'); |
| 115 | }", |
| 116 | "messages": [ |
| 117 | { |
| 118 | "app": "state", |
| 119 | "state": "{ |
| 120 | var[$id || '_collateral'] += trigger.output[[asset=base]]; |
| 121 | response['collateral'] = var[$id || '_collateral']; |
| 122 | }" |
| 123 | } |
| 124 | ] |
| 125 | }, |
| 126 | { |
| 127 | "if": "{ trigger.data.seize AND trigger.data.id AND $asset }", |
| 128 | "init": "{ |
| 129 | $id = trigger.data.id; |
| 130 | if (var[$id || '_repaid']) |
| 131 | bounce('already repaid'); |
| 132 | $amount = var[$id || '_amount']; |
| 133 | if (!$amount) |
| 134 | bounce('no such loan'); |
| 135 | $exchange_rate = $expired ? $expiry_exchange_rate : data_feed[[oracles=$oracle, feed_name=$ma_feed_name]]; |
| 136 | $collateral = var[$id || '_collateral']; |
| 137 | $due_in_bytes = $amount/10^$decimals / $exchange_rate * 1e9; |
| 138 | $min_collateral = ceil($due_in_bytes * $liquidation_ratio); |
| 139 | $opening_collateral = ceil($due_in_bytes * $overcollateralization_ratio); |
| 140 | |
| 141 | if ($collateral >= $min_collateral AND trigger.address != var[$id || '_owner']) |
| 142 | bounce("the loan is sufficiently collateralized, you can't seize it"); |
| 143 | $missing_collateral = $opening_collateral - $collateral; |
| 144 | if (trigger.output[[asset=base]] < $missing_collateral) |
| 145 | bounce('you sent less than the missing collateral'); |
| 146 | $auction_end_ts = var[$id || '_auction_end_ts']; |
| 147 | if ($auction_end_ts){ |
| 148 | if (timestamp > $auction_end_ts) |
| 149 | bounce('auction already expired'); |
| 150 | $current_winner_bid = var[$id || '_winner_bid']; |
| 151 | if (trigger.output[[asset=base]] <= $current_winner_bid) |
| 152 | bounce('your bid is less than the current winner'); |
| 153 | if (trigger.output[[asset=base]] < (1+$auction_min_increment/100)*$current_winner_bid) |
| 154 | bounce('your bid must be at least '||$auction_min_increment||'% better than the current winner'); |
| 155 | $current_winner = var[$id || '_winner']; |
| 156 | |
| 157 | $bRefundToCurrentWinner = !is_aa($current_winner); |
| 158 | } |
| 159 | }", |
| 160 | "messages": [ |
| 161 | { |
| 162 | "if": "{$bRefundToCurrentWinner}", |
| 163 | "app": "payment", |
| 164 | "payload": { |
| 165 | "asset": "base", |
| 166 | "outputs": [ |
| 167 | { |
| 168 | "address": "{$current_winner}", |
| 169 | "amount": "{ $current_winner_bid - 1000 }" |
| 170 | } |
| 171 | ] |
| 172 | } |
| 173 | }, |
| 174 | { |
| 175 | "app": "state", |
| 176 | "state": "{ |
| 177 | if ($current_winner AND !$bRefundToCurrentWinner) |
| 178 | var['balance_' || $current_winner] += $current_winner_bid; |
| 179 | var[$id || '_auction_end_ts'] = timestamp + $auction_period; |
| 180 | var[$id || '_winner'] = trigger.address; |
| 181 | var[$id || '_winner_bid'] = trigger.output[[asset=base]]; |
| 182 | response['new_bid'] = trigger.output[[asset=base]]; |
| 183 | }" |
| 184 | } |
| 185 | ] |
| 186 | }, |
| 187 | { |
| 188 | "if": "{ trigger.data.end_auction AND trigger.data.id AND $asset }", |
| 189 | "init": "{ |
| 190 | $id = trigger.data.id; |
| 191 | if (var[$id || '_repaid']) |
| 192 | bounce('already repaid'); |
| 193 | $auction_end_ts = var[$id || '_auction_end_ts']; |
| 194 | if (!$auction_end_ts) |
| 195 | bounce('not on auction'); |
| 196 | if (timestamp < $auction_end_ts) |
| 197 | bounce('auction still under way'); |
| 198 | $winner_bid = var[$id || '_winner_bid']; |
| 199 | $winner = var[$id || '_winner']; |
| 200 | $owner = var[$id || '_owner']; |
| 201 | $collateral = var[$id || '_collateral']; |
| 202 | $amount = var[$id || '_amount']; |
| 203 | $exchange_rate = $expired ? $expiry_exchange_rate : data_feed[[oracles=$oracle, feed_name=$ma_feed_name]]; |
| 204 | $due_in_bytes = $amount/10^$decimals / $exchange_rate * 1e9; |
| 205 | $min_collateral = ceil($due_in_bytes * $liquidation_ratio); |
| 206 | $bHealthy = ($collateral >= $min_collateral); |
| 207 | if ($bHealthy){ |
| 208 | $bRefundToWinner = !is_aa($winner); |
| 209 | return; |
| 210 | } |
| 211 | |
| 212 | |
| 213 | if ($winner == $owner) |
| 214 | $new_collateral = $collateral + $winner_bid; |
| 215 | else{ |
| 216 | $opening_collateral = ceil($due_in_bytes * $overcollateralization_ratio); |
| 217 | $new_collateral = min($opening_collateral, $collateral + $winner_bid); |
| 218 | } |
| 219 | }", |
| 220 | "messages": [ |
| 221 | { |
| 222 | "if": "{$bHealthy AND $bRefundToWinner}", |
| 223 | "app": "payment", |
| 224 | "payload": { |
| 225 | "asset": "base", |
| 226 | "outputs": [ |
| 227 | { |
| 228 | "address": "{$winner}", |
| 229 | "amount": "{ $winner_bid - 1000 }" |
| 230 | } |
| 231 | ] |
| 232 | } |
| 233 | }, |
| 234 | { |
| 235 | "app": "state", |
| 236 | "state": "{ |
| 237 | var[$id || '_auction_end_ts'] = false; |
| 238 | var[$id || '_winner'] = false; |
| 239 | var[$id || '_winner_bid'] = false; |
| 240 | if (!$bHealthy) { |
| 241 | var[$id || '_owner'] = $winner; |
| 242 | var[$id || '_collateral'] = $new_collateral; |
| 243 | response['new_owner'] = $winner; |
| 244 | response['new_collateral'] = $new_collateral; |
| 245 | } |
| 246 | else { |
| 247 | if (!$bRefundToWinner) |
| 248 | var['balance_' || $winner] += $winner_bid; |
| 249 | } |
| 250 | }" |
| 251 | } |
| 252 | ] |
| 253 | }, |
| 254 | { |
| 255 | "if": "{trigger.data.withdraw}", |
| 256 | "init": "{ |
| 257 | $key = 'balance_' || trigger.address; |
| 258 | $balance = var[$key] + 0; |
| 259 | if ($balance <= 0) |
| 260 | bounce("you have no balance"); |
| 261 | if (trigger.data.to){ |
| 262 | if (!is_valid_address(trigger.data.to)) |
| 263 | bounce("invalid withdrawal address: " || trigger.data.to); |
| 264 | $address = trigger.data.to; |
| 265 | } |
| 266 | else |
| 267 | $address = trigger.address; |
| 268 | $amount = trigger.data.amount OTHERWISE $balance; |
| 269 | if ($amount > $balance) |
| 270 | bounce("withdrawal amount too large, balance: " || $balance); |
| 271 | }", |
| 272 | "messages": [ |
| 273 | { |
| 274 | "app": "payment", |
| 275 | "payload": { |
| 276 | "asset": "base", |
| 277 | "outputs": [ |
| 278 | { |
| 279 | "address": "{$address}", |
| 280 | "amount": "{$amount}" |
| 281 | } |
| 282 | ] |
| 283 | } |
| 284 | }, |
| 285 | { |
| 286 | "app": "state", |
| 287 | "state": "{ |
| 288 | var[$key] -= $amount; |
| 289 | response[$address] = -$amount; |
| 290 | }" |
| 291 | } |
| 292 | ] |
| 293 | }, |
| 294 | { |
| 295 | "if": "{ trigger.data.expire AND $asset AND !$expired AND timestamp > $expiry_ts }", |
| 296 | "messages": [ |
| 297 | { |
| 298 | "app": "state", |
| 299 | "state": "{ |
| 300 | $exchange_rate_last = data_feed[[oracles=$oracle, feed_name=$feed_name]]; |
| 301 | $exchange_rate_ma = data_feed[[oracles=$oracle, feed_name=$ma_feed_name]]; |
| 302 | if (abs($exchange_rate_last - $exchange_rate_ma) > $max_volatility/100 * $exchange_rate_ma) |
| 303 | bounce('recent price changes are too fast ' || $exchange_rate_last || ', MA ' || $exchange_rate_ma); |
| 304 | if (balance[base]/1e9 * min($exchange_rate_ma, $exchange_rate_last) < var['circulating_supply']/10^$decimals * $liquidation_ratio) |
| 305 | bounce("undercollateralized"); |
| 306 | var['expiry_exchange_rate'] = $exchange_rate_last; |
| 307 | response['expiry_exchange_rate'] = $exchange_rate_last; |
| 308 | }" |
| 309 | } |
| 310 | ] |
| 311 | }, |
| 312 | { |
| 313 | "if": "{ $asset AND trigger.output[[asset=$asset]] > 0 AND !$insolvent AND $expired }", |
| 314 | "init": "{ |
| 315 | $asset_amount = trigger.output[[asset=$asset]]; |
| 316 | $underlying_amount = $asset_amount / 10^$decimals; |
| 317 | $gb_amount = $underlying_amount / $expiry_exchange_rate; |
| 318 | $bytes_amount = floor($gb_amount * 1e9); |
| 319 | if ($bytes_amount == 0) |
| 320 | bounce("you would receive 0 bytes"); |
| 321 | }", |
| 322 | "messages": [ |
| 323 | { |
| 324 | "app": "payment", |
| 325 | "payload": { |
| 326 | "asset": "base", |
| 327 | "outputs": [ |
| 328 | { |
| 329 | "address": "{trigger.address}", |
| 330 | "amount": "{ $bytes_amount }" |
| 331 | } |
| 332 | ] |
| 333 | } |
| 334 | }, |
| 335 | { |
| 336 | "app": "state", |
| 337 | "state": "{ |
| 338 | var['circulating_supply'] -= $asset_amount; |
| 339 | }" |
| 340 | } |
| 341 | ] |
| 342 | }, |
| 343 | { |
| 344 | "if": "{ $asset AND trigger.output[[asset=base]] >= 1e5 AND $expired }", |
| 345 | "init": "{ |
| 346 | $bytes_amount = trigger.output[[asset=base]] - 1000; |
| 347 | $gb_amount = $bytes_amount / 1e9; |
| 348 | $underlying_amount = $gb_amount * $expiry_exchange_rate; |
| 349 | $asset_amount = floor($underlying_amount * 10^$decimals); |
| 350 | if ($asset_amount == 0) |
| 351 | bounce("you would receive 0 stablecoins"); |
| 352 | }", |
| 353 | "messages": [ |
| 354 | { |
| 355 | "app": "payment", |
| 356 | "payload": { |
| 357 | "asset": "{$asset}", |
| 358 | "outputs": [ |
| 359 | { |
| 360 | "address": "{trigger.address}", |
| 361 | "amount": "{ $asset_amount }" |
| 362 | } |
| 363 | ] |
| 364 | } |
| 365 | }, |
| 366 | { |
| 367 | "app": "state", |
| 368 | "state": "{ |
| 369 | var['circulating_supply'] += $asset_amount; |
| 370 | }" |
| 371 | } |
| 372 | ] |
| 373 | }, |
| 374 | { |
| 375 | "if": "{ trigger.output[[asset=base]] >= 1e5 AND $asset AND !$expired }", |
| 376 | "init": "{ |
| 377 | $exchange_rate_last = data_feed[[oracles=$oracle, feed_name=$feed_name]]; |
| 378 | $exchange_rate_ma = data_feed[[oracles=$oracle, feed_name=$ma_feed_name]]; |
| 379 | $exchange_rate = min($exchange_rate_last, $exchange_rate_ma); |
| 380 | $underlying_value = trigger.output[[asset=base]] / 1e9 * $exchange_rate; |
| 381 | $loan_value = $underlying_value / $overcollateralization_ratio; |
| 382 | if ($loan_value > $max_loan_value_in_underlying) |
| 383 | bounce('loan would be too large, please split it'); |
| 384 | $loan_value_in_asset = floor($loan_value * 10^$decimals); |
| 385 | if ($loan_value_in_asset == 0) |
| 386 | bounce("loan amount would be 0"); |
| 387 | }", |
| 388 | "messages": [ |
| 389 | { |
| 390 | "app": "payment", |
| 391 | "payload": { |
| 392 | "asset": "{$asset}", |
| 393 | "outputs": [ |
| 394 | { |
| 395 | "address": "{trigger.address}", |
| 396 | "amount": "{ $loan_value_in_asset }" |
| 397 | } |
| 398 | ] |
| 399 | } |
| 400 | }, |
| 401 | { |
| 402 | "app": "state", |
| 403 | "state": "{ |
| 404 | var[response_unit || '_owner'] = trigger.address; |
| 405 | var[response_unit || '_collateral'] = trigger.output[[asset=base]]; |
| 406 | var[response_unit || '_amount'] = $loan_value_in_asset; |
| 407 | var['circulating_supply'] += $loan_value_in_asset; |
| 408 | response['amount'] = $loan_value_in_asset; |
| 409 | response['id'] = response_unit; |
| 410 | }" |
| 411 | } |
| 412 | ] |
| 413 | } |
| 414 | ] |
| 415 | } |
| 416 | } |
| 417 | ] |