diff --git a/go.mod b/go.mod index 693a2d38..82cde197 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,12 @@ go 1.22.8 require ( cosmossdk.io/api v0.7.6 - cosmossdk.io/client/v2 v2.0.0-beta.5 + cosmossdk.io/client/v2 v2.0.0-beta.7 cosmossdk.io/collections v0.4.0 cosmossdk.io/core v0.11.1 cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.5.0 - cosmossdk.io/math v1.4.0 + cosmossdk.io/math v1.5.0 cosmossdk.io/simapp v0.0.0-20240805084742-3fc80745eef2 cosmossdk.io/store v1.1.1 cosmossdk.io/tools/confix v0.1.2 @@ -18,7 +18,7 @@ require ( cosmossdk.io/x/tx v0.13.7 cosmossdk.io/x/upgrade v0.1.4 github.com/cometbft/cometbft v0.38.15 - github.com/cosmos/cosmos-db v1.1.0 + github.com/cosmos/cosmos-db v1.1.1 github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/cosmos-sdk v0.50.11 github.com/cosmos/gogoproto v1.7.0 @@ -30,16 +30,18 @@ require ( github.com/golang/protobuf v1.5.4 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/grpc-gateway v1.16.0 + github.com/onsi/ginkgo/v2 v2.22.2 + github.com/onsi/gomega v1.36.2 github.com/realio-tech/multi-staking-module v0.0.0-00010101000000-000000000000 - github.com/spf13/cast v1.7.0 + github.com/spf13/cast v1.7.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - golang.org/x/sync v0.9.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.68.0 - google.golang.org/protobuf v1.35.2 + golang.org/x/sync v0.10.0 + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 + google.golang.org/grpc v1.69.2 + google.golang.org/protobuf v1.36.2 gopkg.in/yaml.v2 v2.4.0 ) @@ -47,7 +49,7 @@ require ( cloud.google.com/go v0.115.0 // indirect cloud.google.com/go/auth v0.6.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect cloud.google.com/go/iam v1.1.9 // indirect cloud.google.com/go/storage v1.41.0 // indirect cosmossdk.io/depinject v1.1.0 // indirect @@ -80,6 +82,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect @@ -123,6 +126,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect @@ -134,6 +138,7 @@ require ( github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/orderedcode v0.0.1 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect @@ -157,7 +162,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.3.1 // indirect + github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect @@ -169,7 +174,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/linxGnu/grocksdb v1.9.2 // indirect + github.com/linxGnu/grocksdb v1.9.8 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -224,22 +229,23 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.28.0 // indirect google.golang.org/api v0.186.0 // indirect google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -255,10 +261,10 @@ replace ( // use Evmos geth fork github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc4 - github.com/evmos/os => github.com/evmos/os v0.0.0-20250108125741-e95afb000453 + github.com/evmos/os => github.com/evmos/os v0.0.0-20250130185216-d2cab8abc34d // use Realio sdk v0.46.11-realio-4 // github.com/cosmos/cosmos-sdk => github.com/realiotech/cosmos-sdk v0.46.11-realio-4 - github.com/evmos/os/example_chain => github.com/evmos/os/example_chain v0.0.0-20241002122822-02a9121016ee + github.com/evmos/os/example_chain => github.com/evmos/os/example_chain v0.0.0-20250130185216-d2cab8abc34d // github.com/realio-tech/multi-staking-module => ../multi-staking // github.com/evmos/os => ../evmos-os diff --git a/go.sum b/go.sum index 1cb84b49..2bea430f 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= @@ -188,8 +188,8 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= -cosmossdk.io/client/v2 v2.0.0-beta.5 h1:0LVv3nEByn//hFDIrYLs2WvsEU3HodOelh4SDHnA/1I= -cosmossdk.io/client/v2 v2.0.0-beta.5/go.mod h1:4p0P6o0ro+FizakJUYS9SeM94RNbv0thLmkHRw5o5as= +cosmossdk.io/client/v2 v2.0.0-beta.7 h1:O0PfZL5kC3Sp54wZASLNihQ612Gd6duMp11aM9wawNg= +cosmossdk.io/client/v2 v2.0.0-beta.7/go.mod h1:TzwwrzeK+AfSVSESVEIOYO/9xuCh1fPv0HgeocmfVnM= cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= cosmossdk.io/core v0.11.1 h1:h9WfBey7NAiFfIcUhDVNS503I2P2HdZLebJlUIs8LPA= @@ -200,8 +200,8 @@ cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= cosmossdk.io/log v1.5.0 h1:dVdzPJW9kMrnAYyMf1duqacoidB9uZIl+7c6z0mnq0g= cosmossdk.io/log v1.5.0/go.mod h1:Tr46PUJjiUthlwQ+hxYtUtPn4D/oCZXAkYevBeh5+FI= -cosmossdk.io/math v1.4.0 h1:XbgExXFnXmF/CccPPEto40gOO7FpWu9yWNAZPN3nkNQ= -cosmossdk.io/math v1.4.0/go.mod h1:O5PkD4apz2jZs4zqFdTr16e1dcaQCc5z6lkEnrrppuk= +cosmossdk.io/math v1.5.0 h1:sbOASxee9Zxdjd6OkzogvBZ25/hP929vdcYcBJQbkLc= +cosmossdk.io/math v1.5.0/go.mod h1:AAwwBmUhqtk2nlku174JwSll+/DepUXW3rWIXN5q+Nw= cosmossdk.io/simapp v0.0.0-20240805084742-3fc80745eef2 h1:f2zS+kpAqxGEbY1xHUK7ytfKeCVkmu89FMYGAK+FbYA= cosmossdk.io/simapp v0.0.0-20240805084742-3fc80745eef2/go.mod h1:prWoljZuPhMPxw7Xtc7FhceNtg6hSbla9S5GreyfM58= cosmossdk.io/tools/confix v0.1.2 h1:2hoM1oFCNisd0ltSAAZw2i4ponARPmlhuNu3yy0VwI4= @@ -364,6 +364,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= @@ -394,8 +396,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= -github.com/cosmos/cosmos-db v1.1.0 h1:KLHNVQ73h7vawXTpj9UJ7ZR2IXv51tsEHkQJJ9EBDzI= -github.com/cosmos/cosmos-db v1.1.0/go.mod h1:t7c4A6cfGdpUwwVxrQ0gQLeRQqGUBJu0yvE4F/26REg= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= github.com/cosmos/cosmos-sdk v0.50.11 h1:LxR1aAc8kixdrs3itO+3a44sFoc+vjxVAOyPFx22yjk= @@ -501,10 +503,10 @@ github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe h1:CKvjP3Cc github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe/go.mod h1:Bm6h8ZkYgVTytHK5vhHOMKw9OHyiumm3b1UbkYIJ/Ug= github.com/evmos/go-ethereum v1.10.26-evmos-rc4 h1:vwDVMScuB2KSu8ze5oWUuxm6v3bMUp6dL3PWvJNJY+I= github.com/evmos/go-ethereum v1.10.26-evmos-rc4/go.mod h1:/6CsT5Ceen2WPLI/oCA3xMcZ5sWMF/D46SjM/ayY0Oo= -github.com/evmos/os v0.0.0-20250108125741-e95afb000453 h1:ZwRIMj0RPxk37fXeHh6wRJr3S7tj5fxWG5Jf3ZyMCus= -github.com/evmos/os v0.0.0-20250108125741-e95afb000453/go.mod h1:BFkZxXO384jHZ++ETF7Vmh82MGNQ2wzzQV7LOA9cJJs= -github.com/evmos/os/example_chain v0.0.0-20241002122822-02a9121016ee h1:BZpePPAM2zYn8P9EkU14K0p4TTgwEQJLaQG4FjNh99Q= -github.com/evmos/os/example_chain v0.0.0-20241002122822-02a9121016ee/go.mod h1:+SPMqw9wtbWO3jG02uLbLtVVjMHBldmXTN51kxbWqJ8= +github.com/evmos/os v0.0.0-20250130185216-d2cab8abc34d h1:EAHT7l/YzzrbEPl2C8iQz5W7zDwQ8cYKmHsXzI3Uefw= +github.com/evmos/os v0.0.0-20250130185216-d2cab8abc34d/go.mod h1:zgs0dJ8M4AQtRDtGNCa6h2XqQDh4a5ar4Za+B80YJwc= +github.com/evmos/os/example_chain v0.0.0-20250130185216-d2cab8abc34d h1:4K8xoiN5ikYkcbZIxrxmzFtG1y6ZkT2OJqKdvHqU0MM= +github.com/evmos/os/example_chain v0.0.0-20250130185216-d2cab8abc34d/go.mod h1:NyqEZcwpY6sa+Re11S/Q1Q1cnfyy7I9YmSS0Pi72WtU= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= @@ -674,8 +676,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -781,8 +783,8 @@ github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= -github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -858,8 +860,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linxGnu/grocksdb v1.9.2 h1:O3mzvO0wuzQ9mtlHbDrShixyVjVbmuqTjFrzlf43wZ8= -github.com/linxGnu/grocksdb v1.9.2/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= +github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= +github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -941,14 +943,14 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -1078,8 +1080,8 @@ github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIK github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= @@ -1182,14 +1184,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1220,8 +1224,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1321,8 +1325,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1364,8 +1368,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1465,13 +1469,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1482,8 +1486,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1550,8 +1554,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1730,10 +1734,10 @@ google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 h1:6whtk83KtD3FkGrVb2hFXuQ+ZMbCNdakARIn/aHMmG8= google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094/go.mod h1:Zs4wYw8z1zr6RNF4cwYb31mvN/EGaKAdQjNCF3DW6K4= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1775,8 +1779,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1793,8 +1797,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/precompiles/erc20/integration_test.go b/precompiles/erc20/integration_test.go new file mode 100644 index 00000000..27a6efaa --- /dev/null +++ b/precompiles/erc20/integration_test.go @@ -0,0 +1,2868 @@ +package erc20 + +import ( + "fmt" + "math/big" + "slices" + "strings" + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/os/contracts" + auth "github.com/evmos/os/precompiles/authorization" + "github.com/evmos/os/precompiles/erc20" + "github.com/evmos/os/precompiles/erc20/testdata" + "github.com/evmos/os/precompiles/testutil" + testconstants "github.com/evmos/os/testutil/constants" + "github.com/evmos/os/testutil/integration/os/factory" + "github.com/evmos/os/testutil/integration/os/grpc" + "github.com/evmos/os/testutil/integration/os/keyring" + "github.com/evmos/os/testutil/integration/os/network" + integrationutils "github.com/evmos/os/testutil/integration/os/utils" + utiltx "github.com/evmos/os/testutil/tx" + erc20types "github.com/evmos/os/x/erc20/types" + "github.com/evmos/os/x/evm/core/vm" + evmtypes "github.com/evmos/os/x/evm/types" + + //nolint:revive // dot imports are fine for Ginkgo + . "github.com/onsi/ginkgo/v2" + //nolint:revive // dot imports are fine for Ginkgo + . "github.com/onsi/gomega" +) + +var is *IntegrationTestSuite + +type IntegrationTestSuite struct { + // NOTE: we have to use the Unit testing network because we access a keeper in a setup function. + // Might adjust this on a follow-up PR. + network *network.UnitTestNetwork + handler grpc.Handler + keyring keyring.Keyring + factory factory.TxFactory + + bondDenom string + tokenDenom string // erc20 precompile denom with supply + tokenDenomTwo string // erc20 precompile denom with zero supply + + precompile *erc20.Precompile // erc20 precompile with supply + precompileTwo *erc20.Precompile // erc20 precompile with zero supply +} + +func (is *IntegrationTestSuite) SetupTest() { + is.tokenDenom = "xmpl" + is.tokenDenomTwo = "xmpl2" + + keys := keyring.New(2) + genesis := integrationutils.CreateGenesisWithTokenPairs(keys, is.tokenDenom, is.tokenDenomTwo) + + nw := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keys.GetAllAccAddrs()...), + network.WithOtherDenoms([]string{is.tokenDenom}), // add balance (supply) to is.tokenDenom + network.WithCustomGenesis(genesis), + ) + gh := grpc.NewIntegrationHandler(nw) + tf := factory.New(nw, gh) + + is.network = nw + is.factory = tf + is.handler = gh + is.keyring = keys + + is.bondDenom = nw.GetBaseDenom() + + erc20Gen := genesis[erc20types.ModuleName].(*erc20types.GenesisState) + is.precompile = is.setupERC20Precompile(is.tokenDenom, erc20Gen.TokenPairs) + is.precompileTwo = is.setupERC20Precompile(is.tokenDenomTwo, erc20Gen.TokenPairs) +} + +func TestIntegrationSuite(t *testing.T) { + is = new(IntegrationTestSuite) + + // Run Ginkgo integration tests + RegisterFailHandler(Fail) + RunSpecs(t, "ERC20 Extension Suite") +} + +var ( + revertContractAddr common.Address + gasLimit = uint64(5000000) + gasPrice = big.NewInt(800_000_000) +) + +var _ = Describe("ERC20 Extension -", func() { + var ( + // contractsData holds the addresses and ABIs for the different + // contract instances that are subject to testing here. + contractsData ContractsData + + allowanceCallerContract evmtypes.CompiledContract + revertCallerContract evmtypes.CompiledContract + erc20MinterV5Contract evmtypes.CompiledContract + + execRevertedCheck testutil.LogCheckArgs + failCheck testutil.LogCheckArgs + passCheck testutil.LogCheckArgs + ) + + BeforeEach(func() { + is.SetupTest() + + var err error + allowanceCallerContract, err = testdata.LoadERC20AllowanceCaller() + Expect(err).ToNot(HaveOccurred(), "failed to load ERC20 allowance caller contract") + + erc20MinterV5Contract, err = testdata.LoadERC20MinterV5Contract() + Expect(err).ToNot(HaveOccurred(), "failed to load ERC20 minter contract") + + revertCallerContract, err = testdata.LoadERC20TestCaller() + Expect(err).ToNot(HaveOccurred(), "failed to load ERC20 allowance caller contract") + + sender := is.keyring.GetKey(0) + contractAddr, err := is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: allowanceCallerContract, + // NOTE: we're passing the precompile address to the constructor because that initiates the contract + // to make calls to the correct ERC20 precompile. + ConstructorArgs: []interface{}{is.precompile.Address()}, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // commit the changes to update state (account nonce mostly) + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + + contractAddrTokenTwo, err := is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: allowanceCallerContract, + // NOTE: we're passing the precompile address to the constructor because that initiates the contract + // to make calls to the correct ERC20 precompile. + ConstructorArgs: []interface{}{is.precompileTwo.Address()}, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // commit the changes to update state (account nonce mostly) + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + + erc20MinterBurnerAddr, err := is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: contracts.ERC20MinterBurnerDecimalsContract, + ConstructorArgs: []interface{}{ + "Xmpl", "Xmpl", uint8(6), + }, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy ERC20 minter burner contract") + + // commit the changes to update state (account nonce mostly) + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + + ERC20MinterV5Addr, err := is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: erc20MinterV5Contract, + ConstructorArgs: []interface{}{ + "Xmpl", "Xmpl", + }, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy ERC20 minter contract") + + // commit the changes to update state (account nonce mostly) + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + + erc20MinterV5CallerAddr, err := is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: allowanceCallerContract, + ConstructorArgs: []interface{}{ + ERC20MinterV5Addr, + }, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy ERC20 minter caller contract") + + // commit the changes to update state (account nonce mostly) + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + + // Store the data of the deployed contracts + contractsData = ContractsData{ + ownerPriv: sender.Priv, + contractData: map[CallType]ContractData{ + directCall: { + Address: is.precompile.Address(), + ABI: is.precompile.ABI, + }, + directCallToken2: { + Address: is.precompileTwo.Address(), + ABI: is.precompileTwo.ABI, + }, + contractCall: { + Address: contractAddr, + ABI: allowanceCallerContract.ABI, + }, + contractCallToken2: { + Address: contractAddrTokenTwo, + ABI: allowanceCallerContract.ABI, + }, + erc20Call: { + Address: erc20MinterBurnerAddr, + ABI: contracts.ERC20MinterBurnerDecimalsContract.ABI, + }, + erc20V5Call: { + Address: ERC20MinterV5Addr, + ABI: erc20MinterV5Contract.ABI, + }, + erc20V5CallerCall: { + Address: erc20MinterV5CallerAddr, + ABI: allowanceCallerContract.ABI, + }, + }, + } + + failCheck = testutil.LogCheckArgs{ABIEvents: is.precompile.Events} + execRevertedCheck = failCheck.WithErrContains("execution reverted") + passCheck = failCheck.WithExpPass(true) + + erc20Params := is.network.App.Erc20Keeper.GetParams(is.network.GetContext()) + Expect(len(erc20Params.NativePrecompiles)).To(Equal(1)) + Expect(common.HexToAddress(erc20Params.NativePrecompiles[0])).To(Equal(common.HexToAddress(testconstants.WEVMOSContractMainnet))) + + revertContractAddr, err = is.factory.DeployContract( + sender.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: revertCallerContract, + // NOTE: we're passing the precompile address to the constructor because that initiates the contract + // to make calls to the correct ERC20 precompile. + ConstructorArgs: []interface{}{common.HexToAddress(erc20Params.NativePrecompiles[0])}, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy reverter contract") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to advance block") + }) + + Context("basic functionality -", func() { + When("sending tokens to contract", func() { + It("it should return error", func() { + sender := is.keyring.GetKey(0) + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + + // Fund account with some tokens + is.fundWithTokens(directCall, contractsData, sender.Addr, fundCoins) + + // Taking custom args from the table entry + txArgs := evmtypes.EvmTxArgs{} + txArgs.Amount = big.NewInt(int64(1000)) + precompileAddress := is.precompile.Address() + txArgs.To = &precompileAddress + + _, err := is.factory.ExecuteEthTx(sender.Priv, txArgs) + // Currently, this check pass because the erc20 precompile does + // not expose a fallback handler. Adding a fallback handler, the + // test should pass again because of the check on the message + // value in the precompile before the setup. + Expect(err.Error()).To(ContainSubstring(vm.ErrExecutionReverted.Error()), "precompile should not accept transfers") + }, + ) + }) + When("transferring tokens", func() { + DescribeTable("it should transfer tokens to a non-existing address", func(callType CallType, expGasUsedLowerBound int64, expGasUsedUpperBound int64) { + sender := is.keyring.GetKey(0) + receiver := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + senderInitialAmt := is.fundWithTokens(callType, contractsData, sender.Addr, fundCoins) + senderInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, senderInitialAmt)} + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TransferMethod, receiver, transferCoins[0].Amount.BigInt()) + + transferCheck := passCheck.WithExpEvents(erc20.EventTypeTransfer) + + res, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: sender.AccAddr, expCoins: senderInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + + Expect(res.GasUsed > expGasUsedLowerBound).To(BeTrue(), "expected different gas used") + Expect(res.GasUsed < expGasUsedUpperBound).To(BeTrue(), "expected different gas used") + }, + // FIXME: The gas used on the precompile is much higher than on the EVM + Entry(" - direct call", directCall, int64(3_021_000), int64(3_022_000)), + Entry(" - through erc20 contract", erc20Call, int64(54_000), int64(54_500)), + Entry(" - through erc20 v5 contract", erc20V5Call, int64(52_000), int64(52_200)), + ) + + DescribeTable("it should transfer tokens to an existing address", func(callType CallType) { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetKey(1) + fundCoinsSender := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + fundCoinsReceiver := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 500)} + transferCoin := sdk.NewInt64Coin(is.tokenDenom, 100) + + // Fund accounts with some tokens + receiverInitialAmt := is.fundWithTokens(callType, contractsData, receiver.Addr, fundCoinsReceiver) + receiverInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, receiverInitialAmt)} + + senderInitialAmt := is.fundWithTokens(callType, contractsData, sender.Addr, fundCoinsSender) + senderInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, senderInitialAmt)} + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TransferMethod, receiver.Addr, transferCoin.Amount.BigInt()) + + transferCheck := passCheck.WithExpEvents(erc20.EventTypeTransfer) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: sender.AccAddr, expCoins: senderInitialBalance.Sub(transferCoin)}, + {address: receiver.AccAddr, expCoins: receiverInitialBalance.Add(transferCoin)}, + }, + ) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because transferring using a caller contract + // is only supported through transferFrom method. + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should return an error trying to call from a smart contract", func(callType CallType) { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoin := sdk.NewInt64Coin(is.tokenDenom, 100) + + // Fund account with some tokens + is.fundWithTokens(callType, contractsData, sender.Addr, fundCoins) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TransferMethod, receiver, transferCoin.Amount.BigInt()) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + // NOTE: we are not passing the direct call here because this test is specific to the contract calls + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should return an error if the sender does not have enough tokens", func(callType CallType) { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + + // Fund account with some tokens + senderInitialAmt := is.fundWithTokens(callType, contractsData, sender.Addr, fundCoins) + senderInitialBalance := sdk.NewCoin(is.tokenDenom, senderInitialAmt) + + transferCoin := senderInitialBalance.Add(sdk.NewInt64Coin(is.tokenDenom, 100)) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TransferMethod, receiver, transferCoin.Amount.BigInt()) + + insufficientBalanceCheck := failCheck.WithErrContains( + erc20.ErrTransferAmountExceedsBalance.Error(), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, insufficientBalanceCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because this test is for direct calls only + + Entry(" - through erc20 contract", erc20Call), + // // TODO: The ERC20 V5 contract is raising the ERC-6093 standardized error which we are not as of yet + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + When("calling reverter contract", func() { + Context("in a direct call to the WEVMOS contract", func() { + var ( + args factory.CallArgs + txArgs evmtypes.EvmTxArgs + ) + BeforeEach(func() { + args = factory.CallArgs{ + ContractABI: revertCallerContract.ABI, + } + + txArgs = evmtypes.EvmTxArgs{ + To: &revertContractAddr, + GasLimit: gasLimit, + GasPrice: gasPrice, + } + }) + It("should transfer tokens", func() { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetKey(1) + amountToSend := big.NewInt(100) + + balRes, err := is.handler.GetBalanceFromBank(receiver.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + denomInitialBalance := balRes.Balance + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderInitialBalance := balRes.Balance + + args.MethodName = "transferWithRevert" + args.Args = []interface{}{ + receiver.Addr, + amountToSend, + false, + false, + } + txArgs.Amount = amountToSend + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + ) + res, _, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, args, transferCheck) + Expect(err).To(BeNil()) + Expect(is.network.NextBlock()).To(BeNil()) + fees := math.NewIntFromBigInt(gasPrice).MulRaw(res.GasUsed) + + Expect(is.network.NextBlock()).ToNot(HaveOccurred(), "failed to advance block") + + balRes, err = is.handler.GetBalanceFromBank(receiver.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + denomFinalBalance := balRes.Balance + Expect(denomFinalBalance.Amount).To(Equal(denomInitialBalance.Amount.Add(math.NewInt(amountToSend.Int64())))) + + balRes, err = is.handler.GetBalanceFromBank(revertContractAddr.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + contractBalance := balRes.Balance + Expect(contractBalance.Amount).To(Equal(math.ZeroInt())) + + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderFinalBalance := balRes.Balance + denomSpent := fees.Add(math.NewIntFromBigInt(amountToSend)) + Expect(senderFinalBalance.Amount).To(Equal(senderInitialBalance.Amount.Sub(denomSpent))) + }, + ) + DescribeTable("it should revert token transfer from the WEVMOS contract", func(before bool, after bool) { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + amountToSend := big.NewInt(100) + balRes, err := is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomInitialBalance := balRes.Balance + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderInitialBalance := balRes.Balance + + args.MethodName = "transferWithRevert" + args.Args = []interface{}{ + receiver, + amountToSend, + before, + after, + } + txArgs.Amount = amountToSend + + res, _, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, args, execRevertedCheck) + Expect(err).To(BeNil()) + Expect(is.network.NextBlock()).To(BeNil()) + + fees := math.NewIntFromBigInt(gasPrice).MulRaw(res.GasUsed) + + // contract balance should remain unchanged + balRes, err = is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomFinalBalance := balRes.Balance + Expect(denomFinalBalance.Amount).To(Equal(denomInitialBalance.Amount)) + + balRes, err = is.handler.GetBalanceFromBank(revertContractAddr.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + contractBalance := balRes.Balance + Expect(contractBalance.Amount).To(Equal(math.ZeroInt())) + + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderFinalBalance := balRes.Balance + Expect(senderFinalBalance.Amount).To(Equal(senderInitialBalance.Amount.Sub(fees))) + }, + Entry("revert before", true, false), + Entry("revert after", false, true), + ) + It("it should send token transfer and send from WEVMOS contract", func() { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + totalToSend := int64(350) + balRes, err := is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomInitialBalance := balRes.Balance + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderInitialBalance := balRes.Balance + + args.MethodName = "testTransferAndSend" + args.Args = []interface{}{ + receiver, + big.NewInt(100), + big.NewInt(100), + big.NewInt(150), + false, + false, + } + txArgs.Amount = big.NewInt(totalToSend) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + ) + res, _, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, args, transferCheck) + Expect(err).To(BeNil()) + Expect(is.network.NextBlock()).To(BeNil()) + fees := math.NewIntFromBigInt(gasPrice).MulRaw(res.GasUsed) + + // contract balance should remain unchanged + balRes, err = is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomFinalBalance := balRes.Balance + Expect(denomFinalBalance.Amount).To(Equal(denomInitialBalance.Amount.Add(math.NewInt(totalToSend)))) + + balRes, err = is.handler.GetBalanceFromBank(revertContractAddr.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + contractBalance := balRes.Balance + Expect(contractBalance.Amount).To(Equal(math.ZeroInt())) + + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderFinalBalance := balRes.Balance + denomSpent := fees.AddRaw(totalToSend) + Expect(senderFinalBalance.Amount).To(Equal(senderInitialBalance.Amount.Sub(denomSpent))) + }, + ) + DescribeTable("it should revert token transfer and send from WEVMOS contract", func(before bool, after bool) { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + balRes, err := is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomInitialBalance := balRes.Balance + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderInitialBalance := balRes.Balance + + args.MethodName = "testTransferAndSend" + args.Args = []interface{}{ + receiver, + big.NewInt(100), + big.NewInt(100), + big.NewInt(100), + before, + after, + } + txArgs.Amount = big.NewInt(300) + + res, _, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, args, execRevertedCheck) + Expect(err).To(BeNil()) + Expect(is.network.NextBlock()).To(BeNil()) + fees := math.NewIntFromBigInt(gasPrice).MulRaw(res.GasUsed) + + // contract balance should remain unchanged + balRes, err = is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomFinalBalance := balRes.Balance + Expect(denomFinalBalance.Amount).To(Equal(denomInitialBalance.Amount)) + + balRes, err = is.handler.GetBalanceFromBank(revertContractAddr.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + contractBalance := balRes.Balance + Expect(contractBalance.Amount).To(Equal(math.ZeroInt())) + + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderFinalBalance := balRes.Balance + Expect(senderFinalBalance.Amount).To(Equal(senderInitialBalance.Amount.Sub(fees))) + }, + Entry("revert before", true, false), + Entry("revert after", false, true), + ) + It("revert when transfer with try", func() { + sender := is.keyring.GetKey(0) + receiver := is.keyring.GetAddr(1) + amountToSend := big.NewInt(100) + balRes, err := is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomInitialBalance := balRes.Balance + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderInitialBalance := balRes.Balance + + args.MethodName = "transfersWithTry" + args.Args = []interface{}{ + receiver, + amountToSend, + amountToSend, + } + txArgs.Amount = big.NewInt(200) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + ) + res, _, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, args, transferCheck) + Expect(err).To(BeNil()) + Expect(is.network.NextBlock()).To(BeNil()) + fees := math.NewIntFromBigInt(gasPrice).MulRaw(res.GasUsed) + + balRes, err = is.handler.GetBalanceFromBank(receiver.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + denomFinalBalance := balRes.Balance + Expect(denomFinalBalance.Amount).To(Equal(denomInitialBalance.Amount.Add(math.NewInt(amountToSend.Int64())))) + + balRes, err = is.handler.GetBalanceFromBank(revertContractAddr.Bytes(), is.bondDenom) + Expect(err).To(BeNil()) + contractBalance := balRes.Balance + Expect(contractBalance.Amount.Int64()).To(Equal(amountToSend.Int64())) + + balRes, err = is.handler.GetBalanceFromBank(sender.AccAddr, is.bondDenom) + Expect(err).To(BeNil()) + senderFinalBalance := balRes.Balance + denomSpent := fees.AddRaw(amountToSend.Int64() + amountToSend.Int64()) + Expect(senderFinalBalance.Amount).To(Equal(senderInitialBalance.Amount.Sub(denomSpent))) + }) + }) + }) + + When("transferring tokens from another account", func() { + Context("in a direct call to the token contract", func() { + DescribeTable("it should transfer tokens from another account with a sufficient approval set", func(callType CallType) { + owner := is.keyring.GetKey(0) + spender := is.keyring.GetKey(1) + receiver := utiltx.GenerateAddress() + + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(callType, contractsData, owner.Addr, fundCoins) + ownerInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, ownerInitialAmt)} + + // Set allowance + is.setupSendAuthzForContract(callType, contractsData, spender.Addr, owner.Priv, transferCoins) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoins[0].Amount.BigInt(), + ) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + auth.EventTypeApproval, + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(spender.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to the chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferFromMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: owner.AccAddr, expCoins: ownerInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + + // Check that the allowance was removed since we authorized only the transferred amount + is.ExpectNoSendAuthzForContract( + callType, contractsData, + spender.Addr, owner.Addr, + ) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because this test is for direct calls only + + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + When("the spender is the same as the sender", func() { + It("should transfer funds without the need for an approval when calling the EVM extension", func() { + owner := is.keyring.GetKey(0) + spender := owner + receiver := utiltx.GenerateAddress() + + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(directCall, contractsData, owner.Addr, fundCoins) + ownerInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, ownerInitialAmt)} + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + directCall, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoins[0].Amount.BigInt(), + ) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, auth.EventTypeApproval, + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(spender.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferMethod) + is.ExpectBalancesForContract( + directCall, contractsData, + []ExpectedBalance{ + {address: owner.AccAddr, expCoins: ownerInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + }) + + DescribeTable("it should transfer funds from the own account in case sufficient approval is set", func(callType CallType) { + owner := is.keyring.GetKey(0) + receiver := utiltx.GenerateAddress() + + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(callType, contractsData, owner.Addr, fundCoins) + ownerInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, ownerInitialAmt)} + + // NOTE: Here we set up the allowance using the contract calls instead of the helper utils, + // because the `MsgGrant` used there doesn't allow the sender to be the same as the spender, + // but the ERC20 contracts do. + txArgs, approveArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.ApproveMethod, + owner.Addr, transferCoins[0].Amount.BigInt(), + ) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, _, err := is.factory.CallContractAndCheckLogs(owner.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // create new block to commit the changes in the state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectSendAuthzForContract( + callType, contractsData, + owner.Addr, owner.Addr, transferCoins, + ) + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoins[0].Amount.BigInt(), + ) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + auth.EventTypeApproval, + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(owner.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferFromMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: owner.AccAddr, expCoins: ownerInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + + // Check that the allowance was removed since we authorized only the transferred amount + // FIXME: This is not working for the case where we transfer from the own account + // because the allowance is not removed on the SDK side. + is.ExpectNoSendAuthzForContract( + callType, contractsData, + owner.Addr, owner.Addr, + ) + }, + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + DescribeTable("it should return an error when the spender does not have enough allowance", func(callType CallType) { + owner := is.keyring.GetKey(0) + spender := is.keyring.GetKey(1) + receiver := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + transferCoin := sdk.NewInt64Coin(is.tokenDenom, 200) + + // Fund account with some tokens + is.fundWithTokens(callType, contractsData, owner.Addr, fundCoins) + // Set allowance + is.setupSendAuthzForContract( + callType, contractsData, + spender.Addr, owner.Priv, authzCoins, + ) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoin.Amount.BigInt(), + ) + + insufficientAllowanceCheck := failCheck.WithErrContains(erc20.ErrInsufficientAllowance.Error()) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(spender.Priv, txArgs, transferArgs, insufficientAllowanceCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because this test case only covers direct calls + + Entry(" - through erc20 contract", erc20Call), + + // TODO: the ERC20 V5 contract is raising the ERC-6093 standardized error which we are not using as of yet + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should return an error if there is no allowance set", func(callType CallType) { + sender := is.keyring.GetKey(0) + from := is.keyring.GetKey(1) + receiver := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoin := sdk.NewInt64Coin(is.tokenDenom, 100) + + // Fund account with some tokens + is.fundWithTokens(callType, contractsData, from.Addr, fundCoins) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + from.Addr, receiver, transferCoin.Amount.BigInt(), + ) + + insufficientAllowanceCheck := failCheck.WithErrContains( + erc20.ErrInsufficientAllowance.Error(), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, insufficientAllowanceCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because this test case only covers direct calls + + Entry(" - through erc20 contract", erc20Call), + + // TODO: the ERC20 V5 contract is raising the ERC-6093 standardized error which we are not using as of yet + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should return an error if the sender does not have enough tokens", func(callType CallType) { + sender := is.keyring.GetKey(0) + from := is.keyring.GetKey(1) + receiver := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + + // Fund account with some tokens + senderInitialAmt := is.fundWithTokens(callType, contractsData, from.Addr, fundCoins) + senderInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, senderInitialAmt)} + transferCoins := senderInitialBalance.Add(sdk.NewInt64Coin(is.tokenDenom, 100)) + + // Set allowance + is.setupSendAuthzForContract( + callType, contractsData, + sender.Addr, from.Priv, transferCoins, + ) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TransferFromMethod, from.Addr, receiver, transferCoins[0].Amount.BigInt()) + + insufficientBalanceCheck := failCheck.WithErrContains( + erc20.ErrTransferAmountExceedsBalance.Error(), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, transferArgs, insufficientBalanceCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the contract call here because this test case only covers direct calls + + Entry(" - through erc20 contract", erc20Call), + + // TODO: the ERC20 V5 contract is raising the ERC-6093 standardized error which we are not using as of yet + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + Context("in a call from another smart contract to the token contract", func() { + DescribeTable("it should transfer tokens with a sufficient approval set", func(callType CallType) { + owner := is.keyring.GetKey(0) + receiver := utiltx.GenerateAddress() + fundCoin := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // NOTE: the spender will be the contract address + spender := contractsData.GetContractData(callType).Address + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(callType, contractsData, owner.Addr, fundCoin) + ownerInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, ownerInitialAmt)} + + // Set allowance + is.setupSendAuthzForContract( + callType, contractsData, + spender, owner.Priv, transferCoins, + ) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoins[0].Amount.BigInt(), + ) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + auth.EventTypeApproval, + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(owner.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferFromMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: owner.AccAddr, expCoins: ownerInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + + // Check that the allowance was removed since we authorized only the transferred amount + is.ExpectNoSendAuthzForContract( + callType, contractsData, + spender, owner.Addr, + ) + }, + // Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + // NOTE: we are not passing the erc20 contract call here because this is supposed to + // test external contract calls + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should transfer funds with a sufficient allowance and triggered from another account", func(callType CallType) { + msgSender := is.keyring.GetKey(0) + owner := is.keyring.GetKey(1) + receiver := utiltx.GenerateAddress() + + // NOTE: the spender will be the contract address + spender := contractsData.GetContractData(callType).Address + + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 300)} + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(callType, contractsData, owner.Addr, fundCoins) + ownerInitialBalance := sdk.Coins{sdk.NewCoin(is.tokenDenom, ownerInitialAmt)} + + // Set allowance + is.setupSendAuthzForContract( + callType, contractsData, + spender, owner.Priv, transferCoins, + ) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + owner.Addr, receiver, transferCoins[0].Amount.BigInt(), + ) + + transferCheck := passCheck.WithExpEvents( + erc20.EventTypeTransfer, + auth.EventTypeApproval, + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(msgSender.Priv, txArgs, transferArgs, transferCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, erc20.TransferFromMethod) + is.ExpectBalancesForContract( + callType, contractsData, + []ExpectedBalance{ + {address: owner.AccAddr, expCoins: ownerInitialBalance.Sub(transferCoins...)}, + {address: receiver.Bytes(), expCoins: transferCoins}, + }, + ) + + // Check that the allowance was removed since we authorized only the transferred amount + is.ExpectNoSendAuthzForContract( + callType, contractsData, + spender, owner.Addr, + ) + }, + // NOTE: we are not passing the direct call here because this test is specific to the contract calls + + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should return an error when the spender does not have enough allowance", func(callType CallType) { + from := is.keyring.GetKey(0) + receiver := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 400)} + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + transferCoin := sdk.NewInt64Coin(is.tokenDenom, 300) + + // NOTE: the spender will be the contract address + spender := contractsData.GetContractData(callType).Address + + // Fund account with some tokens + is.fundWithTokens(callType, contractsData, from.Addr, fundCoins) + + // Set allowance + is.setupSendAuthzForContract(callType, contractsData, spender, from.Priv, authzCoins) + + // Transfer tokens + txArgs, transferArgs := is.getTxAndCallArgs( + callType, contractsData, + erc20.TransferFromMethod, + from.Addr, receiver, transferCoin.Amount.BigInt(), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(from.Priv, txArgs, transferArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + // NOTE: we are not passing the direct call here because this test is for contract calls only + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + }) + }) + + When("querying balance", func() { + DescribeTable("it should return an existing balance", func(callType CallType) { + sender := is.keyring.GetKey(0) + addedAmt := big.NewInt(100) + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, addedAmt.Int64())} + + // Fund account with some tokens + ownerInitialAmt := is.fundWithTokens(callType, contractsData, sender.Addr, fundCoins) + + // Query the balance + txArgs, balancesArgs := is.getTxAndCallArgs(callType, contractsData, erc20.BalanceOfMethod, sender.Addr) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, balancesArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var balance *big.Int + err = is.precompile.UnpackIntoInterface(&balance, erc20.BalanceOfMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(math.NewIntFromBigInt(balance)).To(Equal(ownerInitialAmt), "expected different balance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should return zero if balance only exists for other tokens", func(callType CallType) { + sender := is.keyring.GetKey(0) + address := utiltx.GenerateAddress() + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 100)} + + // Fund account with some tokens + err := is.factory.FundAccount(is.keyring.GetKey(0), sender.AccAddr, fundCoins) + Expect(err).ToNot(HaveOccurred(), "failed to fund account") + Expect(is.network.NextBlock()).To(BeNil()) + + // Query the balance + txArgs, balancesArgs := is.getTxAndCallArgs(callType, contractsData, erc20.BalanceOfMethod, address) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, balancesArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var balance *big.Int + err = is.precompile.UnpackIntoInterface(&balance, erc20.BalanceOfMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(balance.Int64()).To(BeZero(), "expected zero balance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contracts + // only support the actual token denomination and don't know of other balances. + ) + + DescribeTable("it should return zero if the account does not exist", func(callType CallType) { + sender := is.keyring.GetKey(0) + address := utiltx.GenerateAddress() + + // Query the balance + txArgs, balancesArgs := is.getTxAndCallArgs(callType, contractsData, erc20.BalanceOfMethod, address) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, balancesArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var balance *big.Int + err = is.precompile.UnpackIntoInterface(&balance, erc20.BalanceOfMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(balance.Int64()).To(BeZero(), "expected zero balance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + }) + + When("querying allowance", func() { + DescribeTable("it should return an existing allowance", func(callType CallType) { + grantee := utiltx.GenerateAddress() + granter := is.keyring.GetKey(0) + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + is.setupSendAuthzForContract(callType, contractsData, grantee, granter.Priv, authzCoins) + + txArgs, allowanceArgs := is.getTxAndCallArgs(callType, contractsData, auth.AllowanceMethod, granter.Addr, grantee) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance).To(Equal(authzCoins[0].Amount.BigInt()), "expected different allowance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + When("querying the allowance for the own address", func() { + // NOTE: We differ in behavior from the ERC20 calls here, because the full logic for approving, + // querying allowance and reducing allowance on a transferFrom transaction is not possible without + // changes to the Cosmos SDK. + // + // For reference see this comment: https://github.com/evmos/evmos/pull/2088#discussion_r1407646217 + It("should return the maxUint256 value when calling the EVM extension", func() { + grantee := is.keyring.GetAddr(0) + granter := is.keyring.GetKey(0) + + txArgs, allowanceArgs := is.getTxAndCallArgs(directCall, contractsData, auth.AllowanceMethod, grantee, grantee) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance).To(Equal(abi.MaxUint256), "expected different allowance") + }) + + // NOTE: Since it's possible to set an allowance for the own address with the Solidity ERC20 contracts, + // we describe this case here for completion purposes, to describe the difference in behavior. + DescribeTable("should return the actual allowance value when calling the ERC20 contract", func(callType CallType) { + granter := is.keyring.GetKey(0) + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + is.setupSendAuthzForContract(callType, contractsData, granter.Addr, granter.Priv, authzCoins) + + txArgs, allowanceArgs := is.getTxAndCallArgs(callType, contractsData, auth.AllowanceMethod, granter.Addr, granter.Addr) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance).To(Equal(authzCoins.AmountOf(is.tokenDenom).BigInt()), "expected different allowance") + }, + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + DescribeTable("it should return zero if no allowance exists", func(callType CallType) { + grantee := is.keyring.GetAddr(1) + granter := is.keyring.GetKey(0) + + txArgs, allowanceArgs := is.getTxAndCallArgs(callType, contractsData, auth.AllowanceMethod, granter.Addr, grantee) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance.Int64()).To(BeZero(), "expected zero allowance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should return zero if an allowance exists for other tokens", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 100)} + + is.setupSendAuthz(grantee.AccAddr, granter.Priv, authzCoins) + + txArgs, allowanceArgs := is.getTxAndCallArgs(callType, contractsData, auth.AllowanceMethod, granter.Addr, grantee.Addr) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance.Int64()).To(BeZero(), "expected zero allowance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("it should return zero if the account does not exist", func(callType CallType) { + grantee := utiltx.GenerateAddress() + granter := is.keyring.GetKey(0) + + txArgs, allowanceArgs := is.getTxAndCallArgs(callType, contractsData, auth.AllowanceMethod, granter.Addr, grantee) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, allowanceArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var allowance *big.Int + err = is.precompile.UnpackIntoInterface(&allowance, auth.AllowanceMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(allowance.Int64()).To(BeZero(), "expected zero allowance") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + }) + + When("querying total supply", func() { + DescribeTable("it should return the total supply", func(callType CallType) { + sender := is.keyring.GetKey(0) + expSupply := big.NewInt(100) + fundCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, expSupply.Int64())} + + // Fund account with some tokens + is.fundWithTokens(callType, contractsData, sender.Addr, fundCoins) + + // if is native coin, get expSupply from the bank mod + if slices.Contains(nativeCallTypes, callType) { + qc := is.network.GetBankClient() + qRes, err := qc.SupplyOf(is.network.GetContext(), &banktypes.QuerySupplyOfRequest{Denom: is.tokenDenom}) + Expect(err).To(BeNil()) + Expect(qRes).NotTo(BeNil()) + expSupply = qRes.Amount.Amount.BigInt() + } + + // Query the balance + txArgs, supplyArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TotalSupplyMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, supplyArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var supply *big.Int + err = is.precompile.UnpackIntoInterface(&supply, erc20.TotalSupplyMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(supply).To(Equal(expSupply), "expected different supply") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should return zero if no tokens exist", func(callType CallType) { + sender := is.keyring.GetKey(0) + txArgs, supplyArgs := is.getTxAndCallArgs(callType, contractsData, erc20.TotalSupplyMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, supplyArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var supply *big.Int + err = is.precompile.UnpackIntoInterface(&supply, erc20.TotalSupplyMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(supply.Int64()).To(BeZero(), "expected zero supply") + }, + Entry(" - direct call", directCallToken2), + Entry(" - through contract", contractCallToken2), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + }) + + When("approving an allowance", func() { + Context("in a call to the token contract", func() { + DescribeTable("it should approve an allowance", func(callType CallType) { + grantee := is.keyring.GetKey(0) + granter := is.keyring.GetKey(1) + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, transferCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter.Addr, transferCoins, + ) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should add a new spend limit to an existing allowance with a different token", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + bondCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 200)} + tokenCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // set up a previous authorization + is.setupSendAuthz(grantee.AccAddr, granter.Priv, bondCoins) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, tokenCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check allowance contains both spend limits + is.expectSendAuthz(grantee.AccAddr, granter.AccAddr, bondCoins.Add(tokenCoins...)) + }, + Entry(" - direct call", directCall), + + // NOTE 2: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("it should set the new spend limit for an existing allowance with the same token", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + bondCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 200)} + tokenCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + doubleTokenCoin := sdk.NewInt64Coin(is.tokenDenom, 200) + + // set up a previous authorization + is.setupSendAuthz(grantee.AccAddr, granter.Priv, bondCoins.Add(doubleTokenCoin)) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, tokenCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check allowance contains both spend limits + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, bondCoins.Add(tokenCoins...)) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should remove the token from the spend limit of an existing authorization when approving zero", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + bondCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 200)} + tokenCoin := sdk.NewInt64Coin(is.tokenDenom, 100) + + // set up a previous authorization + is.setupSendAuthz(grantee.AccAddr, granter.Priv, bondCoins.Add(tokenCoin)) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + // Check allowance contains only the spend limit in network denomination + is.expectSendAuthz(grantee.AccAddr, granter.AccAddr, bondCoins) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("it should delete the authorization when approving zero with no other spend limits", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + tokenCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // set up a previous authorization + is.setupSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Priv, tokenCoins) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check allowance was deleted + is.expectNoSendAuthz(grantee.AccAddr, granter.AccAddr) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("it should no-op if approving 0 and no allowance exists", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + + // We are expecting an approval to be made, but no authorization stored since it's 0 + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check still no authorization exists + is.ExpectNoSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + When("the grantee is the same as the granter", func() { + // NOTE: We differ in behavior from the ERC20 calls here, because the full logic for approving, + // querying allowance and reducing allowance on a transferFrom transaction is not possible without + // changes to the Cosmos SDK. + // + // For reference see this comment: https://github.com/evmos/evmos/pull/2088#discussion_r1407646217 + It("should return an error when calling the EVM extension", func() { + grantee := is.keyring.GetKey(0) + granter := is.keyring.GetKey(0) + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs( + directCall, contractsData, + auth.ApproveMethod, + grantee.Addr, authzCoins[0].Amount.BigInt(), + ) + + spenderIsOwnerCheck := failCheck.WithErrContains(erc20.ErrSpenderIsOwner.Error()) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, spenderIsOwnerCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectNoSendAuthzForContract( + directCall, contractsData, + grantee.Addr, granter.Addr, + ) + }) + + DescribeTable("it should create an allowance", func(callType CallType) { + grantee := is.keyring.GetKey(0) + granter := is.keyring.GetKey(0) + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.ApproveMethod, + grantee.Addr, authzCoins[0].Amount.BigInt(), + ) + + approvalCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approvalCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter.Addr, authzCoins, + ) + }, + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + DescribeTable("it should return an error if approving 0 and allowance only exists for other tokens", func(callType CallType) { + grantee := is.keyring.GetKey(1) + granter := is.keyring.GetKey(0) + bondCoins := sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 200)} + + // set up a previous authorization + is.setupSendAuthz(grantee.AccAddr, granter.Priv, bondCoins) + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + + notFoundCheck := failCheck.WithErrContains( + fmt.Sprintf(erc20.ErrNoAllowanceForToken, is.tokenDenom), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, notFoundCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + }) + + // NOTE: We have to split the tests for contract calls into a separate context because + // when approving through a smart contract, the approval is created between the contract address and the + // grantee, instead of the sender address and the grantee. + Context("in a contract call", func() { + DescribeTable("it should approve an allowance", func(callType CallType) { + sender := is.keyring.GetKey(0) + grantee := is.keyring.GetKey(1) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + transferCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, transferCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check allowance + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter, transferCoins, + ) + }, + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should set the new spend limit for an existing allowance with the same token", func(callType CallType) { + sender := is.keyring.GetKey(0) + grantee := is.keyring.GetKey(1) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + initialAmount := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + newAmount := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + + // Set up a first approval + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, initialAmount[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + + // Set up a second approval which should overwrite the initial one + txArgs, approveArgs = is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, newAmount[0].Amount.BigInt()) + approveCheck = passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err = is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // Check allowance has been updated + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter, newAmount, + ) + }, + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should delete the authorization when approving zero with no other spend limits", func(callType CallType) { + sender := is.keyring.GetKey(0) + grantee := is.keyring.GetKey(1) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + tokenCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // set up a previous authorization + // + // TODO: refactor using helper + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, tokenCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + + // Approve allowance + txArgs, approveArgs = is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + _, ethRes, err = is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + + // Check allowance was deleted from the keeper / is returning 0 for smart contracts + is.ExpectNoSendAuthzForContract(callType, contractsData, grantee.Addr, granter) + }, + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + DescribeTable("it should no-op if approving 0 and no allowance exists", func(callType CallType) { + sender := is.keyring.GetKey(0) + grantee := is.keyring.GetKey(1) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs(callType, contractsData, auth.ApproveMethod, grantee.Addr, common.Big0) + + // We are expecting an approval event to be emitted, but no authorization to be stored + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + // Check still no authorization exists + is.ExpectNoSendAuthzForContract(callType, contractsData, grantee.Addr, granter) + }, + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + + When("the grantee is the same as the granter", func() { + // NOTE: We differ in behavior from the ERC20 calls here, because the full logic for approving, + // querying allowance and reducing allowance on a transferFrom transaction is not possible without + // changes to the Cosmos SDK. + // + // For reference see this comment: https://github.com/evmos/evmos/pull/2088#discussion_r1407646217 + It("should return an error when calling the EVM extension", func() { + callType := contractCall + sender := is.keyring.GetKey(0) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + grantee := granter + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.ApproveMethod, + grantee, authzCoins[0].Amount.BigInt(), + ) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectNoSendAuthzForContract( + callType, contractsData, + grantee, granter, + ) + }) + + DescribeTable("it should create an allowance when calling an ERC20 Solidity contract", func(callType CallType) { + sender := is.keyring.GetKey(0) + granter := contractsData.GetContractData(callType).Address // the granter will be the contract address + grantee := granter + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // Approve allowance + txArgs, approveArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.ApproveMethod, + grantee, authzCoins[0].Amount.BigInt(), + ) + + approvalCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(sender.Priv, txArgs, approveArgs, approvalCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee, granter, authzCoins, + ) + }, + Entry(" - through erc20 v5 caller contract", erc20V5CallerCall), + ) + }) + }) + }) + }) + + Context("metadata query -", func() { + Context("for a token without registered metadata", func() { + BeforeEach(func() { + // Deploy ERC20NoMetadata contract for this test + erc20NoMetadataContract, err := testdata.LoadERC20NoMetadataContract() + Expect(err).ToNot(HaveOccurred(), "failed to load contract") + + erc20NoMetadataAddr, err := is.factory.DeployContract( + is.keyring.GetPrivKey(0), + evmtypes.EvmTxArgs{}, + factory.ContractDeploymentData{ + Contract: erc20NoMetadataContract, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // NOTE: update the address but leave the ABI as it is, so that the ABI includes + // the metadata methods but the contract doesn't have them. + contractsData.contractData[erc20Call] = ContractData{ + Address: erc20NoMetadataAddr, + ABI: contracts.ERC20MinterBurnerDecimalsContract.ABI, + } + }) + + DescribeTable("querying the name should return an error", func(callType CallType) { + txArgs, nameArgs := is.getTxAndCallArgs(callType, contractsData, erc20.NameMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, nameArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), // NOTE: we're passing the ERC20 contract call here which was adjusted to point to a contract without metadata to expect the same errors + ) + + DescribeTable("querying the symbol should return an error", func(callType CallType) { + txArgs, symbolArgs := is.getTxAndCallArgs(callType, contractsData, erc20.SymbolMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, symbolArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), // NOTE: we're passing the ERC20 contract call here which was adjusted to point to a contract without metadata to expect the same errors + ) + + DescribeTable("querying the decimals should return an error", func(callType CallType) { + txArgs, decimalsArgs := is.getTxAndCallArgs(callType, contractsData, erc20.DecimalsMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, decimalsArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 contract", erc20Call), // NOTE: we're passing the ERC20 contract call here which was adjusted to point to a contract without metadata to expect the same errors + ) + }) + + Context("for a token with available metadata", func() { + const ( + expSymbol = "Xmpl" + expDecimals = uint8(18) + ) + + var ( + erc20Addr common.Address + expName string + ) + + BeforeEach(func() { + erc20Addr = contractsData.GetContractData(erc20V5Call).Address + expName = erc20types.CreateDenom(erc20Addr.String()) + + // Register ERC20 token pair for this test + tokenPairs, err := integrationutils.RegisterERC20(is.factory, is.network, integrationutils.ERC20RegistrationData{ + Addresses: []string{erc20Addr.Hex()}, + ProposerPriv: is.keyring.GetPrivKey(0), + }) + Expect(err).ToNot(HaveOccurred(), "failed to register ERC20 token") + Expect(tokenPairs).To(HaveLen(1)) + + // overwrite the other precompile with this one, so that the test utils like is.getTxAndCallArgs still work. + is.precompile, err = setupNewERC20PrecompileForTokenPair(is.keyring.GetPrivKey(0), is.network, is.factory, tokenPairs[0]) + Expect(err).ToNot(HaveOccurred(), "failed to set up erc20 precompile") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // update this in the global contractsData + contractsData.contractData[directCall] = ContractData{ + Address: is.precompile.Address(), + ABI: is.precompile.ABI, + } + + // Deploy contract calling the ERC20 precompile + callerAddr, err := is.factory.DeployContract( + is.keyring.GetPrivKey(0), + evmtypes.EvmTxArgs{}, + factory.ContractDeploymentData{ + Contract: allowanceCallerContract, + ConstructorArgs: []interface{}{ + is.precompile.Address(), + }, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + contractsData.contractData[contractCall] = ContractData{ + Address: callerAddr, + ABI: allowanceCallerContract.ABI, + } + }) + + DescribeTable("querying the name should return the name", func(callType CallType) { + txArgs, nameArgs := is.getTxAndCallArgs(callType, contractsData, erc20.NameMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, nameArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var name string + err = is.precompile.UnpackIntoInterface(&name, erc20.NameMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(name).To(Equal(expName), "expected different name") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("querying the symbol should return the symbol", func(callType CallType) { + txArgs, symbolArgs := is.getTxAndCallArgs(callType, contractsData, erc20.SymbolMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, symbolArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var symbol string + err = is.precompile.UnpackIntoInterface(&symbol, erc20.SymbolMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(symbol).To(Equal(expSymbol), "expected different symbol") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("querying the decimals should return the decimals", func(callType CallType) { + txArgs, decimalsArgs := is.getTxAndCallArgs(callType, contractsData, erc20.DecimalsMethod) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(is.keyring.GetPrivKey(0), txArgs, decimalsArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + var decimals uint8 + err = is.precompile.UnpackIntoInterface(&decimals, erc20.DecimalsMethod, ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "failed to unpack result") + Expect(decimals).To(Equal(expDecimals), "expected different decimals") + }, + Entry(" - direct call", directCall), + Entry(" - through contract", contractCall), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + }) + + Context("allowance adjustments -", func() { + var ( + grantee keyring.Key + granter keyring.Key + ) + + BeforeEach(func() { + // Deploying the contract which has the increase / decrease allowance methods + contractAddr, err := is.factory.DeployContract( + is.keyring.GetPrivKey(0), + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: allowanceCallerContract, + ConstructorArgs: []interface{}{is.precompile.Address()}, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + contractsData.contractData[erc20CallerCall] = ContractData{ + Address: contractAddr, + ABI: allowanceCallerContract.ABI, + } + + grantee = is.keyring.GetKey(0) + granter = is.keyring.GetKey(1) + }) + + When("the grantee is the same as the granter", func() { + // NOTE: We differ in behavior from the ERC20 calls here, because the full logic for approving, + // querying allowance and reducing allowance on a transferFrom transaction is not possible without + // changes to the Cosmos SDK. + // + // For reference see this comment: https://github.com/evmos/evmos/pull/2088#discussion_r1407646217 + Context("increasing allowance", func() { + It("should return an error when calling the EVM extension", func() { + granter := is.keyring.GetKey(0) + grantee := granter + + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs( + directCall, contractsData, + auth.IncreaseAllowanceMethod, + grantee.Addr, authzCoins[0].Amount.BigInt(), + ) + + spenderIsOwnerCheck := failCheck.WithErrContains(erc20.ErrSpenderIsOwner.Error()) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, spenderIsOwnerCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectNoSendAuthzForContract( + directCall, contractsData, + grantee.Addr, granter.Addr, + ) + }) + + DescribeTable("it should create an allowance if none existed before", func(callType CallType) { + granter := is.keyring.GetKey(0) + grantee := granter + + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.IncreaseAllowanceMethod, + grantee.Addr, authzCoins[0].Amount.BigInt(), + ) + + approvalCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approvalCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter.Addr, authzCoins, + ) + }, + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + Context("decreasing allowance", func() { + It("should return an error when calling the EVM extension", func() { + granter := is.keyring.GetKey(0) + grantee := granter + + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, decreaseArgs := is.getTxAndCallArgs( + directCall, contractsData, + auth.DecreaseAllowanceMethod, + grantee.Addr, authzCoins[0].Amount.BigInt(), + ) + + spenderIsOwnerCheck := failCheck.WithErrContains(erc20.ErrSpenderIsOwner.Error()) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, spenderIsOwnerCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + is.ExpectNoSendAuthzForContract( + directCall, contractsData, + grantee.Addr, granter.Addr, + ) + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + }) + + DescribeTable("it should decrease an existing allowance", func(callType CallType) { + granter := is.keyring.GetKey(0) + grantee := granter + + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 200)} + decreaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + is.setupSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter.Priv, authzCoins, + ) + + txArgs, decreaseArgs := is.getTxAndCallArgs( + callType, contractsData, + auth.DecreaseAllowanceMethod, + grantee.Addr, decreaseCoins[0].Amount.BigInt(), + ) + + approvalCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, approvalCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract( + callType, contractsData, + grantee.Addr, granter.Addr, decreaseCoins, + ) + }, + Entry(" - through erc20 contract", erc20Call), + Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + }) + + When("no allowance exists", func() { + DescribeTable("decreasing the allowance should return an error", func(callType CallType) { + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, authzCoins[0].Amount.BigInt()) + + notFoundCheck := execRevertedCheck + if callType == directCall { + notFoundCheck = failCheck.WithErrContains( + fmt.Sprintf(auth.ErrAuthzDoesNotExistOrExpired, erc20.SendMsgURL, grantee.Addr.String()), + ) + } + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, notFoundCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + + // NOTE: We have to split between direct and contract calls here because the ERC20 behavior + // for approvals is different, so we expect different authorizations here + Context("in direct calls", func() { + DescribeTable("increasing the allowance should create a new authorization", func(callType CallType) { + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, authzCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.ApproveMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + Context("in contract calls", func() { + DescribeTable("increasing the allowance should create a new authorization", func(callType CallType) { + contractAddr := contractsData.GetContractData(callType).Address + authzCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, authzCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, contractAddr, authzCoins) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + }) + }) + + When("an allowance exists for other tokens", func() { + var bondCoins sdk.Coins + + BeforeEach(func() { + bondCoins = sdk.Coins{sdk.NewInt64Coin(is.network.GetBaseDenom(), 200)} + is.setupSendAuthz(grantee.AccAddr, granter.Priv, bondCoins) + }) + + DescribeTable("increasing the allowance should add the token to the spend limit", func(callType CallType) { + increaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, increaseCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, bondCoins.Add(increaseCoins...)) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("decreasing the allowance should return an error", func(callType CallType) { + decreaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + + notFoundCheck := execRevertedCheck + if callType == directCall { + notFoundCheck = failCheck.WithErrContains( + fmt.Sprintf(erc20.ErrNoAllowanceForToken, is.tokenDenom), + ) + } + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, notFoundCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + }) + + When("an allowance exists for the same token", func() { + var authzCoins sdk.Coins + + BeforeEach(func() { + authzCoins = sdk.NewCoins( + sdk.NewInt64Coin(is.network.GetBaseDenom(), 100), + sdk.NewInt64Coin(is.tokenDenom, 200), + ) + + is.setupSendAuthz(grantee.AccAddr, granter.Priv, authzCoins) + }) + + DescribeTable("increasing the allowance should increase the spend limit", func(callType CallType) { + increaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, increaseCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins.Add(increaseCoins...)) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("decreasing the allowance should decrease the spend limit", func(callType CallType) { + decreaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins.Sub(decreaseCoins...)) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("increasing the allowance beyond the max uint256 value should return an error", func(callType CallType) { + maxUint256Coins := sdk.Coins{sdk.NewCoin(is.tokenDenom, math.NewIntFromBigInt(abi.MaxUint256))} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, maxUint256Coins[0].Amount.BigInt()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("decreasing the allowance to zero should remove the token from the spend limit", func(callType CallType) { + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, authzCoins.AmountOf(is.tokenDenom).BigInt()) + + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // Check that only the spend limit in the network denomination remains + bondDenom := is.network.GetBaseDenom() + expCoins := sdk.Coins{sdk.NewCoin(bondDenom, authzCoins.AmountOf(bondDenom))} + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, expCoins) + }, + Entry(" - direct call", directCall), + // NOTE: we are not passing the erc20 contract call here because the ERC20 contract + // only supports the actual token denomination and doesn't know of other allowances. + ) + + DescribeTable("decreasing the allowance below zero should return an error", func(callType CallType) { + decreaseCoins := sdk.Coins{sdk.NewCoin(is.tokenDenom, authzCoins.AmountOf(is.tokenDenom).AddRaw(100))} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + belowZeroCheck := failCheck.WithErrContains(erc20.ErrDecreasedAllowanceBelowZero.Error()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, belowZeroCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + // Check that the allowance was not changed + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins) + }, + Entry(" - direct call", directCall), + ) + }) + + When("an allowance exists for only the same token", func() { + // NOTE: we have to split between direct and contract calls here because the ERC20 contract + // handles the allowance differently by creating an approval between the contract and the grantee, instead + // of the message sender and the grantee, so we expect different authorizations. + Context("in direct calls", func() { + var authzCoins sdk.Coins + + BeforeEach(func() { + authzCoins = sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + // NOTE: We set up the standard authorization here for the authz keeper and then also + // set up the authorization for the ERC20 contract, so that we can test both. + is.setupSendAuthzForContract(directCall, contractsData, grantee.Addr, granter.Priv, authzCoins) + is.setupSendAuthzForContract(erc20Call, contractsData, grantee.Addr, granter.Priv, authzCoins) + }) + + DescribeTable("increasing the allowance should increase the spend limit", func(callType CallType) { + increaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, increaseCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins.Add(increaseCoins...)) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("decreasing the allowance should decrease the spend limit", func(callType CallType) { + decreaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 50)} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins.Sub(decreaseCoins...)) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("decreasing the allowance to zero should delete the authorization", func(callType CallType) { + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, authzCoins.AmountOf(is.tokenDenom).BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectNoSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("decreasing the allowance below zero should return an error", func(callType CallType) { + decreaseAmount := authzCoins.AmountOf(is.tokenDenom).AddRaw(100) + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseAmount.BigInt()) + + belowZeroCheck := failCheck.WithErrContains(erc20.ErrDecreasedAllowanceBelowZero.Error()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, decreaseArgs, belowZeroCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + // Check that the allowance was not changed + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + + DescribeTable("increasing the allowance beyond the max uint256 value should return an error", func(callType CallType) { + maxUint256Coins := sdk.Coins{sdk.NewCoin(is.tokenDenom, math.NewIntFromBigInt(abi.MaxUint256))} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, maxUint256Coins[0].Amount.BigInt()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, increaseArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + // Check that the allowance was not changed + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granter.Addr, authzCoins) + }, + Entry(" - direct call", directCall), + Entry(" - through erc20 contract", erc20Call), + // NOTE: The ERC20 V5 contract does not contain these methods + // Entry(" - through erc20 v5 contract", erc20V5Call), + ) + }) + + Context("in contract calls", func() { + var ( + authzCoins sdk.Coins + grantee keyring.Key + ) + + BeforeEach(func() { + authzCoins = sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + grantee = is.keyring.GetKey(1) + callerContractAddr := contractsData.GetContractData(contractCall).Address + erc20CallerContractAddr := contractsData.GetContractData(erc20CallerCall).Address + + // NOTE: Here we create an authorization between the contract and the grantee for both contracts. + // This is different from the direct calls, where the authorization is created between the + // message sender and the grantee. + txArgs, approveArgs := is.getTxAndCallArgs(contractCall, contractsData, auth.ApproveMethod, grantee.Addr, authzCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, _, err := is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectSendAuthzForContract(contractCall, contractsData, grantee.Addr, callerContractAddr, authzCoins) + + // Create the authorization for the ERC20 caller contract + txArgs, approveArgs = is.getTxAndCallArgs(erc20CallerCall, contractsData, auth.ApproveMethod, grantee.Addr, authzCoins[0].Amount.BigInt()) + _, _, err = is.factory.CallContractAndCheckLogs(granter.Priv, txArgs, approveArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectSendAuthzForContract(erc20CallerCall, contractsData, grantee.Addr, erc20CallerContractAddr, authzCoins) + }) + + DescribeTable("increasing the allowance should increase the spend limit", func(callType CallType) { //nolint:dupl + senderPriv := is.keyring.GetPrivKey(0) + granterAddr := contractsData.GetContractData(callType).Address + increaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 100)} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, increaseCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(senderPriv, txArgs, increaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.IncreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granterAddr, authzCoins.Add(increaseCoins...)) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + + DescribeTable("increasing the allowance beyond the max uint256 value should return an error", func(callType CallType) { + senderPriv := is.keyring.GetPrivKey(0) + granterAddr := contractsData.GetContractData(callType).Address + maxUint256Coins := sdk.Coins{sdk.NewCoin(is.tokenDenom, math.NewIntFromBigInt(abi.MaxUint256))} + + txArgs, increaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.IncreaseAllowanceMethod, grantee.Addr, maxUint256Coins[0].Amount.BigInt()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(senderPriv, txArgs, increaseArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + // Check that the allowance was not changed + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granterAddr, authzCoins) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + + DescribeTable("decreasing the allowance should decrease the spend limit", func(callType CallType) { //nolint:dupl + senderPriv := is.keyring.GetPrivKey(0) + granterAddr := contractsData.GetContractData(callType).Address + decreaseCoins := sdk.Coins{sdk.NewInt64Coin(is.tokenDenom, 50)} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(senderPriv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granterAddr, authzCoins.Sub(decreaseCoins...)) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + + DescribeTable("decreasing the allowance to zero should delete the authorization", func(callType CallType) { + senderPriv := is.keyring.GetPrivKey(0) + granterAddr := contractsData.GetContractData(callType).Address + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, authzCoins.AmountOf(is.tokenDenom).BigInt()) + approveCheck := passCheck.WithExpEvents(auth.EventTypeApproval) + _, ethRes, err := is.factory.CallContractAndCheckLogs(senderPriv, txArgs, decreaseArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + + // commit the changes to state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + is.ExpectTrueToBeReturned(ethRes, auth.DecreaseAllowanceMethod) + is.ExpectNoSendAuthzForContract(callType, contractsData, grantee.Addr, granterAddr) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + + DescribeTable("decreasing the allowance below zero should return an error", func(callType CallType) { + senderPriv := is.keyring.GetPrivKey(0) + granterAddr := contractsData.GetContractData(callType).Address + decreaseCoins := sdk.Coins{sdk.NewCoin(is.tokenDenom, authzCoins.AmountOf(is.tokenDenom).AddRaw(100))} + + txArgs, decreaseArgs := is.getTxAndCallArgs(callType, contractsData, auth.DecreaseAllowanceMethod, grantee.Addr, decreaseCoins[0].Amount.BigInt()) + _, ethRes, err := is.factory.CallContractAndCheckLogs(senderPriv, txArgs, decreaseArgs, execRevertedCheck) + Expect(err).ToNot(HaveOccurred(), "unexpected result calling contract") + Expect(ethRes).To(BeNil(), "expected empty result") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") + + // Check that the allowance was not changed + is.ExpectSendAuthzForContract(callType, contractsData, grantee.Addr, granterAddr, authzCoins) + }, + Entry(" - contract call", contractCall), + Entry(" - through erc20 caller contract", erc20CallerCall), + ) + }) + }) + }) +}) + +var _ = Describe("ERC20 Extension migration Flows -", func() { + When("migrating an existing ERC20 token", func() { + var ( + contractData ContractsData + erc20MinterV5Contract evmtypes.CompiledContract + + tokenDenom = "xmpl" + tokenName = "Xmpl" + tokenSymbol = strings.ToUpper(tokenDenom) + + supply = sdk.NewInt64Coin(tokenDenom, 1000000000000000000) + ) + + BeforeEach(func() { + is.SetupTest() + + var err error + erc20MinterV5Contract, err = testdata.LoadERC20MinterV5Contract() + Expect(err).ToNot(HaveOccurred(), "failed to load ERC20 minter contract") + + contractOwner := is.keyring.GetKey(0) + + // Deploy an ERC20 contract + erc20Addr, err := is.factory.DeployContract( + contractOwner.Priv, + evmtypes.EvmTxArgs{}, // NOTE: passing empty struct to use default values + factory.ContractDeploymentData{ + Contract: erc20MinterV5Contract, + ConstructorArgs: []interface{}{ + tokenName, tokenSymbol, + }, + }, + ) + Expect(err).ToNot(HaveOccurred(), "failed to deploy contract") + + // NOTE: We need to overwrite the information in the contractData here for this specific + // deployed contract. + contractData = ContractsData{ + ownerPriv: contractOwner.Priv, + contractData: map[CallType]ContractData{ + erc20V5Call: { + Address: erc20Addr, + ABI: erc20MinterV5Contract.ABI, + }, + }, + } + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to commit block") + + // Register the deployed erc20 contract as a token pair + _, err = integrationutils.RegisterERC20(is.factory, is.network, integrationutils.ERC20RegistrationData{ + Addresses: []string{erc20Addr.Hex()}, + ProposerPriv: contractOwner.Priv, + }) + Expect(err).ToNot(HaveOccurred(), "failed to register ERC20 token") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "failed to commit block") + + // Mint the supply of tokens + err = is.MintERC20(erc20V5Call, contractData, contractOwner.Addr, supply.Amount.BigInt()) + Expect(err).ToNot(HaveOccurred(), "failed to mint tokens") + + // Check that the supply was minted + is.ExpectBalancesForERC20(erc20V5Call, contractData, []ExpectedBalance{{ + address: contractOwner.AccAddr, + expCoins: sdk.Coins{supply}, + }}) + }) + + It("should migrate the full token balance to the bank module", func() { + // TODO: implement test on follow-up PR + Skip("will be addressed on follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + }) + + When("migrating an extended ERC20 token (e.g. ERC20Votes)", func() { + It("should migrate the full token balance to the bank module", func() { + // TODO: make sure that extended tokens are compatible with the ERC20 extensions + Skip("not included in first tranche") + + Expect(true).To(BeFalse(), "not implemented") + }) + }) + + When("running the migration logic for a set of existing ERC20 tokens", func() { + BeforeEach(func() { + // TODO: Add some ERC20 tokens and then run migration logic + // TODO: check here that the balance cannot be queried from the bank keeper before migrating the token + }) + + It("should add and enable the corresponding EVM extensions", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + + It("should be possible to query the balances through the bank module", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + + It("should return all tokens when querying all balances for an account", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + }) + + When("registering a native IBC coin", func() { + BeforeEach(func() { + // TODO: Add some IBC coins, register the token pair and then run migration logic + }) + + It("should add the corresponding EVM extensions", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + + It("should be possible to query the balances using an EVM transaction", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + }) + + When("using Evmos (not wEvmos) in smart contracts", func() { + It("should be using straight Evmos for sending funds in smart contracts", func() { + Skip("will be addressed in follow-up PR") + + Expect(true).To(BeFalse(), "not implemented") + }) + }) +}) diff --git a/precompiles/erc20/setup_test.go b/precompiles/erc20/setup_test.go new file mode 100644 index 00000000..c6db9cc4 --- /dev/null +++ b/precompiles/erc20/setup_test.go @@ -0,0 +1,63 @@ +package erc20 + +import ( + "testing" + + erc20precompile "github.com/evmos/os/precompiles/erc20" + "github.com/evmos/os/testutil/integration/os/factory" + "github.com/evmos/os/testutil/integration/os/grpc" + testkeyring "github.com/evmos/os/testutil/integration/os/keyring" + "github.com/evmos/os/testutil/integration/os/network" + "github.com/stretchr/testify/suite" +) + +var s *PrecompileTestSuite + +// PrecompileTestSuite is the implementation of the TestSuite interface for ERC20 precompile +// unit tests. +type PrecompileTestSuite struct { + suite.Suite + + bondDenom string + // tokenDenom is the specific token denomination used in testing the ERC20 precompile. + // This denomination is used to instantiate the precompile. + tokenDenom string + network *network.UnitTestNetwork + factory factory.TxFactory + grpcHandler grpc.Handler + keyring testkeyring.Keyring + + precompile *erc20precompile.Precompile +} + +func TestPrecompileTestSuite(t *testing.T) { + s = new(PrecompileTestSuite) + suite.Run(t, s) +} + +func (s *PrecompileTestSuite) SetupTest() { + keyring := testkeyring.New(2) + integrationNetwork := network.NewUnitTestNetwork( + network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...), + ) + grpcHandler := grpc.NewIntegrationHandler(integrationNetwork) + txFactory := factory.New(integrationNetwork, grpcHandler) + + ctx := integrationNetwork.GetContext() + sk := integrationNetwork.App.StakingKeeper + bondDenom, err := sk.BondDenom(ctx) + s.Require().NoError(err) + s.Require().NotEmpty(bondDenom, "bond denom cannot be empty") + + s.bondDenom = bondDenom + s.factory = txFactory + s.grpcHandler = grpcHandler + s.keyring = keyring + s.network = integrationNetwork + + // Instantiate the precompile with an exemplary token denomination. + // + // NOTE: This has to be done AFTER assigning the suite fields. + s.tokenDenom = "xmpl" + s.precompile = s.setupERC20Precompile(s.tokenDenom) +} diff --git a/precompiles/erc20/tx_test.go b/precompiles/erc20/tx_test.go new file mode 100644 index 00000000..3ac8ec89 --- /dev/null +++ b/precompiles/erc20/tx_test.go @@ -0,0 +1,262 @@ +package erc20 + +import ( + "math/big" + "time" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/evmos/os/precompiles/testutil" + utiltx "github.com/evmos/os/testutil/tx" + erc20types "github.com/evmos/os/x/erc20/types" + "github.com/evmos/os/x/evm/core/vm" + "github.com/evmos/os/x/evm/statedb" +) + +var ( + tokenDenom = "xmpl" + // XMPLCoin is a dummy coin used for testing purposes. + XMPLCoin = sdk.NewCoins(sdk.NewInt64Coin(tokenDenom, 1e18)) + // toAddr is a dummy address used for testing purposes. + toAddr = utiltx.GenerateAddress() +) + +func (s *PrecompileTestSuite) TestTransfer() { + method := s.precompile.Methods[TransferMethod] + // fromAddr is the address of the keyring account used for testing. + fromAddr := s.keyring.GetKey(0).Addr + testcases := []struct { + name string + malleate func() []interface{} + postCheck func() + expErr bool + errContains string + }{ + { + "fail - negative amount", + func() []interface{} { + return []interface{}{toAddr, big.NewInt(-1)} + }, + func() {}, + true, + "coin -1xmpl amount is not positive", + }, + { + "fail - invalid to address", + func() []interface{} { + return []interface{}{"", big.NewInt(100)} + }, + func() {}, + true, + "invalid to address", + }, + { + "fail - invalid amount", + func() []interface{} { + return []interface{}{toAddr, ""} + }, + func() {}, + true, + "invalid amount", + }, + { + "fail - not enough balance", + func() []interface{} { + return []interface{}{toAddr, big.NewInt(2e18)} + }, + func() {}, + true, + ErrTransferAmountExceedsBalance.Error(), + }, + { + "pass", + func() []interface{} { + return []interface{}{toAddr, big.NewInt(100)} + }, + func() { + toAddrBalance := s.network.App.BankKeeper.GetBalance(s.network.GetContext(), toAddr.Bytes(), tokenDenom) + s.Require().Equal(big.NewInt(100), toAddrBalance.Amount.BigInt(), "expected toAddr to have 100 XMPL") + }, + false, + "", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + stateDB := s.network.GetStateDB() + + var contract *vm.Contract + contract, ctx := testutil.NewPrecompileContract(s.T(), s.network.GetContext(), fromAddr, s.precompile, 0) + + // Mint some coins to the module account and then send to the from address + err := s.network.App.BankKeeper.MintCoins(s.network.GetContext(), erc20types.ModuleName, XMPLCoin) + s.Require().NoError(err, "failed to mint coins") + err = s.network.App.BankKeeper.SendCoinsFromModuleToAccount(s.network.GetContext(), erc20types.ModuleName, fromAddr.Bytes(), XMPLCoin) + s.Require().NoError(err, "failed to send coins from module to account") + + _, err = s.precompile.Transfer(ctx, contract, stateDB, &method, tc.malleate()) + if tc.expErr { + s.Require().Error(err, "expected transfer transaction to fail") + s.Require().Contains(err.Error(), tc.errContains, "expected transfer transaction to fail with specific error") + } else { + s.Require().NoError(err, "expected transfer transaction succeeded") + tc.postCheck() + } + }) + } +} + +func (s *PrecompileTestSuite) TestTransferFrom() { + var ( + ctx sdk.Context + stDB *statedb.StateDB + ) + method := s.precompile.Methods[TransferFromMethod] + // owner of the tokens + owner := s.keyring.GetKey(0) + // spender of the tokens + spender := s.keyring.GetKey(1) + + testcases := []struct { + name string + malleate func() []interface{} + postCheck func() + expErr bool + errContains string + }{ + { + "fail - negative amount", + func() []interface{} { + return []interface{}{owner.Addr, toAddr, big.NewInt(-1)} + }, + func() {}, + true, + "coin -1xmpl amount is not positive", + }, + { + "fail - invalid from address", + func() []interface{} { + return []interface{}{"", toAddr, big.NewInt(100)} + }, + func() {}, + true, + "invalid from address", + }, + { + "fail - invalid to address", + func() []interface{} { + return []interface{}{owner.Addr, "", big.NewInt(100)} + }, + func() {}, + true, + "invalid to address", + }, + { + "fail - invalid amount", + func() []interface{} { + return []interface{}{owner.Addr, toAddr, ""} + }, + func() {}, + true, + "invalid amount", + }, + { + "fail - not enough allowance", + func() []interface{} { + return []interface{}{owner.Addr, toAddr, big.NewInt(100)} + }, + func() {}, + true, + ErrInsufficientAllowance.Error(), + }, + { + "fail - not enough balance", + func() []interface{} { + expiration := time.Now().Add(time.Hour) + err := s.network.App.AuthzKeeper.SaveGrant( + ctx, + spender.AccAddr, + owner.AccAddr, + &banktypes.SendAuthorization{SpendLimit: sdk.Coins{sdk.Coin{Denom: s.tokenDenom, Amount: math.NewInt(5e18)}}}, + &expiration, + ) + s.Require().NoError(err, "failed to save grant") + + return []interface{}{owner.Addr, toAddr, big.NewInt(2e18)} + }, + func() {}, + true, + ErrTransferAmountExceedsBalance.Error(), + }, + { + "pass - spend on behalf of other account", + func() []interface{} { + expiration := time.Now().Add(time.Hour) + err := s.network.App.AuthzKeeper.SaveGrant( + ctx, + spender.AccAddr, + owner.AccAddr, + &banktypes.SendAuthorization{SpendLimit: sdk.Coins{sdk.Coin{Denom: tokenDenom, Amount: math.NewInt(300)}}}, + &expiration, + ) + s.Require().NoError(err, "failed to save grant") + + return []interface{}{owner.Addr, toAddr, big.NewInt(100)} + }, + func() { + toAddrBalance := s.network.App.BankKeeper.GetBalance(ctx, toAddr.Bytes(), tokenDenom) + s.Require().Equal(big.NewInt(100), toAddrBalance.Amount.BigInt(), "expected toAddr to have 100 XMPL") + }, + false, + "", + }, + { + "pass - spend on behalf of own account", + func() []interface{} { + // Mint some coins to the module account and then send to the spender address + err := s.network.App.BankKeeper.MintCoins(ctx, erc20types.ModuleName, XMPLCoin) + s.Require().NoError(err, "failed to mint coins") + err = s.network.App.BankKeeper.SendCoinsFromModuleToAccount(ctx, erc20types.ModuleName, spender.AccAddr, XMPLCoin) + s.Require().NoError(err, "failed to send coins from module to account") + + // NOTE: no authorization is necessary to spend on behalf of the same account + return []interface{}{spender.Addr, toAddr, big.NewInt(100)} + }, + func() { + toAddrBalance := s.network.App.BankKeeper.GetBalance(ctx, toAddr.Bytes(), tokenDenom) + s.Require().Equal(big.NewInt(100), toAddrBalance.Amount.BigInt(), "expected toAddr to have 100 XMPL") + }, + false, + "", + }, + } + + for _, tc := range testcases { + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.network.GetContext() + stDB = s.network.GetStateDB() + + var contract *vm.Contract + contract, ctx = testutil.NewPrecompileContract(s.T(), ctx, spender.Addr, s.precompile, 0) + + // Mint some coins to the module account and then send to the from address + err := s.network.App.BankKeeper.MintCoins(ctx, erc20types.ModuleName, XMPLCoin) + s.Require().NoError(err, "failed to mint coins") + err = s.network.App.BankKeeper.SendCoinsFromModuleToAccount(ctx, erc20types.ModuleName, owner.AccAddr, XMPLCoin) + s.Require().NoError(err, "failed to send coins from module to account") + + _, err = s.precompile.TransferFrom(ctx, contract, stDB, &method, tc.malleate()) + if tc.expErr { + s.Require().Error(err, "expected transfer transaction to fail") + s.Require().Contains(err.Error(), tc.errContains, "expected transfer transaction to fail with specific error") + } else { + s.Require().NoError(err, "expected transfer transaction succeeded") + tc.postCheck() + } + }) + } +} diff --git a/precompiles/erc20/utils_test.go b/precompiles/erc20/utils_test.go new file mode 100644 index 00000000..3d7da72f --- /dev/null +++ b/precompiles/erc20/utils_test.go @@ -0,0 +1,598 @@ +package erc20 + +import ( + "fmt" + "math/big" + "slices" + "time" + + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + auth "github.com/evmos/os/precompiles/authorization" + "github.com/evmos/os/precompiles/erc20" + "github.com/evmos/os/precompiles/testutil" + commonfactory "github.com/evmos/os/testutil/integration/common/factory" + commonnetwork "github.com/evmos/os/testutil/integration/common/network" + "github.com/evmos/os/testutil/integration/os/factory" + network "github.com/evmos/os/testutil/integration/os/network" + testutils "github.com/evmos/os/testutil/integration/os/utils" + utiltx "github.com/evmos/os/testutil/tx" + erc20types "github.com/evmos/os/x/erc20/types" + evmtypes "github.com/evmos/os/x/evm/types" + + //nolint:revive // dot imports are fine for Gomega + . "github.com/onsi/gomega" +) + +// setupSendAuthz is a helper function to set up a SendAuthorization for +// a given grantee and granter combination for a given amount. +// +// NOTE: A default expiration of 1 hour after the current block time is used. +func (s *PrecompileTestSuite) setupSendAuthz( + grantee sdk.AccAddress, granterPriv cryptotypes.PrivKey, amount sdk.Coins, +) { + err := setupSendAuthz( + s.network, + s.factory, + grantee, + granterPriv, + amount, + ) + s.Require().NoError(err, "failed to set up send authorization") +} + +func (is *IntegrationTestSuite) setupSendAuthz( + grantee sdk.AccAddress, granterPriv cryptotypes.PrivKey, amount sdk.Coins, +) { + err := setupSendAuthz( + is.network, + is.factory, + grantee, + granterPriv, + amount, + ) + Expect(err).ToNot(HaveOccurred(), "failed to set up send authorization") + + // commit changes to chain state + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") +} + +func setupSendAuthz( + network commonnetwork.Network, + factory commonfactory.BaseTxFactory, + grantee sdk.AccAddress, + granterPriv cryptotypes.PrivKey, + amount sdk.Coins, +) error { + granter := sdk.AccAddress(granterPriv.PubKey().Address()) + expiration := network.GetContext().BlockHeader().Time.Add(time.Hour) + sendAuthz := banktypes.NewSendAuthorization( + amount, + []sdk.AccAddress{}, + ) + + msgGrant, err := authz.NewMsgGrant( + granter, + grantee, + sendAuthz, + &expiration, + ) + if err != nil { + return errorsmod.Wrap(err, "failed to create MsgGrant") + } + + // Create an authorization + txArgs := commonfactory.CosmosTxArgs{Msgs: []sdk.Msg{msgGrant}} + _, err = factory.ExecuteCosmosTx(granterPriv, txArgs) + if err != nil { + return errorsmod.Wrap(err, "failed to execute MsgGrant") + } + + return nil +} + +// setupSendAuthzForContract is a helper function which executes an approval +// for the given contract data. +// +// If: +// - the classic ERC20 contract is used, it calls the `approve` method on the contract. +// - in other cases, it sends a `MsgGrant` to set up the authorization. +func (is *IntegrationTestSuite) setupSendAuthzForContract( + callType CallType, contractData ContractsData, grantee common.Address, granterPriv cryptotypes.PrivKey, amount sdk.Coins, +) { + Expect(amount).To(HaveLen(1), "expected only one coin") + Expect(amount[0].Denom).To(Equal(is.tokenDenom), + "this test utility only works with the token denom in the context of these integration tests", + ) + + switch { + case slices.Contains(nativeCallTypes, callType): + is.setupSendAuthz(grantee.Bytes(), granterPriv, amount) + case slices.Contains(erc20CallTypes, callType): + is.setupSendAuthzForERC20(callType, contractData, grantee, granterPriv, amount) + default: + panic("unknown contract call type") + } + + // commit changes to the chain state + err := is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error while calling NextBlock") +} + +// setupSendAuthzForERC20 is a helper function to set up a SendAuthorization for +// a given grantee and granter combination for a given amount. +func (is *IntegrationTestSuite) setupSendAuthzForERC20( + callType CallType, contractData ContractsData, grantee common.Address, granterPriv cryptotypes.PrivKey, amount sdk.Coins, +) { + if callType == erc20V5CallerCall { + // NOTE: When using the ERC20 caller contract, we must still approve from the actual ERC20 v5 contract. + callType = erc20V5Call + } + + abiEvents := contractData.GetContractData(callType).ABI.Events + + txArgs, callArgs := is.getTxAndCallArgs(callType, contractData, auth.ApproveMethod, grantee, amount.AmountOf(is.tokenDenom).BigInt()) + + approveCheck := testutil.LogCheckArgs{ + ABIEvents: abiEvents, + ExpEvents: []string{auth.EventTypeApproval}, + ExpPass: true, + } + + _, _, err := is.factory.CallContractAndCheckLogs(granterPriv, txArgs, callArgs, approveCheck) + Expect(err).ToNot(HaveOccurred(), "failed to execute approve") +} + +// requireOut is a helper utility to reduce the amount of boilerplate code in the query tests. +// +// It requires the output bytes and error to match the expected values. Additionally, the method outputs +// are unpacked and the first value is compared to the expected value. +// +// NOTE: It's sufficient to only check the first value because all methods in the ERC20 precompile only +// return a single value. +func (s *PrecompileTestSuite) requireOut( + bz []byte, + err error, + method abi.Method, + expPass bool, + errContains string, + expValue interface{}, +) { + if expPass { + s.Require().NoError(err, "expected no error") + s.Require().NotEmpty(bz, "expected bytes not to be empty") + + // Unpack the name into a string + out, err := method.Outputs.Unpack(bz) + s.Require().NoError(err, "expected no error unpacking") + + // Check if expValue is a big.Int. Because of a difference in uninitialized/empty values for big.Ints, + // this comparison is often not working as expected, so we convert to Int64 here and compare those values. + bigExp, ok := expValue.(*big.Int) + if ok { + bigOut, ok := out[0].(*big.Int) + s.Require().True(ok, "expected output to be a big.Int") + s.Require().Equal(bigExp.Int64(), bigOut.Int64(), "expected different value") + } else { + s.Require().Equal(expValue, out[0], "expected different value") + } + } else { + s.Require().Error(err, "expected error") + s.Require().Contains(err.Error(), errContains, "expected different error") + } +} + +// requireSendAuthz is a helper function to check that a SendAuthorization +// exists for a given grantee and granter combination for a given amount. +// +// NOTE: This helper expects only one authorization to exist. +func (s *PrecompileTestSuite) requireSendAuthz(grantee, granter sdk.AccAddress, amount sdk.Coins, allowList []string) { + grants, err := s.grpcHandler.GetGrantsByGrantee(grantee.String()) + s.Require().NoError(err, "expected no error querying the grants") + s.Require().Len(grants, 1, "expected one grant") + s.Require().Equal(grantee.String(), grants[0].Grantee, "expected different grantee") + s.Require().Equal(granter.String(), grants[0].Granter, "expected different granter") + + authzs, err := s.grpcHandler.GetAuthorizationsByGrantee(grantee.String()) + s.Require().NoError(err, "expected no error unpacking the authorization") + s.Require().Len(authzs, 1, "expected one authorization") + + sendAuthz, ok := authzs[0].(*banktypes.SendAuthorization) + s.Require().True(ok, "expected send authorization") + + s.Require().Equal(amount, sendAuthz.SpendLimit, "expected different spend limit amount") + if len(allowList) == 0 { + s.Require().Empty(sendAuthz.AllowList, "expected empty allow list") + } else { + s.Require().Equal(allowList, sendAuthz.AllowList, "expected different allow list") + } +} + +// setupERC20Precompile is a helper function to set up an instance of the ERC20 precompile for +// a given token denomination, set the token pair in the ERC20 keeper and adds the precompile +// to the available and active precompiles. +func (s *PrecompileTestSuite) setupERC20Precompile(denom string) *erc20.Precompile { + tokenPair := erc20types.NewTokenPair(utiltx.GenerateAddress(), denom, erc20types.OWNER_MODULE) + s.network.App.Erc20Keeper.SetTokenPair(s.network.GetContext(), tokenPair) + + precompile, err := setupERC20PrecompileForTokenPair(*s.network, tokenPair) + s.Require().NoError(err, "failed to set up %q erc20 precompile", tokenPair.Denom) + + return precompile +} + +// setupERC20Precompile is a helper function to set up an instance of the ERC20 precompile for +// a given token denomination, set the token pair in the ERC20 keeper and adds the precompile +// to the available and active precompiles. +func (is *IntegrationTestSuite) setupERC20Precompile(denom string, tokenPairs []erc20types.TokenPair) *erc20.Precompile { + var tokenPair erc20types.TokenPair + for _, tp := range tokenPairs { + if tp.Denom != denom { + continue + } + tokenPair = tp + } + + precompile, err := erc20.NewPrecompile( + tokenPair, + is.network.App.BankKeeper, + is.network.App.AuthzKeeper, + is.network.App.TransferKeeper, + ) + Expect(err).ToNot(HaveOccurred(), "failed to set up %q erc20 precompile", tokenPair.Denom) + + return precompile +} + +// setupERC20PrecompileForTokenPair is a helper function to set up an instance of the ERC20 precompile for +// a given token pair and adds the precompile to the available and active precompiles. +// Do not use this function for integration tests. +func setupERC20PrecompileForTokenPair( + unitNetwork network.UnitTestNetwork, tokenPair erc20types.TokenPair, +) (*erc20.Precompile, error) { + precompile, err := erc20.NewPrecompile( + tokenPair, + unitNetwork.App.BankKeeper, + unitNetwork.App.AuthzKeeper, + unitNetwork.App.TransferKeeper, + ) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to create %q erc20 precompile", tokenPair.Denom) + } + + err = unitNetwork.App.Erc20Keeper.EnableDynamicPrecompiles( + unitNetwork.GetContext(), + precompile.Address(), + ) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to add %q erc20 precompile to EVM extensions", tokenPair.Denom) + } + + return precompile, nil +} + +// setupNewERC20PrecompileForTokenPair is a helper function to set up an instance of the ERC20 precompile for +// a given token pair and adds the precompile to the available and active precompiles. +// This function should be used for integration tests +func setupNewERC20PrecompileForTokenPair( + privKey cryptotypes.PrivKey, + unitNetwork *network.UnitTestNetwork, + tf factory.TxFactory, tokenPair erc20types.TokenPair, +) (*erc20.Precompile, error) { + precompile, err := erc20.NewPrecompile( + tokenPair, + unitNetwork.App.BankKeeper, + unitNetwork.App.AuthzKeeper, + unitNetwork.App.TransferKeeper, + ) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to create %q erc20 precompile", tokenPair.Denom) + } + + // Update the params via gov proposal + params := unitNetwork.App.Erc20Keeper.GetParams(unitNetwork.GetContext()) + params.DynamicPrecompiles = append(params.DynamicPrecompiles, precompile.Address().Hex()) + slices.Sort(params.DynamicPrecompiles) + + if err := params.Validate(); err != nil { + return nil, err + } + + if err := testutils.UpdateERC20Params(testutils.UpdateParamsInput{ + Pk: privKey, + Tf: tf, + Network: unitNetwork, + Params: params, + }); err != nil { + return nil, errorsmod.Wrapf(err, "failed to add %q erc20 precompile to EVM extensions", tokenPair.Denom) + } + + return precompile, nil +} + +// CallType indicates which type of contract call is made during the integration tests. +type CallType int + +// callType constants to differentiate between direct calls and calls through a contract. +const ( + directCall CallType = iota + 1 + directCallToken2 + contractCall + contractCallToken2 + erc20Call + erc20CallerCall + erc20V5Call + erc20V5CallerCall +) + +var ( + nativeCallTypes = []CallType{directCall, directCallToken2, contractCall, contractCallToken2} + erc20CallTypes = []CallType{erc20Call, erc20CallerCall, erc20V5Call, erc20V5CallerCall} +) + +// getTxAndCallArgs is a helper function to return the correct call arguments for a given call type. +// +// In case of a direct call to the precompile, the precompile's ABI is used. Otherwise, the +// ERC20CallerContract's ABI is used and the given contract address. +func (is *IntegrationTestSuite) getTxAndCallArgs( + callType CallType, + contractData ContractsData, + methodName string, + args ...interface{}, +) (evmtypes.EvmTxArgs, factory.CallArgs) { + cd := contractData.GetContractData(callType) + + txArgs := evmtypes.EvmTxArgs{ + To: &cd.Address, + GasPrice: gasPrice, + } + + callArgs := factory.CallArgs{ + ContractABI: cd.ABI, + MethodName: methodName, + Args: args, + } + + return txArgs, callArgs +} + +// ExpectedBalance is a helper struct to check the balances of accounts. +type ExpectedBalance struct { + address sdk.AccAddress + expCoins sdk.Coins +} + +// ExpectBalances is a helper function to check if the balances of the given accounts are as expected. +func (is *IntegrationTestSuite) ExpectBalances(expBalances []ExpectedBalance) { + for _, expBalance := range expBalances { + for _, expCoin := range expBalance.expCoins { + coinBalance, err := is.handler.GetBalanceFromBank(expBalance.address, expCoin.Denom) + Expect(err).ToNot(HaveOccurred(), "expected no error getting balance") + Expect(coinBalance.Balance.Amount).To(Equal(expCoin.Amount), "expected different balance") + } + } +} + +// ExpectBalancesForContract is a helper function to check expected balances for given accounts depending +// on the call type. +func (is *IntegrationTestSuite) ExpectBalancesForContract(callType CallType, contractData ContractsData, expBalances []ExpectedBalance) { + switch { + case slices.Contains(nativeCallTypes, callType): + is.ExpectBalances(expBalances) + case slices.Contains(erc20CallTypes, callType): + is.ExpectBalancesForERC20(callType, contractData, expBalances) + default: + panic("unknown contract call type") + } +} + +// ExpectBalancesForERC20 is a helper function to check expected balances for given accounts +// when using the ERC20 contract. +func (is *IntegrationTestSuite) ExpectBalancesForERC20(callType CallType, contractData ContractsData, expBalances []ExpectedBalance) { + contractABI := contractData.GetContractData(callType).ABI + + for _, expBalance := range expBalances { + addr := common.BytesToAddress(expBalance.address.Bytes()) + txArgs, callArgs := is.getTxAndCallArgs(callType, contractData, "balanceOf", addr) + + passCheck := testutil.LogCheckArgs{ExpPass: true} + + _, ethRes, err := is.factory.CallContractAndCheckLogs(contractData.ownerPriv, txArgs, callArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "expected no error getting balance") + + err = is.network.NextBlock() + Expect(err).ToNot(HaveOccurred(), "error on NextBlock call") + + var balance *big.Int + err = contractABI.UnpackIntoInterface(&balance, "balanceOf", ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "expected no error unpacking balance") + Expect(math.NewIntFromBigInt(balance)).To(Equal(expBalance.expCoins.AmountOf(is.tokenDenom)), "expected different balance") + } +} + +// expectSendAuthz is a helper function to check that a SendAuthorization +// exists for a given grantee and granter combination for a given amount and optionally an access list. +// +// NOTE: This helper expects only one authorization to exist. +// +// NOTE 2: This mirrors the requireSendAuthz method but adapted to Ginkgo. +func (is *IntegrationTestSuite) expectSendAuthz(grantee, granter sdk.AccAddress, expAmount sdk.Coins) { + authzs, err := is.handler.GetAuthorizations(grantee.String(), granter.String()) + Expect(err).ToNot(HaveOccurred(), "expected no error unpacking the authorization") + Expect(authzs).To(HaveLen(1), "expected one authorization") + + sendAuthz, ok := authzs[0].(*banktypes.SendAuthorization) + Expect(ok).To(BeTrue(), "expected send authorization") + + Expect(sendAuthz.SpendLimit).To(Equal(expAmount), "expected different spend limit amount") +} + +// expectSendAuthzForERC20 is a helper function to check that a SendAuthorization +// exists for a given grantee and granter combination for a given amount. +func (is *IntegrationTestSuite) expectSendAuthzForERC20(callType CallType, contractData ContractsData, grantee, granter common.Address, expAmount sdk.Coins) { + contractABI := contractData.GetContractData(callType).ABI + + txArgs, callArgs := is.getTxAndCallArgs(callType, contractData, auth.AllowanceMethod, granter, grantee) + + passCheck := testutil.LogCheckArgs{ExpPass: true} + + _, ethRes, err := is.factory.CallContractAndCheckLogs(contractData.ownerPriv, txArgs, callArgs, passCheck) + Expect(err).ToNot(HaveOccurred(), "expected no error getting allowance") + // Increase block to update nonce + Expect(is.network.NextBlock()).To(BeNil()) + + var allowance *big.Int + err = contractABI.UnpackIntoInterface(&allowance, "allowance", ethRes.Ret) + Expect(err).ToNot(HaveOccurred(), "expected no error unpacking allowance") + Expect(math.NewIntFromBigInt(allowance)).To(Equal(expAmount.AmountOf(is.tokenDenom)), "expected different allowance") +} + +// ExpectSendAuthzForContract is a helper function to check that a SendAuthorization +// exists for a given grantee and granter combination for a given amount and optionally an access list. +// +// NOTE: This helper expects only one authorization to exist. +func (is *IntegrationTestSuite) ExpectSendAuthzForContract( + callType CallType, contractData ContractsData, grantee, granter common.Address, expAmount sdk.Coins, +) { + switch { + case slices.Contains(nativeCallTypes, callType): + is.expectSendAuthz(grantee.Bytes(), granter.Bytes(), expAmount) + case slices.Contains(erc20CallTypes, callType): + is.expectSendAuthzForERC20(callType, contractData, grantee, granter, expAmount) + default: + panic("unknown contract call type") + } +} + +// expectNoSendAuthz is a helper function to check that no SendAuthorization +// exists for a given grantee and granter combination. +func (is *IntegrationTestSuite) expectNoSendAuthz(grantee, granter sdk.AccAddress) { + authzs, err := is.handler.GetAuthorizations(grantee.String(), granter.String()) + Expect(err).ToNot(HaveOccurred(), "expected no error unpacking the authorizations") + Expect(authzs).To(HaveLen(0), "expected no authorizations") +} + +// expectNoSendAuthzForERC20 is a helper function to check that no SendAuthorization +// exists for a given grantee and granter combination. +func (is *IntegrationTestSuite) expectNoSendAuthzForERC20(callType CallType, contractData ContractsData, grantee, granter common.Address) { + is.expectSendAuthzForERC20(callType, contractData, grantee, granter, sdk.Coins{}) +} + +// ExpectNoSendAuthzForContract is a helper function to check that no SendAuthorization +// exists for a given grantee and granter combination. +func (is *IntegrationTestSuite) ExpectNoSendAuthzForContract( + callType CallType, contractData ContractsData, grantee, granter common.Address, +) { + switch { + case slices.Contains(nativeCallTypes, callType): + is.expectNoSendAuthz(grantee.Bytes(), granter.Bytes()) + case slices.Contains(erc20CallTypes, callType): + is.expectNoSendAuthzForERC20(callType, contractData, grantee, granter) + default: + panic("unknown contract call type") + } +} + +// ExpectTrueToBeReturned is a helper function to check that the precompile returns true +// in the ethereum transaction response. +func (is *IntegrationTestSuite) ExpectTrueToBeReturned(res *evmtypes.MsgEthereumTxResponse, methodName string) { + var ret bool + err := is.precompile.UnpackIntoInterface(&ret, methodName, res.Ret) + Expect(err).ToNot(HaveOccurred(), "expected no error unpacking") + Expect(ret).To(BeTrue(), "expected true to be returned") +} + +// ContractsData is a helper struct to hold the addresses and ABIs for the +// different contract instances that are subject to testing here. +type ContractsData struct { + contractData map[CallType]ContractData + ownerPriv cryptotypes.PrivKey +} + +// ContractData is a helper struct to hold the address and ABI for a given contract. +type ContractData struct { + Address common.Address + ABI abi.ABI +} + +// GetContractData is a helper function to return the contract data for a given call type. +func (cd ContractsData) GetContractData(callType CallType) ContractData { + data, found := cd.contractData[callType] + if !found { + panic(fmt.Sprintf("no contract data found for call type: %d", callType)) + } + return data +} + +// fundWithTokens is a helper function for the scope of the ERC20 integration tests. +// Depending on the passed call type, it funds the given address with tokens either +// using the Bank module or by minting straight on the ERC20 contract. +// Returns the updated balance amount of the receiver address +func (is *IntegrationTestSuite) fundWithTokens( + callType CallType, + contractData ContractsData, + receiver common.Address, + fundCoins sdk.Coins, +) math.Int { + Expect(fundCoins).To(HaveLen(1), "expected only one coin") + Expect(fundCoins[0].Denom).To(Equal(is.tokenDenom), + "this helper function only supports funding with the token denom in the context of these integration tests", + ) + + var err error + receiverBalance := fundCoins.AmountOf(is.tokenDenom) + balanceInBankMod := slices.Contains(nativeCallTypes, callType) + + switch { + case balanceInBankMod: + err = is.factory.FundAccount(is.keyring.GetKey(0), receiver.Bytes(), fundCoins) + case slices.Contains(erc20CallTypes, callType): + err = is.MintERC20(callType, contractData, receiver, fundCoins.AmountOf(is.tokenDenom).BigInt()) + default: + panic("unknown contract call type") + } + + Expect(err).ToNot(HaveOccurred(), "failed to fund account") + Expect(is.network.NextBlock()).To(BeNil()) + + if balanceInBankMod { + balRes, err := is.handler.GetBalanceFromBank(receiver.Bytes(), fundCoins.Denoms()[0]) + Expect(err).To(BeNil()) + receiverBalance = balRes.Balance.Amount + } + + return receiverBalance +} + +// MintERC20 is a helper function to mint tokens on the ERC20 contract. +// +// NOTE: we are checking that there was a Transfer event emitted (which happens on minting). +func (is *IntegrationTestSuite) MintERC20(callType CallType, contractData ContractsData, receiver common.Address, amount *big.Int) error { + if callType == erc20V5CallerCall { + // NOTE: When using the ERC20 caller contract, we must still mint from the actual ERC20 v5 contract. + callType = erc20V5Call + } + abiEvents := contractData.GetContractData(callType).ABI.Events + + txArgs, callArgs := is.getTxAndCallArgs(callType, contractData, "mint", receiver, amount) + + mintCheck := testutil.LogCheckArgs{ + ABIEvents: abiEvents, + ExpEvents: []string{erc20.EventTypeTransfer}, // NOTE: this event occurs when calling "mint" on ERC20s + ExpPass: true, + } + + if _, _, err := is.factory.CallContractAndCheckLogs(contractData.ownerPriv, txArgs, callArgs, mintCheck); err != nil { + return err + } + + // commit changes to chain state + return is.network.NextBlock() +} diff --git a/x/asset/spec/02_state.md b/x/asset/spec/02_state.md index c495da3c..2bd7e5e8 100644 --- a/x/asset/spec/02_state.md +++ b/x/asset/spec/02_state.md @@ -13,9 +13,8 @@ The `x/asset` module keeps the following objects in state: | `Params` | Params of asset module | `[]byte{1}` | `[]byte(params)` | KV | | `Token` | Token information | `[]byte{2} + []byte(token_id)` | `[]byte{token}` | KV | | `TokenManagement` | TokenManagement info of a denom | `[]byte{3} + []byte(token_id)` | `[]byte{token_manager}` | KV | -| `TokenDistribution` | TokenDistribution info of a denom | `[]byte{4} + []byte(token_id)` | `[]byte{token_distributor}` | KV | -| `WhitelistAddresses` | Whitelist Addresses | `[]byte{5} + []byte(address)` | `[]byte{bool}` | KV | -| `ExtenstionStore` | State store for each extensions | `[]byte{6} + []byte(token_id) + []byte(extension_name)` | Depend on extension implementation | KV | +| `WhitelistAddresses` | Whitelist Addresses | `[]byte{4} + []byte(address)` | `[]byte{bool}` | KV | +| `FreezeAddresses` | Whitelist Addresses | `[]byte{5} + []byte(address)` | `[]byte{bool}` | KV | ### Token @@ -23,13 +22,13 @@ Allows creation of tokens with optional user authorization. ```go type Token struct { - TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` - Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - Symbol string `protobuf:"bytes,4,opt,name=symbol,proto3" json:"symbol,omitempty"` - Decimals uint32 `protobuf:"varint,5,opt,name=decimals,proto3" json:"decimals,omitempty"` - Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` - EVMAddress common.Address `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty"` + TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` + Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Symbol string `protobuf:"bytes,4,opt,name=symbol,proto3" json:"symbol,omitempty"` + Decimal uint32 `protobuf:"varint,5,opt,name=decimal,proto3" json:"decimal,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + EvmAddress string `protobuf:"bytes,9,opt,name=evm_address,json=evmAddress,proto3" json:"evm_address,omitempty"` } ``` @@ -38,25 +37,15 @@ When create the token, `asset` module auto generate for it a evm address. This a ### TokenManagement ```go -type TokenManagement struct{ - Managers []string `protobuf:"bytes,7,rep,name=managers,proto3" json:"managers,omitempty"` - AllowNewExtensions bool `protobuf:"varint,10,opt,name=allow_new_Extensions,json=allowNewExtensions,proto3" json:"allow_new_Extensions,omitempty"` - ExtensionsList []string `protobuf:"bytes,11,rep,name=extensions_list,json=extensionsList,proto3" json:"extensions_list,omitempty"` - EvmEnable bool `protobuf:"varint,9,opt,name=evm_enable,json=evmEnable,proto3" json:"evm_enable,omitempty"` - } -``` - -By setting `allow_new_extensions`, `issuer` can specify whether they accept new extensions or not when creating a new token. If he permits it, when upgrading the chain, the new features will be automatically added to the `extensions_list`and the `manager` can then modify the `extensions_list` as he sees fit. Otherwise, the `manager` can not chaing the `extensions_list`. - -### TokenDistribution - -```go -type TokenDistribution struct{ - Distributors []string - MaxSupply math.Int +type TokenManagement struct { + Managers []string `protobuf:"bytes,1,rep,name=managers,proto3" json:"managers,omitempty"` + ExtensionsList []string `protobuf:"bytes,3,rep,name=extensions_list,json=extensionsList,proto3" json:"extensions_list,omitempty"` + MaxSupply cosmossdk_io_math.Int `protobuf:"bytes,4,opt,name=max_supply,json=maxSupply,proto3,customtype=cosmossdk.io/math.Int" json:"max_supply"` } ``` +`extensions_list` is the list of actions that user can execute. The `manager` can then modify the `extensions_list` as he sees fit. + `MaxSupply` defines the maximum number of tokens can be minted. ### WhitelistAddresses diff --git a/x/asset/spec/04_msgs.md b/x/asset/spec/04_msgs.md index 65e0db41..9ea54c6a 100644 --- a/x/asset/spec/04_msgs.md +++ b/x/asset/spec/04_msgs.md @@ -12,13 +12,13 @@ order: 4 type MsgIssueToken struct { Issuer address Managers [ ]address - Distributors [ ]address Name string Symbol string Decimal uint32 Description string - AllowNewExtensions bool ExtensionsList [ ]string + Distributor [ ]string + InitialSupply [ ]math.Int } ``` @@ -38,7 +38,6 @@ Example token.json: ```json { "Manager": ["realioabc..."], - "Distributor": ["realioabc2..."], "Symbol": "riel", "Decimal": 18, "Description": "", @@ -61,27 +60,27 @@ Flow: 4. Save the token basic information (name, symbol, decimal and description) in the x/bank metadata store 5. Save the token management info and distribution info in the x/asset store. -## 2. AssignRoles +## 2. AssignManagers -`MsgAssignRoles` allow issue to set role likes manager or distributor for the token. +`MsgAssignManagers` allow issue to set managers for the token. ```go - type MsgAssignRoles struct { + type MsgAssignManagers struct { TokenId string Issuer address - Addresses mapping[Role]([]addresses) + Addresses []addresses } ``` ```go - type MsgAssignRolesResponse struct { + type MsgAssignManagersResponse struct { } ``` CLI: ```bash - realio-networkd tx assign-roles [privilege.json] [flags] + realio-networkd tx assign-managers [privilege.json] [flags] ``` Example privilege.json: @@ -91,14 +90,8 @@ Example privilege.json: "TokenId": "asset/realio1.../tokena", "Issuer": "realio1...", "Assign": [ - { - "role": 1 (manager), - "addresses": ["realio2..."], - }, - { - "role": 2 (distributor), - "addresses": ["realio3..."], - } + "realio2...", + "realio3..." ] } ``` @@ -109,12 +102,11 @@ Validation: - Check if caller is issuer of the token - Check if addresses is valid - Check if manager doesn't exist in the current managers list of token -- Check if distributor doesn't exist in the current distributor list of token Flow: -- Get `TokenManager` and `TokenDistributor` from store by token_id -- Loop through addresses and append manager addresses to `TokenManager.Managers`, distributor addresses to `TokenDistributor.Distributors` +- Get `TokenManager` from store by token_id +- Loop through addresses and append manager addresses to `TokenManager.Managers` ## 3. UnassignRoles @@ -136,22 +128,23 @@ Validation: - Check if token exists - Check if caller is issuer of the token - Check if addresses is valid -- Check if addresses is in `TokenManager.Managers` or `TokenDistributor.Distributors` +- Check if addresses is in `TokenManager.Managers` Flow: -- Get `TokenManager` and `TokenDistributor` from store by token_id -- Loop through addresses and remove manager addresses from `TokenManager.Managers`, distributor addresses to `TokenDistributor.Distributors` +- Get `TokenManager` from store by token_id +- Loop through addresses and remove manager addresses from `TokenManager.Managers` -## 4. ExecuteExtension +## 4. Burn -After setting the managers, the managers can execute their allowed extension. +This msg only can be executed when the token's `ExtensionsList` has `burn` extension. ```go - type MsgExecuteExtension struct { + type MsgBurn struct { Manager address TokenId string - ExtensionMsg *types.Any + BurnFromAddr address + Amount math.Int } ``` @@ -159,13 +152,15 @@ Validation: - Checks if the token specified in the msg exists. - Checks if the extension is supported. -- Checks if the `Msg.Address` has the corresponding `Extension` specified by `ExtensionMsg.NeedExtension()` +- Check if addresses is valid +- Checks if the address is in `TokenManager.Managers` +- Checks if address is freezed in `FreezeAddresses` Flow: -- Prepare store for the extension of the token via `MakeExtensionStore(extension name, token denom)`. That store is the only store accessable by the extension's `MsgHandler`. -- `ExtensionMsgRouting` routes the `ExtensionMsg` to the its `MsgHandler`. -- `MsgHandler` now handles the `ExtensionMsg`. +- Get `TokenManager` from store by token_id +- Check if `BurnFromAddr` has enough token to burn +- Burn the asset from `BurnFromAddr` ### 5. Mint @@ -173,7 +168,7 @@ This msg only can be executed when the token's `ExtensionsList` has `mint` exten ```go type MsgMint struct { - Distributor address + Manager address TokenId string Receiver address Amount math.Int @@ -185,24 +180,24 @@ Validation: - Checks if the token specified in the msg exists. - Checks if the extension is supported. - Check if addresses is valid -- Checks if the distributor address is in `TokenDistributor.Distributors` -- Checks if mint amount exceed `MaxSupply` or `MaxRatelimit`. +- Checks if the address is in `TokenManager.Managers` +- Checks if mint amount exceed `MaxSupply`. Flow: -- Get `TokenDistributor` from store by token_id +- Get `TokenManager` from store by token_id - Mint the asset for corresponding receiver -- Increase the Maxsupply in TokenDistribution store. +- Increase the supply. -### 6. UpdateDistributionSetting +### 6. Freeze -Distributor can change the max supply of the token. +This msg only can be executed when the token's `ExtensionsList` has `freeze` extension. ```go - type MsgUpdateDistributionSetting struct { - Distributor address + type MsgFreeze struct { + Manager address TokenId string - NewSettings DistributionSettings + Receiver address } ``` @@ -210,8 +205,14 @@ Validation: - Checks if the token specified in the msg exists. - Checks if the extension is supported. -- Checks if the distributor address is in `TokenDistributor.Distributors` -- Checks if current supply exceed new settings `MaxSupply` +- Check if addresses is valid +- Checks if the address is in `TokenManager.Managers` + +Flow: + +- Get `TokenManager` from store by token_id +- Set address into `FreezeAddresses` +- All account in `FreezeAddresses` can not be transfer token out or burned. ### 7. UpdateExtensionsList @@ -231,4 +232,4 @@ Validation: - Checks if manager addresses is in `TokenManager.Managers` - Checks if the new extension is supported. -### 8. UpdateParams +### 7. UpdateParams diff --git a/x/asset/spec/06_logic.md b/x/asset/spec/06_logic.md index 4bd60b70..1411a1c1 100644 --- a/x/asset/spec/06_logic.md +++ b/x/asset/spec/06_logic.md @@ -30,8 +30,16 @@ The token creation process involves the `Issuer` executing `MsgIssueToken` that We'll go into details on how each of the extension works ### Mint extension +Only manager is allowed to execute `Mint`. Then mint the corresponding amount to the recipient. Note, total supply can not exceed `MaxSupply` ### Burn extension +Only manager is allowed to execute `Burn`. Then burn the corresponding amount from the address. Note, address that be freezed can not burn. + +### Freeze extension +Only manager is allowed to execute `Freeze`. Its will lock all amount of a asset of that address. An address be freezed with a token can not transfer out or burn. + +### TransferAuth extension +Only manager is allowed to execute `TransferAuth`. Its will update the Token's Issue to new receiver. ### @@ -43,6 +51,8 @@ On token creation, all token will be linked to a erc20-precompiles, which allows The token itself exists as a coin within the bank state, maintaining its own logic and extensions independently of any ERC20 or EVM contract logic. The ERC20 contract deployed on the EVM serves purely as an interface, with its logic effectively bypassed. When other EVM contracts interact with this interface, their requests are forwarded via JSON-RPC calls to the `asset` module, which directly handles and executes the necessary operations. This is achieved by creating a `dynamic precompile`, ensuring that the token’s behavior aligns with its internal state while still providing compatibility with the EVM ecosystem. +The precompiles actions will depend on the `AllowExtensionList` when creating the token. Therefore different tokens will have precompiles with different addresses and extensions. + ### EVM Precompiles EVM precompiles are EVM interface contracts with state access. These smart contracts can directly interact with Cosmos SDK modules, enabling their own operations while also interacting with the EVM state and other SDK modules. diff --git a/x/asset/spec/README.md b/x/asset/spec/README.md index 0f31839d..06318b9a 100644 --- a/x/asset/spec/README.md +++ b/x/asset/spec/README.md @@ -46,6 +46,7 @@ We introduce these optional calls to precompile to execute the token's extension - Freeze - TransferAuth + It's important to note that each token has its own set of enabled extensions, the precompile linked to that token must also reflect that. In other words, for each precompile these calls will be enabled/disabled however the linked token's extensions be. ![asset_evm](imgs/asset_evm.png) diff --git a/x/asset/spec/imgs/asset_module.png b/x/asset/spec/imgs/asset_module.png index 193309a3..1cefb9ec 100644 Binary files a/x/asset/spec/imgs/asset_module.png and b/x/asset/spec/imgs/asset_module.png differ