diff --git a/app/app.go b/app/app.go index 0fd9026348..90bbe8576b 100644 --- a/app/app.go +++ b/app/app.go @@ -541,6 +541,7 @@ func New( &app.FungibleKeeper, app.StakingKeeper, app.BankKeeper, + app.DistrKeeper, appCodec, storetypes.TransientGasConfig(), ), diff --git a/changelog.md b/changelog.md index a3a78e0d82..41da059bfb 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ * [3091](https://github.com/zeta-chain/node/pull/3091) - improve build reproducability. `make release{,-build-only}` checksums should now be stable. * [3124](https://github.com/zeta-chain/node/pull/3124) - integrate SPL deposits * [3134](https://github.com/zeta-chain/node/pull/3134) - integrate SPL tokens withdraw to Solana +* [3088](https://github.com/zeta-chain/node/pull/3088) - add functions to check and withdraw zrc20 as delegation rewards * [3182](https://github.com/zeta-chain/node/pull/3182) - enable zetaclient pprof server on port 6061 ### Tests diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index b65d104df6..c95c4e9942 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -258,7 +258,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { deployerRunner.UpdateChainParamsV2Contracts() deployerRunner.ERC20CustodyAddr = deployerRunner.ERC20CustodyV2Addr - deployerRunner.MintERC20OnEvm(1000000) + deployerRunner.MintERC20OnEvm(1e10) logger.Print("✅ setup completed in %s", time.Since(startTime)) } @@ -366,14 +366,14 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests = []string{ - // e2etests.TestPrecompilesPrototypeName, - // e2etests.TestPrecompilesPrototypeThroughContractName, - // e2etests.TestPrecompilesStakingName, - // // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - // // e2etests.TestPrecompilesStakingThroughContractName, - // e2etests.TestPrecompilesBankName, - // e2etests.TestPrecompilesBankFailName, - // e2etests.TestPrecompilesBankThroughContractName, + e2etests.TestPrecompilesPrototypeName, + e2etests.TestPrecompilesPrototypeThroughContractName, + e2etests.TestPrecompilesStakingName, + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + // e2etests.TestPrecompilesStakingThroughContractName, + e2etests.TestPrecompilesBankName, + e2etests.TestPrecompilesBankFailName, + e2etests.TestPrecompilesBankThroughContractName, e2etests.TestPrecompilesDistributeName, e2etests.TestPrecompilesDistributeNonZRC20Name, e2etests.TestPrecompilesDistributeThroughContractName, diff --git a/cmd/zetae2e/local/precompiles.go b/cmd/zetae2e/local/precompiles.go index 46668c9f44..3a095115c0 100644 --- a/cmd/zetae2e/local/precompiles.go +++ b/cmd/zetae2e/local/precompiles.go @@ -4,11 +4,13 @@ import ( "fmt" "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/fatih/color" "github.com/zeta-chain/node/e2e/config" "github.com/zeta-chain/node/e2e/e2etests" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/txserver" ) // statefulPrecompilesTestRoutine runs steateful precompiles related e2e tests @@ -32,11 +34,29 @@ func statefulPrecompilesTestRoutine( return err } + // Initialize a ZetaTxServer with the precompile user account. + // It's needed to send messages on behalf of the precompile user. + zetaTxServer, err := txserver.NewZetaTxServer( + conf.RPCs.ZetaCoreRPC, + []string{ + sdk.AccAddress(conf.AdditionalAccounts.UserPrecompile.EVMAddress().Bytes()).String(), + }, + []string{ + conf.AdditionalAccounts.UserPrecompile.RawPrivateKey.String(), + }, + conf.ZetaChainID, + ) + if err != nil { + return err + } + + precompileRunner.ZetaTxServer = zetaTxServer + precompileRunner.Logger.Print("🏃 starting stateful precompiled contracts tests") startTime := time.Now() // Send ERC20 that will be depositted into ERC20ZRC20 tokens. - txERC20Send := deployerRunner.SendERC20OnEvm(account.EVMAddress(), 10000) + txERC20Send := deployerRunner.SendERC20OnEvm(account.EVMAddress(), 1e7) precompileRunner.WaitForTxReceiptOnEvm(txERC20Send) testsToRun, err := precompileRunner.GetE2ETestsToRunByName( diff --git a/e2e/contracts/testdistribute/TestDistribute.abi b/e2e/contracts/testdistribute/TestDistribute.abi index 26aa07af4e..e8f554bd92 100644 --- a/e2e/contracts/testdistribute/TestDistribute.abi +++ b/e2e/contracts/testdistribute/TestDistribute.abi @@ -1,37 +1,31 @@ [ { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" + "stateMutability": "payable", + "type": "fallback" }, { - "anonymous": false, "inputs": [ { - "indexed": true, "internalType": "address", - "name": "zrc20_distributor", + "name": "delegator", "type": "address" }, { - "indexed": true, - "internalType": "address", - "name": "zrc20_token", - "type": "address" - }, + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewardsThroughContract", + "outputs": [ { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" + "internalType": "bool", + "name": "", + "type": "bool" } ], - "name": "Distributed", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" + "stateMutability": "nonpayable", + "type": "function" }, { "inputs": [ @@ -57,6 +51,61 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidatorsThroughContract", + "outputs": [ + { + "internalType": "string[]", + "name": "", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewardsThroughContract", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "stateMutability": "payable", "type": "receive" diff --git a/e2e/contracts/testdistribute/TestDistribute.bin b/e2e/contracts/testdistribute/TestDistribute.bin index 5840bf96e5..c59ac56b96 100644 --- a/e2e/contracts/testdistribute/TestDistribute.bin +++ b/e2e/contracts/testdistribute/TestDistribute.bin @@ -1 +1 @@ -60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033 +608060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b50610e2d806100616000396000f3fe6080604052600436106100435760003560e01c80630f4865ea1461004c57806350b54e8414610089578063834b902f146100c6578063cdc5ec4a146101035761004a565b3661004a57005b005b34801561005857600080fd5b50610073600480360381019061006e919061059d565b610140565b6040516100809190610614565b60405180910390f35b34801561009557600080fd5b506100b060048036038101906100ab9190610665565b6101e9565b6040516100bd9190610614565b60405180910390f35b3480156100d257600080fd5b506100ed60048036038101906100e8919061059d565b610292565b6040516100fa919061083b565b60405180910390f35b34801561010f57600080fd5b5061012a6004803603810190610125919061085d565b61033d565b604051610137919061094c565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166354dbdc3884846040518363ffffffff1660e01b815260040161019e9291906109c7565b6020604051808303816000875af11580156101bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101e19190610a23565b905092915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b8152600401610247929190610a5f565b6020604051808303816000875af1158015610266573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061028a9190610a23565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16639342879284846040518363ffffffff1660e01b81526004016102ef9291906109c7565b600060405180830381865afa15801561030c573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103359190610c69565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663b6a216ae836040518263ffffffff1660e01b81526004016103989190610cb2565b600060405180830381865afa1580156103b5573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103de9190610dae565b9050919050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610424826103f9565b9050919050565b61043481610419565b811461043f57600080fd5b50565b6000813590506104518161042b565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6104aa82610461565b810181811067ffffffffffffffff821117156104c9576104c8610472565b5b80604052505050565b60006104dc6103e5565b90506104e882826104a1565b919050565b600067ffffffffffffffff82111561050857610507610472565b5b61051182610461565b9050602081019050919050565b82818337600083830152505050565b600061054061053b846104ed565b6104d2565b90508281526020810184848401111561055c5761055b61045c565b5b61056784828561051e565b509392505050565b600082601f83011261058457610583610457565b5b813561059484826020860161052d565b91505092915050565b600080604083850312156105b4576105b36103ef565b5b60006105c285828601610442565b925050602083013567ffffffffffffffff8111156105e3576105e26103f4565b5b6105ef8582860161056f565b9150509250929050565b60008115159050919050565b61060e816105f9565b82525050565b60006020820190506106296000830184610605565b92915050565b6000819050919050565b6106428161062f565b811461064d57600080fd5b50565b60008135905061065f81610639565b92915050565b6000806040838503121561067c5761067b6103ef565b5b600061068a85828601610442565b925050602061069b85828601610650565b9150509250929050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561070b5780820151818401526020810190506106f0565b8381111561071a576000848401525b50505050565b600061072b826106d1565b61073581856106dc565b93506107458185602086016106ed565b61074e81610461565b840191505092915050565b6107628161062f565b82525050565b600060408301600083015184820360008601526107858282610720565b915050602083015161079a6020860182610759565b508091505092915050565b60006107b18383610768565b905092915050565b6000602082019050919050565b60006107d1826106a5565b6107db81856106b0565b9350836020820285016107ed856106c1565b8060005b85811015610829578484038952815161080a85826107a5565b9450610815836107b9565b925060208a019950506001810190506107f1565b50829750879550505050505092915050565b6000602082019050818103600083015261085581846107c6565b905092915050565b600060208284031215610873576108726103ef565b5b600061088184828501610442565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b60006108c28383610720565b905092915050565b6000602082019050919050565b60006108e28261088a565b6108ec8185610895565b9350836020820285016108fe856108a6565b8060005b8581101561093a578484038952815161091b85826108b6565b9450610926836108ca565b925060208a01995050600181019050610902565b50829750879550505050505092915050565b6000602082019050818103600083015261096681846108d7565b905092915050565b61097781610419565b82525050565b600082825260208201905092915050565b6000610999826106d1565b6109a3818561097d565b93506109b38185602086016106ed565b6109bc81610461565b840191505092915050565b60006040820190506109dc600083018561096e565b81810360208301526109ee818461098e565b90509392505050565b610a00816105f9565b8114610a0b57600080fd5b50565b600081519050610a1d816109f7565b92915050565b600060208284031215610a3957610a386103ef565b5b6000610a4784828501610a0e565b91505092915050565b610a598161062f565b82525050565b6000604082019050610a74600083018561096e565b610a816020830184610a50565b9392505050565b600067ffffffffffffffff821115610aa357610aa2610472565b5b602082029050602081019050919050565b600080fd5b600080fd5b600080fd5b6000610ad6610ad1846104ed565b6104d2565b905082815260208101848484011115610af257610af161045c565b5b610afd8482856106ed565b509392505050565b600082601f830112610b1a57610b19610457565b5b8151610b2a848260208601610ac3565b91505092915050565b600081519050610b4281610639565b92915050565b600060408284031215610b5e57610b5d610ab9565b5b610b6860406104d2565b9050600082015167ffffffffffffffff811115610b8857610b87610abe565b5b610b9484828501610b05565b6000830152506020610ba884828501610b33565b60208301525092915050565b6000610bc7610bc284610a88565b6104d2565b90508083825260208201905060208402830185811115610bea57610be9610ab4565b5b835b81811015610c3157805167ffffffffffffffff811115610c0f57610c0e610457565b5b808601610c1c8982610b48565b85526020850194505050602081019050610bec565b5050509392505050565b600082601f830112610c5057610c4f610457565b5b8151610c60848260208601610bb4565b91505092915050565b600060208284031215610c7f57610c7e6103ef565b5b600082015167ffffffffffffffff811115610c9d57610c9c6103f4565b5b610ca984828501610c3b565b91505092915050565b6000602082019050610cc7600083018461096e565b92915050565b600067ffffffffffffffff821115610ce857610ce7610472565b5b602082029050602081019050919050565b6000610d0c610d0784610ccd565b6104d2565b90508083825260208201905060208402830185811115610d2f57610d2e610ab4565b5b835b81811015610d7657805167ffffffffffffffff811115610d5457610d53610457565b5b808601610d618982610b05565b85526020850194505050602081019050610d31565b5050509392505050565b600082601f830112610d9557610d94610457565b5b8151610da5848260208601610cf9565b91505092915050565b600060208284031215610dc457610dc36103ef565b5b600082015167ffffffffffffffff811115610de257610de16103f4565b5b610dee84828501610d80565b9150509291505056fea2646970667358221220d29e8c0ffd7f95c3ae2950ad56c9ec844a4f83f78ebf290ed1f2076d3fa1537864736f6c634300080a0033 diff --git a/e2e/contracts/testdistribute/TestDistribute.go b/e2e/contracts/testdistribute/TestDistribute.go index 18b4201b1a..bdbbfce8e6 100644 --- a/e2e/contracts/testdistribute/TestDistribute.go +++ b/e2e/contracts/testdistribute/TestDistribute.go @@ -29,10 +29,16 @@ var ( _ = abi.ConvertType ) +// DecCoin is an auto generated low-level Go binding around an user-defined struct. +type DecCoin struct { + Denom string + Amount *big.Int +} + // TestDistributeMetaData contains all meta data concerning the TestDistribute contract. var TestDistributeMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distributeThroughContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", - Bin: "0x60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033", + ABI: "[{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"claimRewardsThroughContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distributeThroughContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"}],\"name\":\"getDelegatorValidatorsThroughContract\",\"outputs\":[{\"internalType\":\"string[]\",\"name\":\"\",\"type\":\"string[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getRewardsThroughContract\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"denom\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structDecCoin[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", + Bin: "0x608060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b50610e2d806100616000396000f3fe6080604052600436106100435760003560e01c80630f4865ea1461004c57806350b54e8414610089578063834b902f146100c6578063cdc5ec4a146101035761004a565b3661004a57005b005b34801561005857600080fd5b50610073600480360381019061006e919061059d565b610140565b6040516100809190610614565b60405180910390f35b34801561009557600080fd5b506100b060048036038101906100ab9190610665565b6101e9565b6040516100bd9190610614565b60405180910390f35b3480156100d257600080fd5b506100ed60048036038101906100e8919061059d565b610292565b6040516100fa919061083b565b60405180910390f35b34801561010f57600080fd5b5061012a6004803603810190610125919061085d565b61033d565b604051610137919061094c565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166354dbdc3884846040518363ffffffff1660e01b815260040161019e9291906109c7565b6020604051808303816000875af11580156101bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101e19190610a23565b905092915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b8152600401610247929190610a5f565b6020604051808303816000875af1158015610266573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061028a9190610a23565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16639342879284846040518363ffffffff1660e01b81526004016102ef9291906109c7565b600060405180830381865afa15801561030c573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103359190610c69565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663b6a216ae836040518263ffffffff1660e01b81526004016103989190610cb2565b600060405180830381865afa1580156103b5573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103de9190610dae565b9050919050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610424826103f9565b9050919050565b61043481610419565b811461043f57600080fd5b50565b6000813590506104518161042b565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6104aa82610461565b810181811067ffffffffffffffff821117156104c9576104c8610472565b5b80604052505050565b60006104dc6103e5565b90506104e882826104a1565b919050565b600067ffffffffffffffff82111561050857610507610472565b5b61051182610461565b9050602081019050919050565b82818337600083830152505050565b600061054061053b846104ed565b6104d2565b90508281526020810184848401111561055c5761055b61045c565b5b61056784828561051e565b509392505050565b600082601f83011261058457610583610457565b5b813561059484826020860161052d565b91505092915050565b600080604083850312156105b4576105b36103ef565b5b60006105c285828601610442565b925050602083013567ffffffffffffffff8111156105e3576105e26103f4565b5b6105ef8582860161056f565b9150509250929050565b60008115159050919050565b61060e816105f9565b82525050565b60006020820190506106296000830184610605565b92915050565b6000819050919050565b6106428161062f565b811461064d57600080fd5b50565b60008135905061065f81610639565b92915050565b6000806040838503121561067c5761067b6103ef565b5b600061068a85828601610442565b925050602061069b85828601610650565b9150509250929050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561070b5780820151818401526020810190506106f0565b8381111561071a576000848401525b50505050565b600061072b826106d1565b61073581856106dc565b93506107458185602086016106ed565b61074e81610461565b840191505092915050565b6107628161062f565b82525050565b600060408301600083015184820360008601526107858282610720565b915050602083015161079a6020860182610759565b508091505092915050565b60006107b18383610768565b905092915050565b6000602082019050919050565b60006107d1826106a5565b6107db81856106b0565b9350836020820285016107ed856106c1565b8060005b85811015610829578484038952815161080a85826107a5565b9450610815836107b9565b925060208a019950506001810190506107f1565b50829750879550505050505092915050565b6000602082019050818103600083015261085581846107c6565b905092915050565b600060208284031215610873576108726103ef565b5b600061088184828501610442565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b60006108c28383610720565b905092915050565b6000602082019050919050565b60006108e28261088a565b6108ec8185610895565b9350836020820285016108fe856108a6565b8060005b8581101561093a578484038952815161091b85826108b6565b9450610926836108ca565b925060208a01995050600181019050610902565b50829750879550505050505092915050565b6000602082019050818103600083015261096681846108d7565b905092915050565b61097781610419565b82525050565b600082825260208201905092915050565b6000610999826106d1565b6109a3818561097d565b93506109b38185602086016106ed565b6109bc81610461565b840191505092915050565b60006040820190506109dc600083018561096e565b81810360208301526109ee818461098e565b90509392505050565b610a00816105f9565b8114610a0b57600080fd5b50565b600081519050610a1d816109f7565b92915050565b600060208284031215610a3957610a386103ef565b5b6000610a4784828501610a0e565b91505092915050565b610a598161062f565b82525050565b6000604082019050610a74600083018561096e565b610a816020830184610a50565b9392505050565b600067ffffffffffffffff821115610aa357610aa2610472565b5b602082029050602081019050919050565b600080fd5b600080fd5b600080fd5b6000610ad6610ad1846104ed565b6104d2565b905082815260208101848484011115610af257610af161045c565b5b610afd8482856106ed565b509392505050565b600082601f830112610b1a57610b19610457565b5b8151610b2a848260208601610ac3565b91505092915050565b600081519050610b4281610639565b92915050565b600060408284031215610b5e57610b5d610ab9565b5b610b6860406104d2565b9050600082015167ffffffffffffffff811115610b8857610b87610abe565b5b610b9484828501610b05565b6000830152506020610ba884828501610b33565b60208301525092915050565b6000610bc7610bc284610a88565b6104d2565b90508083825260208201905060208402830185811115610bea57610be9610ab4565b5b835b81811015610c3157805167ffffffffffffffff811115610c0f57610c0e610457565b5b808601610c1c8982610b48565b85526020850194505050602081019050610bec565b5050509392505050565b600082601f830112610c5057610c4f610457565b5b8151610c60848260208601610bb4565b91505092915050565b600060208284031215610c7f57610c7e6103ef565b5b600082015167ffffffffffffffff811115610c9d57610c9c6103f4565b5b610ca984828501610c3b565b91505092915050565b6000602082019050610cc7600083018461096e565b92915050565b600067ffffffffffffffff821115610ce857610ce7610472565b5b602082029050602081019050919050565b6000610d0c610d0784610ccd565b6104d2565b90508083825260208201905060208402830185811115610d2f57610d2e610ab4565b5b835b81811015610d7657805167ffffffffffffffff811115610d5457610d53610457565b5b808601610d618982610b05565b85526020850194505050602081019050610d31565b5050509392505050565b600082601f830112610d9557610d94610457565b5b8151610da5848260208601610cf9565b91505092915050565b600060208284031215610dc457610dc36103ef565b5b600082015167ffffffffffffffff811115610de257610de16103f4565b5b610dee84828501610d80565b9150509291505056fea2646970667358221220d29e8c0ffd7f95c3ae2950ad56c9ec844a4f83f78ebf290ed1f2076d3fa1537864736f6c634300080a0033", } // TestDistributeABI is the input ABI used to generate the binding from. @@ -202,6 +208,89 @@ func (_TestDistribute *TestDistributeTransactorRaw) Transact(opts *bind.Transact return _TestDistribute.Contract.contract.Transact(opts, method, params...) } +// GetDelegatorValidatorsThroughContract is a free data retrieval call binding the contract method 0xcdc5ec4a. +// +// Solidity: function getDelegatorValidatorsThroughContract(address delegator) view returns(string[]) +func (_TestDistribute *TestDistributeCaller) GetDelegatorValidatorsThroughContract(opts *bind.CallOpts, delegator common.Address) ([]string, error) { + var out []interface{} + err := _TestDistribute.contract.Call(opts, &out, "getDelegatorValidatorsThroughContract", delegator) + + if err != nil { + return *new([]string), err + } + + out0 := *abi.ConvertType(out[0], new([]string)).(*[]string) + + return out0, err + +} + +// GetDelegatorValidatorsThroughContract is a free data retrieval call binding the contract method 0xcdc5ec4a. +// +// Solidity: function getDelegatorValidatorsThroughContract(address delegator) view returns(string[]) +func (_TestDistribute *TestDistributeSession) GetDelegatorValidatorsThroughContract(delegator common.Address) ([]string, error) { + return _TestDistribute.Contract.GetDelegatorValidatorsThroughContract(&_TestDistribute.CallOpts, delegator) +} + +// GetDelegatorValidatorsThroughContract is a free data retrieval call binding the contract method 0xcdc5ec4a. +// +// Solidity: function getDelegatorValidatorsThroughContract(address delegator) view returns(string[]) +func (_TestDistribute *TestDistributeCallerSession) GetDelegatorValidatorsThroughContract(delegator common.Address) ([]string, error) { + return _TestDistribute.Contract.GetDelegatorValidatorsThroughContract(&_TestDistribute.CallOpts, delegator) +} + +// GetRewardsThroughContract is a free data retrieval call binding the contract method 0x834b902f. +// +// Solidity: function getRewardsThroughContract(address delegator, string validator) view returns((string,uint256)[]) +func (_TestDistribute *TestDistributeCaller) GetRewardsThroughContract(opts *bind.CallOpts, delegator common.Address, validator string) ([]DecCoin, error) { + var out []interface{} + err := _TestDistribute.contract.Call(opts, &out, "getRewardsThroughContract", delegator, validator) + + if err != nil { + return *new([]DecCoin), err + } + + out0 := *abi.ConvertType(out[0], new([]DecCoin)).(*[]DecCoin) + + return out0, err + +} + +// GetRewardsThroughContract is a free data retrieval call binding the contract method 0x834b902f. +// +// Solidity: function getRewardsThroughContract(address delegator, string validator) view returns((string,uint256)[]) +func (_TestDistribute *TestDistributeSession) GetRewardsThroughContract(delegator common.Address, validator string) ([]DecCoin, error) { + return _TestDistribute.Contract.GetRewardsThroughContract(&_TestDistribute.CallOpts, delegator, validator) +} + +// GetRewardsThroughContract is a free data retrieval call binding the contract method 0x834b902f. +// +// Solidity: function getRewardsThroughContract(address delegator, string validator) view returns((string,uint256)[]) +func (_TestDistribute *TestDistributeCallerSession) GetRewardsThroughContract(delegator common.Address, validator string) ([]DecCoin, error) { + return _TestDistribute.Contract.GetRewardsThroughContract(&_TestDistribute.CallOpts, delegator, validator) +} + +// ClaimRewardsThroughContract is a paid mutator transaction binding the contract method 0x0f4865ea. +// +// Solidity: function claimRewardsThroughContract(address delegator, string validator) returns(bool) +func (_TestDistribute *TestDistributeTransactor) ClaimRewardsThroughContract(opts *bind.TransactOpts, delegator common.Address, validator string) (*types.Transaction, error) { + return _TestDistribute.contract.Transact(opts, "claimRewardsThroughContract", delegator, validator) +} + +// ClaimRewardsThroughContract is a paid mutator transaction binding the contract method 0x0f4865ea. +// +// Solidity: function claimRewardsThroughContract(address delegator, string validator) returns(bool) +func (_TestDistribute *TestDistributeSession) ClaimRewardsThroughContract(delegator common.Address, validator string) (*types.Transaction, error) { + return _TestDistribute.Contract.ClaimRewardsThroughContract(&_TestDistribute.TransactOpts, delegator, validator) +} + +// ClaimRewardsThroughContract is a paid mutator transaction binding the contract method 0x0f4865ea. +// +// Solidity: function claimRewardsThroughContract(address delegator, string validator) returns(bool) +func (_TestDistribute *TestDistributeTransactorSession) ClaimRewardsThroughContract(delegator common.Address, validator string) (*types.Transaction, error) { + return _TestDistribute.Contract.ClaimRewardsThroughContract(&_TestDistribute.TransactOpts, delegator, validator) +} + // DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. // // Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) @@ -264,157 +353,3 @@ func (_TestDistribute *TestDistributeSession) Receive() (*types.Transaction, err func (_TestDistribute *TestDistributeTransactorSession) Receive() (*types.Transaction, error) { return _TestDistribute.Contract.Receive(&_TestDistribute.TransactOpts) } - -// TestDistributeDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the TestDistribute contract. -type TestDistributeDistributedIterator struct { - Event *TestDistributeDistributed // Event containing the contract specifics and raw log - - contract *bind.BoundContract // Generic contract to use for unpacking event data - event string // Event name to use for unpacking event data - - logs chan types.Log // Log channel receiving the found contract events - sub ethereum.Subscription // Subscription for errors, completion and termination - done bool // Whether the subscription completed delivering logs - fail error // Occurred error to stop iteration -} - -// Next advances the iterator to the subsequent event, returning whether there -// are any more events found. In case of a retrieval or parsing error, false is -// returned and Error() can be queried for the exact failure. -func (it *TestDistributeDistributedIterator) Next() bool { - // If the iterator failed, stop iterating - if it.fail != nil { - return false - } - // If the iterator completed, deliver directly whatever's available - if it.done { - select { - case log := <-it.logs: - it.Event = new(TestDistributeDistributed) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - default: - return false - } - } - // Iterator still in progress, wait for either a data or an error event - select { - case log := <-it.logs: - it.Event = new(TestDistributeDistributed) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - case err := <-it.sub.Err(): - it.done = true - it.fail = err - return it.Next() - } -} - -// Error returns any retrieval or parsing error occurred during filtering. -func (it *TestDistributeDistributedIterator) Error() error { - return it.fail -} - -// Close terminates the iteration process, releasing any pending underlying -// resources. -func (it *TestDistributeDistributedIterator) Close() error { - it.sub.Unsubscribe() - return nil -} - -// TestDistributeDistributed represents a Distributed event raised by the TestDistribute contract. -type TestDistributeDistributed struct { - Zrc20Distributor common.Address - Zrc20Token common.Address - Amount *big.Int - Raw types.Log // Blockchain specific contextual infos -} - -// FilterDistributed is a free log retrieval operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. -// -// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) -func (_TestDistribute *TestDistributeFilterer) FilterDistributed(opts *bind.FilterOpts, zrc20_distributor []common.Address, zrc20_token []common.Address) (*TestDistributeDistributedIterator, error) { - - var zrc20_distributorRule []interface{} - for _, zrc20_distributorItem := range zrc20_distributor { - zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) - } - var zrc20_tokenRule []interface{} - for _, zrc20_tokenItem := range zrc20_token { - zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) - } - - logs, sub, err := _TestDistribute.contract.FilterLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) - if err != nil { - return nil, err - } - return &TestDistributeDistributedIterator{contract: _TestDistribute.contract, event: "Distributed", logs: logs, sub: sub}, nil -} - -// WatchDistributed is a free log subscription operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. -// -// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) -func (_TestDistribute *TestDistributeFilterer) WatchDistributed(opts *bind.WatchOpts, sink chan<- *TestDistributeDistributed, zrc20_distributor []common.Address, zrc20_token []common.Address) (event.Subscription, error) { - - var zrc20_distributorRule []interface{} - for _, zrc20_distributorItem := range zrc20_distributor { - zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) - } - var zrc20_tokenRule []interface{} - for _, zrc20_tokenItem := range zrc20_token { - zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) - } - - logs, sub, err := _TestDistribute.contract.WatchLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) - if err != nil { - return nil, err - } - return event.NewSubscription(func(quit <-chan struct{}) error { - defer sub.Unsubscribe() - for { - select { - case log := <-logs: - // New log arrived, parse the event and forward to the user - event := new(TestDistributeDistributed) - if err := _TestDistribute.contract.UnpackLog(event, "Distributed", log); err != nil { - return err - } - event.Raw = log - - select { - case sink <- event: - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - } - }), nil -} - -// ParseDistributed is a log parse operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. -// -// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) -func (_TestDistribute *TestDistributeFilterer) ParseDistributed(log types.Log) (*TestDistributeDistributed, error) { - event := new(TestDistributeDistributed) - if err := _TestDistribute.contract.UnpackLog(event, "Distributed", log); err != nil { - return nil, err - } - event.Raw = log - return event, nil -} diff --git a/e2e/contracts/testdistribute/TestDistribute.json b/e2e/contracts/testdistribute/TestDistribute.json index 05ee369e1c..44201da094 100644 --- a/e2e/contracts/testdistribute/TestDistribute.json +++ b/e2e/contracts/testdistribute/TestDistribute.json @@ -1,38 +1,32 @@ { "abi": [ { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" + "stateMutability": "payable", + "type": "fallback" }, { - "anonymous": false, "inputs": [ { - "indexed": true, "internalType": "address", - "name": "zrc20_distributor", + "name": "delegator", "type": "address" }, { - "indexed": true, - "internalType": "address", - "name": "zrc20_token", - "type": "address" - }, + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewardsThroughContract", + "outputs": [ { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" + "internalType": "bool", + "name": "", + "type": "bool" } ], - "name": "Distributed", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" + "stateMutability": "nonpayable", + "type": "function" }, { "inputs": [ @@ -58,10 +52,65 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidatorsThroughContract", + "outputs": [ + { + "internalType": "string[]", + "name": "", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewardsThroughContract", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "stateMutability": "payable", "type": "receive" } ], - "bin": "60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033" + "bin": "608060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b50610e2d806100616000396000f3fe6080604052600436106100435760003560e01c80630f4865ea1461004c57806350b54e8414610089578063834b902f146100c6578063cdc5ec4a146101035761004a565b3661004a57005b005b34801561005857600080fd5b50610073600480360381019061006e919061059d565b610140565b6040516100809190610614565b60405180910390f35b34801561009557600080fd5b506100b060048036038101906100ab9190610665565b6101e9565b6040516100bd9190610614565b60405180910390f35b3480156100d257600080fd5b506100ed60048036038101906100e8919061059d565b610292565b6040516100fa919061083b565b60405180910390f35b34801561010f57600080fd5b5061012a6004803603810190610125919061085d565b61033d565b604051610137919061094c565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166354dbdc3884846040518363ffffffff1660e01b815260040161019e9291906109c7565b6020604051808303816000875af11580156101bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101e19190610a23565b905092915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b8152600401610247929190610a5f565b6020604051808303816000875af1158015610266573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061028a9190610a23565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16639342879284846040518363ffffffff1660e01b81526004016102ef9291906109c7565b600060405180830381865afa15801561030c573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103359190610c69565b905092915050565b606060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663b6a216ae836040518263ffffffff1660e01b81526004016103989190610cb2565b600060405180830381865afa1580156103b5573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052508101906103de9190610dae565b9050919050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610424826103f9565b9050919050565b61043481610419565b811461043f57600080fd5b50565b6000813590506104518161042b565b92915050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6104aa82610461565b810181811067ffffffffffffffff821117156104c9576104c8610472565b5b80604052505050565b60006104dc6103e5565b90506104e882826104a1565b919050565b600067ffffffffffffffff82111561050857610507610472565b5b61051182610461565b9050602081019050919050565b82818337600083830152505050565b600061054061053b846104ed565b6104d2565b90508281526020810184848401111561055c5761055b61045c565b5b61056784828561051e565b509392505050565b600082601f83011261058457610583610457565b5b813561059484826020860161052d565b91505092915050565b600080604083850312156105b4576105b36103ef565b5b60006105c285828601610442565b925050602083013567ffffffffffffffff8111156105e3576105e26103f4565b5b6105ef8582860161056f565b9150509250929050565b60008115159050919050565b61060e816105f9565b82525050565b60006020820190506106296000830184610605565b92915050565b6000819050919050565b6106428161062f565b811461064d57600080fd5b50565b60008135905061065f81610639565b92915050565b6000806040838503121561067c5761067b6103ef565b5b600061068a85828601610442565b925050602061069b85828601610650565b9150509250929050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561070b5780820151818401526020810190506106f0565b8381111561071a576000848401525b50505050565b600061072b826106d1565b61073581856106dc565b93506107458185602086016106ed565b61074e81610461565b840191505092915050565b6107628161062f565b82525050565b600060408301600083015184820360008601526107858282610720565b915050602083015161079a6020860182610759565b508091505092915050565b60006107b18383610768565b905092915050565b6000602082019050919050565b60006107d1826106a5565b6107db81856106b0565b9350836020820285016107ed856106c1565b8060005b85811015610829578484038952815161080a85826107a5565b9450610815836107b9565b925060208a019950506001810190506107f1565b50829750879550505050505092915050565b6000602082019050818103600083015261085581846107c6565b905092915050565b600060208284031215610873576108726103ef565b5b600061088184828501610442565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b60006108c28383610720565b905092915050565b6000602082019050919050565b60006108e28261088a565b6108ec8185610895565b9350836020820285016108fe856108a6565b8060005b8581101561093a578484038952815161091b85826108b6565b9450610926836108ca565b925060208a01995050600181019050610902565b50829750879550505050505092915050565b6000602082019050818103600083015261096681846108d7565b905092915050565b61097781610419565b82525050565b600082825260208201905092915050565b6000610999826106d1565b6109a3818561097d565b93506109b38185602086016106ed565b6109bc81610461565b840191505092915050565b60006040820190506109dc600083018561096e565b81810360208301526109ee818461098e565b90509392505050565b610a00816105f9565b8114610a0b57600080fd5b50565b600081519050610a1d816109f7565b92915050565b600060208284031215610a3957610a386103ef565b5b6000610a4784828501610a0e565b91505092915050565b610a598161062f565b82525050565b6000604082019050610a74600083018561096e565b610a816020830184610a50565b9392505050565b600067ffffffffffffffff821115610aa357610aa2610472565b5b602082029050602081019050919050565b600080fd5b600080fd5b600080fd5b6000610ad6610ad1846104ed565b6104d2565b905082815260208101848484011115610af257610af161045c565b5b610afd8482856106ed565b509392505050565b600082601f830112610b1a57610b19610457565b5b8151610b2a848260208601610ac3565b91505092915050565b600081519050610b4281610639565b92915050565b600060408284031215610b5e57610b5d610ab9565b5b610b6860406104d2565b9050600082015167ffffffffffffffff811115610b8857610b87610abe565b5b610b9484828501610b05565b6000830152506020610ba884828501610b33565b60208301525092915050565b6000610bc7610bc284610a88565b6104d2565b90508083825260208201905060208402830185811115610bea57610be9610ab4565b5b835b81811015610c3157805167ffffffffffffffff811115610c0f57610c0e610457565b5b808601610c1c8982610b48565b85526020850194505050602081019050610bec565b5050509392505050565b600082601f830112610c5057610c4f610457565b5b8151610c60848260208601610bb4565b91505092915050565b600060208284031215610c7f57610c7e6103ef565b5b600082015167ffffffffffffffff811115610c9d57610c9c6103f4565b5b610ca984828501610c3b565b91505092915050565b6000602082019050610cc7600083018461096e565b92915050565b600067ffffffffffffffff821115610ce857610ce7610472565b5b602082029050602081019050919050565b6000610d0c610d0784610ccd565b6104d2565b90508083825260208201905060208402830185811115610d2f57610d2e610ab4565b5b835b81811015610d7657805167ffffffffffffffff811115610d5457610d53610457565b5b808601610d618982610b05565b85526020850194505050602081019050610d31565b5050509392505050565b600082601f830112610d9557610d94610457565b5b8151610da5848260208601610cf9565b91505092915050565b600060208284031215610dc457610dc36103ef565b5b600082015167ffffffffffffffff811115610de257610de16103f4565b5b610dee84828501610d80565b9150509291505056fea2646970667358221220d29e8c0ffd7f95c3ae2950ad56c9ec844a4f83f78ebf290ed1f2076d3fa1537864736f6c634300080a0033" } diff --git a/e2e/contracts/testdistribute/TestDistribute.sol b/e2e/contracts/testdistribute/TestDistribute.sol index 5cf2277b88..00b384b871 100644 --- a/e2e/contracts/testdistribute/TestDistribute.sol +++ b/e2e/contracts/testdistribute/TestDistribute.sol @@ -1,43 +1,65 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.10; +struct DecCoin { + string denom; + uint256 amount; +} + // @dev Interface to interact with distribute. interface IDistribute { function distribute( address zrc20, uint256 amount ) external returns (bool success); + + function claimRewards( + address delegator, + string memory validator + ) external returns (bool success); + + function getDelegatorValidators( + address delegator + ) external view returns (string[] calldata validators); + + function getRewards( + address delegator, + string memory validator + ) external view returns (DecCoin[] calldata rewards); } // @dev Call IBank contract functions contract TestDistribute { - event Distributed( - address indexed zrc20_distributor, - address indexed zrc20_token, - uint256 amount - ); - IDistribute distr = IDistribute(0x0000000000000000000000000000000000000066); - address immutable owner; - - constructor() { - owner = msg.sender; - } + fallback() external payable {} - modifier onlyOwner() { - require(msg.sender == owner); - _; - } + receive() external payable {} function distributeThroughContract( address zrc20, uint256 amount - ) external onlyOwner returns (bool) { + ) external returns (bool) { return distr.distribute(zrc20, amount); } - fallback() external payable {} + function claimRewardsThroughContract( + address delegator, + string memory validator + ) external returns (bool) { + return distr.claimRewards(delegator, validator); + } - receive() external payable {} + function getDelegatorValidatorsThroughContract( + address delegator + ) external view returns (string[] memory) { + return distr.getDelegatorValidators(delegator); + } + + function getRewardsThroughContract( + address delegator, + string memory validator + ) external view returns (DecCoin[] memory) { + return distr.getRewards(delegator, validator); + } } diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index db99a5ce0d..2eca541adf 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -1158,7 +1158,7 @@ var AllE2ETests = []runner.E2ETest{ TestPrecompilesDistributeName, "test stateful precompiled contracts distribute", []runner.ArgDefinition{}, - TestPrecompilesDistribute, + TestPrecompilesDistributeAndClaim, ), runner.NewE2ETest( TestPrecompilesDistributeNonZRC20Name, @@ -1170,6 +1170,6 @@ var AllE2ETests = []runner.E2ETest{ TestPrecompilesDistributeThroughContractName, "test stateful precompiled contracts distribute through contract", []runner.ArgDefinition{}, - TestPrecompilesDistributeThroughContract, + TestPrecompilesDistributeAndClaimThroughContract, ), } diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 4baa6fefb5..18532f2bd6 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -17,13 +17,16 @@ import ( func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") - spender := r.EVMAddress() - bankAddress := bank.ContractAddress - zrc20Address := r.ERC20ZRC20Addr - oneThousand := big.NewInt(1e3) - oneThousandOne := big.NewInt(1001) - fiveHundred := big.NewInt(500) - fiveHundredOne := big.NewInt(501) + var ( + spender = r.EVMAddress() + bankAddress = bank.ContractAddress + zrc20Address = r.ERC20ZRC20Addr + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + zero = big.NewInt(0) + ) // Get ERC20ZRC20. txHash := r.DepositERC20WithAmountAndMessage(r.EVMAddress(), oneThousand, []byte{}) @@ -58,18 +61,18 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { }() // Check initial balances. - balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) + balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) // Deposit without previous alllowance should fail. receipt = depositThroughTestBank(r, testBank, zrc20Address, oneThousand) utils.RequiredTxFailed(r, receipt, "Deposit ERC20ZRC20 without allowance should fail") // Check balances, should be the same. - balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) + balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) // Allow 500 ZRC20 to bank precompile. approveAllowance(r, bankAddress, fiveHundred) @@ -80,9 +83,9 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) + balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) // Allow 1000 ZRC20 to bank precompile. approveAllowance(r, bankAddress, oneThousand) @@ -93,18 +96,18 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) + balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) // Deposit 500 ERC20ZRC20 tokens to the bank contract, it's within allowance and balance. Should pass. receipt = depositThroughTestBank(r, testBank, zrc20Address, fiveHundred) utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") // Balances should be transferred. Bank now locks 500 ZRC20 tokens. - balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, fiveHundred, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, spender)) + balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, bankAddress)) // Check the deposit event. eventDeposit, err := bankPrecompileCaller.ParseDeposit(*receipt.Logs[0]) @@ -118,18 +121,18 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Withdrawing an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, fiveHundred, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, spender)) + balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, bankAddress)) // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. receipt = withdrawThroughTestBank(r, testBank, zrc20Address, fiveHundred) utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") // Balances should be reverted to initial state. - balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) + balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) // Check the withdraw event. eventWithdraw, err := bankPrecompileCaller.ParseWithdraw(*receipt.Logs[0]) @@ -146,8 +149,8 @@ func approveAllowance(r *runner.E2ERunner, target common.Address, amount *big.In utils.RequireTxSuccessful(r, receipt, "Approve ERC20ZRC20 allowance tx failed") } -func balanceShouldBe(r *runner.E2ERunner, expected uint64, balance *big.Int) { - require.Equal(r, expected, balance.Uint64(), "Balance should be %d, got: %d", expected, balance.Uint64()) +func balanceShouldBe(r *runner.E2ERunner, expected *big.Int, balance *big.Int) { + require.Equal(r, expected.Uint64(), balance.Uint64(), "Balance should be %d, got: %d", expected, balance.Uint64()) } func checkZRC20Balance(r *runner.E2ERunner, target common.Address) *big.Int { diff --git a/e2e/e2etests/test_precompiles_distribute.go b/e2e/e2etests/test_precompiles_distribute.go deleted file mode 100644 index 36870e090c..0000000000 --- a/e2e/e2etests/test_precompiles_distribute.go +++ /dev/null @@ -1,239 +0,0 @@ -package e2etests - -import ( - "math/big" - - "github.com/cosmos/cosmos-sdk/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/precompiles/bank" - "github.com/zeta-chain/node/precompiles/staking" - precompiletypes "github.com/zeta-chain/node/precompiles/types" -) - -func TestPrecompilesDistribute(r *runner.E2ERunner, args []string) { - require.Len(r, args, 0, "No arguments expected") - - var ( - spenderAddress = r.EVMAddress() - distributeContractAddress = staking.ContractAddress - lockerAddress = bank.ContractAddress - - zrc20Address = r.ERC20ZRC20Addr - zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) - - oneThousand = big.NewInt(1e3) - oneThousandOne = big.NewInt(1001) - fiveHundred = big.NewInt(500) - fiveHundredOne = big.NewInt(501) - - previousGasLimit = r.ZEVMAuth.GasLimit - ) - - // Set new gas limit to avoid out of gas errors. - r.ZEVMAuth.GasLimit = 10_000_000 - - // Set the test to reset the state after it finishes. - defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) - - // Get ERC20ZRC20. - txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) - utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) - - dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) - require.NoError(r, err, "failed to create distribute contract caller") - - // DO NOT REMOVE - will be used in a subsequent PR when the ability to withdraw delegator rewards is introduced. - // Get validators through staking contract. - // validators, err := dstrContract.GetAllValidators(&bind.CallOpts{}) - // require.NoError(r, err) - - // Check initial balances. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - tx, err := dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousand) - require.NoError(r, err) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Allow 500. - approveAllowance(r, distributeContractAddress, fiveHundred) - - // Shouldn't be able to distribute more than allowed. - tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundredOne) - require.NoError(r, err) - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Raise the allowance to 1000. - approveAllowance(r, distributeContractAddress, oneThousand) - - // Shouldn't be able to distribute more than owned balance. - tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousandOne) - require.NoError(r, err) - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Should be able to distribute 500, which is within balance and allowance. - tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundred) - require.NoError(r, err) - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") - - balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) - balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) - require.NoError(r, err) - require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) - require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) - require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) - - // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. - r.WaitForBlocks(1) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // DO NOT REMOVE THE FOLLOWING CODE - // This section is commented until a following PR introduces the ability to withdraw delegator rewards. - // This validator checks will be used then to complete the whole e2e. - - // res, err := r.DistributionClient.ValidatorDistributionInfo( - // r.Ctx, - // &distributiontypes.QueryValidatorDistributionInfoRequest{ - // ValidatorAddress: validators[0].OperatorAddress, - // }, - // ) - // require.NoError(r, err) - // fmt.Printf("Validator 0 distribution info: %+v\n", res) - - // res2, err := r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ - // ValidatorAddress: validators[0].OperatorAddress, - // }) - // require.NoError(r, err) - // fmt.Printf("Validator 0 outstanding rewards: %+v\n", res2) - - // res3, err := r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ - // ValidatorAddress: validators[0].OperatorAddress, - // }) - // require.NoError(r, err) - // fmt.Printf("Validator 0 commission: %+v\n", res3) - - // // Validator 1 - // res, err = r.DistributionClient.ValidatorDistributionInfo( - // r.Ctx, - // &distributiontypes.QueryValidatorDistributionInfoRequest{ - // ValidatorAddress: validators[1].OperatorAddress, - // }, - // ) - // require.NoError(r, err) - // fmt.Printf("Validator 1 distribution info: %+v\n", res) - - // res2, err = r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ - // ValidatorAddress: validators[1].OperatorAddress, - // }) - // require.NoError(r, err) - // fmt.Printf("Validator 1 outstanding rewards: %+v\n", res2) - - // res3, err = r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ - // ValidatorAddress: validators[1].OperatorAddress, - // }) - // require.NoError(r, err) - // fmt.Printf("Validator 1 commission: %+v\n", res3) -} - -func TestPrecompilesDistributeNonZRC20(r *runner.E2ERunner, args []string) { - require.Len(r, args, 0, "No arguments expected") - - // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. - previousGasLimit := r.ZEVMAuth.GasLimit - r.ZEVMAuth.GasLimit = 10_000_000 - defer func() { - r.ZEVMAuth.GasLimit = previousGasLimit - }() - - spender, dstrAddress := r.EVMAddress(), staking.ContractAddress - - // Create a staking contract caller. - dstrContract, err := staking.NewIStaking(dstrAddress, r.ZEVMClient) - require.NoError(r, err, "Failed to create staking contract caller") - - // Deposit and approve 50 WZETA for the test. - approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) - r.DepositAndApproveWZeta(approveAmount) - - // Allow the staking contract to spend 25 WZeta tokens. - tx, err := r.WZeta.Approve(r.ZEVMAuth, dstrAddress, big.NewInt(25)) - require.NoError(r, err, "Error approving allowance for staking contract") - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") - - // Check the allowance of the staking in WZeta tokens. Should be 25. - allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, dstrAddress) - require.NoError(r, err, "Error retrieving staking allowance") - require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for staking contract") - - // Call Distribute with 25 Non ZRC20 tokens. Should fail. - tx, err = dstrContract.Distribute(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) - require.NoError(r, err, "Error calling staking.distribute()") - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") -} - -// checkCosmosBalance checks the cosmos coin balance for an address. The coin is specified by its denom. -func checkCosmosBalance(r *runner.E2ERunner, address types.AccAddress, denom string) *big.Int { - bal, err := r.BankClient.Balance( - r.Ctx, - &banktypes.QueryBalanceRequest{Address: address.String(), Denom: denom}, - ) - require.NoError(r, err) - - return bal.Balance.Amount.BigInt() -} - -func resetDistributionTest( - r *runner.E2ERunner, - lockerAddress common.Address, - previousGasLimit uint64, - amount *big.Int, -) { - r.ZEVMAuth.GasLimit = previousGasLimit - - // Reset the allowance to 0; this is needed when running upgrade tests where this test runs twice. - tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, lockerAddress, big.NewInt(0)) - require.NoError(r, err) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "Resetting allowance failed") - - // Reset balance to 0 for spender; this is needed when running upgrade tests where this test runs twice. - tx, err = r.ERC20ZRC20.Transfer( - r.ZEVMAuth, - common.HexToAddress("0x000000000000000000000000000000000000dEaD"), - amount, - ) - require.NoError(r, err) - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") -} diff --git a/e2e/e2etests/test_precompiles_distribute_and_claim.go b/e2e/e2etests/test_precompiles_distribute_and_claim.go new file mode 100644 index 0000000000..91aa81fc44 --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute_and_claim.go @@ -0,0 +1,342 @@ +package e2etests + +import ( + "math/big" + "strings" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistributeAndClaim(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + // Addresses. + staker = r.EVMAddress() + distrContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + // Stake amount. + stakeAmt = new(big.Int) + + // ZRC20 distribution. + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + zrc20DistrAmt = big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(1e6)) + + // Amounts to test with. + higherThanBalance = big.NewInt(0).Add(zrc20DistrAmt, big.NewInt(1)) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + zero = big.NewInt(0) + stake = "1000000000000000000000" + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // stakeAmt has to be as big as the validator self delegation. + // This way the rewards will be distributed 50%. + _, ok := stakeAmt.SetString(stake, 10) + require.True(r, ok) + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + distrContract, err := staking.NewIStaking(distrContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + // Retrieve the list of validators. + validators, err := distrContract.GetAllValidators(&bind.CallOpts{}) + require.NoError(r, err) + require.GreaterOrEqual(r, len(validators), 2) + + // Save first validator bech32 address and as it will be used through the test. + validatorAddr, validatorValAddr := getValidatorAddresses(r, distrContract) + + // Reset the test after it finishes. + defer resetDistributionTest(r, distrContract, lockerAddress, previousGasLimit, staker, validatorValAddr) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(staker, zrc20DistrAmt, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + // There is no delegation, so the response should be empty. + dv, err := distrContract.GetDelegatorValidators(&bind.CallOpts{}, staker) + require.NoError(r, err) + require.Empty(r, dv, "DelegatorValidators response should be empty") + + // Shares at this point should be 0. + sharesBefore, err := distrContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validatorAddr) + require.NoError(r, err) + require.Equal(r, int64(0), sharesBefore.Int64(), "shares should be 0 when there are no delegations") + + // There should be no rewards. + rewards, err := distrContract.GetRewards(&bind.CallOpts{}, staker, validatorAddr) + require.NoError(r, err) + require.Empty(r, rewards, "rewards should be empty when there are no delegations") + + // Stake with spender so it's registered as a delegator. + err = stakeThroughCosmosAPI(r, validatorValAddr, staker, stakeAmt) + require.NoError(r, err) + + // Check initial balances. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zero, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Failed attempt! + tx, err := distrContract.Distribute(r.ZEVMAuth, zrc20Address, zrc20DistrAmt) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zero, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distrContractAddress, fiveHundred) + + // Failed attempt! Shouldn't be able to distribute more than allowed. + tx, err = distrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundredOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zero, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to the maximum ZRC20 amount. + approveAllowance(r, distrContractAddress, zrc20DistrAmt) + + // Failed attempt! Shouldn't be able to distribute more than owned balance. + tx, err = distrContract.Distribute(r.ZEVMAuth, zrc20Address, higherThanBalance) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zero, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Should be able to distribute an amount which is within balance and allowance. + tx, err = distrContract.Distribute(r.ZEVMAuth, zrc20Address, zrc20DistrAmt) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, zero, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zrc20DistrAmt, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := distrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, staker, eventDitributed.Zrc20Distributor) + require.Equal(r, zrc20DistrAmt.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // DelegatorValidators returns the list of validator this delegator has delegated to. + // The result should include the validator address. + dv, err = distrContract.GetDelegatorValidators(&bind.CallOpts{}, staker) + require.NoError(r, err) + require.Contains(r, dv, validatorAddr, "DelegatorValidators response should include validator address") + + // Get rewards and check it contains zrc20 tokens. + rewards, err = distrContract.GetRewards(&bind.CallOpts{}, staker, validatorAddr) + require.NoError(r, err) + require.GreaterOrEqual(r, len(rewards), 2) + found := false + for _, coin := range rewards { + if strings.Contains(coin.Denom, config.ZRC20DenomPrefix) { + found = true + break + } + } + require.True(r, found, "rewards should include the ZRC20 token") + + // Claim the rewards, they'll be unlocked as ZRC20 tokens. + tx, err = distrContract.ClaimRewards(r.ZEVMAuth, staker, validatorAddr) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "claim rewards should succeed") + + // Before claiming rewards the ZRC20 balance is 0. After claiming rewards the ZRC20 balance should be 14239697290875601808. + // Which is the amount of ZRC20 distributed, divided by two validators, and subtracted the commissions. + zrc20RewardsAmt, ok := big.NewInt(0).SetString("14239697290875601808", 10) + require.True(r, ok) + balanceShouldBe(r, zrc20RewardsAmt, checkZRC20Balance(r, staker)) + + eventClaimed, err := distrContract.ParseClaimedRewards(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventClaimed.Zrc20Token) + require.Equal(r, staker, eventClaimed.ClaimAddress) + require.Equal(r, common.BytesToAddress(validatorValAddr.Bytes()), eventClaimed.Validator) + require.Equal(r, zrc20RewardsAmt.Uint64(), eventClaimed.Amount.Uint64()) + + // Locker final balance should be zrc20Disitributed - zrc20RewardsAmt. + lockerFinalBalance := big.NewInt(0).Sub(zrc20DistrAmt, zrc20RewardsAmt) + balanceShouldBe(r, lockerFinalBalance, checkZRC20Balance(r, lockerAddress)) + + // Staker final cosmos balance should be 0. + balanceShouldBe(r, zero, checkCosmosBalance(r, sdk.AccAddress(staker.Bytes()), zrc20Denom)) +} + +func TestPrecompilesDistributeNonZRC20(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + + spender, dstrAddress := r.EVMAddress(), staking.ContractAddress + + // Create a staking contract caller. + dstrContract, err := staking.NewIStaking(dstrAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create staking contract caller") + + // Deposit and approve 50 WZETA for the test. + approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) + r.DepositAndApproveWZeta(approveAmount) + + // Allow the staking contract to spend 25 WZeta tokens. + tx, err := r.WZeta.Approve(r.ZEVMAuth, dstrAddress, big.NewInt(25)) + require.NoError(r, err, "Error approving allowance for staking contract") + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") + + // Check the allowance of the staking in WZeta tokens. Should be 25. + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, dstrAddress) + require.NoError(r, err, "Error retrieving staking allowance") + require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for staking contract") + + // Call Distribute with 25 Non ZRC20 tokens. Should fail. + tx, err = dstrContract.Distribute(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + require.NoError(r, err, "Error calling staking.distribute()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") +} + +// checkCosmosBalance checks the cosmos coin balance for an address. The coin is specified by its denom. +func checkCosmosBalance(r *runner.E2ERunner, address sdk.AccAddress, denom string) *big.Int { + bal, err := r.BankClient.Balance( + r.Ctx, + &banktypes.QueryBalanceRequest{Address: address.String(), Denom: denom}, + ) + require.NoError(r, err) + + return bal.Balance.Amount.BigInt() +} + +func stakeThroughCosmosAPI( + r *runner.E2ERunner, + validator sdk.ValAddress, + staker common.Address, + amount *big.Int, +) error { + msg := stakingtypes.NewMsgDelegate( + sdk.AccAddress(staker.Bytes()), + validator, + sdk.Coin{ + Denom: config.BaseDenom, + Amount: math.NewIntFromBigInt(amount), + }, + ) + + _, err := r.ZetaTxServer.BroadcastTx(sdk.AccAddress(staker.Bytes()).String(), msg) + if err != nil { + return err + } + + return nil +} + +func resetDistributionTest( + r *runner.E2ERunner, + distrContract *staking.IStaking, + lockerAddress common.Address, + previousGasLimit uint64, + staker common.Address, + validator sdk.ValAddress, +) { + validatorAddr, _ := getValidatorAddresses(r, distrContract) + + amount, err := distrContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validatorAddr) + require.NoError(r, err) + + // Restore the gas limit. + r.ZEVMAuth.GasLimit = previousGasLimit + + // Reset the allowance to 0; this is needed when running upgrade tests where this test runs twice. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, lockerAddress, big.NewInt(0)) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "resetting allowance failed") + + // Reset balance to 0 for spender; this is needed when running upgrade tests where this test runs twice. + balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + + // Burn all ERC20 balance. + tx, err = r.ERC20ZRC20.Transfer( + r.ZEVMAuth, + common.HexToAddress("0x000000000000000000000000000000000000dEaD"), + balance, + ) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") + + // Clean the delegation. + // Delegator will always delegate on the first validator. + msg := stakingtypes.NewMsgUndelegate( + sdk.AccAddress(staker.Bytes()), + validator, + sdk.Coin{ + Denom: config.BaseDenom, + Amount: math.NewIntFromBigInt(amount.Div(amount, big.NewInt(1e18))), + }, + ) + + _, err = r.ZetaTxServer.BroadcastTx(sdk.AccAddress(staker.Bytes()).String(), msg) + require.NoError(r, err) +} + +func getValidatorAddresses(r *runner.E2ERunner, distrContract *staking.IStaking) (string, sdk.ValAddress) { + // distrContract, err := staking.NewIStaking(staking.ContractAddress, r.ZEVMClient) + // require.NoError(r, err, "failed to create distribute contract caller") + + // Retrieve the list of validators. + validators, err := distrContract.GetAllValidators(&bind.CallOpts{}) + require.NoError(r, err) + require.GreaterOrEqual(r, len(validators), 2) + + // Save first validators as it will be used through the test. + validatorAddr, err := sdk.ValAddressFromBech32(validators[0].OperatorAddress) + require.NoError(r, err) + + return validators[0].OperatorAddress, validatorAddr +} diff --git a/e2e/e2etests/test_precompiles_distribute_and_claim_through_contract.go b/e2e/e2etests/test_precompiles_distribute_and_claim_through_contract.go new file mode 100644 index 0000000000..284185de77 --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute_and_claim_through_contract.go @@ -0,0 +1,204 @@ +package e2etests + +import ( + "math/big" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/e2e/contracts/testdistribute" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistributeAndClaimThroughContract(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + // Addresses. + staker = r.EVMAddress() + distrContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + // Stake amount. + stakeAmt = new(big.Int) + + // ZRC20 distribution. + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + zrc20DistrAmt = big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(1e6)) + + // carry is carried from the TestPrecompilesDistributeName test. It's applicable only to locker address. + // This is needed because there's no easy way to retrieve that balance from the locker. + carry = big.NewInt(6210810988040846448) + zrc20DistrAmtCarry = new(big.Int).Add(zrc20DistrAmt, carry) + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + zero = big.NewInt(0) + stake = "1000000000000000000000" + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // stakeAmt has to be as big as the validator self delegation. + // This way the rewards will be distributed 50%. + _, ok := stakeAmt.SetString(stake, 10) + require.True(r, ok) + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + distrContract, err := staking.NewIStaking(distrContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + // testDstrContract is the dApp contract that uses the staking precompile under the hood. + _, tx, testDstrContract, err := testdistribute.DeployTestDistribute(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "deployment of disitributor caller contract failed") + + // Save first validator bech32 address and ValAddress as it will be used through the test. + validatorAddr, validatorValAddr := getValidatorAddresses(r, distrContract) + + // Reset the test after it finishes. + defer resetDistributionTest(r, distrContract, lockerAddress, previousGasLimit, staker, validatorValAddr) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(staker, zrc20DistrAmt, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + // There is no delegation, so the response should be empty. + dv, err := testDstrContract.GetDelegatorValidatorsThroughContract( + &bind.CallOpts{}, + staker, + ) + require.NoError(r, err) + require.Empty(r, dv, "DelegatorValidators response should be empty") + + // There should be no rewards. + rewards, err := testDstrContract.GetRewardsThroughContract(&bind.CallOpts{}, staker, validatorAddr) + require.NoError(r, err) + require.Empty(r, rewards, "rewards should be empty when there are no delegations") + + // Stake with spender so it's registered as a delegator. + err = stakeThroughCosmosAPI(r, validatorValAddr, staker, stakeAmt) + require.NoError(r, err) + + // Check initial balances. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, carry, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + tx, err = testDstrContract.DistributeThroughContract(r.ZEVMAuth, zrc20Address, oneThousand) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, carry, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distrContractAddress, fiveHundred) + + tx, err = testDstrContract.DistributeThroughContract(r.ZEVMAuth, zrc20Address, fiveHundredOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, carry, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to 1000. + approveAllowance(r, distrContractAddress, oneThousand) + + // Shouldn't be able to distribute more than owned balance. + tx, err = testDstrContract.DistributeThroughContract(r.ZEVMAuth, zrc20Address, oneThousandOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, zrc20DistrAmt, checkZRC20Balance(r, staker)) + balanceShouldBe(r, carry, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to max tokens. + approveAllowance(r, distrContractAddress, zrc20DistrAmt) + + // Should be able to distribute an amount which is within balance and allowance. + tx, err = testDstrContract.DistributeThroughContract(r.ZEVMAuth, zrc20Address, zrc20DistrAmt) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, zero, checkZRC20Balance(r, staker)) + balanceShouldBe(r, zrc20DistrAmtCarry, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, zrc20DistrAmt, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := distrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, staker, eventDitributed.Zrc20Distributor) + require.Equal(r, zrc20DistrAmt.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, zero, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // DelegatorValidators returns the list of validator this delegator has delegated to. + // The result should include the validator address. + dv, err = testDstrContract.GetDelegatorValidatorsThroughContract(&bind.CallOpts{}, staker) + require.NoError(r, err) + require.Contains(r, dv, validatorAddr, "DelegatorValidators response should include validator address") + + // Get rewards and check it contains zrc20 tokens. + rewards, err = testDstrContract.GetRewardsThroughContract(&bind.CallOpts{}, staker, validatorAddr) + require.NoError(r, err) + require.GreaterOrEqual(r, len(rewards), 2) + found := false + for _, coin := range rewards { + if strings.Contains(coin.Denom, config.ZRC20DenomPrefix) { + found = true + break + } + } + require.True(r, found, "rewards should include the ZRC20 token") + + tx, err = testDstrContract.ClaimRewardsThroughContract(r.ZEVMAuth, staker, validatorAddr) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "claim rewards should succeed") + + // Before claiming rewards the ZRC20 balance is 0. After claiming rewards the ZRC20 balance should be 14239697290875601808. + // Which is the amount of ZRC20 distributed, divided by two validators, and subtracted the commissions. + zrc20RewardsAmt, ok := big.NewInt(0).SetString("14239697290875601808", 10) + require.True(r, ok) + balanceShouldBe(r, zrc20RewardsAmt, checkZRC20Balance(r, staker)) + + eventClaimed, err := distrContract.ParseClaimedRewards(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventClaimed.Zrc20Token) + require.Equal(r, staker, eventClaimed.ClaimAddress) + require.Equal(r, common.BytesToAddress(validatorValAddr.Bytes()), eventClaimed.Validator) + require.Equal(r, zrc20RewardsAmt.Uint64(), eventClaimed.Amount.Uint64()) + + // Locker final balance should be zrc20Distributed with carry - zrc20RewardsAmt. + lockerFinalBalance := big.NewInt(0).Sub(zrc20DistrAmtCarry, zrc20RewardsAmt) + balanceShouldBe(r, lockerFinalBalance, checkZRC20Balance(r, lockerAddress)) + + // Staker final cosmos balance should be 0. + balanceShouldBe(r, zero, checkCosmosBalance(r, sdk.AccAddress(staker.Bytes()), zrc20Denom)) +} diff --git a/e2e/e2etests/test_precompiles_distribute_through_contract.go b/e2e/e2etests/test_precompiles_distribute_through_contract.go deleted file mode 100644 index 444d8e6a59..0000000000 --- a/e2e/e2etests/test_precompiles_distribute_through_contract.go +++ /dev/null @@ -1,120 +0,0 @@ -package e2etests - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/e2e/contracts/testdistribute" - "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/precompiles/bank" - "github.com/zeta-chain/node/precompiles/staking" - precompiletypes "github.com/zeta-chain/node/precompiles/types" -) - -func TestPrecompilesDistributeThroughContract(r *runner.E2ERunner, args []string) { - require.Len(r, args, 0, "No arguments expected") - - var ( - spenderAddress = r.EVMAddress() - distributeContractAddress = staking.ContractAddress - lockerAddress = bank.ContractAddress - - zrc20Address = r.ERC20ZRC20Addr - zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) - - oneThousand = big.NewInt(1e3) - oneThousandOne = big.NewInt(1001) - fiveHundred = big.NewInt(500) - fiveHundredOne = big.NewInt(501) - - previousGasLimit = r.ZEVMAuth.GasLimit - ) - - // Get ERC20ZRC20. - txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) - utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) - - dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) - require.NoError(r, err, "failed to create distribute contract caller") - - _, tx, testDstrContract, err := testdistribute.DeployTestDistribute(r.ZEVMAuth, r.ZEVMClient) - require.NoError(r, err) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "deployment of disitributor caller contract failed") - - // Set new gas limit to avoid out of gas errors. - r.ZEVMAuth.GasLimit = 10_000_000 - - // Set the test to reset the state after it finishes. - defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) - - // Check initial balances. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousand) - utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Allow 500. - approveAllowance(r, distributeContractAddress, fiveHundred) - - receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundredOne) - utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Raise the allowance to 1000. - approveAllowance(r, distributeContractAddress, oneThousand) - - // Shouldn't be able to distribute more than owned balance. - receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousandOne) - utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") - - // Balances shouldn't change after a failed attempt. - balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - // Should be able to distribute 500, which is within balance and allowance. - receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundred) - utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") - - balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) - balanceShouldBe(r, 1000, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. - balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) - - eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) - require.NoError(r, err) - require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) - require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) - require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) - - // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. - r.WaitForBlocks(1) - balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) -} - -func distributeThroughContract( - r *runner.E2ERunner, - dstr *testdistribute.TestDistribute, - zrc20Address common.Address, - amount *big.Int, -) *types.Receipt { - tx, err := dstr.DistributeThroughContract(r.ZEVMAuth, zrc20Address, amount) - require.NoError(r, err) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - return receipt -} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 43ba2a492f..48edc76ac5 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -80,7 +80,7 @@ func (c *Contract) deposit( // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". // - Mint coins to the fungible module. // - Send coins from fungible to the caller. - coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) + coinSet, err := precompiletypes.CreateZRC20CoinSet(zrc20Addr, amount) if err != nil { return nil, err } diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 24f83bf5e4..642e113047 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -79,7 +79,7 @@ func (c *Contract) withdraw( } } - coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) + coinSet, err := precompiletypes.CreateZRC20CoinSet(zrc20Addr, amount) if err != nil { return nil, err } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 24d574695d..3c9a066812 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -5,6 +5,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -31,6 +32,7 @@ func StatefulContracts( fungibleKeeper *fungiblekeeper.Keeper, stakingKeeper *stakingkeeper.Keeper, bankKeeper bankkeeper.Keeper, + distributionKeeper distrkeeper.Keeper, cdc codec.Codec, gasConfig storetypes.GasConfig, ) (precompiledContracts []evmkeeper.CustomContractFn) { @@ -50,7 +52,15 @@ func StatefulContracts( // Define the staking contract function. if EnabledStatefulContracts[staking.ContractAddress] { stakingContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.StatefulPrecompiledContract { - return staking.NewIStakingContract(ctx, stakingKeeper, *fungibleKeeper, bankKeeper, cdc, gasConfig) + return staking.NewIStakingContract( + ctx, + stakingKeeper, + *fungibleKeeper, + bankKeeper, + distributionKeeper, + cdc, + gasConfig, + ) } // Append the staking contract to the precompiledContracts slice. diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go index a0b55572ea..b60d65c314 100644 --- a/precompiles/precompiles_test.go +++ b/precompiles/precompiles_test.go @@ -25,7 +25,14 @@ func Test_StatefulContracts(t *testing.T) { } // StatefulContracts() should return all the enabled contracts. - contracts := StatefulContracts(k, &sdkk.StakingKeeper, sdkk.BankKeeper, appCodec, gasConfig) + contracts := StatefulContracts( + k, + &sdkk.StakingKeeper, + sdkk.BankKeeper, + sdkk.DistributionKeeper, + appCodec, + gasConfig, + ) require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") diff --git a/precompiles/staking/IStaking.abi b/precompiles/staking/IStaking.abi index da1a9e6ffc..76028d79f3 100644 --- a/precompiles/staking/IStaking.abi +++ b/precompiles/staking/IStaking.abi @@ -1,4 +1,35 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "claim_address", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimedRewards", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -105,6 +136,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewards", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -164,6 +219,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidators", + "outputs": [ + { + "internalType": "string[]", + "name": "validators", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewards", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "rewards", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/precompiles/staking/IStaking.gen.go b/precompiles/staking/IStaking.gen.go index d4f7495d37..3a6e1bc759 100644 --- a/precompiles/staking/IStaking.gen.go +++ b/precompiles/staking/IStaking.gen.go @@ -29,6 +29,12 @@ var ( _ = abi.ConvertType ) +// DecCoin is an auto generated low-level Go binding around an user-defined struct. +type DecCoin struct { + Denom string + Amount *big.Int +} + // Validator is an auto generated low-level Go binding around an user-defined struct. type Validator struct { OperatorAddress string @@ -39,7 +45,7 @@ type Validator struct { // IStakingMetaData contains all meta data concerning the IStaking contract. var IStakingMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distribute\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claim_address\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"ClaimedRewards\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"claimRewards\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distribute\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"}],\"name\":\"getDelegatorValidators\",\"outputs\":[{\"internalType\":\"string[]\",\"name\":\"validators\",\"type\":\"string[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getRewards\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"denom\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structDecCoin[]\",\"name\":\"rewards\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IStakingABI is the input ABI used to generate the binding from. @@ -219,6 +225,68 @@ func (_IStaking *IStakingCallerSession) GetAllValidators() ([]Validator, error) return _IStaking.Contract.GetAllValidators(&_IStaking.CallOpts) } +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingCaller) GetDelegatorValidators(opts *bind.CallOpts, delegator common.Address) ([]string, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getDelegatorValidators", delegator) + + if err != nil { + return *new([]string), err + } + + out0 := *abi.ConvertType(out[0], new([]string)).(*[]string) + + return out0, err + +} + +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingSession) GetDelegatorValidators(delegator common.Address) ([]string, error) { + return _IStaking.Contract.GetDelegatorValidators(&_IStaking.CallOpts, delegator) +} + +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingCallerSession) GetDelegatorValidators(delegator common.Address) ([]string, error) { + return _IStaking.Contract.GetDelegatorValidators(&_IStaking.CallOpts, delegator) +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingCaller) GetRewards(opts *bind.CallOpts, delegator common.Address, validator string) ([]DecCoin, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getRewards", delegator, validator) + + if err != nil { + return *new([]DecCoin), err + } + + out0 := *abi.ConvertType(out[0], new([]DecCoin)).(*[]DecCoin) + + return out0, err + +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingSession) GetRewards(delegator common.Address, validator string) ([]DecCoin, error) { + return _IStaking.Contract.GetRewards(&_IStaking.CallOpts, delegator, validator) +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingCallerSession) GetRewards(delegator common.Address, validator string) ([]DecCoin, error) { + return _IStaking.Contract.GetRewards(&_IStaking.CallOpts, delegator, validator) +} + // GetShares is a free data retrieval call binding the contract method 0x0d1b3daf. // // Solidity: function getShares(address staker, string validator) view returns(uint256 shares) @@ -250,6 +318,27 @@ func (_IStaking *IStakingCallerSession) GetShares(staker common.Address, validat return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) } +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingTransactor) ClaimRewards(opts *bind.TransactOpts, delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "claimRewards", delegator, validator) +} + +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingSession) ClaimRewards(delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.Contract.ClaimRewards(&_IStaking.TransactOpts, delegator, validator) +} + +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingTransactorSession) ClaimRewards(delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.Contract.ClaimRewards(&_IStaking.TransactOpts, delegator, validator) +} + // Distribute is a paid mutator transaction binding the contract method 0xfb932108. // // Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) @@ -334,6 +423,169 @@ func (_IStaking *IStakingTransactorSession) Unstake(staker common.Address, valid return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) } +// IStakingClaimedRewardsIterator is returned from FilterClaimedRewards and is used to iterate over the raw logs and unpacked data for ClaimedRewards events raised by the IStaking contract. +type IStakingClaimedRewardsIterator struct { + Event *IStakingClaimedRewards // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IStakingClaimedRewardsIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IStakingClaimedRewards) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IStakingClaimedRewards) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IStakingClaimedRewardsIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IStakingClaimedRewardsIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IStakingClaimedRewards represents a ClaimedRewards event raised by the IStaking contract. +type IStakingClaimedRewards struct { + ClaimAddress common.Address + Zrc20Token common.Address + Validator common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterClaimedRewards is a free log retrieval operation binding the contract event 0xfad55f843dbd67b821d107dd22535d77fb9384daa21dc35a976588f81997b7b3. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, address indexed validator, uint256 amount) +func (_IStaking *IStakingFilterer) FilterClaimedRewards(opts *bind.FilterOpts, claim_address []common.Address, zrc20_token []common.Address, validator []common.Address) (*IStakingClaimedRewardsIterator, error) { + + var claim_addressRule []interface{} + for _, claim_addressItem := range claim_address { + claim_addressRule = append(claim_addressRule, claim_addressItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var validatorRule []interface{} + for _, validatorItem := range validator { + validatorRule = append(validatorRule, validatorItem) + } + + logs, sub, err := _IStaking.contract.FilterLogs(opts, "ClaimedRewards", claim_addressRule, zrc20_tokenRule, validatorRule) + if err != nil { + return nil, err + } + return &IStakingClaimedRewardsIterator{contract: _IStaking.contract, event: "ClaimedRewards", logs: logs, sub: sub}, nil +} + +// WatchClaimedRewards is a free log subscription operation binding the contract event 0xfad55f843dbd67b821d107dd22535d77fb9384daa21dc35a976588f81997b7b3. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, address indexed validator, uint256 amount) +func (_IStaking *IStakingFilterer) WatchClaimedRewards(opts *bind.WatchOpts, sink chan<- *IStakingClaimedRewards, claim_address []common.Address, zrc20_token []common.Address, validator []common.Address) (event.Subscription, error) { + + var claim_addressRule []interface{} + for _, claim_addressItem := range claim_address { + claim_addressRule = append(claim_addressRule, claim_addressItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var validatorRule []interface{} + for _, validatorItem := range validator { + validatorRule = append(validatorRule, validatorItem) + } + + logs, sub, err := _IStaking.contract.WatchLogs(opts, "ClaimedRewards", claim_addressRule, zrc20_tokenRule, validatorRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IStakingClaimedRewards) + if err := _IStaking.contract.UnpackLog(event, "ClaimedRewards", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseClaimedRewards is a log parse operation binding the contract event 0xfad55f843dbd67b821d107dd22535d77fb9384daa21dc35a976588f81997b7b3. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, address indexed validator, uint256 amount) +func (_IStaking *IStakingFilterer) ParseClaimedRewards(log types.Log) (*IStakingClaimedRewards, error) { + event := new(IStakingClaimedRewards) + if err := _IStaking.contract.UnpackLog(event, "ClaimedRewards", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // IStakingDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the IStaking contract. type IStakingDistributedIterator struct { Event *IStakingDistributed // Event containing the contract specifics and raw log diff --git a/precompiles/staking/IStaking.json b/precompiles/staking/IStaking.json index d4e0bb75f0..c781eee11d 100644 --- a/precompiles/staking/IStaking.json +++ b/precompiles/staking/IStaking.json @@ -1,5 +1,36 @@ { "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "claim_address", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimedRewards", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -106,6 +137,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewards", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -165,6 +220,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidators", + "outputs": [ + { + "internalType": "string[]", + "name": "validators", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewards", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "rewards", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/precompiles/staking/IStaking.sol b/precompiles/staking/IStaking.sol index dece711d71..c673531b0e 100644 --- a/precompiles/staking/IStaking.sol +++ b/precompiles/staking/IStaking.sol @@ -23,6 +23,13 @@ struct Validator { BondStatus bondStatus; } +/// @notice Cosmos coin representation. +/// ref: https://github.com/cosmos/cosmos-sdk/blob/470e0859462b28a53adb411843539561d11d7bf5/x/distribution/README.md?plain=1#L139 +struct DecCoin { + string denom; + uint256 amount; +} + interface IStaking { /// @notice Stake event is emitted when stake function is called /// @param staker Staker address @@ -66,6 +73,18 @@ interface IStaking { uint256 amount ); + /// @notice ClaimedRewards is emitted when a delegator claims ZRC20. + /// @param claim_address Delegator address where the funds were withdrawed. + /// @param zrc20_token ZRC20 token address. + /// @param validator Validator address. + /// @param amount Claimed amount. + event ClaimedRewards( + address indexed claim_address, + address indexed zrc20_token, + address indexed validator, + uint256 amount + ); + /// @notice Stake coins to validator /// @param staker Staker address /// @param validator Validator address @@ -123,4 +142,28 @@ interface IStaking { address zrc20, uint256 amount ) external returns (bool success); + + /// @notice Claim ZRC20 staking rewards. + /// @param validator The validator address to claim rewards from. + /// @return success Boolean indicating whether the claim was successful. + function claimRewards( + address delegator, + string memory validator + ) external returns (bool success); + + /// @dev Queries all validators the delegator has delegated to. + /// @param delegator The delegator address to query rewards from. + /// @return validators List of the validators the caller has delegated to. + function getDelegatorValidators( + address delegator + ) external view returns (string[] calldata validators); + + /// @notice Query ZRC20 outstanding staking rewards. + /// @param delegator The delegator address to query rewards from. + /// @param validator The validator address to query rewards from. + /// @return rewards The list of coins rewarded on the validator. + function getRewards( + address delegator, + string memory validator + ) external view returns (DecCoin[] calldata rewards); } diff --git a/precompiles/staking/const.go b/precompiles/staking/const.go index 8500e723f4..a12d7070e1 100644 --- a/precompiles/staking/const.go +++ b/precompiles/staking/const.go @@ -1,17 +1,15 @@ package staking const ( + // State changing methods. + ClaimRewardsMethodName = "claimRewards" + ClaimRewardsEventName = "ClaimedRewards" + ClaimRewardsMethodGas = 10000 + DistributeMethodName = "distribute" DistributeEventName = "Distributed" DistributeMethodGas = 10000 - GetAllValidatorsMethodName = "getAllValidators" - GetSharesMethodName = "getShares" - - MoveStakeMethodName = "moveStake" - MoveStakeEventName = "MoveStake" - MoveStakeMethodGas = 10000 - StakeMethodName = "stake" StakeEventName = "Stake" StakeMethodGas = 10000 @@ -19,4 +17,14 @@ const ( UnstakeMethodName = "unstake" UnstakeEventName = "Unstake" UnstakeMethodGas = 1000 + + MoveStakeMethodName = "moveStake" + MoveStakeEventName = "MoveStake" + MoveStakeMethodGas = 10000 + + // Query methods. + GetAllValidatorsMethodName = "getAllValidators" + GetSharesMethodName = "getShares" + GetRewardsMethodName = "getRewards" + GetValidatorsMethodName = "getDelegatorValidators" ) diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index c8d1db24e2..cb07c5cf08 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -147,3 +147,35 @@ func (c *Contract) addDistributeLog( return nil } + +func (c *Contract) addClaimRewardsLog( + ctx sdk.Context, + stateDB vm.StateDB, + delegator common.Address, + zrc20Token common.Address, + validator sdk.ValAddress, + amount *big.Int, +) error { + event := c.Abi().Events[ClaimRewardsEventName] + + topics, err := logs.MakeTopics( + event, + []interface{}{delegator}, + []interface{}{zrc20Token}, + []interface{}{common.BytesToAddress(validator.Bytes())}, + ) + if err != nil { + return err + } + + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} diff --git a/precompiles/staking/method_claim_rewards.go b/precompiles/staking/method_claim_rewards.go new file mode 100644 index 0000000000..963e36b884 --- /dev/null +++ b/precompiles/staking/method_claim_rewards.go @@ -0,0 +1,150 @@ +package staking + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/precompiles/bank" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// claimRewards claims all the rewards for a delegator from a validator. +// As F1 Cosmos distribution scheme implements an all or nothing withdrawal, the precompile will +// withdraw all the rewards for the delegator, filter ZRC20 and unlock them to the delegator EVM address. +func (c *Contract) claimRewards( + ctx sdk.Context, + evm *vm.EVM, + _ *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + delegatorAddr, validatorAddr, err := unpackClaimRewardsArgs(args) + if err != nil { + return nil, err + } + + // Get delegator Cosmos address. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Get validator Cosmos address. + validatorCosmosAddr, err := sdk.ValAddressFromBech32(validatorAddr) + if err != nil { + return nil, err + } + + // Withdraw all the delegation rewards. + // The F1 Cosmos distribution scheme implements an all or nothing withdrawal. + // The coins could be of multiple denomination, and a mix of ZRC20 and Cosmos coins. + coins, err := c.distributionKeeper.WithdrawDelegationRewards(ctx, delegatorCosmosAddr, validatorCosmosAddr) + if err != nil { + return nil, precompiletypes.ErrUnexpected{ + When: "WithdrawDelegationRewards", + Got: err.Error(), + } + } + + // For all the ZRC20 coins withdrawed: + // - Check the amount to unlock is valid. + // - Burn the Cosmos coins. + // - Unlock the ZRC20 coins. + for _, coin := range coins { + // Filter out invalid coins. + if !coin.IsValid() || !coin.Amount.IsPositive() || !precompiletypes.CoinIsZRC20(coin.Denom) { + continue + } + + // Notice that instead of returning errors we just skip the coin. This is because there might be + // more than one ZRC20 coin in the delegation rewards, and we want to unlock as many as possible. + // Coins are locked in the bank precompile, so it should be possible to unlock them afterwards. + var ( + zrc20Addr = common.HexToAddress(strings.TrimPrefix(coin.Denom, config.ZRC20DenomPrefix)) + zrc20Amount = coin.Amount.BigInt() + ) + + // Check if bank address has enough ZRC20 balance. + // This check is also made inside UnlockZRC20, but repeat it here to avoid burning the coins. + if err := c.fungibleKeeper.CheckZRC20Balance(ctx, zrc20Addr, bank.ContractAddress, zrc20Amount); err != nil { + ctx.Logger().Error( + "Claimed invalid amount of ZRC20 Validator Rewards", + "Total", zrc20Amount, + "Denom", precompiletypes.ZRC20ToCosmosDenom(zrc20Addr), + ) + + continue + } + + coinSet := sdk.NewCoins(coin) + + // Send the coins to the fungible module to burn them. + if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorCosmosAddr, fungibletypes.ModuleName, coinSet); err != nil { + continue + } + + if err := c.bankKeeper.BurnCoins(ctx, fungibletypes.ModuleName, coinSet); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "BurnCoins", + Got: err.Error(), + } + } + + // Finally, unlock the ZRC20 coins. + if err := c.fungibleKeeper.UnlockZRC20(ctx, zrc20Addr, delegatorAddr, bank.ContractAddress, zrc20Amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "UnlockZRC20", + Got: err.Error(), + } + } + + // Emit an event per ZRC20 coin unlocked. + // This keeps events as granular and deterministic as possible. + if err := c.addClaimRewardsLog(ctx, evm.StateDB, delegatorAddr, zrc20Addr, validatorCosmosAddr, zrc20Amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "AddClaimRewardLog", + Got: err.Error(), + } + } + + ctx.Logger().Debug( + "Claimed ZRC20 rewards", + "Delegator", delegatorCosmosAddr, + "Denom", precompiletypes.ZRC20ToCosmosDenom(zrc20Addr), + "Amount", coin.Amount, + ) + } + + return method.Outputs.Pack(true) +} + +func unpackClaimRewardsArgs(args []interface{}) (delegator common.Address, validator string, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + validator, ok = args[1].(string) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: validator, + } + } + + return delegator, validator, nil +} diff --git a/precompiles/staking/method_claim_rewards_test.go b/precompiles/staking/method_claim_rewards_test.go new file mode 100644 index 0000000000..3ff8222757 --- /dev/null +++ b/precompiles/staking/method_claim_rewards_test.go @@ -0,0 +1,85 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_ClaimRewards(t *testing.T) { + t.Run("should return an error when passing empty delegator", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + + /* ACT */ + // Call claimRewardsMethod. + claimRewardsMethod := s.stkContractABI.Methods[ClaimRewardsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + claimRewardsMethod, + []interface{}{common.Address{}, validator.OperatorAddress}..., + ) + + _, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.Contains( + t, + err.Error(), + "invalid address 0x0000000000000000000000000000000000000000, reason: empty address", + ) + }) + + t.Run("should return an error when passing incorrect validator", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + + /* ACT */ + // Call claimRewardsMethod. + claimRewardsMethod := s.stkContractABI.Methods[ClaimRewardsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + claimRewardsMethod, + []interface{}{stakerEVMAddr, "cosmosvaloper100000000000000000000000000000000000000"}..., + ) + + _, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.Contains(t, err.Error(), "decoding bech32 failed") + }) + + t.Run("should return an error when there's no delegation", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + + /* ACT */ + // Call claimRewardsMethod. + claimRewardsMethod := s.stkContractABI.Methods[ClaimRewardsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + claimRewardsMethod, + []interface{}{stakerEVMAddr, validator.OperatorAddress}..., + ) + + _, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.Contains( + t, + err.Error(), + "unexpected error in WithdrawDelegationRewards: no delegation distribution info", + ) + }) +} diff --git a/precompiles/staking/method_distribute.go b/precompiles/staking/method_distribute.go index c173517aeb..7a285d20af 100644 --- a/precompiles/staking/method_distribute.go +++ b/precompiles/staking/method_distribute.go @@ -42,7 +42,7 @@ func (c *Contract) distribute( } // Create the coinSet in advance, if this step fails do not lock ZRC20. - coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) + coinSet, err := precompiletypes.CreateZRC20CoinSet(zrc20Addr, amount) if err != nil { return nil, err } diff --git a/precompiles/staking/method_distribute_test.go b/precompiles/staking/method_distribute_test.go index dddb467b74..93df75157b 100644 --- a/precompiles/staking/method_distribute_test.go +++ b/precompiles/staking/method_distribute_test.go @@ -16,17 +16,19 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to run distribute as read only method", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(0)}..., ) // Call method as read only. - result, err := s.contract.Run(s.mockEVM, s.mockVMContract, true) + result, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, true) // Check error and result. require.ErrorIs(t, err, precompiletypes.ErrWriteMethod{ @@ -49,17 +51,18 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to distribute with 0 token balance", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(0)}..., ) // Call method. - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.ErrorAs( @@ -71,7 +74,7 @@ func Test_Distribute(t *testing.T) { ) // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) @@ -89,6 +92,7 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to distribute with 0 allowance", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Set caller balance. @@ -98,19 +102,19 @@ func Test_Distribute(t *testing.T) { // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(1000)}..., ) // Call method. - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.Error(t, err) require.Contains(t, err.Error(), "invalid allowance, got 0") // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) @@ -128,6 +132,7 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to distribute 0 token", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Set caller balance. @@ -140,19 +145,19 @@ func Test_Distribute(t *testing.T) { // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(0)}..., ) // Call method. - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.Error(t, err) require.Contains(t, err.Error(), "invalid token amount: 0") // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) @@ -170,6 +175,7 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to distribute more than allowed to staking", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Set caller balance. @@ -182,19 +188,19 @@ func Test_Distribute(t *testing.T) { // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(1000)}..., ) // Call method. - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.Error(t, err) require.Contains(t, err.Error(), "invalid allowance, got 999, wanted 1000") // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) @@ -212,6 +218,7 @@ func Test_Distribute(t *testing.T) { t.Run("should fail to distribute more than user balance", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) // Set caller balance. @@ -224,18 +231,18 @@ func Test_Distribute(t *testing.T) { // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(1001)}..., ) - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.Error(t, err) require.Contains(t, err.Error(), "execution reverted") // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) @@ -253,6 +260,7 @@ func Test_Distribute(t *testing.T) { t.Run("should distribute and lock ZRC20", func(t *testing.T) { // Setup test. s := newTestSuite(t) + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] // Set caller balance. _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) @@ -264,17 +272,17 @@ func Test_Distribute(t *testing.T) { // Setup method input. s.mockVMContract.Input = packInputArgs( t, - s.methodID, + distributeMethod, []interface{}{s.zrc20Address, big.NewInt(1000)}..., ) - success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // Check error. require.NoError(t, err) // Unpack and check result boolean. - res, err := s.methodID.Outputs.Unpack(success) + res, err := distributeMethod.Outputs.Unpack(success) require.NoError(t, err) ok := res[0].(bool) diff --git a/precompiles/staking/method_get_all_validators_test.go b/precompiles/staking/method_get_all_validators_test.go index 8a80793de3..433c0f1cb4 100644 --- a/precompiles/staking/method_get_all_validators_test.go +++ b/precompiles/staking/method_get_all_validators_test.go @@ -19,11 +19,11 @@ func Test_GetAllValidators(t *testing.T) { s.sdkKeepers.StakingKeeper.RemoveValidator(s.ctx, v.GetOperator()) } - methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + methodID := s.stkContractABI.Methods[GetAllValidatorsMethodName] s.mockVMContract.Input = methodID.ID // ACT - validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + validators, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.NoError(t, err) @@ -37,14 +37,14 @@ func Test_GetAllValidators(t *testing.T) { t.Run("should return validators if set", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + methodID := s.stkContractABI.Methods[GetAllValidatorsMethodName] s.mockVMContract.Input = methodID.ID r := rand.New(rand.NewSource(42)) validator := sample.Validator(t, r) s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) // ACT - validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + validators, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.NoError(t, err) diff --git a/precompiles/staking/method_get_rewards.go b/precompiles/staking/method_get_rewards.go new file mode 100644 index 0000000000..2b88d2315d --- /dev/null +++ b/precompiles/staking/method_get_rewards.go @@ -0,0 +1,97 @@ +package staking + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +// getRewards returns the list of ZRC20 cosmos coins, available for withdrawal by the delegator. +func (c *Contract) getRewards( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + delegatorAddr, validatorAddr, err := unpackGetRewardsArgs(args) + if err != nil { + return nil, err + } + + // Get delegator Cosmos address. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Query the delegation rewards through the distribution keeper querier. + dstrQuerier := distrkeeper.NewQuerier(c.distributionKeeper) + + res, err := dstrQuerier.DelegationRewards(ctx, &distrtypes.QueryDelegationRewardsRequest{ + DelegatorAddress: delegatorCosmosAddr.String(), + ValidatorAddress: validatorAddr, + }) + + // DelegationRewards returns an error if the delegation does not exist. + // In this case, simply return an empty list of rewards, so external contracts + // can process this case without failing. + if err != nil { + if errors.Is(err, distrtypes.ErrNoDelegationExists) { + rewards := make([]DecCoin, 0) + return method.Outputs.Pack(rewards) + } + + return nil, &precompiletypes.ErrUnexpected{ + When: "DelegationRewards", + Got: err.Error(), + } + } + + coins := res.GetRewards() + if !coins.IsValid() { + return nil, precompiletypes.ErrUnexpected{ + When: "GetRewards", + Got: "invalid coins", + } + } + + rewards := make([]DecCoin, 0) + for _, coin := range coins { + rewards = append(rewards, DecCoin{ + Denom: coin.Denom, + Amount: coin.Amount.BigInt(), + }) + } + + return method.Outputs.Pack(rewards) +} + +func unpackGetRewardsArgs(args []interface{}) (delegator common.Address, validator string, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + validator, ok = args[1].(string) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: validator, + } + } + + return delegator, validator, nil +} diff --git a/precompiles/staking/method_get_rewards_test.go b/precompiles/staking/method_get_rewards_test.go new file mode 100644 index 0000000000..8ddda98b90 --- /dev/null +++ b/precompiles/staking/method_get_rewards_test.go @@ -0,0 +1,100 @@ +package staking + +import ( + "math/big" + "math/rand" + "testing" + + "cosmossdk.io/math" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/stretchr/testify/require" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetRewards(t *testing.T) { + t.Run("should return empty rewards list to a non staker", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create validator. + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + + /* ACT */ + // Call getRewards. + getRewardsMethod := s.stkContractABI.Methods[GetRewardsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getRewardsMethod, + []interface{}{stakerEVMAddr, validator.GetOperator().String()}..., + ) + + /* ASSERT */ + bytes, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + res, err := getRewardsMethod.Outputs.Unpack(bytes) + require.NoError(t, err) + require.Empty(t, res[0]) + }) + + t.Run("should return the zrc20 rewards list for a staker", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + s.sdkKeepers.DistributionKeeper.SetFeePool(s.ctx, distrtypes.InitialFeePool()) + + // Create validator. + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + stakerCosmosAddr, err := precompiletypes.GetCosmosAddress(s.sdkKeepers.BankKeeper, stakerEVMAddr) + require.NoError(t, err) + + // Become a staker. + stakeThroughCosmosAPI( + t, + s.ctx, + s.sdkKeepers.BankKeeper, + s.sdkKeepers.StakingKeeper, + validator, + stakerCosmosAddr, + math.NewInt(100), + ) + + err = s.sdkKeepers.DistributionKeeper.Hooks(). + AfterDelegationModified(s.ctx, stakerCosmosAddr, validator.GetOperator()) + require.NoError(t, err) + + /* Distribute 1000 ZRC20 tokens to the staking contract */ + distributeZRC20(t, s, big.NewInt(1000)) + + // TODO: Simulate a distribution of rewards. + // emissions.BeginBlocker(s.ctx, *s.sdkKeepers.EmissionsKeeper) + // staking.BeginBlocker(s.ctx, &s.sdkKeepers.StakingKeeper) + // distribution.BeginBlocker(s.ctx, abci.RequestBeginBlock{}, s.sdkKeepers.DistributionKeeper) + // s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 1) + + /* ACT */ + // Call getRewards. + getRewardsMethod := s.stkContractABI.Methods[GetRewardsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getRewardsMethod, + []interface{}{stakerEVMAddr, validator.GetOperator().String()}..., + ) + + bytes, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + + /* ASSERT */ + _, err = getRewardsMethod.Outputs.Unpack(bytes) + require.NoError(t, err) + }) +} diff --git a/precompiles/staking/method_get_shares_test.go b/precompiles/staking/method_get_shares_test.go index d0038886f4..df3f132610 100644 --- a/precompiles/staking/method_get_shares_test.go +++ b/precompiles/staking/method_get_shares_test.go @@ -56,13 +56,13 @@ func Test_GetShares(t *testing.T) { t.Run("should fail if wrong args amount", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[GetSharesMethodName] + methodID := s.stkContractABI.Methods[GetSharesMethodName] staker := sample.Bech32AccAddress() stakerEthAddr := common.BytesToAddress(staker.Bytes()) args := []interface{}{stakerEthAddr} // ACT - _, err := s.contract.GetShares(s.ctx, &methodID, args) + _, err := s.stkContract.GetShares(s.ctx, &methodID, args) // ASSERT require.Error(t, err) @@ -71,13 +71,13 @@ func Test_GetShares(t *testing.T) { t.Run("should fail if invalid staker arg", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[GetSharesMethodName] + methodID := s.stkContractABI.Methods[GetSharesMethodName] r := rand.New(rand.NewSource(42)) validator := sample.Validator(t, r) args := []interface{}{42, validator.OperatorAddress} // ACT - _, err := s.contract.GetShares(s.ctx, &methodID, args) + _, err := s.stkContract.GetShares(s.ctx, &methodID, args) // ASSERT require.Error(t, err) @@ -86,13 +86,13 @@ func Test_GetShares(t *testing.T) { t.Run("should fail if invalid val address", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[GetSharesMethodName] + methodID := s.stkContractABI.Methods[GetSharesMethodName] staker := sample.Bech32AccAddress() stakerEthAddr := common.BytesToAddress(staker.Bytes()) args := []interface{}{stakerEthAddr, staker.String()} // ACT - _, err := s.contract.GetShares(s.ctx, &methodID, args) + _, err := s.stkContract.GetShares(s.ctx, &methodID, args) // ASSERT require.Error(t, err) @@ -101,13 +101,13 @@ func Test_GetShares(t *testing.T) { t.Run("should fail if invalid val address format", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[GetSharesMethodName] + methodID := s.stkContractABI.Methods[GetSharesMethodName] staker := sample.Bech32AccAddress() stakerEthAddr := common.BytesToAddress(staker.Bytes()) args := []interface{}{stakerEthAddr, 42} // ACT - _, err := s.contract.GetShares(s.ctx, &methodID, args) + _, err := s.stkContract.GetShares(s.ctx, &methodID, args) // ASSERT require.Error(t, err) diff --git a/precompiles/staking/method_get_validators.go b/precompiles/staking/method_get_validators.go new file mode 100644 index 0000000000..bdc4e80132 --- /dev/null +++ b/precompiles/staking/method_get_validators.go @@ -0,0 +1,64 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +// getValidators queries the list of validators for a given delegator. +func (c *Contract) getValidatorListForDelegator( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 1 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 1, + } + } + + delegatorAddr, err := unpackGetValidatorsArgs(args) + if err != nil { + return nil, err + } + + // Get the cosmos address of the caller. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Query the validator list of the given delegator. + dstrQuerier := distrkeeper.NewQuerier(c.distributionKeeper) + + res, err := dstrQuerier.DelegatorValidators(ctx, &distrtypes.QueryDelegatorValidatorsRequest{ + DelegatorAddress: delegatorCosmosAddr.String(), + }) + if err != nil { + return nil, precompiletypes.ErrUnexpected{ + When: "DelegatorValidators", + Got: err.Error(), + } + } + + // Return immediately, no need to check the slice. + // If there are no validators we simply return an empty array to calling contracts. + return method.Outputs.Pack(res.Validators) +} + +func unpackGetValidatorsArgs(args []interface{}) (delegator common.Address, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + return delegator, nil +} diff --git a/precompiles/staking/method_get_validators_test.go b/precompiles/staking/method_get_validators_test.go new file mode 100644 index 0000000000..8dd06eff58 --- /dev/null +++ b/precompiles/staking/method_get_validators_test.go @@ -0,0 +1,170 @@ +package staking + +import ( + "math/rand" + "testing" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetValidators(t *testing.T) { + t.Run("should return an empty list for a non staker address", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create validator. + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + + /* ACT */ + // Call getValidatorListForDelegator. + getValidatorsMethod := s.stkContractABI.Methods[GetValidatorsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getValidatorsMethod, + []interface{}{stakerEVMAddr}..., + ) + + bytes, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + + res, err := getValidatorsMethod.Outputs.Unpack(bytes) + require.NoError(t, err) + require.NotEmpty(t, res) + + list, ok := res[0].([]string) + require.True(t, ok) + require.Len(t, list, 0) + }) + + t.Run("should return an error for zero address", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create validator. + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + /* ACT */ + // Call getValidatorListForDelegator. + getValidatorsMethod := s.stkContractABI.Methods[GetValidatorsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getValidatorsMethod, + []interface{}{common.Address{}}..., + ) + + _, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.Contains( + t, + err.Error(), + "invalid address 0x0000000000000000000000000000000000000000, reason: empty address", + ) + }) + + t.Run("should return staker's validator list", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create validator. + validator := sample.Validator(t, rand.New(rand.NewSource(42))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + stakerCosmosAddr, err := precompiletypes.GetCosmosAddress(s.sdkKeepers.BankKeeper, stakerEVMAddr) + require.NoError(t, err) + + // Become a staker. + stakeThroughCosmosAPI( + t, + s.ctx, + s.sdkKeepers.BankKeeper, + s.sdkKeepers.StakingKeeper, + validator, + stakerCosmosAddr, + math.NewInt(100), + ) + + /* ACT */ + // Call getValidatorListForDelegator. + getValidatorsMethod := s.stkContractABI.Methods[GetValidatorsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getValidatorsMethod, + []interface{}{stakerEVMAddr}..., + ) + + bytes, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + + res, err := getValidatorsMethod.Outputs.Unpack(bytes) + require.NoError(t, err) + require.NotEmpty(t, res) + + list, ok := res[0].([]string) + require.True(t, ok) + require.Len(t, list, 1) + require.Equal(t, validator.GetOperator().String(), list[0]) + }) + + t.Run(" should return staker's validator list - heavy test with 100 validators", func(t *testing.T) { + /* ARRANGE */ + s := newTestSuite(t) + + // Create staker. + stakerEVMAddr := sample.EthAddress() + stakerCosmosAddr, err := precompiletypes.GetCosmosAddress(s.sdkKeepers.BankKeeper, stakerEVMAddr) + require.NoError(t, err) + + // Create 100 validators, and stake on each of them. + for n := range 100 { + validator := sample.Validator(t, rand.New(rand.NewSource(int64(n)))) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + stakeThroughCosmosAPI( + t, + s.ctx, + s.sdkKeepers.BankKeeper, + s.sdkKeepers.StakingKeeper, + validator, + stakerCosmosAddr, + math.NewInt(100), + ) + } + + /* ACT */ + // Call getValidatorListForDelegator. + getValidatorsMethod := s.stkContractABI.Methods[GetValidatorsMethodName] + + s.mockVMContract.Input = packInputArgs( + t, + getValidatorsMethod, + []interface{}{stakerEVMAddr}..., + ) + + bytes, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + + res, err := getValidatorsMethod.Outputs.Unpack(bytes) + require.NoError(t, err) + require.NotEmpty(t, res) + + list, ok := res[0].([]string) + require.True(t, ok) + + // The returned list should contain 100 entries. + require.Len(t, list, 100) + }) +} diff --git a/precompiles/staking/method_move_stake_test.go b/precompiles/staking/method_move_stake_test.go index 8882442069..32ec0c1b92 100644 --- a/precompiles/staking/method_move_stake_test.go +++ b/precompiles/staking/method_move_stake_test.go @@ -17,7 +17,7 @@ func Test_MoveStake(t *testing.T) { t.Run("should fail with error disabled", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[MoveStakeMethodName] + methodID := s.stkContractABI.Methods[MoveStakeMethodName] r := rand.New(rand.NewSource(42)) validatorSrc := sample.Validator(t, r) s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validatorSrc) @@ -41,9 +41,9 @@ func Test_MoveStake(t *testing.T) { } // stake to validator src - stakeMethodID := s.contractABI.Methods[StakeMethodName] + stakeMethodID := s.stkContractABI.Methods[StakeMethodName] s.mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + _, err = s.stkContract.Run(s.mockEVM, s.mockVMContract, false) require.Error(t, err) require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ Method: StakeMethodName, @@ -58,7 +58,7 @@ func Test_MoveStake(t *testing.T) { s.mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) // ACT - _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + _, err = s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.Error(t, err) diff --git a/precompiles/staking/method_stake_test.go b/precompiles/staking/method_stake_test.go index bd5558abdb..16f10db4a7 100644 --- a/precompiles/staking/method_stake_test.go +++ b/precompiles/staking/method_stake_test.go @@ -17,7 +17,7 @@ func Test_Stake(t *testing.T) { t.Run("should fail with error disabled", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[StakeMethodName] + methodID := s.stkContractABI.Methods[StakeMethodName] r := rand.New(rand.NewSource(42)) validator := sample.Validator(t, r) @@ -35,7 +35,7 @@ func Test_Stake(t *testing.T) { s.mockVMContract.Input = packInputArgs(t, methodID, args...) // ACT - _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + _, err = s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.Error(t, err) diff --git a/precompiles/staking/method_unstake_test.go b/precompiles/staking/method_unstake_test.go index e770020946..2789bd67d9 100644 --- a/precompiles/staking/method_unstake_test.go +++ b/precompiles/staking/method_unstake_test.go @@ -17,7 +17,7 @@ func Test_Unstake(t *testing.T) { t.Run("should fail with error disabled", func(t *testing.T) { // ARRANGE s := newTestSuite(t) - methodID := s.contractABI.Methods[UnstakeMethodName] + methodID := s.stkContractABI.Methods[UnstakeMethodName] r := rand.New(rand.NewSource(42)) validator := sample.Validator(t, r) @@ -36,7 +36,7 @@ func Test_Unstake(t *testing.T) { s.mockVMContract.Input = packInputArgs(t, methodID, args...) // ACT - _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + _, err = s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.Error(t, err) diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index 4d8115336a..9347978e3c 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -47,12 +48,20 @@ func initABI() { GasRequiredByMethod[methodID] = MoveStakeMethodGas case DistributeMethodName: GasRequiredByMethod[methodID] = DistributeMethodGas + case ClaimRewardsMethodName: + GasRequiredByMethod[methodID] = ClaimRewardsMethodGas case GetAllValidatorsMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true case GetSharesMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true + case GetRewardsMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true + case GetValidatorsMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true default: GasRequiredByMethod[methodID] = 0 } @@ -62,11 +71,12 @@ func initABI() { type Contract struct { precompiletypes.BaseContract - stakingKeeper stakingkeeper.Keeper - fungibleKeeper fungiblekeeper.Keeper - bankKeeper bankkeeper.Keeper - cdc codec.Codec - kvGasConfig storetypes.GasConfig + stakingKeeper stakingkeeper.Keeper + fungibleKeeper fungiblekeeper.Keeper + bankKeeper bankkeeper.Keeper + distributionKeeper distrkeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig } func NewIStakingContract( @@ -74,6 +84,7 @@ func NewIStakingContract( stakingKeeper *stakingkeeper.Keeper, fungibleKeeper fungiblekeeper.Keeper, bankKeeper bankkeeper.Keeper, + distributionKeeper distrkeeper.Keeper, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { @@ -83,12 +94,13 @@ func NewIStakingContract( } return &Contract{ - BaseContract: precompiletypes.NewBaseContract(ContractAddress), - stakingKeeper: *stakingKeeper, - fungibleKeeper: fungibleKeeper, - bankKeeper: bankKeeper, - cdc: cdc, - kvGasConfig: kvGasConfig, + BaseContract: precompiletypes.NewBaseContract(ContractAddress), + stakingKeeper: *stakingKeeper, + fungibleKeeper: fungibleKeeper, + bankKeeper: bankKeeper, + distributionKeeper: distributionKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, } } @@ -228,6 +240,41 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, err } return res, nil + case GetRewardsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.getRewards(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case GetValidatorsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.getValidatorListForDelegator(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case ClaimRewardsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.claimRewards(ctx, evm, contract, method, args) + return err + }) + if execErr != nil { + res, errPack := method.Outputs.Pack(false) + if errPack != nil { + return nil, errPack + } + + return res, err + } + return res, nil default: return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index 977090e4dd..4b0307c4dc 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -6,6 +6,7 @@ import ( "math/big" + "cosmossdk.io/math" tmdb "github.com/cometbft/cometbft-db" "github.com/cosmos/cosmos-sdk/store" "github.com/holiman/uint256" @@ -13,6 +14,8 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -39,20 +42,24 @@ func Test_IStakingContract(t *testing.T) { gasConfig := storetypes.TransientGasConfig() t.Run("should check methods are present in ABI", func(t *testing.T) { - require.NotNil(t, s.contractABI.Methods[StakeMethodName], "stake method should be present in the ABI") - require.NotNil(t, s.contractABI.Methods[UnstakeMethodName], "unstake method should be present in the ABI") + require.NotNil(t, s.stkContractABI.Methods[StakeMethodName], "stake method should be present in the ABI") + require.NotNil(t, s.stkContractABI.Methods[UnstakeMethodName], "unstake method should be present in the ABI") require.NotNil( t, - s.contractABI.Methods[MoveStakeMethodName], + s.stkContractABI.Methods[MoveStakeMethodName], "moveStake method should be present in the ABI", ) require.NotNil( t, - s.contractABI.Methods[GetAllValidatorsMethodName], + s.stkContractABI.Methods[GetAllValidatorsMethodName], "getAllValidators method should be present in the ABI", ) - require.NotNil(t, s.contractABI.Methods[GetSharesMethodName], "getShares method should be present in the ABI") + require.NotNil( + t, + s.stkContractABI.Methods[GetSharesMethodName], + "getShares method should be present in the ABI", + ) }) t.Run("should check gas requirements for methods", func(t *testing.T) { @@ -60,9 +67,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("stake", func(t *testing.T) { // ACT - stake := s.contract.RequiredGas(s.contractABI.Methods[StakeMethodName].ID) + stake := s.stkContract.RequiredGas(s.stkContractABI.Methods[StakeMethodName].ID) // ASSERT - copy(method[:], s.contractABI.Methods[StakeMethodName].ID[:4]) + copy(method[:], s.stkContractABI.Methods[StakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -76,9 +83,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("unstake", func(t *testing.T) { // ACT - unstake := s.contract.RequiredGas(s.contractABI.Methods[UnstakeMethodName].ID) + unstake := s.stkContract.RequiredGas(s.stkContractABI.Methods[UnstakeMethodName].ID) // ASSERT - copy(method[:], s.contractABI.Methods[UnstakeMethodName].ID[:4]) + copy(method[:], s.stkContractABI.Methods[UnstakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -92,9 +99,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("moveStake", func(t *testing.T) { // ACT - moveStake := s.contract.RequiredGas(s.contractABI.Methods[MoveStakeMethodName].ID) + moveStake := s.stkContract.RequiredGas(s.stkContractABI.Methods[MoveStakeMethodName].ID) // ASSERT - copy(method[:], s.contractABI.Methods[MoveStakeMethodName].ID[:4]) + copy(method[:], s.stkContractABI.Methods[MoveStakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -108,9 +115,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getAllValidators", func(t *testing.T) { // ACT - getAllValidators := s.contract.RequiredGas(s.contractABI.Methods[GetAllValidatorsMethodName].ID) + getAllValidators := s.stkContract.RequiredGas(s.stkContractABI.Methods[GetAllValidatorsMethodName].ID) // ASSERT - copy(method[:], s.contractABI.Methods[GetAllValidatorsMethodName].ID[:4]) + copy(method[:], s.stkContractABI.Methods[GetAllValidatorsMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -124,9 +131,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getShares", func(t *testing.T) { // ACT - getShares := s.contract.RequiredGas(s.contractABI.Methods[GetSharesMethodName].ID) + getShares := s.stkContract.RequiredGas(s.stkContractABI.Methods[GetSharesMethodName].ID) // ASSERT - copy(method[:], s.contractABI.Methods[GetSharesMethodName].ID[:4]) + copy(method[:], s.stkContractABI.Methods[GetSharesMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -142,7 +149,7 @@ func Test_IStakingContract(t *testing.T) { // ARRANGE invalidMethodBytes := []byte("invalidMethod") // ACT - gasInvalidMethod := s.contract.RequiredGas(invalidMethodBytes) + gasInvalidMethod := s.stkContract.RequiredGas(invalidMethodBytes) // ASSERT require.Equal( t, @@ -159,7 +166,7 @@ func Test_IStakingContract(t *testing.T) { func Test_InvalidMethod(t *testing.T) { s := newTestSuite(t) - _, doNotExist := s.contractABI.Methods["invalidMethod"] + _, doNotExist := s.stkContractABI.Methods["invalidMethod"] require.False(t, doNotExist, "invalidMethod should not be present in the ABI") } @@ -190,7 +197,7 @@ func Test_RunInvalidMethod(t *testing.T) { s.mockVMContract.Input = packInputArgs(t, methodID, args...) // ACT - _, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + _, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) // ASSERT require.Error(t, err) @@ -241,6 +248,7 @@ func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *v &sdkKeepers.StakingKeeper, *fungibleKeeper, sdkKeepers.BankKeeper, + sdkKeepers.DistributionKeeper, appCodec, gasConfig, ) @@ -277,13 +285,12 @@ func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *v type testSuite struct { ctx sdk.Context - contract *Contract - contractABI *abi.ABI + stkContract *Contract + stkContractABI *abi.ABI fungibleKeeper *fungiblekeeper.Keeper sdkKeepers keeper.SDKKeepers mockEVM *vm.EVM mockVMContract *vm.Contract - methodID abi.Method defaultCaller common.Address defaultLocker common.Address zrc20Address common.Address @@ -314,6 +321,7 @@ func newTestSuite(t *testing.T) testSuite { &sdkKeepers.StakingKeeper, *fungibleKeeper, sdkKeepers.BankKeeper, + sdkKeepers.DistributionKeeper, appCodec, gasConfig, ) @@ -362,7 +370,6 @@ func newTestSuite(t *testing.T) testSuite { sdkKeepers, mockEVM, mockVMContract, - abi.Methods[DistributeMethodName], caller, locker, zrc20Address, @@ -385,7 +392,7 @@ func allowStaking(t *testing.T, ts testSuite, amount *big.Int) { fungibletypes.ModuleAddressEVM, ts.zrc20Address, "approve", - []interface{}{ts.contract.Address(), amount}, + []interface{}{ts.stkContract.Address(), amount}, ) require.NoError(t, err, "error allowing staking to spend ZRC20 tokens") @@ -394,6 +401,65 @@ func allowStaking(t *testing.T, ts testSuite, amount *big.Int) { require.True(t, allowed) } +func stakeThroughCosmosAPI( + t *testing.T, + ctx sdk.Context, + bankKeeper bankkeeper.Keeper, + stakingKeeper stakingkeeper.Keeper, + validator stakingtypes.Validator, + staker sdk.AccAddress, + amount math.Int, +) { + // Coins to stake with default cosmos denom. + coins := sdk.NewCoins(sdk.NewCoin("stake", amount)) + + err := bankKeeper.MintCoins(ctx, fungibletypes.ModuleName, coins) + require.NoError(t, err) + + err = bankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + shares, err := stakingKeeper.Delegate( + ctx, + staker, + coins.AmountOf(coins.Denoms()[0]), + validator.Status, + validator, + true, + ) + require.NoError(t, err) + require.Equal(t, amount.Uint64(), shares.TruncateInt().Uint64()) +} + +func distributeZRC20( + t *testing.T, + s testSuite, + amount *big.Int, +) { + distributeMethod := s.stkContractABI.Methods[DistributeMethodName] + + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, amount) + require.NoError(t, err) + allowStaking(t, s, amount) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + distributeMethod, + []interface{}{s.zrc20Address, amount}..., + ) + + // Call distribute method. + success, err := s.stkContract.Run(s.mockEVM, s.mockVMContract, false) + require.NoError(t, err) + + res, err := distributeMethod.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.True(t, ok) +} + func callEVM( t *testing.T, ctx sdk.Context, diff --git a/precompiles/types/address.go b/precompiles/types/address.go index 5e6654cc5b..b8226dfb01 100644 --- a/precompiles/types/address.go +++ b/precompiles/types/address.go @@ -29,6 +29,13 @@ func GetEVMCallerAddress(evm *vm.EVM, contract *vm.Contract) (common.Address, er // GetCosmosAddress returns the counterpart cosmos address of the given ethereum address. // It checks if the address is empty or blocked by the bank keeper. func GetCosmosAddress(bankKeeper bank.Keeper, addr common.Address) (sdk.AccAddress, error) { + if (addr == common.Address{}) { + return nil, &ErrInvalidAddr{ + Got: addr.String(), + Reason: "empty address", + } + } + toAddr := sdk.AccAddress(addr.Bytes()) if toAddr.Empty() { return nil, &ErrInvalidAddr{ diff --git a/precompiles/types/coin.go b/precompiles/types/coin.go index 4219040d42..b2f04c2eed 100644 --- a/precompiles/types/coin.go +++ b/precompiles/types/coin.go @@ -2,6 +2,7 @@ package types import ( "math/big" + "strings" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -16,13 +17,20 @@ func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { return config.ZRC20DenomPrefix + ZRC20Address.String() } -func CreateCoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, error) { +func CreateZRC20CoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, error) { defer func() { if r := recover(); r != nil { return } }() + if (zrc20address == common.Address{}) { + return nil, &ErrInvalidAddr{ + Got: zrc20address.String(), + Reason: "empty address", + } + } + denom := ZRC20ToCosmosDenom(zrc20address) coin := sdk.NewCoin(denom, math.NewIntFromBigInt(amount)) @@ -49,3 +57,17 @@ func CreateCoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, err return coinSet, nil } + +// CoinIsZRC20 checks if a given coin is a ZRC20 coin based on its denomination. +func CoinIsZRC20(denom string) bool { + // Fail fast if the prefix is not set. + if !strings.HasPrefix(denom, config.ZRC20DenomPrefix) { + return false + } + + // Prefix is correctly set, extract the zrc20 address. + zrc20Addr := strings.TrimPrefix(denom, config.ZRC20DenomPrefix) + + // Return true only if address is not empty and is a valid hex address. + return common.HexToAddress(zrc20Addr) != common.Address{} && common.IsHexAddress(zrc20Addr) +} diff --git a/precompiles/types/coin_test.go b/precompiles/types/coin_test.go index 6a8aac7d46..25bc983c01 100644 --- a/precompiles/types/coin_test.go +++ b/precompiles/types/coin_test.go @@ -20,7 +20,7 @@ func Test_createCoinSet(t *testing.T) { tokenDenom := ZRC20ToCosmosDenom(tokenAddr) amount := big.NewInt(100) - coinSet, err := CreateCoinSet(tokenAddr, amount) + coinSet, err := CreateZRC20CoinSet(tokenAddr, amount) require.NoError(t, err, "createCoinSet should not return an error") require.NotNil(t, coinSet, "coinSet should not be nil") @@ -28,3 +28,27 @@ func Test_createCoinSet(t *testing.T) { require.Equal(t, tokenDenom, coin.Denom, "coin denom should be %s, got %s", tokenDenom, coin.Denom) require.Equal(t, amount, coin.Amount.BigInt(), "coin amount should be %s, got %s", amount, coin.Amount.BigInt()) } + +func Test_CoinIsZRC20(t *testing.T) { + test := []struct { + denom string + expected bool + }{ + {"", false}, // Empty string. + {"zrc20/", false}, // Missing address. + {"zrc20/0x0000000000000000000000000000000000000000", false}, // Zero address. + {"zrc20/0x514910771af9ca656af840dff83e8264ecf986ca", true}, // Valid ZRC20 address. + {"zrc20/0xCa14007Eff0dB1f8135f4C25B34De49AB0d42766", true}, // Valid ZRC20 address. + {"zrc20/0x12345", false}, // Valid prefix, invalid ZRC20 address. + {"zrc200xabcdef", false}, // Malformed prefix. + {"foo/0x0123456789", false}, // Invalid prefix. + {"ZRC20/0x0123456789abcdef", false}, // Invalid prefix. + } + + for _, tt := range test { + t.Run(tt.denom, func(t *testing.T) { + result := CoinIsZRC20(tt.denom) + require.Equal(t, tt.expected, result, "got %v, want %v", result, tt.expected) + }) + } +} diff --git a/testutil/keeper/keeper.go b/testutil/keeper/keeper.go index ed3a107dac..5819c6b74b 100644 --- a/testutil/keeper/keeper.go +++ b/testutil/keeper/keeper.go @@ -111,6 +111,8 @@ type SDKKeepers struct { TransferKeeper ibctransferkeeper.Keeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedTransferKeeper capabilitykeeper.ScopedKeeper + DistributionKeeper distrkeeper.Keeper + EmissionsKeeper *emissionskeeper.Keeper IBCRouter *porttypes.Router } @@ -150,6 +152,22 @@ var ( ) testTransientKeys = sdk.NewTransientStoreKeys(evmtypes.TransientKey) testMemKeys = sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) + + maccPerms = map[string][]string{ + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + govtypes.ModuleName: {authtypes.Burner}, + //ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + crosschaintypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + //ibccrosschaintypes.ModuleName: nil, + evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + fungibletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + emissionstypes.ModuleName: nil, + emissionstypes.UndistributedObserverRewardsPool: nil, + emissionstypes.UndistributedTSSRewardsPool: nil, + } ) // ModuleAccountAddrs returns all the app's module account addresses. @@ -401,6 +419,20 @@ func NewSDKKeepersWithKeys( tKeys map[string]*storetypes.TransientStoreKey, allKeys map[string]storetypes.StoreKey, ) SDKKeepers { + authorityKeeper := authoritykeeper.NewKeeper( + cdc, + keys[authoritytypes.StoreKey], + memKeys[authoritytypes.MemStoreKey], + AuthorityGovAddress, + ) + accountKeeper := authkeeper.NewAccountKeeper( + cdc, + keys[authtypes.StoreKey], + ethermint.ProtoAccount, + maccPerms, + sdk.GetConfig().GetBech32AccountAddrPrefix(), + authtypes.NewModuleAddress(authtypes.ModuleName).String(), + ) paramsKeeper := paramskeeper.NewKeeper( cdc, fungibletypes.Amino, @@ -470,16 +502,54 @@ func NewSDKKeepersWithKeys( keys[capabilitytypes.StoreKey], memKeys[capabilitytypes.MemStoreKey], ) + dstrKeeper := distrkeeper.NewKeeper( + cdc, + keys[distrtypes.StoreKey], + accountKeeper, + bankKeeper, + stakingKeeper, + authtypes.FeeCollectorName, + authtypes.NewModuleAddress(distrtypes.ModuleName).String(), + ) + lightclientKeeper := lightclientkeeper.NewKeeper( + cdc, + keys[lightclienttypes.StoreKey], + memKeys[lightclienttypes.MemStoreKey], + authorityKeeper, + ) + observerKeeper := observerkeeper.NewKeeper( + cdc, + keys[observertypes.StoreKey], + memKeys[observertypes.MemStoreKey], + stakingKeeper, + slashingKeeper, + authorityKeeper, + lightclientKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + emissionsKeeper := emissionskeeper.NewKeeper( + cdc, + keys[emissionstypes.StoreKey], + memKeys[emissionstypes.MemStoreKey], + authtypes.FeeCollectorName, + bankKeeper, + stakingKeeper, + observerKeeper, + authKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) return SDKKeepers{ - ParamsKeeper: paramsKeeper, - AuthKeeper: authKeeper, - BankKeeper: bankKeeper, - StakingKeeper: stakingKeeper, - FeeMarketKeeper: feeMarketKeeper, - EvmKeeper: evmKeeper, - SlashingKeeper: slashingKeeper, - CapabilityKeeper: capabilityKeeper, + ParamsKeeper: paramsKeeper, + AuthKeeper: authKeeper, + BankKeeper: bankKeeper, + StakingKeeper: stakingKeeper, + FeeMarketKeeper: feeMarketKeeper, + EvmKeeper: evmKeeper, + SlashingKeeper: slashingKeeper, + CapabilityKeeper: capabilityKeeper, + DistributionKeeper: dstrKeeper, + EmissionsKeeper: emissionsKeeper, } }