github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/core/native/native_nep17.go (about) 1 package native 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "math/big" 8 9 "github.com/nspcc-dev/neo-go/pkg/config" 10 "github.com/nspcc-dev/neo-go/pkg/core/dao" 11 "github.com/nspcc-dev/neo-go/pkg/core/interop" 12 "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" 13 "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" 14 "github.com/nspcc-dev/neo-go/pkg/core/state" 15 "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" 16 "github.com/nspcc-dev/neo-go/pkg/smartcontract" 17 "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" 18 "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" 19 "github.com/nspcc-dev/neo-go/pkg/util" 20 "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" 21 ) 22 23 // prefixAccount is the standard prefix used to store account data. 24 const prefixAccount = 20 25 26 // makeAccountKey creates a key from the account script hash. 27 func makeAccountKey(h util.Uint160) []byte { 28 return makeUint160Key(prefixAccount, h) 29 } 30 31 // nep17TokenNative represents a NEP-17 token contract. 32 type nep17TokenNative struct { 33 interop.ContractMD 34 symbol string 35 decimals int64 36 factor int64 37 incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int, *big.Int) (func(), error) 38 balFromBytes func(item *state.StorageItem) (*big.Int, error) 39 } 40 41 // totalSupplyKey is the key used to store totalSupply value. 42 var totalSupplyKey = []byte{11} 43 44 func (c *nep17TokenNative) Metadata() *interop.ContractMD { 45 return &c.ContractMD 46 } 47 48 func newNEP17Native(name string, id int32) *nep17TokenNative { 49 n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name, id, func(m *manifest.Manifest) { 50 m.SupportedStandards = []string{manifest.NEP17StandardName} 51 })} 52 53 desc := newDescriptor("symbol", smartcontract.StringType) 54 md := newMethodAndPrice(n.Symbol, 0, callflag.NoneFlag) 55 n.AddMethod(md, desc) 56 57 desc = newDescriptor("decimals", smartcontract.IntegerType) 58 md = newMethodAndPrice(n.Decimals, 0, callflag.NoneFlag) 59 n.AddMethod(md, desc) 60 61 desc = newDescriptor("totalSupply", smartcontract.IntegerType) 62 md = newMethodAndPrice(n.TotalSupply, 1<<15, callflag.ReadStates) 63 n.AddMethod(md, desc) 64 65 desc = newDescriptor("balanceOf", smartcontract.IntegerType, 66 manifest.NewParameter("account", smartcontract.Hash160Type)) 67 md = newMethodAndPrice(n.balanceOf, 1<<15, callflag.ReadStates) 68 n.AddMethod(md, desc) 69 70 transferParams := []manifest.Parameter{ 71 manifest.NewParameter("from", smartcontract.Hash160Type), 72 manifest.NewParameter("to", smartcontract.Hash160Type), 73 manifest.NewParameter("amount", smartcontract.IntegerType), 74 } 75 desc = newDescriptor("transfer", smartcontract.BoolType, 76 append(transferParams, manifest.NewParameter("data", smartcontract.AnyType))..., 77 ) 78 md = newMethodAndPrice(n.Transfer, 1<<17, callflag.States|callflag.AllowCall|callflag.AllowNotify) 79 md.StorageFee = 50 80 n.AddMethod(md, desc) 81 82 eDesc := newEventDescriptor("Transfer", transferParams...) 83 eMD := newEvent(eDesc) 84 n.AddEvent(eMD) 85 86 return n 87 } 88 89 func (c *nep17TokenNative) Initialize(_ *interop.Context) error { 90 return nil 91 } 92 93 func (c *nep17TokenNative) Symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item { 94 return stackitem.NewByteArray([]byte(c.symbol)) 95 } 96 97 func (c *nep17TokenNative) Decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item { 98 return stackitem.NewBigInteger(big.NewInt(c.decimals)) 99 } 100 101 func (c *nep17TokenNative) TotalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { 102 _, supply := c.getTotalSupply(ic.DAO) 103 return stackitem.NewBigInteger(supply) 104 } 105 106 func (c *nep17TokenNative) getTotalSupply(d *dao.Simple) (state.StorageItem, *big.Int) { 107 si := d.GetStorageItem(c.ID, totalSupplyKey) 108 if si == nil { 109 si = []byte{} 110 } 111 return si, bigint.FromBytes(si) 112 } 113 114 func (c *nep17TokenNative) saveTotalSupply(d *dao.Simple, si state.StorageItem, supply *big.Int) { 115 d.PutBigInt(c.ID, totalSupplyKey, supply) 116 } 117 118 func (c *nep17TokenNative) Transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { 119 from := toUint160(args[0]) 120 to := toUint160(args[1]) 121 amount := toBigInt(args[2]) 122 err := c.TransferInternal(ic, from, to, amount, args[3]) 123 return stackitem.NewBool(err == nil) 124 } 125 126 func addrToStackItem(u *util.Uint160) stackitem.Item { 127 if u == nil { 128 return stackitem.Null{} 129 } 130 return stackitem.NewByteArray(u.BytesBE()) 131 } 132 133 func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int, 134 data stackitem.Item, callOnPayment bool, postCalls ...func()) { 135 var skipPostCalls bool 136 defer func() { 137 if skipPostCalls { 138 return 139 } 140 for _, f := range postCalls { 141 if f != nil { 142 f() 143 } 144 } 145 }() 146 c.emitTransfer(ic, from, to, amount) 147 if to == nil || !callOnPayment { 148 return 149 } 150 cs, err := ic.GetContract(*to) 151 if err != nil { 152 return 153 } 154 155 fromArg := stackitem.Item(stackitem.Null{}) 156 if from != nil { 157 fromArg = stackitem.NewByteArray((*from).BytesBE()) 158 } 159 args := []stackitem.Item{ 160 fromArg, 161 stackitem.NewBigInteger(amount), 162 data, 163 } 164 if err := contract.CallFromNative(ic, c.Hash, cs, manifest.MethodOnNEP17Payment, args, false); err != nil { 165 skipPostCalls = true 166 panic(err) 167 } 168 } 169 170 func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) { 171 ic.AddNotification(c.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{ 172 addrToStackItem(from), 173 addrToStackItem(to), 174 stackitem.NewBigInteger(amount), 175 })) 176 } 177 178 // updateAccBalance adds the specified amount to the acc's balance. If requiredBalance 179 // is set and amount is 0, the acc's balance is checked against requiredBalance. 180 func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint160, amount *big.Int, requiredBalance *big.Int) (func(), error) { 181 key := makeAccountKey(acc) 182 si := ic.DAO.GetStorageItem(c.ID, key) 183 if si == nil { 184 if amount.Sign() < 0 { 185 return nil, errors.New("insufficient funds") 186 } 187 if requiredBalance != nil && requiredBalance.Sign() > 0 { 188 return nil, errors.New("insufficient funds") 189 } 190 if amount.Sign() == 0 { 191 // it's OK to transfer 0 if the balance is 0, no need to put si to the storage 192 return nil, nil 193 } 194 si = state.StorageItem{} 195 } 196 197 postF, err := c.incBalance(ic, acc, &si, amount, requiredBalance) 198 if err != nil { 199 if si != nil && amount.Sign() <= 0 { 200 ic.DAO.PutStorageItem(c.ID, key, si) 201 } 202 return nil, err 203 } 204 if si == nil { 205 ic.DAO.DeleteStorageItem(c.ID, key) 206 } else { 207 ic.DAO.PutStorageItem(c.ID, key, si) 208 } 209 return postF, nil 210 } 211 212 // TransferInternal transfers NEO across accounts. 213 func (c *nep17TokenNative) TransferInternal(ic *interop.Context, from, to util.Uint160, amount *big.Int, data stackitem.Item) error { 214 var postF1, postF2 func() 215 216 if amount.Sign() == -1 { 217 return errors.New("negative amount") 218 } 219 220 caller := ic.VM.GetCallingScriptHash() 221 if caller.Equals(util.Uint160{}) || !from.Equals(caller) { 222 ok, err := runtime.CheckHashedWitness(ic, from) 223 if err != nil { 224 return err 225 } else if !ok { 226 return errors.New("invalid signature") 227 } 228 } 229 isEmpty := from.Equals(to) || amount.Sign() == 0 230 inc := amount 231 if isEmpty { 232 inc = big.NewInt(0) 233 } else { 234 inc = new(big.Int).Neg(inc) 235 } 236 237 postF1, err := c.updateAccBalance(ic, from, inc, amount) 238 if err != nil { 239 return err 240 } 241 242 if !isEmpty { 243 postF2, err = c.updateAccBalance(ic, to, amount, nil) 244 if err != nil { 245 return err 246 } 247 } 248 249 c.postTransfer(ic, &from, &to, amount, data, true, postF1, postF2) 250 return nil 251 } 252 253 func (c *nep17TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { 254 h := toUint160(args[0]) 255 return stackitem.NewBigInteger(c.balanceOfInternal(ic.DAO, h)) 256 } 257 258 func (c *nep17TokenNative) balanceOfInternal(d *dao.Simple, h util.Uint160) *big.Int { 259 key := makeAccountKey(h) 260 si := d.GetStorageItem(c.ID, key) 261 if si == nil { 262 return big.NewInt(0) 263 } 264 balance, err := c.balFromBytes(&si) 265 if err != nil { 266 panic(fmt.Errorf("can not deserialize balance state: %w", err)) 267 } 268 return balance 269 } 270 271 func (c *nep17TokenNative) mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) { 272 if amount.Sign() == 0 { 273 return 274 } 275 postF := c.addTokens(ic, h, amount) 276 c.postTransfer(ic, nil, &h, amount, stackitem.Null{}, callOnPayment, postF) 277 } 278 279 func (c *nep17TokenNative) burn(ic *interop.Context, h util.Uint160, amount *big.Int) { 280 if amount.Sign() == 0 { 281 return 282 } 283 amount.Neg(amount) 284 postF := c.addTokens(ic, h, amount) 285 amount.Neg(amount) 286 c.postTransfer(ic, &h, nil, amount, stackitem.Null{}, false, postF) 287 } 288 289 func (c *nep17TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount *big.Int) func() { 290 if amount.Sign() == 0 { 291 return nil 292 } 293 294 key := makeAccountKey(h) 295 si := ic.DAO.GetStorageItem(c.ID, key) 296 if si == nil { 297 si = state.StorageItem{} 298 } 299 postF, err := c.incBalance(ic, h, &si, amount, nil) 300 if err != nil { 301 panic(err) 302 } 303 if si == nil { 304 ic.DAO.DeleteStorageItem(c.ID, key) 305 } else { 306 ic.DAO.PutStorageItem(c.ID, key, si) 307 } 308 309 buf, supply := c.getTotalSupply(ic.DAO) 310 supply.Add(supply, amount) 311 c.saveTotalSupply(ic.DAO, buf, supply) 312 return postF 313 } 314 315 func newDescriptor(name string, ret smartcontract.ParamType, ps ...manifest.Parameter) *manifest.Method { 316 if len(ps) == 0 { 317 ps = []manifest.Parameter{} 318 } 319 return &manifest.Method{ 320 Name: name, 321 Parameters: ps, 322 ReturnType: ret, 323 } 324 } 325 326 func newMethodAndPrice(f interop.Method, cpuFee int64, flags callflag.CallFlag, activeFrom ...config.Hardfork) *interop.MethodAndPrice { 327 md := &interop.MethodAndPrice{ 328 HFSpecificMethodAndPrice: interop.HFSpecificMethodAndPrice{ 329 Func: f, 330 CPUFee: cpuFee, 331 RequiredFlags: flags, 332 }, 333 } 334 if len(activeFrom) != 0 { 335 md.ActiveFrom = &activeFrom[0] 336 } 337 return md 338 } 339 340 func newEventDescriptor(name string, ps ...manifest.Parameter) *manifest.Event { 341 if len(ps) == 0 { 342 ps = []manifest.Parameter{} 343 } 344 return &manifest.Event{ 345 Name: name, 346 Parameters: ps, 347 } 348 } 349 350 func newEvent(desc *manifest.Event, activeFrom ...config.Hardfork) interop.Event { 351 md := interop.Event{ 352 HFSpecificEvent: interop.HFSpecificEvent{ 353 MD: desc, 354 }, 355 } 356 if len(activeFrom) != 0 { 357 md.ActiveFrom = &activeFrom[0] 358 } 359 return md 360 } 361 362 func toBigInt(s stackitem.Item) *big.Int { 363 bi, err := s.TryInteger() 364 if err != nil { 365 panic(err) 366 } 367 return bi 368 } 369 370 func toUint160(s stackitem.Item) util.Uint160 { 371 buf, err := s.TryBytes() 372 if err != nil { 373 panic(err) 374 } 375 u, err := util.Uint160DecodeBytesBE(buf) 376 if err != nil { 377 panic(err) 378 } 379 return u 380 } 381 382 func toUint64(s stackitem.Item) uint64 { 383 bigInt := toBigInt(s) 384 if !bigInt.IsUint64() { 385 panic("bigint is not a uint64") 386 } 387 return bigInt.Uint64() 388 } 389 390 func toUint32(s stackitem.Item) uint32 { 391 uint64Value := toUint64(s) 392 if uint64Value > math.MaxUint32 { 393 panic("bigint does not fit into uint32") 394 } 395 return uint32(uint64Value) 396 } 397 398 func toUint8(s stackitem.Item) uint8 { 399 uint64Value := toUint64(s) 400 if uint64Value > math.MaxUint8 { 401 panic("bigint does not fit into uint8") 402 } 403 return uint8(uint64Value) 404 } 405 406 func toInt64(s stackitem.Item) int64 { 407 bigInt := toBigInt(s) 408 if !bigInt.IsInt64() { 409 panic("bigint is not an uint64") 410 } 411 return bigInt.Int64() 412 }