From 4751868396c12437516ed42e7baf7f2d46d81661 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 15 Nov 2023 01:48:36 +0300 Subject: [PATCH] [framework] Closed Loop Token (#14702) ## Description Adds the `sui::token` module to the framework. ## Test Plan Adds a number of tests to test out the new token module along with examples. --- If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process. ### Type of Change (Check all that apply) - [x] protocol change - [ ] user-visible impact - [ ] breaking change for a client SDKs - [ ] breaking change for FNs (FN binary must upgrade) - [ ] breaking change for validators or node operators (must upgrade binaries) - [ ] breaking change for on-chain data layout - [ ] necessitate either a data wipe or data migration ### Release notes - Adds a closed loop `token` to the framework --------- Co-authored-by: Tim Zakian <2895723+tzakian@users.noreply.github.com> Co-authored-by: Ashok Menon --- ...00000000000000000000000000000000000000dee9 | Bin 31767 -> 31352 bytes ...000000000000000000000000000000000000000001 | Bin 6419 -> 0 bytes ...000000000000000000000000000000000000000002 | Bin 44093 -> 0 bytes ...000000000000000000000000000000000000000003 | Bin 41753 -> 0 bytes ...00000000000000000000000000000000000000dee9 | Bin 31767 -> 0 bytes crates/sui-framework-snapshot/manifest.json | 11 +- .../sui-framework/sources/balance.move | 6 + .../packages/sui-framework/sources/coin.move | 31 +- .../sources/kiosk/transfer_policy.move | 7 +- .../sui-framework}/sources/token.move | 102 +++--- .../tests/kiosk/kiosk_tests.move | 1 + .../sui-framework/tests/pay_tests.move | 12 +- .../tests/token/token_actions_tests.move | 123 +++++++ .../tests/token/token_config_tests.move | 115 ++++++ .../token/token_public_actions_tests.move | 47 +++ .../tests/token/token_request_tests.move | 131 +++++++ .../tests/token/token_test_utils.move | 50 +++ .../tests/token/token_treasury_cap_tests.move | 56 +++ crates/sui-protocol-config/src/lib.rs | 1 + ..._populated_genesis_snapshot_matches-2.snap | 28 +- dapps/regulated-token/Move.toml | 2 +- dapps/regulated-token/sources/denylist.move | 4 +- dapps/regulated-token/sources/reg.move | 8 +- examples/move/token/Move.toml | 9 + examples/move/token/sources/coffee.move | 99 ++++++ examples/move/token/sources/gems.move | 133 +++++++ examples/move/token/sources/loyalty.move | 102 ++++++ .../move/token/sources/regulated_coin.move | 328 ++++++++++++++++++ .../token/sources/rules/allowlist_rule.move | 100 ++++++ .../token/sources/rules/denylist_rule.move | 169 +++++++++ .../token/sources/rules/limiter_rule.move | 159 +++++++++ examples/move/token/sources/simple_token.move | 171 +++++++++ 32 files changed, 1887 insertions(+), 118 deletions(-) delete mode 100644 crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000001 delete mode 100644 crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000002 delete mode 100644 crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000003 delete mode 100644 crates/sui-framework-snapshot/bytecode_snapshot/32/0x000000000000000000000000000000000000000000000000000000000000dee9 rename {dapps/regulated-token => crates/sui-framework/packages/sui-framework}/sources/token.move (89%) create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_actions_tests.move create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_config_tests.move create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_public_actions_tests.move create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_request_tests.move create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_test_utils.move create mode 100644 crates/sui-framework/packages/sui-framework/tests/token/token_treasury_cap_tests.move create mode 100644 examples/move/token/Move.toml create mode 100644 examples/move/token/sources/coffee.move create mode 100644 examples/move/token/sources/gems.move create mode 100644 examples/move/token/sources/loyalty.move create mode 100644 examples/move/token/sources/regulated_coin.move create mode 100644 examples/move/token/sources/rules/allowlist_rule.move create mode 100644 examples/move/token/sources/rules/denylist_rule.move create mode 100644 examples/move/token/sources/rules/limiter_rule.move create mode 100644 examples/move/token/sources/simple_token.move diff --git a/crates/sui-framework-snapshot/bytecode_snapshot/31/0x000000000000000000000000000000000000000000000000000000000000dee9 b/crates/sui-framework-snapshot/bytecode_snapshot/31/0x000000000000000000000000000000000000000000000000000000000000dee9 index a6e89ffe869b5f4fbf4c9a91896e519f237c8918..0d5182e695c336a8d8080e1218de0e9722b5edb8 100644 GIT binary patch delta 8207 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 delta 8565 zcma)BYmgmBb)N3&zBAL^clN&T-o1C#iQNYw7B-vaPpdYbC8#kJU=Mdf0kd zE7@Qk*I+Oq3E(gR0we@N2q6Xoaq=UNqLLp;szQ>g{D4X-B$ZTtQK=MA2^4`MO zKJWbO6ZMm!C$IINA za`rfTom=c}?6Zu0k?}shg>U0W`DOlA{!zw1&iH2-|1IWZJ;&+rxWn)-9e;E0aHZl= z-Cz+#;W!zS^yQ2bXJ*T+9FNWRHamrr&T}U$816B_G9~UfU&b&9=P~X&&Ow19k}$H< zbE$QQZ^d+A#V$`#W5d@Z9to_N32fuz#;WX3Tk$FjJ4oj%EVR}?#xhynM8mJKT$Va+ z<_jclB;x6!eR`3V%G7$UvqIwCtgA}$9hI8n9PYM;kMvmaaIY0Fv%Y@Ed1$~sj15|` zdB}?8;Sm~J9UdhyKemB8nE&+nMtVGzh?AQtj&o*WlA3zP9wMT=JiSAD^ zDvwd|%WTZ9YwKjc!!}+y4+uBea`Maq>c_8CP(E>G3FY}aRKIw-hVu65S(JB9&o&`e z<>M%yyFyLxWmbHMooO(^yPmBuhgYASMOiz%!{M{Hx1zf5B9&*4xF}a{Q~3VAS*U`q&sfEY3i(C6=8?#d< z8YiZL`kBV5#_q<#(KA!0rgl#)99ua4Xk+2YN2dz)Z2if`$;KA3O*||vP9Ll+O`n_I z*XXS8Y8;*uOHnGJSqJ5;LMMj?FJz+bZ@or*=&}(U=yw`sv1eBVRu~ zwP$Kiugh1qknlj6ek((KvUr}tl+ zeMnrM-O<=Fb)<5*vRSw(Uwc~dzzLY=SD4PTfH7Y?%ugEpbjrn#&N9F3HT{Y}Jx_JN zl615lc*}^dS*zx@?UGF=<)1J|JiY8nG50fZYNuFxq zjy|S*A5&t|b|cpvxLNoW?G_hCavUR3vGYQGt&ePAg&0mNLKVMQR@23jh$?^(OCva4 zW<`Q6ouOZ1v;ipMm#S7jg&v8j#1PA#j9-#|(bs;-&%idB!X3lLF~R5Y!yYC|C1&$qh=HuvDxU;)Vf^TEz0 zaltCKcJ(?q7K1fxe#Uq}WO2bEVUI~+Z}u8^*jxLmJJ>(fF{0wN4h5Lkk|Uz?rBk;Z zP$E5Dh+jTbINb&Z4ee^c%(^LQavYLfMRI1z%5hZ_4*9s72^W~=7Q?h(jDLKnGLl?P zDiI8PETw%l~jJaf1@MoPpH5WJ<;MN4mAw1j1tCjRQ-Vg-t%*HWxm9ww$< zIox9*e^A52WyJLc4-=RrQ3rF`|Dt`(+mcy|rRbKsD9GO@oyr#yu5VhweK81;$}MT+`n|=`sKxY zcb9IhURk*t@0g2Z&olRL-}~^fLTd5Zr5pEG;_nPi$6uPOc&XKuTg%Y-%eimIKiO4_ z|McWin11g1y=%*Nm!4a^va+)JTKB1Uu;zQ1U+niYzX)&g8_d1#{w8y;z()tv{&u(S zF0ywq`2ur?+?SYp%;nsFW6uEfAURmCZQBIrq}y!sUh;G@_LI!L2E(6V@@kW854yu- zhbF@BB2kZNacP#j&4Y=Qa&|}MI z-TUtEkv~j4%CXzZ8rMRRKNHa9^y1XiBMjlU5Bi;!xjE0NI*D?w? z-c+33=Ry%FuhuV8QfbM&a5T~;6&Yg?!gzK+y*trQ#u*YtSs|6os7fglpFPvvylQl) zv)2kHP%?Yfc%hfQZYc9X%3zcZG_HYi6jKO@IVVbMjb(@Q>y^Du$d2}gP|sjhS*$J> z<)aL zBY+4^(=6)bhID&F7FFW!ovFmf7UUoyLi$}%^+A0e)p(kw?2i9ztTv<)W3+D=bE9sJ zxsrp6dN^K1TZ)4ZDvGEVilB%+w?2*x-Z>HfU}5k;QMn_4AKeV0SEr!CJUJT5r~Fgk@#4nv4e0&aYcSQrnr(=r-O`tuM ziiWfgB5@t?EKdE@OKxk4W=OkcJj$7ZPMHy4Crn=DC!^u`yJrjV;+xE5U^ecaw`4(K zN=a|`xoUINOsI*BjK-o31v6-?<9*^ExtyiC8jLoYnyO_|1yc>oC~96o$zgHYH(Fj$ zEV~m;exV5(@>9LS~%=D^WY*!Yt2jZWedt_1;O4-MgzQ@s?Dstj*SW!FDaI=uzk}X}-gPa;i%$ONpN3F8e>=SW* zu>x{!P#fCh>bF&p>&sm3p*o;hPcHDJMXr;+G4k9xav5#JM?fxI-ZzkI;B|5lt3WPd z705-TB61O{KrUhx$d!Xg$A7-q#pdI0E%tUBY>Lt z-J-^*>RIE(DDk4$;ze6wP*)O#2Z_?DM|2NZ^3jXLl76rR+$NTMfa_jjNx%FMY7ela zk65w+Ea@Ybe8I8=j~KmeH}NXDMWe8i=gFZL}iU*pc8lj-?Q@#yhvP5 zpV%lz)Hpn*Ny&)-5i$j4gSBIkc>~ctus~yuNj*`@C!C(4EmPQ%P6W z@rtHSzlm4GqlbtN%#KSJR5{uml^=9KRXCu8SEnw&fmbiD@#@2CyrP-leIDf17rCxl znps}ebeFEd`_OjRG3)oa#Gy>LnDuq?MB|Ak+C$9B6SE4|6}1RAW5z6Gak$*m1A_f4 z5v=We26+Lo@9C?^XyC=;gUiQ;=Mdz&qP-?FoI z?E^_8;!73UOpt4D+KUdO%U5A^q)*(zRH(vnAB~R5yBhD&6b(WgMjDCdah>KRAaef| zu7sEG7g8b*V{zA&dn^@y=*m!YZb+Pwm+8hSER(^2*D?o^T_&X_LicPM{Mo%Ep zle-F3DS8sl&q0k*?x=mBl>^3z$)N$8LtVl#pD3H6rCAo+0l$DrRu`hG{KJHOfpVjn zIip*K5;Gc*=MmH0FdYq+O&I2*6KiJDYi9CMHNJ86{p@7?gR6T`M%M=Rm2n!b(2mzR z$U9MgZi99V;wfEA7Xt2FK){_#2;9mP1lH_9zz^WN8*b&Nu9Xg);`q|+ymbb{J>}9k zqnQDoIAxctbwI#O1_EXP6q(5>N|aof(xMD&F0B3J+Su09ybCArwpB_ujj~6lq9Y=Z zX`t>Y3IV=S(H3kvt(b8qwHwXH9b3yoXDG*ESr}8k*o={Zk#Y;9FcSPT(TvgY!L7BX zAlC;@Yt59(R*~TOVkHc)6clh+2aG2cwMTCU_NRn>(QznbkPUf*4oNGkA8l%-yI~Mb z@G7+5z70lQ3K)*!MouD^TtKvSqqCwz=4lJ8i9IL|pPN!C1Qj>hMu{e%(V|}rN*n_v zvK}ARDH}iPl*dxQkXxwBt)I@sfipPyqN1xkSLJLf9k)Gq>$n$?*>}${Q4SN z72I*aMIpgQzIi16HAia^eY06_`NIK?;y9K=_-9pSm$HAn+=vV4zEv zRVVNRj^ceRED?Yu%f$HA8>K_8>V%7G$PZ!@K(Ny)0Kf|J$q76FKn+0`4*~$DApoEY z0RROA0F)5vxZ`Hm!Fios$L*{fNH!B@2)Vyzx}q}{xgH?b{N@^RHSlcCltaF!LOE&Z z?%5Hq-kd--_nDiOCNSy>U6tFr18*x9+$wq8DM`WK$CkI)w}`GJTFv)~S!7x0>_rH- zNi*OpsV=~W*Czq2z&kBZLkfDo!GV=j&w#kAOQr|0y~mhId!>uwn!KZXk&GDY9*m%5^|~tISu(qi>j_#1)y3BD?qhEsHP!+Y6t;T z1q4u)5I{8r0aQH*&=N63r{llBm2b}L+&XY`60f)nL*0NdM23T+E1&hYu8&O{xiBOE;6^eLvLS&G0_AJ&1<;Kz6F^WYMx= zGK^LX-enMCux{$D!G?%NsF%w=ObG?-1RsV3W*&g4_pp`S6lv^_3D(tRGKD~}?!-C~ zmbIj$IeAh?-rD)N`j5MjWU4dJvXspexjj{t@t=ep{L{NN4;58I{NEJ%P zl|x!El}b2K2PYJLXR(AB1_&>)EjCuuArMKESlw&|YfxK)+6Z-uMf0MJ`+&jLb82b= zpRqUOAYvQ@2r%0O5dp_egu=qH{Ed7Bf8?j{ecJ^eZC3+6)~aSC-#}F)GhjL&r}{iS zqzimwyTdE+O;{}{ZFiy*e1baQo9J#hbT>#H1qD*N8xGwKhwg?$cf+xFgR3y!vf4k2 pj0l;Wz-Mvau^%((qh?s|sAurE)?7VbFVu^5=bf*;bM;55{{!!De$xN| diff --git a/crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000001 b/crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000001 deleted file mode 100644 index 7b516d4fa4e06235bac33f03568fb1c6000c0b8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6419 zcmb7IU5F%C6~4b!x9+{w-8J3wlXVk%c6N4S2%FtavYD(Ab|$-0Z8l zctanI`@u@^iq8JW%DcEj{d-} z+BbyuKN8;(>6=P0#aK)~s3NK0LXuJ_w8Ud16?8X@OCcr4opYt|Q_NrQ-_Cn4jG*r0 z7h5~yyj$d*-SKF@Gwv4s(XcyMyu0yM=Vo_snA=|eX1|wr4lg~C?vjYkK`A4>kr$Xd z9TxW|otN|R$nPHxivGdi4o%QO!bjOcHU2ZbW+SD6k;8*Dg276-q>)ukRlR!J1{0tk zb9BqGCu_XHT2HQAXrK4OikPjdy3S&G)`=%*|CoL-jG(;^*C`e2BvxlJ^+Nlxa`uVY z)D?Xq7GbI&1E04TbmcLx9jC!wS#@zHXLdgo*5hZLcIt$eR^DgCN#liXJ60*N>V*?( zO|QrLtd*qnBl=O)oqS>edy2JV=@_bts;b8DjzUvC3iTsFWUw#H+^TeDtG$ft%jQhm zUoV{*V`v<=LhW@YduFoNz1X?<6t%WQ%mV0b^2bw_2*(tf20)tRT#h!mx) zQ5n_hD1A(`;s*6LsGeA7@nV=1Q6UB0Bt@-=QJdZ*kI_jD9KCdxGW1gm4|8Y+m!Xe( zaUrV8XBl-54)S3y+8&L^qg%GTb@mSnvojhN-TrWr_VP(F9^L8W`v=7x9ZJ(5_VU}E z(QbqR`M59$o6r#r^5KnQ&m4>nI@^GeB)XaJ6r=Gm8#Zg^fQMPsOt(Is8&MzUyk zS(fq*R^uDIt-OvcOZhc>)oOtOj$f}dga8cIj&0gXg;n9vxUgC^AhOX9wS|>y>SNlH zE?d^}Rkal4af48-6oFbA60r_Xg7Yn`9V6=Gyr|`=C?}2+R!I>xDA-&tt*I=x>WuQ3 z9<#GfReCG~YtXf(PU%I4Hsl!?9~tP;kihY>)7=r)N?vb4V=Z`#-2# zVK3ypd!Z==MT3MaLkO8Z(zaA92%Xam7IoYSh@gepyA>oLOqEcPw71nOyr#d$U*+-Z z;;-ENOSYr$tCoFJ#qKR7{M+hlDt+6C+Ajh5b+&|v#>%V~03!>q?Il598cL<9&Vz4B zO`6!4(qMv|2arezt3`m51w-|wAs~qzxjNh%b$}1b7Kd|iRU^=GDU}U!m!?<{8L$lT zkEDz)0txy>Wepx4DnN*UE9Da*OK3)GSs{!eA0^bO5UIiz%qD0OH&(voKVms zT2V%yKA=qfLC9%N(dSmb*z5H2-7a!)1w(!YL&MQ9uO23&{k%2nnV%Quw~4_NYPUZa zG$_Z<_f~jC`JM#Qon$F`h(OrU(-v&>v`Oi}4+tji?H9eb^PK3E31ETpp%?F+zBo#Inp*p0I!pjH7gx!F9k3 zSfKFm5>SK?@b!mkY!l8^dA{clBYlcB^?-IYX89ItWwxza2>UtKfh?-^!^E>UK=!ak z9SO8?jHCy(r_NHAYpWFk?0cm}51Rlp1{t9a3e(`Nt1KXmfg{z~HHNn#Pcne0EZ&4e z(gR!@zDaOE)h1HFtXeW68hzob>miyWz`=#~83i!7@RfDEflCli$aI8^zo-HOM5;Vt zuIZ1Tjz6_;-r?=&FEGn1!uW5Cf6DZI!~|v`A_6OqDGor^p#v4b8`zu-iecoe2D%!Y zLj@dJ;;4xmAPB^oYZ-$$%Veuxyc9@jvfVF${rRA0sa7&o56>zI@gy&%j@2HFhBrW* zJ7~iHONaSzC#R$JxPSw7x`iC(wbI4K%cdde!O06K=sq&&H*?_rY* za4^Sllp`lLEx0AQm|lYq6MGI|SI3BF0kOu3#o*8%F)pET(7E=Y75AN%dV+S{Rns>G&<)*rqomPVEJ;! z^)s_7zL^9_Br7a=t=Ga-$Cu~CyriF};n}@opTmkFPGojpdo*B=EnS{?C48R@R&w<9 z>HE%^kEk*2Ku~&pG#m`WL zqn`vyP2rE3cPFXoA=#B@<<4QTyHRGPG9lUU{VAlWdk@A#4tw44oum1a!|iGInag$+kG)SiyDN7c%{IuCrK*(ANFPPT|4d-9 zv*DsoTGLB#jl&W;lBC8mf0W)zWJo9>GyoIiLiEsN84e|QkUoqDLq(oq0DQZFa|{rl z4n;|sI_TpHO=>V?I10=1VY3 zHA}q&&5d~i-X~Q{Q^pI3crJ@Pouj!|=PI;uDzhG$8%IOyBd^clgII(nlLD=>7^TT# zhpe?2nCtR+($+^d+H7KB;bMHa!}?;KRxOuYu~AS%=@W@pP66ZKcF+rGli;JAR%i4h zE(*jwq9bWmg8_;1COD%Jh%{JM7amcR;$OqRjz6w^7WuFhPJV!TeE6xG2ipGW2D(E! z;4<#^dgFXDnflFiBagdXYR1ge61jjJqp$K1$P7}(};FO4I{5iO%nnw1N zW(rsq5GwE;1SX71T4`RqbC5sZ-Opz)dJmmUr$^0_&q%BVHJuvhOaS6U zQKDC~2=uYMR^YMd$YQTz#7gC(AyFWmpVauogybl;OzSF=?pz|n_jjidU0F%w?Adv- z)dEY^0vZb_BuBZ0Z@e+W)8gcF3S?k8RZfufqfcU(1?ifA8qs3P6X^8gE)@U#-A);4 m6UX(`oNW7Nh^^qw@uQCr*xK!#31L*A`qP&_?iMV@*na_Wwzk6n diff --git a/crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000002 b/crates/sui-framework-snapshot/bytecode_snapshot/32/0x0000000000000000000000000000000000000000000000000000000000000002 deleted file mode 100644 index ac3940ae7b33a86c7ae2e7571a1c6a34d7b98be5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44093 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 diff --git a/crates/sui-framework-snapshot/bytecode_snapshot/32/0x000000000000000000000000000000000000000000000000000000000000dee9 b/crates/sui-framework-snapshot/bytecode_snapshot/32/0x000000000000000000000000000000000000000000000000000000000000dee9 deleted file mode 100644 index a6e89ffe869b5f4fbf4c9a91896e519f237c8918..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31767 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 VF$qNb22)u<=bip2eaAohe*+|jbhH2f diff --git a/crates/sui-framework-snapshot/manifest.json b/crates/sui-framework-snapshot/manifest.json index dc061f163eb78..7a6d30e6823ff 100644 --- a/crates/sui-framework-snapshot/manifest.json +++ b/crates/sui-framework-snapshot/manifest.json @@ -241,16 +241,7 @@ ] }, "31": { - "git_revision": "c87e139af9-dirty", - "package_ids": [ - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x000000000000000000000000000000000000000000000000000000000000dee9" - ] - }, - "32": { - "git_revision": "e4c5b06356", + "git_revision": "ac208a8cb1", "package_ids": [ "0x0000000000000000000000000000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000000000000000000000000000002", diff --git a/crates/sui-framework/packages/sui-framework/sources/balance.move b/crates/sui-framework/packages/sui-framework/sources/balance.move index 17191e08ee97b..163e89dee34c1 100644 --- a/crates/sui-framework/packages/sui-framework/sources/balance.move +++ b/crates/sui-framework/packages/sui-framework/sources/balance.move @@ -154,6 +154,12 @@ module sui::balance { let Balance { value } = self; value } + + #[test_only] + /// Create a `Supply` of any coin for testing purposes. + public fun create_supply_for_testing(): Supply { + Supply { value: 0 } + } } #[test_only] diff --git a/crates/sui-framework/packages/sui-framework/sources/coin.move b/crates/sui-framework/packages/sui-framework/sources/coin.move index fbd3bec331db6..ac1eb593bf905 100644 --- a/crates/sui-framework/packages/sui-framework/sources/coin.move +++ b/crates/sui-framework/packages/sui-framework/sources/coin.move @@ -364,33 +364,23 @@ module sui::coin { // === Get coin metadata fields for on-chain consumption === - public fun get_decimals( - metadata: &CoinMetadata - ): u8 { + public fun get_decimals(metadata: &CoinMetadata): u8 { metadata.decimals } - public fun get_name( - metadata: &CoinMetadata - ): string::String { + public fun get_name(metadata: &CoinMetadata): string::String { metadata.name } - public fun get_symbol( - metadata: &CoinMetadata - ): ascii::String { + public fun get_symbol(metadata: &CoinMetadata): ascii::String { metadata.symbol } - public fun get_description( - metadata: &CoinMetadata - ): string::String { + public fun get_description(metadata: &CoinMetadata): string::String { metadata.description } - public fun get_icon_url( - metadata: &CoinMetadata - ): Option { + public fun get_icon_url(metadata: &CoinMetadata): Option { metadata.icon_url } @@ -410,6 +400,17 @@ module sui::coin { balance::destroy_for_testing(balance) } + #[test_only] + /// Create a `TreasuryCap` for any `Coin` for testing purposes. + public fun create_treasury_cap_for_testing( + ctx: &mut TxContext + ): TreasuryCap { + TreasuryCap { + id: object::new(ctx), + total_supply: balance::create_supply_for_testing() + } + } + // === Deprecated code === // oops, wanted treasury: &TreasuryCap diff --git a/crates/sui-framework/packages/sui-framework/sources/kiosk/transfer_policy.move b/crates/sui-framework/packages/sui-framework/sources/kiosk/transfer_policy.move index 2300175ea6f6d..bfb24b4e74168 100644 --- a/crates/sui-framework/packages/sui-framework/sources/kiosk/transfer_policy.move +++ b/crates/sui-framework/packages/sui-framework/sources/kiosk/transfer_policy.move @@ -96,6 +96,10 @@ module sui::transfer_policy { /// making the discoverability and tracking the supported types easier. struct TransferPolicyCreated has copy, drop { id: ID } + /// Event that is emitted when a publisher destroys a `TransferPolicyCap`. + /// Allows for tracking supported policies. + struct TransferPolicyDestroyed has copy, drop { id: ID } + /// Key to store "Rule" configuration for a specific `TransferPolicy`. struct RuleKey has copy, store, drop {} @@ -166,11 +170,12 @@ module sui::transfer_policy { ): Coin { assert!(object::id(&self) == cap.policy_id, ENotOwner); - let TransferPolicyCap { id: cap_id, policy_id: _ } = cap; + let TransferPolicyCap { id: cap_id, policy_id } = cap; let TransferPolicy { id, rules: _, balance } = self; object::delete(id); object::delete(cap_id); + event::emit(TransferPolicyDestroyed { id: policy_id }); coin::from_balance(balance, ctx) } diff --git a/dapps/regulated-token/sources/token.move b/crates/sui-framework/packages/sui-framework/sources/token.move similarity index 89% rename from dapps/regulated-token/sources/token.move rename to crates/sui-framework/packages/sui-framework/sources/token.move index 97ba68678be0c..3cf21fa321e44 100644 --- a/dapps/regulated-token/sources/token.move +++ b/crates/sui-framework/packages/sui-framework/sources/token.move @@ -19,7 +19,7 @@ /// The Token system allows for fine-grained control over the actions performed /// on the token. And hence it is highly suitable for applications that require /// control over the currency which a simple open-loop system can't provide. -module regulated_token::token { +module sui::token { use std::vector; use std::string::{Self, String}; use std::option::{Self, Option}; @@ -46,9 +46,11 @@ module regulated_token::token { /// The balance is not zero when trying to confirm with `TransferPolicyCap`. const ECantConsumeBalance: u64 = 5; /// Trying to perform an owner-gated action without being the owner. - const ENotOwner: u64 = 6; /// Rule is trying to access a missing config (with type). const ENoConfig: u64 = 7; + /// Using `confirm_request_mut` without `spent_balance`. Immutable version + /// of the function must be used instead. + const EUseImmutableConfirm: u64 = 8; // === Protected Actions === @@ -67,12 +69,6 @@ module regulated_token::token { id: UID, /// The Balance of the `Token`. balance: Balance, - /// The owner of the `Token`. - /// Defaults to `tx_context::sender`, however, in the `transfer` action - /// it is changed to the "recipient". This field is future-proofing for - /// the "Transfer To Object" (TTO) feature, making it impossible to send - /// a `Token` to an object and do arbitrary transfers through it. - owner: address, } /// A Capability that manages a single `TokenPolicy` specified in the `for` @@ -81,7 +77,7 @@ module regulated_token::token { /// `TokenPolicy` represents a set of rules that define what actions can be /// performed on a `Token` and which `Rules` must be satisfied for the - /// action for the transaction to succeeed. + /// action to succeed. /// /// - For the sake of availability, `TokenPolicy` is a `key`-only object. /// - Each `TokenPolicy` is managed by a matching `TokenPolicyCap`. @@ -104,7 +100,7 @@ module regulated_token::token { } /// A request to perform an "Action" on a token. Stores the information - /// about the performed action and must be consumed by the `confirm_request` + /// about the action to be performed and must be consumed by the `confirm_request` /// or `confirm_request_mut` functions when the Rules are satisfied. struct ActionRequest { /// Name of the Action to look up in the Policy. Name can be one of the @@ -127,7 +123,7 @@ module regulated_token::token { } /// Dynamic field key for the `TokenPolicy` to store the `Config` for a - /// specific `Rule` for an action. There can be only one configuration per + /// specific action `Rule`. There can be only one configuration per /// `Rule` per `TokenPolicy`. struct RuleKey has store, copy, drop { is_protected: bool } @@ -153,6 +149,7 @@ module regulated_token::token { (policy, cap) } + #[lint_allow(share_owned)] /// Share the `TokenPolicy`. Due to `key`-only restriction, it must be /// shared after initialization. public fun share_policy(policy: TokenPolicy) { @@ -164,18 +161,14 @@ module regulated_token::token { /// Transfer a `Token` to a `recipient`. Creates an `ActionRequest` for the /// "transfer" action. The `ActionRequest` contains the `recipient` field /// to be used in verification. - /// - /// Aborts if the `Token.owner` is not the transaction sender. public fun transfer( t: Token, recipient: address, ctx: &mut TxContext ): ActionRequest { - assert!(t.owner == tx_context::sender(ctx), ENotOwner); let amount = balance::value(&t.balance); - t.owner = recipient; transfer::transfer(t, recipient); new_request( - string::utf8(TRANSFER), + transfer_action(), amount, option::some(recipient), option::none(), @@ -189,15 +182,12 @@ module regulated_token::token { /// /// Spend action requires `confirm_request_mut` to be called to confirm the /// request and join the spent balance with the `TokenPolicy.spent_balance`. - /// - /// Aborts if the `Token.owner` is not the transaction sender. public fun spend(t: Token, ctx: &mut TxContext): ActionRequest { - let Token { id, balance, owner } = t; - assert!(owner == tx_context::sender(ctx), ENotOwner); + let Token { id, balance } = t; object::delete(id); new_request( - string::utf8(SPEND), + spend_action(), balance::value(&balance), option::none(), option::some(balance), @@ -207,20 +197,17 @@ module regulated_token::token { /// Convert `Token` into an open `Coin`. Creates an `ActionRequest` for the /// "to_coin" action. - /// - /// Aborts if the `Token.owner` is not the transaction sender. public fun to_coin( t: Token, ctx: &mut TxContext ): (Coin, ActionRequest) { - let Token { id, balance, owner } = t; + let Token { id, balance } = t; let amount = balance::value(&balance); - assert!(owner == tx_context::sender(ctx), ENotOwner); object::delete(id); ( coin::from_balance(balance, ctx), new_request( - string::utf8(TO_COIN), + to_coin_action(), amount, option::none(), option::none(), @@ -230,21 +217,20 @@ module regulated_token::token { } /// Convert an open `Coin` into a `Token`. Creates an `ActionRequest` for - /// the "from_coin" action. The owner of the `Token` is set to the sender. + /// the "from_coin" action. public fun from_coin( coin: Coin, ctx: &mut TxContext ): (Token, ActionRequest) { let amount = coin::value(&coin); let token = Token { id: object::new(ctx), - owner: tx_context::sender(ctx), balance: coin::into_balance(coin) }; ( token, new_request( - string::utf8(FROM_COIN), + from_coin_action(), amount, option::none(), option::none(), @@ -256,15 +242,13 @@ module regulated_token::token { // === Public Actions === /// Join two `Token`s into one, always available. - /// Aborts if the `Token.owner` fields don't match. public fun join(token: &mut Token, another: Token) { - let Token { id, balance, owner } = another; - assert!(token.owner == owner, ENotOwner); + let Token { id, balance } = another; balance::join(&mut token.balance, balance); object::delete(id); } - /// Split a `Token` with `amount`. The `Token.owner` is preserved. + /// Split a `Token` with `amount`. /// Aborts if the `Token.balance` is lower than `amount`. public fun split( token: &mut Token, amount: u64, ctx: &mut TxContext @@ -273,7 +257,6 @@ module regulated_token::token { Token { id: object::new(ctx), balance: balance::split(&mut token.balance, amount), - owner: token.owner } } @@ -282,23 +265,21 @@ module regulated_token::token { Token { id: object::new(ctx), balance: balance::zero(), - owner: tx_context::sender(ctx) } } /// Destroy an empty `Token`, fails if the balance is non-zero. - /// Aborts if the `Token.balance` is not zero. Ignores the `Token.owner`. + /// Aborts if the `Token.balance` is not zero. public fun destroy_zero(token: Token) { - let Token { id, balance, owner: _ } = token; + let Token { id, balance } = token; assert!(balance::value(&balance) == 0, ENotZero); balance::destroy_zero(balance); object::delete(id); } + #[lint_allow(self_transfer)] /// Transfer the `Token` to the transaction sender. - /// Aborts if the `Token.owner` is not the transaction sender. public fun keep(token: Token, ctx: &mut TxContext) { - assert!(token.owner == tx_context::sender(ctx), ENotOwner); transfer::transfer(token, tx_context::sender(ctx)) } @@ -338,8 +319,8 @@ module regulated_token::token { request: ActionRequest, _ctx: &mut TxContext ): (String, u64, address, Option
) { - assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction); assert!(option::is_none(&request.spent_balance), ECantConsumeBalance); + assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction); let ActionRequest { name, approvals, @@ -376,12 +357,12 @@ module regulated_token::token { ctx: &mut TxContext ): (String, u64, address, Option
) { assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction); - if (option::is_some(&request.spent_balance)) { - balance::join( - &mut policy.spent_balance, - option::extract(&mut request.spent_balance) - ); - }; + assert!(option::is_some(&request.spent_balance), EUseImmutableConfirm); + + balance::join( + &mut policy.spent_balance, + option::extract(&mut request.spent_balance) + ); confirm_request(policy, request, ctx) } @@ -496,8 +477,8 @@ module regulated_token::token { assert!(has_rule_config_with_type(self), ENoConfig); assert!(object::id(self) == cap.for, ENotAuthorized); df::borrow_mut(&mut self.id, key()) - } + /// Remove a `Config` for a `Rule` in the `TokenPolicy`. /// Unlike the `add_rule_config`, this function does not require a `Rule` /// witness, hence can be performed by the `TokenPolicy` owner on their own. @@ -566,7 +547,6 @@ module regulated_token::token { /// /// Aborts if the `TokenPolicyCap` is not matching the `TokenPolicy`. public fun add_rule_for_action( - // _rule: Rule, // TODO: keep or remove self: &mut TokenPolicy, cap: &TokenPolicyCap, action: String, @@ -603,20 +583,17 @@ module regulated_token::token { // === Protected: Treasury Management === - /// Mint a `Token` with a given `amount` using the `TreasuryCap`. The - /// `owner` field is set to the transaction sender; if a `Token` is minted - /// for some other account it will require `transfer` to be performed anyway + /// Mint a `Token` with a given `amount` using the `TreasuryCap`. public fun mint( cap: &mut TreasuryCap, amount: u64, ctx: &mut TxContext ): Token { let balance = balance::increase_supply(coin::supply_mut(cap), amount); - Token { id: object::new(ctx), balance, owner: tx_context::sender(ctx) } + Token { id: object::new(ctx), balance } } - /// Burn a `Token` using the `TreasuryCap`. Avoids the `owner` check due to - /// the action only being available to the `TreasuryCap` owner. + /// Burn a `Token` using the `TreasuryCap`. public fun burn(cap: &mut TreasuryCap, token: Token) { - let Token { id, balance, owner: _ } = token; + let Token { id, balance } = token; balance::decrease_supply(coin::supply_mut(cap), balance); object::delete(id); } @@ -673,8 +650,8 @@ module regulated_token::token { // === Action Request Fields == - /// Name of the `ActionRequest`. - public fun name(self: &ActionRequest): String { self.name } + /// The Action in the `ActionRequest`. + public fun action(self: &ActionRequest): String { self.name } /// Amount of the `ActionRequest`. public fun amount(self: &ActionRequest): u64 { self.amount } @@ -688,8 +665,8 @@ module regulated_token::token { } /// Approvals of the `ActionRequest`. - public fun approvals(self: &ActionRequest): &VecSet { - &self.approvals + public fun approvals(self: &ActionRequest): VecSet { + self.approvals } /// Burned balance of the `ActionRequest`. @@ -706,6 +683,9 @@ module regulated_token::token { /// Create a new `RuleKey` for a `Rule`. The `is_protected` field is kept /// for potential future use, if Rules were to have a freely modifiable /// storage as addition / replacement for the `Config` system. + /// + /// The goal of `is_protected` is to potentially allow Rules store a mutable + /// version of their configuration and mutate state on user action. fun key(): RuleKey { RuleKey { is_protected: true } } // === Testing === @@ -742,12 +722,12 @@ module regulated_token::token { #[test_only] public fun mint_for_testing(amount: u64, ctx: &mut TxContext): Token { let balance = balance::create_for_testing(amount); - Token { id: object::new(ctx), balance, owner: tx_context::sender(ctx) } + Token { id: object::new(ctx), balance } } #[test_only] public fun burn_for_testing(token: Token) { - let Token { id, balance, owner: _ } = token; + let Token { id, balance } = token; balance::destroy_for_testing(balance); object::delete(id); } diff --git a/crates/sui-framework/packages/sui-framework/tests/kiosk/kiosk_tests.move b/crates/sui-framework/packages/sui-framework/tests/kiosk/kiosk_tests.move index 2b3893c2a349b..c6e4eb1b66749 100644 --- a/crates/sui-framework/packages/sui-framework/tests/kiosk/kiosk_tests.move +++ b/crates/sui-framework/packages/sui-framework/tests/kiosk/kiosk_tests.move @@ -128,6 +128,7 @@ module sui::kiosk_tests { abort 1337 } + #[allow(unused_field)] struct WrongAsset has key, store { id: sui::object::UID } #[test] diff --git a/crates/sui-framework/packages/sui-framework/tests/pay_tests.move b/crates/sui-framework/packages/sui-framework/tests/pay_tests.move index be74b90273072..c3440a44a6f56 100644 --- a/crates/sui-framework/packages/sui-framework/tests/pay_tests.move +++ b/crates/sui-framework/packages/sui-framework/tests/pay_tests.move @@ -14,7 +14,7 @@ module sui::pay_tests { const TEST_SENDER_ADDR: address = @0xA11CE; #[test] - public entry fun test_coin_split_n() { + fun test_coin_split_n() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); @@ -45,7 +45,7 @@ module sui::pay_tests { } #[test] - public entry fun test_coin_split_n_to_vec() { + fun test_coin_split_n_to_vec() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); @@ -69,7 +69,7 @@ module sui::pay_tests { } #[test] - public entry fun test_split_vec() { + fun test_split_vec() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); @@ -96,7 +96,7 @@ module sui::pay_tests { } #[test] - public entry fun test_split_and_transfer() { + fun test_split_and_transfer() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); @@ -118,7 +118,7 @@ module sui::pay_tests { #[test] #[expected_failure(abort_code = balance::ENotEnough)] - public entry fun test_split_and_transfer_fail() { + fun test_split_and_transfer_fail() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); @@ -137,7 +137,7 @@ module sui::pay_tests { } #[test] - public entry fun test_join_vec_and_transfer() { + fun test_join_vec_and_transfer() { let scenario_val = test_scenario::begin(TEST_SENDER_ADDR); let scenario = &mut scenario_val; let ctx = test_scenario::ctx(scenario); diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_actions_tests.move b/crates/sui-framework/packages/sui-framework/tests/token/token_actions_tests.move new file mode 100644 index 0000000000000..670042ce6bbb6 --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_actions_tests.move @@ -0,0 +1,123 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// This testing block makes sure the protected (restricted) actions behave as +/// intended, that the request is well formed and that APIs are usable. +/// +/// It also tests custom actions which can be implemented by policy owner. +module sui::token_actions_tests { + use std::option; + use std::string; + use sui::token; + use sui::token_test_utils as test; + + #[test] + /// Scenario: perform a transfer operation, and confirm that the request + /// is well-formed. + fun test_transfer_action() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + let token = test::mint(1000, ctx); + let request = token::transfer(token, @0x2, ctx); + + assert!(token::action(&request) == token::transfer_action(), 0); + assert!(token::amount(&request) == 1000, 1); + assert!(token::sender(&request) == @0x0, 2); + + let recipient = token::recipient(&request); + + assert!(option::is_some(&recipient), 3); + assert!(option::borrow(&recipient) == &@0x2, 4); + assert!(option::is_none(&token::spent(&request)), 5); + + token::confirm_with_policy_cap(&cap, request, ctx); + test::return_policy(policy, cap); + } + + #[test] + /// Scenario: spend 1000 tokens, make sure the request is well-formed, and + /// confirm request in the policy making sure the balance is updated. + fun test_spend_action() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + let token = test::mint(1000, ctx); + let request = token::spend(token, ctx); + + token::allow(&mut policy, &cap, token::spend_action(), ctx); + + assert!(token::action(&request) == token::spend_action(), 0); + assert!(token::amount(&request) == 1000, 1); + assert!(token::sender(&request) == @0x0, 2); + assert!(option::is_none(&token::recipient(&request)), 3); + assert!(option::is_some(&token::spent(&request)), 4); + assert!(option::borrow(&token::spent(&request)) == &1000, 5); + + token::confirm_request_mut(&mut policy, request, ctx); + + assert!(token::spent_balance(&policy) == 1000, 6); + + test::return_policy(policy, cap); + } + + #[test] + /// Scenario: turn 1000 tokens into Coin, make sure the request is well-formed, + /// and perform a from_coin action to turn the Coin back into tokens. + fun test_to_from_coin_action() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + let token = test::mint(1000, ctx); + let (coin, to_request) = token::to_coin(token, ctx); + + assert!(token::action(&to_request) == token::to_coin_action(), 0); + assert!(token::amount(&to_request) == 1000, 1); + assert!(token::sender(&to_request) == @0x0, 2); + assert!(option::is_none(&token::recipient(&to_request)), 3); + assert!(option::is_none(&token::spent(&to_request)), 4); + + let (token, from_request) = token::from_coin(coin, ctx); + + assert!(token::action(&from_request) == token::from_coin_action(), 5); + assert!(token::amount(&from_request) == 1000, 6); + assert!(token::sender(&from_request) == @0x0, 7); + assert!(option::is_none(&token::recipient(&from_request)), 8); + assert!(option::is_none(&token::spent(&from_request)), 9); + + token::keep(token, ctx); + token::confirm_with_policy_cap(&cap, to_request, ctx); + token::confirm_with_policy_cap(&cap, from_request, ctx); + test::return_policy(policy, cap); + } + + #[test] + /// Scenario: create a custom request, allow it in the policy, make sure + /// that the request matches the expected values, and confirm it in the + /// policy. + fun test_custom_action() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let custom_action = string::utf8(b"custom"); + + token::allow(&mut policy, &cap, custom_action, ctx); + + let request = token::new_request( + custom_action, + 1000, + option::none(), + option::none(), + ctx + ); + + assert!(token::action(&request) == custom_action, 0); + assert!(token::amount(&request) == 1000, 1); + assert!(token::sender(&request) == @0x0, 2); + assert!(option::is_none(&token::recipient(&request)), 3); + assert!(option::is_none(&token::spent(&request)), 4); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap); + } +} diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_config_tests.move b/crates/sui-framework/packages/sui-framework/tests/token/token_config_tests.move new file mode 100644 index 0000000000000..bab9006a0f0a3 --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_config_tests.move @@ -0,0 +1,115 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// The goal of this module is to test Rule configuration setting and how Rules +/// can read / modify the configuration in 'em. +module sui::token_config_tests { + use sui::token_test_utils::{Self as test, TEST}; + use sui::token; + + /// Rule witness to store confuration for + struct Rule1 has drop {} + + /// Configuration for the Rule1. + struct Config1 has store, drop { value: u64 } + + #[test] + /// Scenario: create a Config, read it, mutate it, check existence and remove + fun test_create_and_use_rule_config() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let config = Config1 { value: 0 }; + + // add a rule config + token::add_rule_config(Rule1 {}, &mut policy, &cap, config, ctx); + + let config_mut: &mut Config1 = token::rule_config_mut(Rule1 {}, &mut policy, &cap); + config_mut.value = 1000; + + // make sure rule can read config without Policy Owner + let config_ref: &Config1 = token::rule_config(Rule1 {}, &policy); + assert!(config_ref.value == 1000, 0); + assert!(token::has_rule_config(&policy), 1); + assert!(token::has_rule_config_with_type(&policy), 2); + + let config = token::remove_rule_config(&mut policy, &cap, ctx); + assert!(config.value == 1000, 3); + + test::return_policy(policy, cap); + } + + #[test, expected_failure(abort_code = token::ENotAuthorized)] + /// Scenario: try to add config while not being authorized + fun test_add_config_not_authorized_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = test::get_policy(ctx); + let (_policy, cap) = test::get_policy(ctx); + let config = Config1 { value: 0 }; + + token::add_rule_config(Rule1 {}, &mut policy, &cap, config, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ENotAuthorized)] + /// Scenario: try to add config while not being authorized + fun test_remove_config_not_authorized_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let (_policy, wrong_cap) = test::get_policy(ctx); + let config = Config1 { value: 0 }; + + token::add_rule_config(Rule1 {}, &mut policy, &cap, config, ctx); + token::remove_rule_config(&mut policy, &wrong_cap, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ENotAuthorized)] + /// Scenario: try to mutate config while not being authorized + fun test_mutate_config_not_authorized_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let (_policy, wrong_cap) = test::get_policy(ctx); + let config = Config1 { value: 0 }; + + token::add_rule_config(Rule1 {}, &mut policy, &cap, config, ctx); + token::rule_config_mut(Rule1 {}, &mut policy, &wrong_cap); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ENoConfig)] + /// Scenario: rule tries to access a missing config + fun test_rule_config_missing_config_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = test::get_policy(ctx); + + token::rule_config(Rule1 {}, &policy); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ENoConfig)] + /// Scenario: rule tries to access a missing config + fun test_rule_config_mut_missing_config_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + token::rule_config_mut(Rule1 {}, &mut policy, &cap); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ENoConfig)] + /// Scenario: trying to remove a non existing config + fun test_remove_rule_config_missing_config_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + token::remove_rule_config(&mut policy, &cap, ctx); + + abort 1337 + } +} diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_public_actions_tests.move b/crates/sui-framework/packages/sui-framework/tests/token/token_public_actions_tests.move new file mode 100644 index 0000000000000..f4499f81e449c --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_public_actions_tests.move @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// This module tests `join`, `split`, `zero` and `destroy_zero` functions +module sui::token_public_actions_tests { + use sui::token_test_utils::{Self as test, TEST}; + use sui::token; + + #[test] + /// Scenario: mint a Token, split it, merge back, then issue a zero and + /// destroy it. + fun test_public_split_join_zero_destroy() { + let ctx = &mut test::ctx(@0x0); + let token = test::mint(100, ctx); + + let split = token::split(&mut token, 50, ctx); + let zero = token::zero(ctx); + + token::join(&mut token, split); + token::join(&mut token, zero); + + let zero = token::split(&mut token, 0, ctx); + token::destroy_zero(zero); + token::keep(token, ctx); + } + + #[test, expected_failure(abort_code = token::ENotZero)] + /// Scenario: try to destroy a non-zero Token. + fun test_public_destroy_non_zero_fail() { + let ctx = &mut test::ctx(@0x0); + let token = test::mint(100, ctx); + + token::destroy_zero(token) + } + + #[test, expected_failure(abort_code = token::EBalanceTooLow)] + /// Scenario: try to split more than in the Token. + fun test_split_excessive_fail() { + let ctx = &mut test::ctx(@0x0); + let token = test::mint(0, ctx); + + let _t = token::split(&mut token, 100, ctx); + + abort 1337 + } +} diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_request_tests.move b/crates/sui-framework/packages/sui-framework/tests/token/token_request_tests.move new file mode 100644 index 0000000000000..b7a7d4910ebcd --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_request_tests.move @@ -0,0 +1,131 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// This module implements tests for the request formation and approval in the +/// `TokenPolicy`. +module sui::token_request_tests { + use std::string; + use std::option::none; + use sui::token; + use sui::token_test_utils::{Self as test, TEST}; + + struct Rule1 has drop {} + struct Rule2 has drop {} + + #[test] + /// Scenario: allow test action, create request, confirm request + fun test_request_confirm() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let action = string::utf8(b"test"); + + token::allow(&mut policy, &cap, action, ctx); + + let request = token::new_request(action, 100, none(), none(), ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap) + } + + #[test] + /// Scenario: issue a non-spend request, confirm with `TokenPolicyCap` + fun test_request_confirm_with_cap() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let token = test::mint(100, ctx); + + let request = token::transfer(token, @0x2, ctx); + token::confirm_with_policy_cap(&cap, request, ctx); + test::return_policy(policy, cap); + } + + #[test] + /// Scenario: Policy requires only Rule1 but request gets approval from + /// Rule2 and Rule1 + fun test_request_confirm_excessive_approvals_pass() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let action = string::utf8(b"test"); + + token::add_rule_for_action(&mut policy, &cap, action, ctx); + + let request = token::new_request(action, 100, none(), none(), ctx); + + token::add_approval(Rule1 {}, &mut request, ctx); + token::add_approval(Rule2 {}, &mut request, ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap) + } + + #[test, expected_failure(abort_code = token::EUnknownAction)] + /// Scenario: Policy does not allow test action, create request, try confirm + fun test_request_confirm_unknown_action_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let action = string::utf8(b"test"); + + let request = token::new_request(action, 100, none(), none(), ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap) + } + + #[test, expected_failure(abort_code = token::ENotApproved)] + /// Scenario: Policy requires Rule1 but request gets approval from Rule2 + fun test_request_confirm_not_approved_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let action = string::utf8(b"test"); + + token::add_rule_for_action(&mut policy, &cap, action, ctx); + + let request = token::new_request(action, 100, none(), none(), ctx); + + token::add_approval(Rule2 {}, &mut request, ctx); + token::confirm_request(&policy, request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::ECantConsumeBalance)] + /// Scenario: issue a Spend request, try to confirm it with `TokenPolicyCap` + fun test_request_cant_consume_balance_with_cap() { + let ctx = &mut test::ctx(@0x0); + let (_policy, cap) = test::get_policy(ctx); + let token = test::mint(100, ctx); + let request = token::spend(token, ctx); + + token::confirm_with_policy_cap(&cap, request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::EUseImmutableConfirm)] + /// Scenario: issue a transfer request, try to confirm it with `_mut` + fun test_request_use_mutable_confirm_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let token = test::mint(100, ctx); + let request = token::transfer(token, @0x2, ctx); + + token::allow(&mut policy, &cap, token::transfer_action(), ctx); + token::confirm_request_mut(&mut policy, request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = token::EUnknownAction)] + /// Scenario: issue a transfer request with balance, action not allowed + fun test_request_use_mutable_action_not_allowed_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = test::get_policy(ctx); + let token = test::mint(100, ctx); + let request = token::spend(token, ctx); + + token::confirm_request_mut(&mut policy, request, ctx); + + abort 1337 + } +} diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_test_utils.move b/crates/sui-framework/packages/sui-framework/tests/token/token_test_utils.move new file mode 100644 index 0000000000000..cc58c851e8f55 --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_test_utils.move @@ -0,0 +1,50 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// This module defines base testing utilities for the +module sui::token_test_utils { + use sui::coin::{Self, TreasuryCap}; + use sui::tx_context::{Self, TxContext}; + use sui::token::{Self, Token, TokenPolicy, TokenPolicyCap}; + + /// The type of the test Token. + struct TEST has drop {} + + /// Get a context for testing. + public fun ctx(sender: address): TxContext { + let tx_hash = x"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"; + tx_context::new(sender, tx_hash, 0, 0, 0) + } + + /// Get `TreasuryCap` for the TEST token. + public fun get_treasury_cap(ctx: &mut TxContext): TreasuryCap { + coin::create_treasury_cap_for_testing(ctx) + } + + #[lint_allow(share_owned)] + /// Return `TreasuryCap` (shares it for now). + public fun return_treasury_cap(treasury_cap: TreasuryCap) { + sui::transfer::public_share_object(treasury_cap) + } + + /// Get a policy for testing. + public fun get_policy(ctx: &mut TxContext): (TokenPolicy, TokenPolicyCap) { + token::new_policy_for_testing(ctx) + } + + /// Gracefully unpack policy after the tests have been performed. + public fun return_policy(policy: TokenPolicy, cap: TokenPolicyCap) { + token::burn_policy_for_testing(policy, cap) + } + + /// Mint a test token. + public fun mint(amount: u64, ctx: &mut TxContext): Token { + token::mint_for_testing(amount, ctx) + } + + /// Burn a test token. + public fun burn(token: Token) { + token::burn_for_testing(token) + } +} diff --git a/crates/sui-framework/packages/sui-framework/tests/token/token_treasury_cap_tests.move b/crates/sui-framework/packages/sui-framework/tests/token/token_treasury_cap_tests.move new file mode 100644 index 0000000000000..fb54aa7c4cbfe --- /dev/null +++ b/crates/sui-framework/packages/sui-framework/tests/token/token_treasury_cap_tests.move @@ -0,0 +1,56 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// This module implements tests for the TreasuryCap-related functionality such +/// as spending, "flush"-ing, issuing new coins and performing marketplace-like +/// operations. +module sui::token_treasury_cap_tests { + use sui::token_test_utils as test; + use sui::token; + + #[test] + /// Scenario: mint and spend a Token, confirm spending request with the + /// `TreasuryCap`. + fun test_treasury_spend_flush() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + let treasury_cap = test::get_treasury_cap(ctx); + + let token = token::mint(&mut treasury_cap, 1000, ctx); + let request = token::spend(token, ctx); + + token::allow(&mut policy, &cap, token::spend_action(), ctx); + token::confirm_request_mut(&mut policy, request, ctx); + token::flush(&mut policy, &mut treasury_cap, ctx); + + test::return_treasury_cap(treasury_cap); + test::return_policy(policy, cap); + } + + #[test] + /// Scenario: mint and spend a Token, confirm spending request with the + /// `TreasuryCap`. + fun test_treasury_resolve_request() { + let ctx = &mut test::ctx(@0x0); + let treasury_cap = test::get_treasury_cap(ctx); + + let token = token::mint(&mut treasury_cap, 1000, ctx); + let request = token::spend(token, ctx); + + token::confirm_with_treasury_cap(&mut treasury_cap, request, ctx); + test::return_treasury_cap(treasury_cap); + } + + #[test] + /// Scenario: mint and burn a Token with TreasuryCap. + fun test_treasury_mint_burn() { + let ctx = &mut test::ctx(@0x0); + let treasury_cap = test::get_treasury_cap(ctx); + + let token = token::mint(&mut treasury_cap, 1000, ctx); + token::burn(&mut treasury_cap, token); + + test::return_treasury_cap(treasury_cap); + } +} diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 1f8aa0d52c44f..afc20fc2b8425 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -93,6 +93,7 @@ const MAX_PROTOCOL_VERSION: u64 = 32; // Create new execution layer version, and preserve previous behavior in v1. // Update semantics of `sui::transfer::receive` and add `sui::transfer::public_receive`. // Version 32: Add delete functions for VerifiedID and VerifiedIssuer. +// Add sui::token module to sui framework. #[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ProtocolVersion(u64); diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap index ee55f2318b725..75a1bfbdf96d7 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap @@ -240,13 +240,13 @@ validators: next_epoch_worker_address: ~ extra_fields: id: - id: "0xdd06d2690b6cf0daaa8e467fb8ca750b6a9f74048f31cb1a8825140a2e3386dd" + id: "0x8ae3895f1cd77c44235933e3e0608a50dff570f363784e26bbb6c9f17a09ed4d" size: 0 voting_power: 10000 - operation_cap_id: "0x5b3acebd4973238c56df874d9006f9e41593a65bd5298a3476732a0bf71c940f" + operation_cap_id: "0x85a9086d42b908440c4923385f5a053edc8ff9b46d10f2db0ba7eedcfa8e3db7" gas_price: 1000 staking_pool: - id: "0xd01c37f6222af1b86e8e78e9d68bab5ec70a10c8cd581ac91d564a302db1a69c" + id: "0x920d290daced2e37b645083e3c86fd7c56bf2da203044ce2a7bc412469a429c7" activation_epoch: 0 deactivation_epoch: ~ sui_balance: 20000000000000000 @@ -254,14 +254,14 @@ validators: value: 0 pool_token_balance: 20000000000000000 exchange_rates: - id: "0x96e610d760ef5cacf80d02fe94efaa4948dd05b5c575ab0bb0869dbac94f404d" + id: "0xf67e7c09a89c2c9d0ee60a020b3fbd0e586698fd27a891b1728211f81ed8e5d6" size: 1 pending_stake: 0 pending_total_sui_withdraw: 0 pending_pool_token_withdraw: 0 extra_fields: id: - id: "0x07e324f698bb688de1a8baaeb83387f170e3a4d1a26641eb3ae6248b8158144f" + id: "0x5deb7d507be33b91a01896354f579a5903ac68fa4d2ddbb60b6240a5670831a3" size: 0 commission_rate: 200 next_epoch_stake: 20000000000000000 @@ -269,27 +269,27 @@ validators: next_epoch_commission_rate: 200 extra_fields: id: - id: "0x2a184b792cab8f12f93778b7b7b93b2ffd72b9c7dce553fcffa2cb29b17be8f6" + id: "0x9ca396529acc9c45f98c9d191e9c70647a51afcb446c4a65d9551e606077d773" size: 0 pending_active_validators: contents: - id: "0x95bb8afc37a1145f9fb0aa973031d530fef6ce9be21d71e4c0f7a1918b76d0fa" + id: "0xe05ed8e175e92e73b0b162c0cce65c2f201984b274e49a0bb9896a9ff5e35c5a" size: 0 pending_removals: [] staking_pool_mappings: - id: "0xfc0bc0cca3c6f51b9f9ddb0f669bc2c88be1b2273fa0e96bd0d5156609da9580" + id: "0x282b2c60fc6c1044cc22853d86743ee6b32e577fa0e9d89b278b97f9a78ca921" size: 1 inactive_validators: - id: "0x1cd2a53692663cccbc352434ba04b143427573acf76ec1d26d103c0b72ed30bb" + id: "0xe1d6c1311bca780793a58b0554a8b218123df1117ec81c7959197b20136cf010" size: 0 validator_candidates: - id: "0x0cf36538434f9c834853d787a58c902e29b619f7bf577d0b8381cb48139c589b" + id: "0x9515821b51655e76e0b6ead48938dcf1a1ba04af7b9b2125ac32ac88deb0cb65" size: 0 at_risk_validators: contents: [] extra_fields: id: - id: "0x3e7212912d1a9827e75c036e938cbd2a01c25c546ed488aa3a9b0a50d16572c7" + id: "0x57eccd126a9831dc2ce20818ed531ade3d47409a6fca8b10b269681d629019c2" size: 0 storage_fund: total_object_storage_rebates: @@ -306,7 +306,7 @@ parameters: validator_low_stake_grace_period: 7 extra_fields: id: - id: "0x87cbaebce00cecadfd60e901b4850dfd41e5e5edf9c9ad09d90a13b84a9e9547" + id: "0x8d3084d1f12045b6f7bcdfbc9f6685c6915c1f0f15e11ac00ee0862ae6970ad3" size: 0 reference_gas_price: 1000 validator_report_records: @@ -320,7 +320,7 @@ stake_subsidy: stake_subsidy_decrease_rate: 1000 extra_fields: id: - id: "0x5b50f18eef83ec47912f76505472f30e20a450bd13f3eebd6b5ba81ee0cf9ab9" + id: "0x4fc3e664d15f86074969b1b8720f1385be9048c3ba44bbf2a22eec21b63b5a3a" size: 0 safe_mode: false safe_mode_storage_rewards: @@ -332,6 +332,6 @@ safe_mode_non_refundable_storage_fee: 0 epoch_start_timestamp_ms: 10 extra_fields: id: - id: "0x21d2a5b0142ef9e2b15802479b6aac1350b9e7d24a77bc5a3289284890388635" + id: "0xf080e6cdd40e0fdc08793743fa0450d0f1f6c88bf1de92f5d5e9e20dce501a17" size: 0 diff --git a/dapps/regulated-token/Move.toml b/dapps/regulated-token/Move.toml index 63cd1d7adf05d..1bd63e39a90df 100644 --- a/dapps/regulated-token/Move.toml +++ b/dapps/regulated-token/Move.toml @@ -3,7 +3,7 @@ name = "regulated-token" version = "0.0.1" [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } +Sui = { local = "../../crates/sui-framework/packages/sui-framework" } [addresses] regulated_token = "0x0" diff --git a/dapps/regulated-token/sources/denylist.move b/dapps/regulated-token/sources/denylist.move index a09917a5422ca..b2a6aff60be82 100644 --- a/dapps/regulated-token/sources/denylist.move +++ b/dapps/regulated-token/sources/denylist.move @@ -18,9 +18,7 @@ module regulated_token::denylist_rule { use std::vector; use sui::bag::{Self, Bag}; use sui::tx_context::TxContext; - // use sui::token::{Self, TokenPolicy, TokenPolicyCap, ActionRequest}; - - use regulated_token::token::{Self, TokenPolicy, TokenPolicyCap, ActionRequest}; + use sui::token::{Self, TokenPolicy, TokenPolicyCap, ActionRequest}; /// Trying to `verify` but the sender or the recipient is on the denylist. const EUserBlocked: u64 = 0; diff --git a/dapps/regulated-token/sources/reg.move b/dapps/regulated-token/sources/reg.move index ab6519662761f..67146ea3b8e5e 100644 --- a/dapps/regulated-token/sources/reg.move +++ b/dapps/regulated-token/sources/reg.move @@ -6,14 +6,8 @@ module regulated_token::reg { use sui::tx_context::{sender, TxContext}; use sui::transfer; use sui::coin::{Self, TreasuryCap}; + use sui::token::{Self, Token, TokenPolicy}; - // TODO: uncomment this when `token` is landed on one of the environments. - // ...or when tooling is set up for local network development. - // use sui::token::{Self, Token, TokenPolicy}; - - // WARNING: we're using local dependency only for demonstration purposes - // until `token` is landed on one of the environments. - use regulated_token::token::{Self, Token, TokenPolicy}; use regulated_token::denylist_rule::{Self as denylist, Denylist}; /// The OTW and the type for the Token diff --git a/examples/move/token/Move.toml b/examples/move/token/Move.toml new file mode 100644 index 0000000000000..403b1f9743b59 --- /dev/null +++ b/examples/move/token/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "Closed Loop Token" +version = "0.0.1" + +[dependencies] +Sui = { local = "../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +examples = "0x0" diff --git a/examples/move/token/sources/coffee.move b/examples/move/token/sources/coffee.move new file mode 100644 index 0000000000000..ca97bf899c789 --- /dev/null +++ b/examples/move/token/sources/coffee.move @@ -0,0 +1,99 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This example illustrates how to use the `Token` without a `TokenPolicy`. And +/// only rely on `TreasuryCap` for minting and burning tokens. +module examples::coffee { + use sui::tx_context::{sender, TxContext}; + use sui::coin::{Self, TreasuryCap, Coin}; + use sui::balance::{Self, Balance}; + use sui::token::{Self, Token}; + use sui::object::{Self, UID}; + use sui::sui::SUI; + + /// Error code for incorrect amount. + const EIncorrectAmount: u64 = 0; + /// Trying to claim a free coffee without enough points. + /// Or trying to transfer but not enough points to pay the commission. + const ENotEnoughPoints: u64 = 1; + + /// 10 SUI for a coffee. + const COFFEE_PRICE: u64 = 10_000_000_000; + + /// OTW for the Token. + struct COFFEE has drop {} + + /// The shop that sells Coffee and allows to buy a Coffee if the customer + /// has 10 COFFEE points. + struct CoffeeShop has key { + id: UID, + /// The treasury cap for the `COFFEE` points. + coffee_points: TreasuryCap, + /// The SUI balance of the shop; the shop can sell Coffee for SUI. + balance: Balance, + } + + /// Event marking that a Coffee was purchased; transaction sender serves as + /// the customer ID. + struct CoffeePurchased has copy, store, drop {} + + // Create and share the `CoffeeShop` object. + fun init(otw: COFFEE, ctx: &mut TxContext) { + let (coffee_points, metadata) = coin::create_currency( + otw, 0, b"COFFEE", b"Coffee Point", + b"Buy 4 coffees and get 1 free", + std::option::none(), + ctx + ); + + sui::transfer::public_freeze_object(metadata); + sui::transfer::share_object(CoffeeShop { + coffee_points, + id: object::new(ctx), + balance: balance::zero(), + }); + } + + /// Buy a coffee from the shop. Emitted event is tracked by the real coffee + /// shop and the customer gets a free coffee after 4 purchases. + public fun buy_coffee(app: &mut CoffeeShop, payment: Coin, ctx: &mut TxContext) { + // Check if the customer has enough SUI to pay for the coffee. + assert!(coin::value(&payment) > COFFEE_PRICE, EIncorrectAmount); + + let token = token::mint(&mut app.coffee_points, 1, ctx); + let request = token::transfer(token, sender(ctx), ctx); + + token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx); + coin::put(&mut app.balance, payment); + sui::event::emit(CoffeePurchased {}) + } + + /// Claim a free coffee from the shop. Emitted event is tracked by the real + /// coffee shop and the customer gets a free coffee after 4 purchases. The + /// `COFFEE` tokens are spent. + public fun claim_free(app: &mut CoffeeShop, points: Token, ctx: &mut TxContext) { + // Check if the customer has enough `COFFEE` points to claim a free one. + assert!(token::value(&points) == 4, EIncorrectAmount); + + // While we could use `burn`, spend illustrates another way of doing this + let request = token::spend(points, ctx); + token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx); + sui::event::emit(CoffeePurchased {}) + } + + /// We allow transfer of `COFFEE` points to other customers but we charge 1 + /// `COFFEE` point for the transfer. + public fun transfer( + app: &mut CoffeeShop, + points: Token, + recipient: address, + ctx: &mut TxContext + ) { + assert!(token::value(&points) > 1, ENotEnoughPoints); + let commission = token::split(&mut points, 1, ctx); + let request = token::transfer(points, recipient, ctx); + + token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx); + token::burn(&mut app.coffee_points, commission); + } +} diff --git a/examples/move/token/sources/gems.move b/examples/move/token/sources/gems.move new file mode 100644 index 0000000000000..b1fa83f0c35d9 --- /dev/null +++ b/examples/move/token/sources/gems.move @@ -0,0 +1,133 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This is a simple example of a permissionless module for an imaginary game +/// that sells swords for Gems. Gems are an in-game currency that can be bought +/// with SUI. +module examples::sword { + use sui::tx_context::TxContext; + use sui::object::{Self, UID}; + + use sui::token::{Self, Token, ActionRequest}; + use examples::gem::GEM; + + /// Trying to purchase a sword with an incorrect amount. + const EWrongAmount: u64 = 0; + + /// The price of a sword in Gems. + const SWORD_PRICE: u64 = 10; + + /// A game item that can be purchased with Gems. + struct Sword has key, store { id: UID } + + /// Purchase a sword with Gems. + public fun buy_sword( + gems: Token, ctx: &mut TxContext + ): (Sword, ActionRequest) { + assert!(SWORD_PRICE == token::value(&gems), EWrongAmount); + ( + Sword { id: object::new(ctx) }, + token::spend(gems, ctx) + ) + } +} + +/// Module that defines the in-game currency: GEMs which can be purchased with +/// SUI and used to buy swords (in the `sword` module). +module examples::gem { + use std::option::none; + use std::string::{Self, String}; + use sui::sui::SUI; + use sui::transfer; + use sui::object::{Self, UID}; + use sui::balance::{Self, Balance}; + use sui::tx_context::{sender, TxContext}; + use sui::coin::{Self, Coin, TreasuryCap}; + + use sui::token::{Self, Token, ActionRequest}; + + /// Trying to purchase Gems with an unexpected amount. + const EUnknownAmount: u64 = 0; + + /// 10 SUI is the price of a small bundle of Gems. + const SMALL_BUNDLE: u64 = 10_000_000_000; + const SMALL_AMOUNT: u64 = 100; + + /// 100 SUI is the price of a medium bundle of Gems. + const MEDIUM_BUNDLE: u64 = 100_000_000_000; + const MEDIUM_AMOUNT: u64 = 5_000; + + /// 1000 SUI is the price of a large bundle of Gems. + /// This is the best deal. + const LARGE_BUNDLE: u64 = 1_000_000_000_000; + const LARGE_AMOUNT: u64 = 100_000; + + #[lint_allow(coin_field)] + /// Gems can be purchased through the `Store`. + struct GemStore has key { + id: UID, + /// Profits from selling Gems. + profits: Balance, + /// The Treasury Cap for the in-game currency. + gem_treasury: TreasuryCap, + } + + /// The OTW to create the in-game currency. + struct GEM has drop {} + + // In the module initializer we create the in-game currency and define the + // rules for different types of actions. + fun init(otw: GEM, ctx: &mut TxContext) { + let (treasury_cap, coin_metadata) = coin::create_currency( + otw, 0, b"GEM", b"Capy Gems", // otw, decimal, symbol, name + b"In-game currency for Capy Miners", none(), // description, url + ctx + ); + + // create a `TokenPolicy` for GEMs + let (policy, cap) = token::new(&treasury_cap, ctx); + + token::allow(&mut policy, &cap, buy_action(), ctx); + token::allow(&mut policy, &cap, token::spend_action(), ctx); + + // create and share the GemStore + transfer::share_object(GemStore { + id: object::new(ctx), + gem_treasury: treasury_cap, + profits: balance::zero() + }); + + // deal with `TokenPolicy`, `CoinMetadata` and `TokenPolicyCap` + transfer::public_freeze_object(coin_metadata); + transfer::public_transfer(cap, sender(ctx)); + token::share_policy(policy); + } + + /// Purchase Gems from the GemStore. Very silly value matching against module + /// constants... + public fun buy_gems( + self: &mut GemStore, payment: Coin, ctx: &mut TxContext + ): (Token, ActionRequest) { + let amount = coin::value(&payment); + let purchased = if (amount == SMALL_BUNDLE) { + SMALL_AMOUNT + } else if (amount == MEDIUM_BUNDLE) { + MEDIUM_AMOUNT + } else if (amount == LARGE_BUNDLE) { + LARGE_AMOUNT + } else { + abort EUnknownAmount + }; + + coin::put(&mut self.profits, payment); + + // create custom request and mint some Gems + let gems = token::mint(&mut self.gem_treasury, purchased, ctx); + let req = token::new_request(buy_action(), purchased, none(), none(), ctx); + + (gems, req) + } + + /// The name of the `buy` action in the `GemStore`. + public fun buy_action(): String { string::utf8(b"buy") } +} diff --git a/examples/move/token/sources/loyalty.move b/examples/move/token/sources/loyalty.move new file mode 100644 index 0000000000000..1decad417c69d --- /dev/null +++ b/examples/move/token/sources/loyalty.move @@ -0,0 +1,102 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module illustrates a Closed Loop Loyalty Token. The `Token` is sent to +/// users as a reward for their loyalty by the application Admin. The `Token` +/// can be used to buy a `Gift` in the shop. +/// +/// Actions: +/// - spend - spend the token in the shop +module examples::loyalty { + use std::option; + use sui::transfer; + use sui::object::{Self, UID}; + use sui::coin::{Self, TreasuryCap}; + use sui::tx_context::{Self, TxContext}; + + use sui::token::{Self, ActionRequest, Token}; + + /// Token amount does not match the `GIFT_PRICE`. + const EIncorrectAmount: u64 = 0; + + /// The price for the `Gift`. + const GIFT_PRICE: u64 = 10; + + /// The OTW for the Token / Coin. + struct LOYALTY has drop {} + + /// This is the Rule requirement for the `GiftShop`. The Rules don't need + /// to be separate applications, some rules make sense to be part of the + /// application itself, like this one. + struct GiftShop has drop {} + + /// The Gift object - can be purchased for 10 tokens. + struct Gift has key, store { + id: UID + } + + // Create a new LOYALTY currency, create a `TokenPolicy` for it and allow + // everyone to spend `Token`s if they were `reward`ed. + fun init(otw: LOYALTY, ctx: &mut TxContext) { + let (treasury_cap, coin_metadata) = coin::create_currency( + otw, + 0, // no decimals + b"LOY", // symbol + b"Loyalty Token", // name + b"Token for Loyalty", // description + option::none(), // url + ctx + ); + + let (policy, policy_cap) = token::new(&treasury_cap, ctx); + + // but we constrain spend by this shop: + token::add_rule_for_action( + &mut policy, + &policy_cap, + token::spend_action(), + ctx + ); + + token::share_policy(policy); + + transfer::public_freeze_object(coin_metadata); + transfer::public_transfer(policy_cap, tx_context::sender(ctx)); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); + } + + /// Handy function to reward users. Can be called by the application admin + /// to reward users for their loyalty :) + /// + /// `Mint` is available to the holder of the `TreasuryCap` by default and + /// hence does not need to be confirmed; however, the `transfer` action + /// does require a confirmation and can be confirmed with `TreasuryCap`. + public fun reward_user( + cap: &mut TreasuryCap, + amount: u64, + recipient: address, + ctx: &mut TxContext + ) { + let token = token::mint(cap, amount, ctx); + let req = token::transfer(token, recipient, ctx); + + token::confirm_with_treasury_cap(cap, req, ctx); + } + + /// Buy a gift for 10 tokens. The `Gift` is received, and the `Token` is + /// spent (stored in the `ActionRequest`'s `burned_balance` field). + public fun buy_a_gift( + token: Token, + ctx: &mut TxContext + ): (Gift, ActionRequest) { + assert!(token::value(&token) == GIFT_PRICE, EIncorrectAmount); + + let gift = Gift { id: object::new(ctx) }; + let req = token::spend(token, ctx); + + // only required because we've set this rule + token::add_approval(GiftShop {}, &mut req, ctx); + + (gift, req) + } +} diff --git a/examples/move/token/sources/regulated_coin.move b/examples/move/token/sources/regulated_coin.move new file mode 100644 index 0000000000000..9831def640355 --- /dev/null +++ b/examples/move/token/sources/regulated_coin.move @@ -0,0 +1,328 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This example demonstrates how to use Closed Loop to create a regulated coin +/// that follows different regulatory requirements for actions: +/// +/// 1. A new Token can only be minted by admin (out of scope) +/// 2. Tokens can only be transferred between KYC-d (approved) addresses +/// 3. A single transfer can't exceed 3000.00 REG +/// 4. A single "withdraw" operation `to_coin` can't exceed 1000.00 REG +/// 5. All actions are regulated by a denylist rule +/// +/// With this set of rules new accounts can either be created by admin (with a +/// mint and transfer operation) or if the account is KYC-d it can be created +/// with a transfer operation from an existing account. Similarly, an account +/// that has "Coin" can only convert it to `Token` if it's KYC-d. +/// +/// Notes: +/// +/// - best analogy for regulated account (Token) and unregulated account (Coin) +/// is a Bank account and Cash. Bank account is regulated and requires KYC to +/// open, Cash is unregulated and can be used by anyone and passed freely. +/// However should someone decide to put Cash into a Bank account, the Bank will +/// require KYC. +/// +/// - KYC in this example is represented by an allowlist rule +module examples::regulated_coin { + use std::option; + use sui::vec_map; + use sui::transfer; + use sui::coin::{Self, TreasuryCap}; + use sui::tx_context::{sender, TxContext}; + + use sui::token::{Self, TokenPolicy, TokenPolicyCap}; + + // import rules and use them for this app + use examples::allowlist_rule::Allowlist; + use examples::denylist_rule::Denylist; + use examples::limiter_rule::{Self as limiter, Limiter}; + + /// OTW and the type for the Token. + struct REGULATED_COIN has drop {} + + // Most of the magic happens in the initializer for the demonstration + // purposes; however half of what's happening here could be implemented as + // a single / set of PTBs. + fun init(otw: REGULATED_COIN, ctx: &mut TxContext) { + let treasury_cap = create_currency(otw, ctx); + let (policy, cap) = token::new(&treasury_cap, ctx); + + set_rules(&mut policy, &cap, ctx); + + transfer::public_transfer(treasury_cap, sender(ctx)); + transfer::public_transfer(cap, sender(ctx)); + token::share_policy(policy); + } + + /// Internal: not necessary, but moving this call to a separate function for + /// better visibility of the Closed Loop setup in `init` and easier testing. + public(friend) fun set_rules( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + ctx: &mut TxContext + ) { + // Create a denylist rule and add it to every action + // Now all actions are allowed but require a denylist + token::add_rule_for_action(policy, cap, token::spend_action(), ctx); + token::add_rule_for_action(policy, cap, token::to_coin_action(), ctx); + token::add_rule_for_action(policy, cap, token::transfer_action(), ctx); + token::add_rule_for_action(policy, cap, token::from_coin_action(), ctx); + + // Set limits for each action: + // transfer - 3000.00 REG, to_coin - 1000.00 REG + token::add_rule_for_action(policy, cap, token::transfer_action(), ctx); + token::add_rule_for_action(policy, cap, token::to_coin_action(), ctx); + + let config = { + let config = vec_map::empty(); + vec_map::insert(&mut config, token::transfer_action(), 3000_000000); + vec_map::insert(&mut config, token::to_coin_action(), 1000_000000); + config + }; + + limiter::set_config(policy, cap, config, ctx); + + // Using allowlist to mock a KYC process; transfer and from_coin can + // only be performed by KYC-d (allowed) addresses. Just like a Bank + // account. + token::add_rule_for_action(policy, cap, token::from_coin_action(), ctx); + token::add_rule_for_action(policy, cap, token::transfer_action(), ctx); + } + + /// Internal: not necessary, but moving this call to a separate function for + /// better visibility of the Closed Loop setup in `init`. + fun create_currency( + otw: T, + ctx: &mut TxContext + ): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + otw, 6, + b"REG", + b"Regulated Coin", + b"Coin that illustrates different regulatory requirements", + option::none(), + ctx + ); + + transfer::public_freeze_object(metadata); + treasury_cap + } + + #[test_only] friend examples::regulated_coin_tests; +} + +#[test_only] +/// Implements tests for most common scenarios for the regulated coin example. +/// We don't test the currency itself but rather use the same set of regulations +/// on a test currency. +module examples::regulated_coin_tests { + use sui::coin; + use sui::tx_context::TxContext; + + use sui::token::{Self, TokenPolicy, TokenPolicyCap}; + use sui::token_test_utils::{Self as test, TEST}; + + use examples::regulated_coin::set_rules; + + use examples::allowlist_rule as allowlist; + use examples::denylist_rule as denylist; + use examples::limiter_rule as limiter; + + const ALICE: address = @0x0; + const BOB: address = @0x1; + + + // === Limiter Tests === + + #[test] + /// Transfer 3000 REG to self + fun test_limiter_transfer_allowed_pass() { + let ctx = &mut test::ctx(ALICE); + let (policy, cap) = policy_with_allowlist(ctx); + + let token = test::mint(3000_000000, ctx); + let request = token::transfer(token, ALICE, ctx); + + limiter::verify(&policy, &mut request, ctx); + denylist::verify(&policy, &mut request, ctx); + allowlist::verify(&policy, &mut request, ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap); + } + + #[test, expected_failure(abort_code = limiter::ELimitExceeded)] + /// Try to transfer more than 3000.00 REG. + fun test_limiter_transfer_to_not_allowed_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_allowlist(ctx); + + let token = test::mint(3001_000000, ctx); + let request = token::transfer(token, ALICE, ctx); + + limiter::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test] + /// Turn 1000 REG into Coin from. + fun test_limiter_to_coin_allowed_pass() { + let ctx = &mut test::ctx(ALICE); + let (policy, cap) = policy_with_allowlist(ctx); + + let token = test::mint(1000_000000, ctx); + let (coin, request) = token::to_coin(token, ctx); + + limiter::verify(&policy, &mut request, ctx); + denylist::verify(&policy, &mut request, ctx); + allowlist::verify(&policy, &mut request, ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap); + coin::burn_for_testing(coin); + } + + #[test, expected_failure(abort_code = limiter::ELimitExceeded)] + /// Try to convert more than 1000.00 REG in a single operation. + fun test_limiter_to_coin_exceeded_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_allowlist(ctx); + + let token = test::mint(1001_000000, ctx); + let (_coin, request) = token::to_coin(token, ctx); + + limiter::verify(&policy, &mut request, ctx); + + abort 1337 + } + + // === Allowlist Tests === + + // Test from allowed account is already covered in the + // `test_limiter_transfer_allowed_pass` + + #[test, expected_failure(abort_code = allowlist::EUserNotAllowed)] + /// Try to `transfer` to a not allowed account. + fun test_allowlist_transfer_to_not_allowed_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_allowlist(ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + allowlist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = allowlist::EUserNotAllowed)] + /// Try to `from_coin` from a not allowed account. + fun test_allowlist_from_coin_not_allowed_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, cap) = test::get_policy(ctx); + + set_rules(&mut policy, &cap, ctx); + + let coin = coin::mint_for_testing(1000_000000, ctx); + let (_token, request) = token::from_coin(coin, ctx); + + allowlist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + // === Denylist Tests === + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `transfer` from a blocked account. + fun test_denylist_transfer_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `transfer` to a blocked account. + fun test_denylist_transfer_to_recipient_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `spend` from a blocked account. + fun test_denylist_spend_fail() { + let ctx = &mut test::ctx(BOB); + let (policy, cap) = test::get_policy(ctx); + + set_rules(&mut policy, &cap, ctx); + denylist::add_records(&mut policy, &cap, vector[ BOB ], ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::spend(token, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `to_coin` from a blocked account. + fun test_denylist_to_coin_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let (_coin, request) = token::to_coin(token, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `from_coin` from a blocked account. + fun test_denylist_from_coin_fail() { + let ctx = &mut test::ctx(ALICE); + let (policy, _cap) = policy_with_denylist(ctx); + + let coin = coin::mint_for_testing(1000_000000, ctx); + let (_token, request) = token::from_coin(coin, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + /// Internal: prepare a policy with a denylist rule where sender is banned; + fun policy_with_denylist(ctx: &mut TxContext): (TokenPolicy, TokenPolicyCap) { + let (policy, cap) = test::get_policy(ctx); + set_rules(&mut policy, &cap, ctx); + + denylist::add_records(&mut policy, &cap, vector[ ALICE ], ctx); + (policy, cap) + } + + /// Internal: prepare a policy with an allowlist rule where sender is allowed; + fun policy_with_allowlist(ctx: &mut TxContext): (TokenPolicy, TokenPolicyCap) { + let (policy, cap) = test::get_policy(ctx); + set_rules(&mut policy, &cap, ctx); + + allowlist::add_records(&mut policy, &cap, vector[ ALICE ], ctx); + (policy, cap) + } +} diff --git a/examples/move/token/sources/rules/allowlist_rule.move b/examples/move/token/sources/rules/allowlist_rule.move new file mode 100644 index 0000000000000..799cc310b9c65 --- /dev/null +++ b/examples/move/token/sources/rules/allowlist_rule.move @@ -0,0 +1,100 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A simple allowlist rule - allows only the addresses on the allowlist to +/// perform an Action. +module examples::allowlist_rule { + use std::option; + use std::vector; + use sui::bag::{Self, Bag}; + use sui::tx_context::TxContext; + use sui::token::{ + Self, + TokenPolicy, + TokenPolicyCap, + ActionRequest + }; + + /// The `sender` or `recipient` is not on the allowlist. + const EUserNotAllowed: u64 = 0; + + /// The Rule witness. + struct Allowlist has drop {} + + /// Verifies that the sender and the recipient (if set) are both on the + /// `allowlist_rule` for a given action. + /// + /// Aborts if: + /// - there's no config + /// - the sender is not on the allowlist + /// - the recipient is not on the allowlist + public fun verify( + policy: &TokenPolicy, + request: &mut ActionRequest, + ctx: &mut TxContext + ) { + assert!(has_config(policy), EUserNotAllowed); + + let config = config(policy); + let sender = token::sender(request); + let recipient = token::recipient(request); + + assert!(bag::contains(config, sender), EUserNotAllowed); + + if (option::is_some(&recipient)) { + let recipient = *option::borrow(&recipient); + assert!(bag::contains(config, recipient), EUserNotAllowed); + }; + + token::add_approval(Allowlist {}, request, ctx); + } + + // === Protected: List Management === + + /// Adds records to the `denylist_rule` for a given action. The Policy + /// owner can batch-add records. + public fun add_records( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + addresses: vector
, + ctx: &mut TxContext, + ) { + if (!has_config(policy)) { + token::add_rule_config(Allowlist {}, policy, cap, bag::new(ctx), ctx); + }; + + let config_mut = config_mut(policy, cap); + while (vector::length(&addresses) > 0) { + bag::add(config_mut, vector::pop_back(&mut addresses), true) + } + } + + /// Removes records from the `denylist_rule` for a given action. The Policy + /// owner can batch-remove records. + public fun remove_records( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + addresses: vector
, + ) { + let config_mut = config_mut(policy, cap); + + while (vector::length(&addresses) > 0) { + let record = vector::pop_back(&mut addresses); + let _: bool = bag::remove(config_mut, record); + }; + } + + // === Internal === + + fun has_config(self: &TokenPolicy): bool { + token::has_rule_config_with_type(self) + } + + fun config(self: &TokenPolicy): &Bag { + token::rule_config(Allowlist {}, self) + } + + fun config_mut(self: &mut TokenPolicy, cap: &TokenPolicyCap): &mut Bag { + token::rule_config_mut(Allowlist {}, self, cap) + } +} diff --git a/examples/move/token/sources/rules/denylist_rule.move b/examples/move/token/sources/rules/denylist_rule.move new file mode 100644 index 0000000000000..606532209a1ce --- /dev/null +++ b/examples/move/token/sources/rules/denylist_rule.move @@ -0,0 +1,169 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// An implementation of a simple `Denylist` for the Closed Loop system. For +/// demonstration purposes it is implemented as a `VecSet`, however for a larger +/// number of records there needs to be a different storage implementation +/// utilizing dynamic fields. +/// +/// Denylist checks both the sender and the recipient of the transaction. +/// +/// Notes: +/// - current implementation uses a separate dataset for each action, which will +/// be fixed / improved in the future; +/// - the current implementation is not optimized for a large number of records +/// and the final one will feature better collection type; +module examples::denylist_rule { + use std::option; + use std::vector; + use sui::bag::{Self, Bag}; + use sui::tx_context::TxContext; + use sui::token::{Self, TokenPolicy, TokenPolicyCap, ActionRequest}; + + /// Trying to `verify` but the sender or the recipient is on the denylist. + const EUserBlocked: u64 = 0; + + /// The Rule witness. + struct Denylist has drop {} + + /// Verifies that the sender and the recipient (if set) are not on the + /// denylist for the given action. + public fun verify( + policy: &TokenPolicy, + request: &mut ActionRequest, + ctx: &mut TxContext + ) { + // early return if no records are added; + if (!has_config(policy)) { + token::add_approval(Denylist {}, request, ctx); + return + }; + + let config = config(policy); + let sender = token::sender(request); + let receiver = token::recipient(request); + + assert!(!bag::contains(config, sender), EUserBlocked); + + if (option::is_some(&receiver)) { + let receiver = *option::borrow(&receiver); + assert!(!bag::contains(config, receiver), EUserBlocked); + }; + + token::add_approval(Denylist {}, request, ctx); + } + + // === Protected: List Management === + + /// Adds records to the `denylist_rule` for a given action. The Policy + /// owner can batch-add records. + public fun add_records( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + addresses: vector
, + ctx: &mut TxContext + ) { + if (!has_config(policy)) { + token::add_rule_config(Denylist {}, policy, cap, bag::new(ctx), ctx); + }; + + let config_mut = config_mut(policy, cap); + while (vector::length(&addresses) > 0) { + bag::add(config_mut, vector::pop_back(&mut addresses), true) + } + } + + /// Removes records from the `denylist_rule` for a given action. The Policy + /// owner can batch-remove records. + public fun remove_records( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + addresses: vector
, + _ctx: &mut TxContext + ) { + let config_mut = config_mut(policy, cap); + + while (vector::length(&addresses) > 0) { + let record = vector::pop_back(&mut addresses); + if (bag::contains(config_mut, record)) { + let _: bool = bag::remove(config_mut, record); + }; + }; + } + + // === Internal === + + fun has_config(self: &TokenPolicy): bool { + token::has_rule_config_with_type(self) + } + + fun config(self: &TokenPolicy): &Bag { + token::rule_config(Denylist {}, self) + } + + fun config_mut(self: &mut TokenPolicy, cap: &TokenPolicyCap): &mut Bag { + token::rule_config_mut(Denylist {}, self, cap) + } +} + +#[test_only] +module examples::denylist_rule_tests { + use std::string::utf8; + use std::option::{none, some}; + use sui::token; + use sui::token_test_utils::{Self as test, TEST}; + + use examples::denylist_rule::{Self as denylist, Denylist}; + + #[test] + // Scenario: add a denylist with addresses, sender is not on the list and + // transaction is confirmed. + fun denylist_pass_not_on_the_list() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + // first add the list for action and then add records + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + denylist::add_records(&mut policy, &cap, vector[ @0x1 ], ctx); + + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + + denylist::verify(&policy, &mut request, ctx); + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap); + } + + #[test, expected_failure(abort_code = examples::denylist_rule::EUserBlocked)] + // Scenario: add a denylist with addresses, sender is on the list and + // transaction fails with `EUserBlocked`. + fun denylist_on_the_list_banned_fail() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + denylist::add_records(&mut policy, &cap, vector[ @0x0 ], ctx); + + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = examples::denylist_rule::EUserBlocked)] + // Scenario: add a denylist with addresses, Recipient is on the list and + // transaction fails with `EUserBlocked`. + fun denylist_recipient_on_the_list_banned_fail() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + denylist::add_records(&mut policy, &cap, vector[ @0x1 ], ctx); + + let request = token::new_request(utf8(b"action"), 100, some(@0x1), none(), ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } +} diff --git a/examples/move/token/sources/rules/limiter_rule.move b/examples/move/token/sources/rules/limiter_rule.move new file mode 100644 index 0000000000000..e47bc219bc765 --- /dev/null +++ b/examples/move/token/sources/rules/limiter_rule.move @@ -0,0 +1,159 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// An example of a Rule for the Closed Loop Token which limits the amount per +/// operation. Can be used to limit any action (eg transfer, toCoin, fromCoin). +module examples::limiter_rule { + use std::string::String; + use sui::vec_map::{Self, VecMap}; + use sui::tx_context::TxContext; + use sui::token::{ + Self, + TokenPolicy, + TokenPolicyCap, + ActionRequest + }; + + /// Trying to perform an action that exceeds the limit. + const ELimitExceeded: u64 = 0; + + /// The Rule witness. + struct Limiter has drop {} + + /// The Config object for the `lo + struct Config has store, drop { + /// Mapping of Action -> Limit + limits: VecMap + } + + /// Verifies that the request does not exceed the limit and adds an approval + /// to the `ActionRequest`. + public fun verify( + policy: &TokenPolicy, + request: &mut ActionRequest, + ctx: &mut TxContext + ) { + if (!token::has_rule_config(policy)) { + return token::add_approval(Limiter {}, request, ctx) + }; + + let config: &Config = token::rule_config(Limiter {}, policy); + if (!vec_map::contains(&config.limits, &token::action(request))) { + return token::add_approval(Limiter {}, request, ctx) + }; + + let action_limit = *vec_map::get(&config.limits, &token::action(request)); + + assert!(token::amount(request) <= action_limit, ELimitExceeded); + token::add_approval(Limiter {}, request, ctx); + } + + /// Updates the config for the `Limiter` rule. Uses the `VecMap` to store + /// the limits for each action. + public fun set_config( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + limits: VecMap, + ctx: &mut TxContext + ) { + // if there's no stored config for the rule, add a new one + if (!token::has_rule_config(policy)) { + let config = Config { limits }; + token::add_rule_config(Limiter {}, policy, cap, config, ctx); + } else { + let config: &mut Config = token::rule_config_mut(Limiter {}, policy, cap); + config.limits = limits; + } + } + + /// Returns the config for the `Limiter` rule. + public fun get_config(policy: &TokenPolicy): VecMap { + token::rule_config(Limiter {}, policy).limits + } +} + +#[test_only] +module examples::limiter_rule_tests { + use std::string::utf8; + use std::option::{none, /* some */}; + use sui::token; + use sui::vec_map; + use sui::token_test_utils::{Self as test, TEST}; + + use examples::limiter_rule::{Self as limiter, Limiter}; + + #[test] + // Scenario: add a limiter rule for 100 tokens per operation, verify that + // the request with 100 tokens is confirmed + fun add_limiter_default() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + + limiter::verify(&policy, &mut request, ctx); + + token::confirm_request(&policy, request, ctx); + test::return_policy(policy, cap); + } + + #[test] + // Scenario: add a limiter rule for 100 tokens per operation, verify that + // the request with 100 tokens is confirmed; then remove the rule and verify + // that the request with 100 tokens is not confirmed and repeat step (1) + fun add_remove_limiter() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + let config = vec_map::empty(); + vec_map::insert(&mut config, utf8(b"action"), 100); + limiter::set_config(&mut policy, &cap, config, ctx); + + // adding limiter - confirmation required + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + { + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + limiter::verify(&policy, &mut request, ctx); + token::confirm_request(&policy, request, ctx); + }; + + // limiter removed - no confirmation required + token::remove_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + { + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + token::confirm_request(&policy, request, ctx); + }; + + // limiter added but no limit now + limiter::set_config(&mut policy, &cap, vec_map::empty(), ctx); + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + { + let request = token::new_request(utf8(b"action"), 100, none(), none(), ctx); + limiter::verify(&policy, &mut request, ctx); + token::confirm_request(&policy, request, ctx); + }; + + test::return_policy(policy, cap); + } + + #[test, expected_failure(abort_code = examples::limiter_rule::ELimitExceeded)] + // Scenario: add a limiter rule for 100 tokens per operation, verify that + // the request with 101 tokens aborts with `ELimitExceeded` + fun add_limiter_limit_exceeded_fail() { + let ctx = &mut sui::tx_context::dummy(); + let (policy, cap) = test::get_policy(ctx); + + let config = vec_map::empty(); + vec_map::insert(&mut config, utf8(b"action"), 100); + limiter::set_config(&mut policy, &cap, config, ctx); + + token::add_rule_for_action(&mut policy, &cap, utf8(b"action"), ctx); + + let request = token::new_request(utf8(b"action"), 101, none(), none(), ctx); + limiter::verify(&policy, &mut request, ctx); + + abort 1337 + } +} diff --git a/examples/move/token/sources/simple_token.move b/examples/move/token/sources/simple_token.move new file mode 100644 index 0000000000000..e9f6e6eaa9b14 --- /dev/null +++ b/examples/move/token/sources/simple_token.move @@ -0,0 +1,171 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Create a simple Token with Denylist for every action; all four default +/// actions are allowed as long as the user is not on the denylist. +module examples::simple_token { + use std::option; + use sui::transfer; + use sui::coin::{Self, TreasuryCap}; + use sui::tx_context::{sender, TxContext}; + + use sui::token::{Self, TokenPolicy, TokenPolicyCap}; + + // import rules and use them for this app + use examples::denylist_rule::Denylist; + + /// OTW and the type for the Token. + struct SIMPLE_TOKEN has drop {} + + // Most of the magic happens in the initializer for the demonstration + // purposes; however half of what's happening here could be implemented as + // a single / set of PTBs. + fun init(otw: SIMPLE_TOKEN, ctx: &mut TxContext) { + let treasury_cap = create_currency(otw, ctx); + let (policy, cap) = token::new(&treasury_cap, ctx); + + set_rules(&mut policy, &cap, ctx); + + transfer::public_transfer(treasury_cap, sender(ctx)); + transfer::public_transfer(cap, sender(ctx)); + token::share_policy(policy); + } + + /// Internal: not necessary, but moving this call to a separate function for + /// better visibility of the Closed Loop setup in `init` and easier testing. + public(friend) fun set_rules( + policy: &mut TokenPolicy, + cap: &TokenPolicyCap, + ctx: &mut TxContext + ) { + // Create a denylist rule and add it to every action + // Now all actions are allowed but require a denylist + token::add_rule_for_action(policy, cap, token::spend_action(), ctx); + token::add_rule_for_action(policy, cap, token::to_coin_action(), ctx); + token::add_rule_for_action(policy, cap, token::transfer_action(), ctx); + token::add_rule_for_action(policy, cap, token::from_coin_action(), ctx); + } + + /// Internal: not necessary, but moving this call to a separate function for + /// better visibility of the Closed Loop setup in `init`. + fun create_currency( + otw: T, + ctx: &mut TxContext + ): TreasuryCap { + let (treasury_cap, metadata) = coin::create_currency( + otw, 6, + b"SMPL", + b"Simple Token", + b"Token that showcases denylist", + option::none(), + ctx + ); + + transfer::public_freeze_object(metadata); + treasury_cap + } + + #[test_only] friend examples::simple_token_tests; +} + +#[test_only] +/// Implements tests for most common scenarios for the regulated coin example. +/// We don't test the currency itself but rather use the same set of regulations +/// on a test currency. +module examples::simple_token_tests { + use sui::coin; + use sui::tx_context::TxContext; + + use sui::token::{Self, TokenPolicy, TokenPolicyCap}; + use sui::token_test_utils::{Self as test, TEST}; + + use examples::simple_token::set_rules; + use examples::denylist_rule as denylist; + + const ALICE: address = @0x0; + const BOB: address = @0x1; + + // === Denylist Tests === + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `transfer` from a blocked account. + fun test_denylist_transfer_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `transfer` to a blocked account. + fun test_denylist_transfer_to_recipient_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `spend` from a blocked account. + fun test_denylist_spend_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, cap) = test::get_policy(ctx); + + set_rules(&mut policy, &cap, ctx); + denylist::add_records(&mut policy, &cap, vector[ BOB ], ctx); + + let token = test::mint(1000_000000, ctx); + let request = token::transfer(token, BOB, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `to_coin` from a blocked account. + fun test_denylist_to_coin_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = policy_with_denylist(ctx); + + let token = test::mint(1000_000000, ctx); + let (_coin, request) = token::to_coin(token, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + #[test, expected_failure(abort_code = denylist::EUserBlocked)] + /// Try to `from_coin` from a blocked account. + fun test_denylist_from_coin_fail() { + let ctx = &mut test::ctx(@0x0); + let (policy, _cap) = policy_with_denylist(ctx); + + let coin = coin::mint_for_testing(1000_000000, ctx); + let (_token, request) = token::from_coin(coin, ctx); + + denylist::verify(&policy, &mut request, ctx); + + abort 1337 + } + + /// Internal: prepare a policy with a denylist rule where sender is banned; + fun policy_with_denylist(ctx: &mut TxContext): (TokenPolicy, TokenPolicyCap) { + let (policy, cap) = test::get_policy(ctx); + set_rules(&mut policy, &cap, ctx); + + denylist::add_records(&mut policy, &cap, vector[ ALICE ], ctx); + (policy, cap) + } +}