diff --git a/action/protocol/execution/protocol_test.go b/action/protocol/execution/protocol_test.go index fe5664d89a..3b2df0908a 100644 --- a/action/protocol/execution/protocol_test.go +++ b/action/protocol/execution/protocol_test.go @@ -959,6 +959,9 @@ func TestProtocol_Handle(t *testing.T) { t.Run("CVE-2021-39137-attack-replay", func(t *testing.T) { NewSmartContractTest(t, "testdata/CVE-2021-39137-attack-replay.json") }) + t.Run("system-staking", func(t *testing.T) { + NewSmartContractTest(t, "testdata/system-staking.json") + }) } func TestMaxTime(t *testing.T) { diff --git a/action/protocol/execution/testdata/system-staking.json b/action/protocol/execution/testdata/system-staking.json new file mode 100644 index 0000000000..8b3672621b --- /dev/null +++ b/action/protocol/execution/testdata/system-staking.json @@ -0,0 +1,53 @@ +{ + "initGenesis": { + "isBering": true, + "isIceland": true + }, + "initBalances": [ + { + "account": "io1llupp3n8q5x8usnr5w08j6hc6hn55x64l46rr7", + "rawBalance": "1000000000000000000000000000" + } + ], + "deployments":[ + { + "rawByteCode": "60806040523480156200001157600080fd5b5060405180604001604052806009815260200168109d58dad95d13919560ba1b815250604051806040016040528060038152602001621092d560ea1b81525081600090816200006191906200019b565b5060016200007082826200019b565b5050506200008d62000087620000a060201b60201c565b620000a4565b6006805460ff60a01b1916905562000267565b3390565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b634e487b7160e01b600052604160045260246000fd5b600181811c908216806200012157607f821691505b6020821081036200014257634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200019657600081815260208120601f850160051c81016020861015620001715750805b601f850160051c820191505b8181101562000192578281556001016200017d565b5050505b505050565b81516001600160401b03811115620001b757620001b7620000f6565b620001cf81620001c884546200010c565b8462000148565b602080601f831160018114620002075760008415620001ee5750858301515b600019600386901b1c1916600185901b17855562000192565b600085815260208120601f198616915b82811015620002385788860151825594840194600190910190840162000217565b5085821015620002575787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b613dc980620002776000396000f3fe6080604052600436106102925760003560e01c806378bfca101161015a578063c87b56dd116100c1578063e0028ecf1161007a578063e0028ecf146107dd578063e449f341146107fd578063e985e9c51461081d578063eb0ffb2e14610866578063f0b56b5d14610886578063f2fde38b1461089c57600080fd5b8063c87b56dd14610741578063c8e7792314610761578063cd0a02d014610781578063d0949f9914610794578063d6605fd8146107aa578063d6819bcc146107ca57600080fd5b8063a22cb46511610113578063a22cb4651461069b578063ad46fc64146106bb578063b2383e55146106db578063b88d4fde146106ee578063b8f4bd7b1461070e578063bbe33ea51461072e57600080fd5b806378bfca10146105f15780638456cb591461061e5780638da5cb5b1461063357806393b6ef591461065157806395d89b41146106715780639f7d5b001461068657600080fd5b806342842e0e116101fe5780635d36598f116101b75780635d36598f1461053c5780636198e3391461055c5780636352211e1461057c5780636faa5c271461059c57806370a08231146105bc578063715018a6146105dc57600080fd5b806342842e0e14610458578063431cd92a1461047857806343e06c59146104ca578063597cc14a146104ea5780635c975abb146104fd5780635ceb8b5b1461051c57600080fd5b80631338736f116102505780631338736f1461039657806323b872dd146103b65780632dc83008146103d65780632e17de78146103f65780633f4ba83a146104165780633fac69af1461042b57600080fd5b8062f714ce1461029757806301ffc9a7146102b957806303459b16146102ee57806306fdde031461031c578063081812fc1461033e578063095ea7b314610376575b600080fd5b3480156102a357600080fd5b506102b76102b236600461341d565b6108bc565b005b3480156102c557600080fd5b506102d96102d4366004613463565b610983565b60405190151581526020015b60405180910390f35b3480156102fa57600080fd5b5061030e610309366004613480565b6109d5565b6040519081526020016102e5565b34801561032857600080fd5b506103316109fb565b6040516102e591906134e9565b34801561034a57600080fd5b5061035e610359366004613480565b610a8d565b6040516001600160a01b0390911681526020016102e5565b34801561038257600080fd5b506102b76103913660046134fc565b610ab4565b3480156103a257600080fd5b506102b76103b1366004613528565b610bc9565b3480156103c257600080fd5b506102b76103d136600461354a565b610c3c565b3480156103e257600080fd5b506102b76103f13660046135a8565b610c6d565b34801561040257600080fd5b506102b7610411366004613480565b610cdb565b34801561042257600080fd5b506102b7610d8a565b34801561043757600080fd5b5061044b61044636600461361f565b610d9c565b6040516102e59190613660565b34801561046457600080fd5b506102b761047336600461354a565b610f18565b34801561048457600080fd5b50610498610493366004613480565b610f33565b6040805195865260208601949094529284019190915260608301526001600160a01b031916608082015260a0016102e5565b3480156104d657600080fd5b506102d96104e5366004613528565b610fa9565b61030e6104f83660046135a8565b610fc4565b34801561050957600080fd5b50600654600160a01b900460ff166102d9565b34801561052857600080fd5b506102b76105373660046136ea565b61103a565b34801561054857600080fd5b506102b761055736600461361f565b6110e1565b34801561056857600080fd5b506102b7610577366004613480565b611177565b34801561058857600080fd5b5061035e610597366004613480565b6111d9565b3480156105a857600080fd5b5061044b6105b736600461361f565b611239565b3480156105c857600080fd5b5061030e6105d7366004613735565b6113ad565b3480156105e857600080fd5b506102b7611433565b3480156105fd57600080fd5b5061061161060c366004613528565b611445565b6040516102e59190613752565b34801561062a57600080fd5b506102b761157a565b34801561063f57600080fd5b506006546001600160a01b031661035e565b34801561065d57600080fd5b5061030e61066c366004613480565b61158a565b34801561067d57600080fd5b506103316115b5565b34801561069257600080fd5b50600b5461030e565b3480156106a757600080fd5b506102b76106b63660046137ab565b6115c4565b3480156106c757600080fd5b506102b76106d63660046137de565b6115d3565b6102b76106e9366004613528565b61166a565b3480156106fa57600080fd5b506102b7610709366004613877565b611772565b34801561071a57600080fd5b506102b761072936600461393a565b6117aa565b6102b761073c3660046136ea565b611899565b34801561074d57600080fd5b5061033161075c366004613480565b611aa0565b34801561076d57600080fd5b506102b761077c366004613528565b611b13565b61030e61078f366004613990565b611cb8565b3480156107a057600080fd5b5061030e60001981565b3480156107b657600080fd5b506102b76107c5366004613528565b611d82565b61030e6107d83660046139cd565b611e6b565b3480156107e957600080fd5b506102b76107f8366004613528565b611f5d565b34801561080957600080fd5b506102b761081836600461361f565b611fd1565b34801561082957600080fd5b506102d9610838366004613a8e565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b34801561087257600080fd5b506102b7610881366004613528565b6120ad565b34801561089257600080fd5b5061030e61ca8081565b3480156108a857600080fd5b506102b76108b7366004613735565b612123565b6108c461219c565b816108ce816121e9565b600083815260086020526040902060028101546108ea9061223e565b156109345760405162461bcd60e51b81526020600482015260156024820152746e6f7420726561647920746f20776974686472617760581b60448201526064015b60405180910390fd5b61093d846122b3565b6109478184612356565b6040516001600160a01b0384169085907fd964a27d45f595739c13d8b1160b57491050cacf3a2e5602207277d6228f64ee90600090a350505050565b60006001600160e01b031982166380ac58cd60e01b14806109b457506001600160e01b03198216635b5e139f60e01b145b806109cf57506301ffc9a760e01b6001600160e01b03198316145b92915050565b60006109e082612415565b6000828152600860205260409020600201546109cf9061223e565b606060008054610a0a90613abc565b80601f0160208091040260200160405190810160405280929190818152602001828054610a3690613abc565b8015610a835780601f10610a5857610100808354040283529160200191610a83565b820191906000526020600020905b815481529060010190602001808311610a6657829003601f168201915b5050505050905090565b6000610a9882612415565b506000908152600460205260409020546001600160a01b031690565b6000610abf826111d9565b9050806001600160a01b0316836001600160a01b031603610b2c5760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b606482015260840161092b565b336001600160a01b0382161480610b485750610b488133610838565b610bba5760405162461bcd60e51b815260206004820152603d60248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f7420746f60448201527f6b656e206f776e6572206f7220617070726f76656420666f7220616c6c000000606482015260840161092b565b610bc48383612474565b505050565b610bd161219c565b81610bdb816121e9565b6000838152600860205260409020610bf2816124e2565b610bfc818461252c565b837f907fece23ce39fbcbceb71e515043fe29408353fbb393b25b35eb8a70a4bad0b84604051610c2e91815260200190565b60405180910390a250505050565b610c463382612614565b610c625760405162461bcd60e51b815260040161092b90613af6565b610bc4838383612692565b610c7561219c565b81610c7f816121e9565b6000838152600860205260409020610c979083612803565b6040516001600160a01b03198316815283907ffec7db38481afeb8686a62ee7bba420143bd43540fe5e57b7316be50bdaa220c9060200160405180910390a2505050565b610ce361219c565b80610ced816121e9565b6000828152600860205260409020610d04816124e2565b610d0d81612906565b15610d515760405162461bcd60e51b81526020600482015260146024820152736e6f7420726561647920746f20756e7374616b6560601b604482015260640161092b565b610d5a816129ab565b60405183907f11725367022c3ff288940f4b5473aa61c2da6a24af7363a1128ee2401e8983b290600090a2505050565b610d926129e6565b610d9a612a40565b565b6060816001600160401b03811115610db657610db6613831565b604051908082528060200260200182016040528015610de957816020015b6060815260200190600190039081610dd45790505b5090506000610df7600b5490565b905060005b83811015610f1057816001600160401b03811115610e1c57610e1c613831565b604051908082528060200260200182016040528015610e45578160200160208202803683370190505b50838281518110610e5857610e58613b43565b6020026020010181905250600060096000878785818110610e7b57610e7b613b43565b9050602002016020810190610e909190613b59565b6001600160a01b03191681526020810191909152604001600090812091505b83811015610f06576000818152602083905260409020548551869085908110610eda57610eda613b43565b60200260200101518281518110610ef357610ef3613b43565b6020908102919091010152600101610eaf565b5050600101610dfc565b505092915050565b610bc483838360405180602001604052806000815250611772565b6000806000806000610f4486612415565b60008681526008602052604081208054600b80549293929091908110610f6c57610f6c613b43565b6000918252602090912060039182020180546001918201549185015460028601549590930154909b919a5091985092965060a01b94509092505050565b6000610fbd610fb88484612a95565b612afc565b9392505050565b6000610fce61219c565b346000610fdb8286612a95565b9050610fe681612b2d565b610ff08185612b79565b60075460405181907f1f44b78b04f7c6f80fc97ae8b196d9e9d7a81663114744f18fe5d073cd70ce4a9061102990889087908b90613b74565b60405180910390a295945050505050565b61104261219c565b60008060005b848110156110d95785858281811061106257611062613b43565b905060200201359250611074836121e9565b6000838152600860205260409020915061108d826124e2565b611097828561252c565b827f907fece23ce39fbcbceb71e515043fe29408353fbb393b25b35eb8a70a4bad0b856040516110c991815260200190565b60405180910390a2600101611048565b505050505050565b6110e961219c565b60008060005b838110156111705784848281811061110957611109613b43565b90506020020135925061111b836121e9565b6000838152600860205260409020915061113482612c19565b61113d82612c63565b60405183907ff27b6ce5b2f5e68ddb2fd95a8a909d4ecf1daaac270935fff052feacb24f184290600090a26001016110ef565b5050505050565b61117f61219c565b80611189816121e9565b60008281526008602052604090206111a081612c19565b6111a981612c63565b60405183907ff27b6ce5b2f5e68ddb2fd95a8a909d4ecf1daaac270935fff052feacb24f184290600090a2505050565b6000818152600260205260408120546001600160a01b0316806109cf5760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b604482015260640161092b565b6060816001600160401b0381111561125357611253613831565b60405190808252806020026020018201604052801561128657816020015b60608152602001906001900390816112715790505b5090506000611294600b5490565b905060005b83811015610f1057816001600160401b038111156112b9576112b9613831565b6040519080825280602002602001820160405280156112e2578160200160208202803683370190505b508382815181106112f5576112f5613b43565b60200260200101819052506000600a600087878581811061131857611318613b43565b905060200201602081019061132d9190613b59565b6001600160a01b03191681526020810191909152604001600090812091505b838110156113a357600081815260208390526040902054855186908590811061137757611377613b43565b6020026020010151828151811061139057611390613b43565b602090810291909101015260010161134c565b5050600101611299565b60006001600160a01b0382166114175760405162461bcd60e51b815260206004820152602960248201527f4552433732313a2061646472657373207a65726f206973206e6f7420612076616044820152683634b21037bbb732b960b91b606482015260840161092b565b506001600160a01b031660009081526003602052604090205490565b61143b6129e6565b610d9a6000612cbb565b60606000821180156114625750600b5461145f8385613bac565b11155b61147e5760405162461bcd60e51b815260040161092b90613bbf565b816001600160401b0381111561149657611496613831565b6040519080825280602002602001820160405280156114eb57816020015b6114d860405180606001604052806000815260200160008152602001600081525090565b8152602001906001900390816114b45790505b50905060005b8281101561157357600b8185018154811061150e5761150e613b43565b9060005260206000209060030201604051806060016040529081600082015481526020016001820154815260200160028201548152505082828151811061155757611557613b43565b602002602001018190525061156c8160010190565b90506114f1565b5092915050565b6115826129e6565b610d9a612d0d565b600061159582612415565b60008281526008602052604090206115ac816124e2565b610fbd81612906565b606060018054610a0a90613abc565b6115cf338383612d50565b5050565b6115db61219c565b6000805b83811015611170578484828181106115f9576115f9613b43565b90506020020135915061160b826121e9565b60008281526008602052604090206116239084612803565b6040516001600160a01b03198416815282907ffec7db38481afeb8686a62ee7bba420143bd43540fe5e57b7316be50bdaa220c9060200160405180910390a26001016115df565b61167261219c565b8161167c816121e9565b600083815260086020526040902061169381612c19565b8054600b805460009190839081106116ad576116ad613b43565b90600052602060002090600302019050848160000154346116ce9190613bac565b146116eb5760405162461bcd60e51b815260040161092b90613beb565b600383015460a01b6001600160a01b0319166000908152600a602090815260408083208584529091529020805460001901905560018101546117309084908790612e1e565b857f1d9c4d2b3e13eb9ac08a42625750ac17ec6ca94b4755c49285e9467b4e48c89d8660405161176291815260200190565b60405180910390a2505050505050565b61177c3383612614565b6117985760405162461bcd60e51b815260040161092b90613af6565b6117a484848484612e6e565b50505050565b6117b261219c565b60008060005b848110156110d9578585828181106117d2576117d2613b43565b9050602002013592506117e4836121e9565b600083815260086020526040902060028101549092506118039061223e565b156118485760405162461bcd60e51b81526020600482015260156024820152746e6f7420726561647920746f20776974686472617760581b604482015260640161092b565b611851836122b3565b61185b8285612356565b6040516001600160a01b0385169084907fd964a27d45f595739c13d8b1160b57491050cacf3a2e5602207277d6228f64ee90600090a36001016117b8565b6118a161219c565b600182116118e25760405162461bcd60e51b815260206004820152600e60248201526d0d2dcecc2d8d2c840d8cadccee8d60931b604482015260640161092b565b3460008080855b8015611a96576000190187878281811061190557611905613b43565b905060200201359350611917846121e9565b60008481526008602052604090209250611930836124e2565b82546003840154600b805460a09290921b918390811061195257611952613b43565b9060005260206000209060030201935083600101548810156119a95760405162461bcd60e51b815260206004820152601060248201526f34b73b30b634b210323ab930ba34b7b760811b604482015260640161092b565b83546119b59088613bac565b96506119c78560010154600019141590565b156119fd576001600160a01b03198116600090815260096020908152604080832085845290915290208054600019019055611a2a565b6001600160a01b031981166000908152600a60209081526040808320858452909152902080546000190190555b8215611a3e57611a39866122b3565b611a8f565b6000196001860155611a5185888a612e1e565b7fb3f4c8ca702dbbd32d9a25ce17b1942a5060284d9d69fc4fcac8fb0397891b128a8a898b604051611a869493929190613c16565b60405180910390a15b50506118e9565b5050505050505050565b6060611aab82612415565b6000611ac260408051602081019091526000815290565b90506000815111611ae25760405180602001604052806000815250610fbd565b80611aec84612ea1565b604051602001611afd929190613c5c565b6040516020818303038152906040529392505050565b611b1b6129e6565b81600003611b5f5760405162461bcd60e51b8152602060048201526011602482015270185b5bdd5b9d081a5cc81a5b9d985b1a59607a1b604482015260640161092b565b6000828152600c6020908152604080832084845290915290205415611bbe5760405162461bcd60e51b81526020600482015260156024820152746475706c6963617465206275636b6574207479706560581b604482015260640161092b565b60408051606081018252838152602080820184815243838501908152600b8054600181018255600082815295517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db960039092029182015592517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba84015590517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb9092019190915554858352600c82528383208584528252918390209190915581518481529081018390527f6b39e3267efcd6611c8d7d2534c4715dcb4824322b90d85540a3a82967b6e7b791015b60405180910390a15050565b6000611cc261219c565b600082118015611cda575034611cd88387613c8b565b145b611cf65760405162461bcd60e51b815260040161092b90613bbf565b6000611d028686612a95565b9050611d0d81612b2d565b600754600101915060005b83811015611d7757611d2a8286612b79565b611d348184613bac565b7f1f44b78b04f7c6f80fc97ae8b196d9e9d7a81663114744f18fe5d073cd70ce4a868989604051611d6793929190613b74565b60405180910390a2600101611d18565b50505b949350505050565b611d8a61219c565b81611d94816121e9565b6000838152600860205260409020611dab81612c19565b8054600b80546000919083908110611dc557611dc5613b43565b9060005260206000209060030201905080600101548511611df85760405162461bcd60e51b815260040161092b90613beb565b600383015460a01b6001600160a01b0319166000908152600a60209081526040808320858452909152902080546000190190558054611e3990849087612e1e565b857fc599168ac63ff28ec278088a2c424383a36ca26c931eb41af05e014f19252ea48660405161176291815260200190565b6000611e7561219c565b34825185611e839190613c8b565b14611ea05760405162461bcd60e51b815260040161092b90613bbf565b6000611eac8585612a95565b9050611eb781612b2d565b600754600101915060005b8351811015611f5457611eee82858381518110611ee157611ee1613b43565b6020026020010151612b79565b611ef88184613bac565b7f1f44b78b04f7c6f80fc97ae8b196d9e9d7a81663114744f18fe5d073cd70ce4a858381518110611f2b57611f2b613b43565b60200260200101518888604051611f4493929190613b74565b60405180910390a2600101611ec2565b50509392505050565b611f656129e6565b43600b611f728484612a95565b81548110611f8257611f82613b43565b9060005260206000209060030201600201819055507f6b39e3267efcd6611c8d7d2534c4715dcb4824322b90d85540a3a82967b6e7b78282604051611cac929190918252602082015260400190565b611fd961219c565b60008060005b8381101561117057848482818110611ff957611ff9613b43565b90506020020135925061200b836121e9565b60008381526008602052604090209150612024826124e2565b61202d82612906565b156120715760405162461bcd60e51b81526020600482015260146024820152736e6f7420726561647920746f20756e7374616b6560601b604482015260640161092b565b61207a826129ab565b60405183907f11725367022c3ff288940f4b5473aa61c2da6a24af7363a1128ee2401e8983b290600090a2600101611fdf565b6120b56129e6565b600019600b6120c48484612a95565b815481106120d4576120d4613b43565b9060005260206000209060030201600201819055507f099df2bf9247b43481cf1b791a4dd5fa1220c40c62940da539082fbcb30241d68282604051611cac929190918252602082015260400190565b61212b6129e6565b6001600160a01b0381166121905760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b606482015260840161092b565b61219981612cbb565b50565b600654600160a01b900460ff1615610d9a5760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b604482015260640161092b565b6121f2816111d9565b6001600160a01b0316336001600160a01b0316146121995760405162461bcd60e51b81526020600482015260096024820152683737ba1037bbb732b960b91b604482015260640161092b565b6000600019820361228a5760405162461bcd60e51b81526020600482015260166024820152751b9bdd08185b881d5b9cdd185ad95908189d58dad95d60521b604482015260640161092b565b600061229861ca8084613bac565b90504381116122aa5750600092915050565b43900392915050565b60006122be826111d9565b90506122ce816000846001612f33565b6122d7826111d9565b600083815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0385168085526003845282852080546000190190558785526002909352818420805490911690555192935084927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b6000600b83600001548154811061236f5761236f613b43565b600091825260208220600390910201546040519092506001600160a01b0384169083908381818185875af1925050503d80600081146123ca576040519150601f19603f3d011682016040523d82523d6000602084013e6123cf565b606091505b50509050806117a45760405162461bcd60e51b81526020600482015260126024820152713330b4b632b2103a37903a3930b739b332b960711b604482015260640161092b565b6000818152600260205260409020546001600160a01b03166121995760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b604482015260640161092b565b600081815260046020526040902080546001600160a01b0319166001600160a01b03841690811790915581906124a9826111d9565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b6002810154600019146121995760405162461bcd60e51b81526020600482015260126024820152713737ba10309039ba30b5b2b2103a37b5b2b760711b604482015260640161092b565b8154600383015460a01b61253f84612906565b8310156125815760405162461bcd60e51b815260206004820152601060248201526f34b73b30b634b210323ab930ba34b7b760811b604482015260640161092b565b60006125b1600b848154811061259957612599613b43565b90600052602060002090600302016000015485612a95565b90506125bc81612b2d565b60001960018681018290556001600160a01b03199390931660008181526009602090815260408083209783529681528682208054909401909355968390558652600a8152838620918652529220805490920190915550565b600080612620836111d9565b9050806001600160a01b0316846001600160a01b0316148061266757506001600160a01b0380821660009081526005602090815260408083209388168352929052205460ff165b80611d7a5750836001600160a01b031661268084610a8d565b6001600160a01b031614949350505050565b826001600160a01b03166126a5826111d9565b6001600160a01b0316146126cb5760405162461bcd60e51b815260040161092b90613ca2565b6001600160a01b03821661272d5760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b606482015260840161092b565b61273a8383836001612f33565b826001600160a01b031661274d826111d9565b6001600160a01b0316146127735760405162461bcd60e51b815260040161092b90613ca2565b600081815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0387811680865260038552838620805460001901905590871680865283862080546001019055868652600290945282852080549092168417909155905184937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b61280c826124e2565b8154600383015460a01b6001600160a01b0319808416908216036128425760405162461bcd60e51b815260040161092b90613beb565b600184015460001914612899576001600160a01b03198181166000908152600960208181526040808420878552825280842080546000190190559387168352908152828220858352905220805460010190556128df565b6001600160a01b03198181166000908152600a60208181526040808420878552825280842080546000190190559387168352908152828220858352905220805460010190555b505060039190910180546bffffffffffffffffffffffff191660a09290921c919091179055565b600181015460009060001981036129585760405162461bcd60e51b81526020600482015260166024820152751b9bdd08185b881d5b9b1bd8dad95908189d58dad95d60521b604482015260640161092b565b6000600b84600001548154811061297157612971613b43565b9060005260206000209060030201600101548261298e9190613bac565b90504381116129a1575060009392505050565b4390039392505050565b436002820155600381015460a01b6001600160a01b031916600090815260096020908152604080832093548352929052208054600019019055565b6006546001600160a01b03163314610d9a5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161092b565b612a48612ffc565b6006805460ff60a01b191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b6000828152600c6020908152604080832084845290915281205480612af25760405162461bcd60e51b8152602060048201526013602482015272696e76616c6964206275636b6574207479706560681b604482015260640161092b565b6000198101611d7a565b600043600b8381548110612b1257612b12613b43565b90600052602060002090600302016002015411159050919050565b612b3681612afc565b6121995760405162461bcd60e51b8152602060048201526014602482015273696e616374697665206275636b6574207479706560601b604482015260640161092b565b6007805460019081018083556040805160808101825286815260001960208083018281528385019283526001600160a01b0319891660608501818152600097885260088452868820955186559151858901559251600285015551600390930180546bffffffffffffffffffffffff191660a09490941c939093179092558352600a81528183208784529052902080549091019055546115cf90339061304c565b6001810154600019146121995760405162461bcd60e51b81526020600482015260126024820152713737ba1030903637b1b5b2b2103a37b5b2b760711b604482015260640161092b565b805460038201544360019384015560a01b6001600160a01b0319166000818152600a60209081526040808320858452825280832080546000190190559282526009815282822093825292909252902080549091019055565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b612d1561219c565b6006805460ff60a01b1916600160a01b1790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258612a783390565b816001600160a01b0316836001600160a01b031603612db15760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c657200000000000000604482015260640161092b565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b6000612e2a8383612a95565b9050612e3581612b2d565b600384015460a01b6001600160a01b0319166000908152600a602090815260408083208484529091529020805460010190559092555050565b612e79848484612692565b612e8584848484613066565b6117a45760405162461bcd60e51b815260040161092b90613ce7565b60606000612eae83613164565b60010190506000816001600160401b03811115612ecd57612ecd613831565b6040519080825280601f01601f191660200182016040528015612ef7576020820181803683370190505b5090508181016020015b600019016f181899199a1a9b1b9c1cb0b131b232b360811b600a86061a8153600a8504945084612f0157509392505050565b80600114612f835760405162461bcd60e51b815260206004820152601f60248201527f6261746368207472616e73666572206973206e6f7420737570706f7274656400604482015260640161092b565b6001600160a01b0383161580612fab5750600082815260086020526040902060020154600019145b612ff75760405162461bcd60e51b815260206004820152601e60248201527f63616e6e6f74207472616e7366657220756e7374616b656420746f6b656e0000604482015260640161092b565b6117a4565b600654600160a01b900460ff16610d9a5760405162461bcd60e51b815260206004820152601460248201527314185d5cd8589b194e881b9bdd081c185d5cd95960621b604482015260640161092b565b6115cf82826040518060200160405280600081525061323c565b60006001600160a01b0384163b1561315c57604051630a85bd0160e11b81526001600160a01b0385169063150b7a02906130aa903390899088908890600401613d39565b6020604051808303816000875af19250505080156130e5575060408051601f3d908101601f191682019092526130e291810190613d76565b60015b613142573d808015613113576040519150601f19603f3d011682016040523d82523d6000602084013e613118565b606091505b50805160000361313a5760405162461bcd60e51b815260040161092b90613ce7565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050611d7a565b506001611d7a565b60008072184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b83106131a35772184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b830492506040015b6d04ee2d6d415b85acef810000000083106131cf576d04ee2d6d415b85acef8100000000830492506020015b662386f26fc1000083106131ed57662386f26fc10000830492506010015b6305f5e1008310613205576305f5e100830492506008015b612710831061321957612710830492506004015b6064831061322b576064830492506002015b600a83106109cf5760010192915050565b613246838361326f565b6132536000848484613066565b610bc45760405162461bcd60e51b815260040161092b90613ce7565b6001600160a01b0382166132c55760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f2061646472657373604482015260640161092b565b6000818152600260205260409020546001600160a01b03161561332a5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000604482015260640161092b565b613338600083836001612f33565b6000818152600260205260409020546001600160a01b03161561339d5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000604482015260640161092b565b6001600160a01b038216600081815260036020908152604080832080546001019055848352600290915280822080546001600160a01b0319168417905551839291907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b6001600160a01b038116811461219957600080fd5b6000806040838503121561343057600080fd5b82359150602083013561344281613408565b809150509250929050565b6001600160e01b03198116811461219957600080fd5b60006020828403121561347557600080fd5b8135610fbd8161344d565b60006020828403121561349257600080fd5b5035919050565b60005b838110156134b457818101518382015260200161349c565b50506000910152565b600081518084526134d5816020860160208601613499565b601f01601f19169290920160200192915050565b602081526000610fbd60208301846134bd565b6000806040838503121561350f57600080fd5b823561351a81613408565b946020939093013593505050565b6000806040838503121561353b57600080fd5b50508035926020909101359150565b60008060006060848603121561355f57600080fd5b833561356a81613408565b9250602084013561357a81613408565b929592945050506040919091013590565b80356001600160a01b0319811681146135a357600080fd5b919050565b600080604083850312156135bb57600080fd5b823591506135cb6020840161358b565b90509250929050565b60008083601f8401126135e657600080fd5b5081356001600160401b038111156135fd57600080fd5b6020830191508360208260051b850101111561361857600080fd5b9250929050565b6000806020838503121561363257600080fd5b82356001600160401b0381111561364857600080fd5b613654858286016135d4565b90969095509350505050565b6000602080830181845280855180835260408601915060408160051b87010192508387016000805b838110156136dc57888603603f19018552825180518088529088019088880190845b818110156136c65783518352928a0192918a01916001016136aa565b5090975050509386019391860191600101613688565b509398975050505050505050565b6000806000604084860312156136ff57600080fd5b83356001600160401b0381111561371557600080fd5b613721868287016135d4565b909790965060209590950135949350505050565b60006020828403121561374757600080fd5b8135610fbd81613408565b602080825282518282018190526000919060409081850190868401855b8281101561379e578151805185528681015187860152850151858501526060909301929085019060010161376f565b5091979650505050505050565b600080604083850312156137be57600080fd5b82356137c981613408565b91506020830135801515811461344257600080fd5b6000806000604084860312156137f357600080fd5b83356001600160401b0381111561380957600080fd5b613815868287016135d4565b909450925061382890506020850161358b565b90509250925092565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b038111828210171561386f5761386f613831565b604052919050565b6000806000806080858703121561388d57600080fd5b843561389881613408565b93506020858101356138a981613408565b93506040860135925060608601356001600160401b03808211156138cc57600080fd5b818801915088601f8301126138e057600080fd5b8135818111156138f2576138f2613831565b613904601f8201601f19168501613847565b9150808252898482850101111561391a57600080fd5b808484018584013760008482840101525080935050505092959194509250565b60008060006040848603121561394f57600080fd5b83356001600160401b0381111561396557600080fd5b613971868287016135d4565b909450925050602084013561398581613408565b809150509250925092565b600080600080608085870312156139a657600080fd5b84359350602085013592506139bd6040860161358b565b9396929550929360600135925050565b6000806000606084860312156139e257600080fd5b83359250602080850135925060408501356001600160401b0380821115613a0857600080fd5b818701915087601f830112613a1c57600080fd5b813581811115613a2e57613a2e613831565b8060051b9150613a3f848301613847565b818152918301840191848101908a841115613a5957600080fd5b938501935b83851015613a7e57613a6f8561358b565b82529385019390850190613a5e565b8096505050505050509250925092565b60008060408385031215613aa157600080fd5b8235613aac81613408565b9150602083013561344281613408565b600181811c90821680613ad057607f821691505b602082108103613af057634e487b7160e01b600052602260045260246000fd5b50919050565b6020808252602d908201527f4552433732313a2063616c6c6572206973206e6f7420746f6b656e206f776e6560408201526c1c881bdc88185c1c1c9bdd9959609a1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215613b6b57600080fd5b610fbd8261358b565b6001600160a01b03199390931683526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b808201808211156109cf576109cf613b96565b602080825260129082015271696e76616c696420706172616d657465727360701b604082015260600190565b60208082526011908201527034b73b30b634b21037b832b930ba34b7b760791b604082015260600190565b6060808252810184905260006001600160fb1b03851115613c3657600080fd5b8460051b8087608085013760208301949094525060408101919091520160800192915050565b60008351613c6e818460208801613499565b835190830190613c82818360208801613499565b01949350505050565b80820281158282048414176109cf576109cf613b96565b60208082526025908201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060408201526437bbb732b960d91b606082015260800190565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090613d6c908301846134bd565b9695505050505050565b600060208284031215613d8857600080fd5b8151610fbd8161344d56fea2646970667358221220838d751ed1580567aada864b691317738d1db46809b8ca2091226ee62191d65464736f6c63430008130033", + "rawPrivateKey": "f964b7ccc40ccace513d3159fa9c30514c4a186ebfdd7c63d69cd79a29b804b0", + "rawAmount": "0", + "rawGasLimit": 20000000, + "rawGasPrice": "0", + "rawExpectedGasConsumed": 4888337, + "expectedStatus": 1, + "expectedBalances": [], + "comment": "deploy iip13 system staking contract" + } + ], + "executions": [ + { + "rawPrivateKey": "f964b7ccc40ccace513d3159fa9c30514c4a186ebfdd7c63d69cd79a29b804b0", + "rawByteCode": "c8e77923000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000064", + "rawAmount": "0", + "rawGasLimit": 1000000, + "rawGasPrice": "0", + "rawAccessList": [], + "rawExpectedGasConsumed": 122220, + "expectedStatus": 1, + "expectedLogs": [{}], + "rawReturnValue": "", + "comment": "add bucket type" + }, + { + "rawPrivateKey": "f964b7ccc40ccace513d3159fa9c30514c4a186ebfdd7c63d69cd79a29b804b0", + "rawByteCode": "597cc14a00000000000000000000000000000000000000000000000000000000000000640000000000000000792020100000000000000000000000000000000000000000", + "rawAmount": "10", + "rawGasLimit": 1000000, + "rawGasPrice": "0", + "rawAccessList": [], + "rawExpectedGasConsumed": 175893, + "expectedStatus": 1, + "expectedLogs": [{},{}], + "rawReturnValue": "", + "comment": "stake" + } + ] +} \ No newline at end of file diff --git a/action/protocol/execution/testdata/system-staking.sol b/action/protocol/execution/testdata/system-staking.sol new file mode 100644 index 0000000000..842b29a38d --- /dev/null +++ b/action/protocol/execution/testdata/system-staking.sol @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +struct BucketInfo { + uint256 typeIndex; + uint256 unlockedAt; // UINT256_MAX: in lock + uint256 unstakedAt; // UINT256_MAX: in stake + bytes12 delegate; +} + +struct BucketType { + uint256 amount; + uint256 duration; + uint256 activatedAt; +} + +contract SystemStaking is ERC721, Ownable, Pausable { + uint256 public constant UINT256_MAX = type(uint256).max; + uint256 public constant UNSTAKE_FREEZE_BLOCKS = 51840; // (3 * 24 * 60 * 60) / 5; + + event BucketTypeActivated(uint256 amount, uint256 duration); + event BucketTypeDeactivated(uint256 amount, uint256 duration); + event Staked(uint256 indexed tokenId, bytes12 delegate, uint256 amount, uint256 duration); + event Locked(uint256 indexed tokenId, uint256 duration); + event Unlocked(uint256 indexed tokenId); + event Unstaked(uint256 indexed tokenId); + event Merged(uint256[] tokenIds, uint256 amount, uint256 duration); + event DurationExtended(uint256 indexed tokenId, uint256 duration); + event AmountIncreased(uint256 indexed tokenId, uint256 amount); + event DelegateChanged(uint256 indexed tokenId, bytes12 newDelegate); + event Withdrawal(uint256 indexed tokenId, address indexed recipient); + + modifier onlyTokenOwner(uint256 _tokenId) { + _assertOnlyTokenOwner(_tokenId); + _; + } + + // token id + uint256 private __currTokenId; + // mapping from token ID to bucket + mapping(uint256 => BucketInfo) private __buckets; + // delegate name -> bucket type -> count + mapping(bytes12 => mapping(uint256 => uint256)) private __unlockedVotes; + // delegate name -> bucket type -> count + mapping(bytes12 => mapping(uint256 => uint256)) private __lockedVotes; + // bucket type + BucketType[] private __bucketTypes; + // amount -> duration -> index + mapping(uint256 => mapping(uint256 => uint256)) private __bucketTypeIndices; + + constructor() ERC721("BucketNFT", "BKT") {} + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function unsafeInc(uint256 x) private pure returns (uint256) { + unchecked { + return x + 1; + } + } + + function unsafeDec(uint256 x) private pure returns (uint256) { + unchecked { + return x - 1; + } + } + + // bucket type related functions + function addBucketType(uint256 _amount, uint256 _duration) external onlyOwner { + require(_amount != 0, "amount is invalid"); + require(__bucketTypeIndices[_amount][_duration] == 0, "duplicate bucket type"); + __bucketTypes.push(BucketType(_amount, _duration, block.number)); + __bucketTypeIndices[_amount][_duration] = __bucketTypes.length; + emit BucketTypeActivated(_amount, _duration); + } + + function deactivateBucketType(uint256 _amount, uint256 _duration) external onlyOwner { + __bucketTypes[_bucketTypeIndex(_amount, _duration)].activatedAt = UINT256_MAX; + emit BucketTypeDeactivated(_amount, _duration); + } + + function activateBucketType(uint256 _amount, uint256 _duration) external onlyOwner { + __bucketTypes[_bucketTypeIndex(_amount, _duration)].activatedAt = block.number; + emit BucketTypeActivated(_amount, _duration); + } + + function isActiveBucketType(uint256 _amount, uint256 _duration) external view returns (bool) { + return _isActiveBucketType(_bucketTypeIndex(_amount, _duration)); + } + + function numOfBucketTypes() public view returns (uint256) { + return __bucketTypes.length; + } + + function bucketTypes( + uint256 _offset, + uint256 _size + ) external view returns (BucketType[] memory types_) { + require(_size > 0 && _offset + _size <= numOfBucketTypes(), "invalid parameters"); + types_ = new BucketType[](_size); + for (uint256 i = 0; i < _size; i = unsafeInc(i)) { + unchecked { + types_[i] = __bucketTypes[_offset + i]; + } + } + } + + // token related functions + function blocksToUnstake(uint256 _tokenId) external view returns (uint256) { + _assertOnlyValidToken(_tokenId); + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyStakedToken(bucket); + return _blocksToUnstake(bucket); + } + + function blocksToWithdraw(uint256 _tokenId) external view returns (uint256) { + _assertOnlyValidToken(_tokenId); + return _blocksToWithdraw(__buckets[_tokenId].unstakedAt); + } + + function bucketOf( + uint256 _tokenId + ) + external + view + returns ( + uint256 amount_, + uint256 duration_, + uint256 unlockedAt_, + uint256 unstakedAt_, + bytes12 delegate_ + ) + { + _assertOnlyValidToken(_tokenId); + BucketInfo storage bucket = __buckets[_tokenId]; + BucketType storage bucketType = __bucketTypes[bucket.typeIndex]; + + return ( + bucketType.amount, + bucketType.duration, + bucket.unlockedAt, + bucket.unstakedAt, + bucket.delegate + ); + } + + function stake( + uint256 _duration, + bytes12 _delegate + ) external payable whenNotPaused returns (uint256) { + uint256 msgValue = msg.value; + uint256 index = _bucketTypeIndex(msgValue, _duration); + _assertOnlyActiveBucketType(index); + + _stake(index, _delegate); + uint256 tokenId = __currTokenId; + emit Staked(tokenId, _delegate, msgValue, _duration); + + return tokenId; + } + + function stake( + uint256 _amount, + uint256 _duration, + bytes12[] memory _delegates + ) external payable whenNotPaused returns (uint256 firstTokenId_) { + require(_amount * _delegates.length == msg.value, "invalid parameters"); + uint256 index = _bucketTypeIndex(_amount, _duration); + _assertOnlyActiveBucketType(index); + unchecked { + firstTokenId_ = __currTokenId + 1; + } + for (uint256 i = 0; i < _delegates.length; i = unsafeInc(i)) { + _stake(index, _delegates[i]); + emit Staked(firstTokenId_ + i, _delegates[i], _amount, _duration); + } + + return firstTokenId_; + } + + function stake( + uint256 _amount, + uint256 _duration, + bytes12 _delegate, + uint256 _count + ) external payable whenNotPaused returns (uint256 firstTokenId_) { + require(_count > 0 && _amount * _count == msg.value, "invalid parameters"); + uint256 index = _bucketTypeIndex(_amount, _duration); + _assertOnlyActiveBucketType(index); + unchecked { + firstTokenId_ = __currTokenId + 1; + } + for (uint256 i = 0; i < _count; i = unsafeInc(i)) { + _stake(index, _delegate); + emit Staked(firstTokenId_ + i, _delegate, _amount, _duration); + } + + return firstTokenId_; + } + + function unlock(uint256 _tokenId) external whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyLockedToken(bucket); + _unlock(bucket); + emit Unlocked(_tokenId); + } + + function unlock(uint256[] calldata _tokenIds) external whenNotPaused { + uint256 tokenId; + BucketInfo storage bucket; + for (uint256 i = 0; i < _tokenIds.length; i = unsafeInc(i)) { + tokenId = _tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + bucket = __buckets[tokenId]; + _assertOnlyLockedToken(bucket); + _unlock(bucket); + emit Unlocked(tokenId); + } + } + + function lock( + uint256 _tokenId, + uint256 _duration + ) external whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyStakedToken(bucket); + _lock(bucket, _duration); + emit Locked(_tokenId, _duration); + } + + function lock(uint256[] calldata _tokenIds, uint256 _duration) external whenNotPaused { + uint256 tokenId; + BucketInfo storage bucket; + for (uint256 i = 0; i < _tokenIds.length; i = unsafeInc(i)) { + tokenId = _tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + bucket = __buckets[tokenId]; + _assertOnlyStakedToken(bucket); + _lock(bucket, _duration); + emit Locked(tokenId, _duration); + } + } + + function unstake(uint256 _tokenId) external whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyStakedToken(bucket); + require(_blocksToUnstake(bucket) == 0, "not ready to unstake"); + _unstake(bucket); + emit Unstaked(_tokenId); + } + + function unstake(uint256[] calldata _tokenIds) external whenNotPaused { + uint256 tokenId; + BucketInfo storage bucket; + for (uint256 i = 0; i < _tokenIds.length; i = unsafeInc(i)) { + tokenId = _tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + bucket = __buckets[tokenId]; + _assertOnlyStakedToken(bucket); + require(_blocksToUnstake(bucket) == 0, "not ready to unstake"); + _unstake(bucket); + emit Unstaked(tokenId); + } + } + + function withdraw( + uint256 _tokenId, + address payable _recipient + ) external whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + require(_blocksToWithdraw(bucket.unstakedAt) == 0, "not ready to withdraw"); + _burn(_tokenId); + _withdraw(bucket, _recipient); + emit Withdrawal(_tokenId, _recipient); + } + + function withdraw( + uint256[] calldata _tokenIds, + address payable _recipient + ) external whenNotPaused { + uint256 tokenId; + BucketInfo storage bucket; + for (uint256 i = 0; i < _tokenIds.length; i = unsafeInc(i)) { + tokenId = _tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + bucket = __buckets[tokenId]; + require(_blocksToWithdraw(bucket.unstakedAt) == 0, "not ready to withdraw"); + _burn(tokenId); + _withdraw(bucket, _recipient); + emit Withdrawal(tokenId, _recipient); + } + } + + function merge( + uint256[] calldata tokenIds, + uint256 _newDuration + ) external payable whenNotPaused { + require(tokenIds.length > 1, "invalid length"); + uint256 amount = msg.value; + uint256 tokenId; + BucketInfo storage bucket; + BucketType storage bucketType; + for (uint256 i = tokenIds.length; i > 0; ) { + i = unsafeDec(i); + tokenId = tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + bucket = __buckets[tokenId]; + _assertOnlyStakedToken(bucket); + uint256 typeIndex = bucket.typeIndex; + bytes12 delegate = bucket.delegate; + bucketType = __bucketTypes[typeIndex]; + require(_newDuration >= bucketType.duration, "invalid duration"); + amount += bucketType.amount; + if (_isTriggered(bucket.unlockedAt)) { + __unlockedVotes[delegate][typeIndex] = unsafeDec( + __unlockedVotes[delegate][typeIndex] + ); + } else { + __lockedVotes[delegate][typeIndex] = unsafeDec(__lockedVotes[delegate][typeIndex]); + } + if (i != 0) { + _burn(tokenId); + } else { + bucket.unlockedAt = UINT256_MAX; + _updateBucketInfo(bucket, amount, _newDuration); + emit Merged(tokenIds, amount, _newDuration); + } + } + } + + function extendDuration( + uint256 _tokenId, + uint256 _newDuration + ) external whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyLockedToken(bucket); + uint256 typeIndex = bucket.typeIndex; + BucketType storage bucketType = __bucketTypes[typeIndex]; + require(_newDuration > bucketType.duration, "invalid operation"); + __lockedVotes[bucket.delegate][typeIndex] = unsafeDec( + __lockedVotes[bucket.delegate][typeIndex] + ); + _updateBucketInfo(bucket, bucketType.amount, _newDuration); + emit DurationExtended(_tokenId, _newDuration); + } + + function increaseAmount( + uint256 _tokenId, + uint256 _newAmount + ) external payable whenNotPaused onlyTokenOwner(_tokenId) { + BucketInfo storage bucket = __buckets[_tokenId]; + _assertOnlyLockedToken(bucket); + uint256 typeIndex = bucket.typeIndex; + BucketType storage bucketType = __bucketTypes[typeIndex]; + require(msg.value + bucketType.amount == _newAmount, "invalid operation"); + __lockedVotes[bucket.delegate][typeIndex] = unsafeDec( + __lockedVotes[bucket.delegate][typeIndex] + ); + _updateBucketInfo(bucket, _newAmount, bucketType.duration); + emit AmountIncreased(_tokenId, _newAmount); + } + + function changeDelegate( + uint256 _tokenId, + bytes12 _delegate + ) external whenNotPaused onlyTokenOwner(_tokenId) { + _changeDelegate(__buckets[_tokenId], _delegate); + emit DelegateChanged(_tokenId, _delegate); + } + + function changeDelegates( + uint256[] calldata _tokenIds, + bytes12 _delegate + ) external whenNotPaused { + uint256 tokenId; + for (uint256 i = 0; i < _tokenIds.length; i = unsafeInc(i)) { + tokenId = _tokenIds[i]; + _assertOnlyTokenOwner(tokenId); + _changeDelegate(__buckets[tokenId], _delegate); + emit DelegateChanged(tokenId, _delegate); + } + } + + function lockedVotesTo( + bytes12[] calldata _delegates + ) external view returns (uint256[][] memory counts_) { + counts_ = new uint256[][](_delegates.length); + uint256 tl = numOfBucketTypes(); + for (uint256 i = 0; i < _delegates.length; i = unsafeInc(i)) { + counts_[i] = new uint256[](tl); + mapping(uint256 => uint256) storage votes = __lockedVotes[_delegates[i]]; + for (uint256 j = 0; j < tl; j = unsafeInc(j)) { + counts_[i][j] = votes[j]; + } + } + + return counts_; + } + + function unlockedVotesTo( + bytes12[] calldata _delegates + ) external view returns (uint256[][] memory counts_) { + counts_ = new uint256[][](_delegates.length); + uint256 tl = numOfBucketTypes(); + for (uint256 i = 0; i < _delegates.length; i = unsafeInc(i)) { + counts_[i] = new uint256[](tl); + mapping(uint256 => uint256) storage votes = __unlockedVotes[_delegates[i]]; + for (uint256 j = 0; j < tl; j = unsafeInc(j)) { + counts_[i][j] = votes[j]; + } + } + + return counts_; + } + + ///////////////////////////////////////////// + // Private Functions + function _bucketTypeIndex(uint256 _amount, uint256 _duration) internal view returns (uint256) { + uint256 index = __bucketTypeIndices[_amount][_duration]; + require(index > 0, "invalid bucket type"); + + return unsafeDec(index); + } + + // bucket type index `_index` must be valid + function _isActiveBucketType(uint256 _index) internal view returns (bool) { + return __bucketTypes[_index].activatedAt <= block.number; + } + + function _isTriggered(uint256 _value) internal pure returns (bool) { + return _value != UINT256_MAX; + } + + function _assertOnlyTokenOwner(uint256 _tokenId) internal view { + require(msg.sender == ownerOf(_tokenId), "not owner"); + } + + function _assertOnlyLockedToken(BucketInfo storage _bucket) internal view { + require(!_isTriggered(_bucket.unlockedAt), "not a locked token"); + } + + function _assertOnlyStakedToken(BucketInfo storage _bucket) internal view { + require(!_isTriggered(_bucket.unstakedAt), "not a staked token"); + } + + function _assertOnlyValidToken(uint256 _tokenId) internal view { + require(_exists(_tokenId), "ERC721: invalid token ID"); + } + + function _assertOnlyActiveBucketType(uint256 _index) internal view { + require(_isActiveBucketType(_index), "inactive bucket type"); + } + + function _beforeTokenTransfer( + address _from, + address _to, + uint256 _firstTokenId, + uint256 _batchSize + ) internal override { + require(_batchSize == 1, "batch transfer is not supported"); + require( + _to == address(0) || !_isTriggered(__buckets[_firstTokenId].unstakedAt), + "cannot transfer unstaked token" + ); + super._beforeTokenTransfer(_from, _to, _firstTokenId, _batchSize); + } + + function _blocksToWithdraw(uint256 _unstakedAt) internal view returns (uint256) { + require(_isTriggered(_unstakedAt), "not an unstaked bucket"); + uint256 withdrawBlock = _unstakedAt + UNSTAKE_FREEZE_BLOCKS; + if (withdrawBlock <= block.number) { + return 0; + } + + unchecked { + return withdrawBlock - block.number; + } + } + + function _blocksToUnstake(BucketInfo storage _bucket) internal view returns (uint256) { + uint256 unlockedAt = _bucket.unlockedAt; + require(_isTriggered(unlockedAt), "not an unlocked bucket"); + uint256 unstakeBlock = unlockedAt + __bucketTypes[_bucket.typeIndex].duration; + if (unstakeBlock <= block.number) { + return 0; + } + unchecked { + return unstakeBlock - block.number; + } + } + + function _stake(uint256 _index, bytes12 _delegate) internal { + __currTokenId = unsafeInc(__currTokenId); + __buckets[__currTokenId] = BucketInfo(_index, UINT256_MAX, UINT256_MAX, _delegate); + __lockedVotes[_delegate][_index] = unsafeInc(__lockedVotes[_delegate][_index]); + _safeMint(msg.sender, __currTokenId); + } + + function _unlock(BucketInfo storage _bucket) internal { + uint256 typeIndex = _bucket.typeIndex; + bytes12 delegate = _bucket.delegate; + _bucket.unlockedAt = block.number; + __lockedVotes[delegate][typeIndex] = unsafeDec(__lockedVotes[delegate][typeIndex]); + __unlockedVotes[delegate][typeIndex] = unsafeInc(__unlockedVotes[delegate][typeIndex]); + } + + function _lock(BucketInfo storage _bucket, uint256 _duration) internal { + uint256 typeIndex = _bucket.typeIndex; + bytes12 delegate = _bucket.delegate; + require(_duration >= _blocksToUnstake(_bucket), "invalid duration"); + uint256 newIndex = _bucketTypeIndex(__bucketTypes[typeIndex].amount, _duration); + _assertOnlyActiveBucketType(newIndex); + _bucket.unlockedAt = UINT256_MAX; + __unlockedVotes[delegate][typeIndex] = unsafeDec(__unlockedVotes[delegate][typeIndex]); + _bucket.typeIndex = newIndex; + __lockedVotes[delegate][newIndex] = unsafeInc(__lockedVotes[delegate][newIndex]); + } + + function _unstake(BucketInfo storage _bucket) internal { + _bucket.unstakedAt = block.number; + __unlockedVotes[_bucket.delegate][_bucket.typeIndex] = unsafeDec( + __unlockedVotes[_bucket.delegate][_bucket.typeIndex] + ); + } + + function _withdraw(BucketInfo storage _bucket, address payable _recipient) internal { + uint256 amount = __bucketTypes[_bucket.typeIndex].amount; + (bool success, ) = _recipient.call{value: amount}(""); + require(success, "failed to transfer"); + } + + function _updateBucketInfo( + BucketInfo storage _bucket, + uint256 _amount, + uint256 _duration + ) internal { + uint256 index = _bucketTypeIndex(_amount, _duration); + _assertOnlyActiveBucketType(index); + __lockedVotes[_bucket.delegate][index] = unsafeInc(__lockedVotes[_bucket.delegate][index]); + _bucket.typeIndex = index; + } + + function _changeDelegate(BucketInfo storage _bucket, bytes12 _newDelegate) internal { + _assertOnlyStakedToken(_bucket); + uint256 typeIndex = _bucket.typeIndex; + bytes12 delegate = _bucket.delegate; + require(delegate != _newDelegate, "invalid operation"); + if (_isTriggered(_bucket.unlockedAt)) { + __unlockedVotes[delegate][typeIndex] = unsafeDec(__unlockedVotes[delegate][typeIndex]); + __unlockedVotes[_newDelegate][typeIndex] = unsafeInc( + __unlockedVotes[_newDelegate][typeIndex] + ); + } else { + __lockedVotes[delegate][typeIndex] = unsafeDec(__lockedVotes[delegate][typeIndex]); + __lockedVotes[_newDelegate][typeIndex] = unsafeInc( + __lockedVotes[_newDelegate][typeIndex] + ); + } + _bucket.delegate = _newDelegate; + } +} \ No newline at end of file diff --git a/blockindex/indexpb/contractstaking_bucket.pb.go b/blockindex/indexpb/contractstaking_bucket.pb.go new file mode 100644 index 0000000000..e5b4635118 --- /dev/null +++ b/blockindex/indexpb/contractstaking_bucket.pb.go @@ -0,0 +1,286 @@ +// Copyright (c) 2019 IoTeX +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.21.12 +// source: blockindex/indexpb/contractstaking_bucket.proto + +package indexpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type BucketType struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Amount string `protobuf:"bytes,1,opt,name=amount,proto3" json:"amount,omitempty"` + Duration uint64 `protobuf:"varint,2,opt,name=duration,proto3" json:"duration,omitempty"` + ActivatedAt uint64 `protobuf:"varint,3,opt,name=activatedAt,proto3" json:"activatedAt,omitempty"` +} + +func (x *BucketType) Reset() { + *x = BucketType{} + if protoimpl.UnsafeEnabled { + mi := &file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BucketType) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BucketType) ProtoMessage() {} + +func (x *BucketType) ProtoReflect() protoreflect.Message { + mi := &file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BucketType.ProtoReflect.Descriptor instead. +func (*BucketType) Descriptor() ([]byte, []int) { + return file_blockindex_indexpb_contractstaking_bucket_proto_rawDescGZIP(), []int{0} +} + +func (x *BucketType) GetAmount() string { + if x != nil { + return x.Amount + } + return "" +} + +func (x *BucketType) GetDuration() uint64 { + if x != nil { + return x.Duration + } + return 0 +} + +func (x *BucketType) GetActivatedAt() uint64 { + if x != nil { + return x.ActivatedAt + } + return 0 +} + +type BucketInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TypeIndex uint64 `protobuf:"varint,1,opt,name=typeIndex,proto3" json:"typeIndex,omitempty"` + CreatedAt uint64 `protobuf:"varint,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + UnlockedAt uint64 `protobuf:"varint,3,opt,name=unlockedAt,proto3" json:"unlockedAt,omitempty"` + UnstakedAt uint64 `protobuf:"varint,4,opt,name=unstakedAt,proto3" json:"unstakedAt,omitempty"` + Delegate string `protobuf:"bytes,5,opt,name=delegate,proto3" json:"delegate,omitempty"` + Owner string `protobuf:"bytes,6,opt,name=owner,proto3" json:"owner,omitempty"` +} + +func (x *BucketInfo) Reset() { + *x = BucketInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BucketInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BucketInfo) ProtoMessage() {} + +func (x *BucketInfo) ProtoReflect() protoreflect.Message { + mi := &file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BucketInfo.ProtoReflect.Descriptor instead. +func (*BucketInfo) Descriptor() ([]byte, []int) { + return file_blockindex_indexpb_contractstaking_bucket_proto_rawDescGZIP(), []int{1} +} + +func (x *BucketInfo) GetTypeIndex() uint64 { + if x != nil { + return x.TypeIndex + } + return 0 +} + +func (x *BucketInfo) GetCreatedAt() uint64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *BucketInfo) GetUnlockedAt() uint64 { + if x != nil { + return x.UnlockedAt + } + return 0 +} + +func (x *BucketInfo) GetUnstakedAt() uint64 { + if x != nil { + return x.UnstakedAt + } + return 0 +} + +func (x *BucketInfo) GetDelegate() string { + if x != nil { + return x.Delegate + } + return "" +} + +func (x *BucketInfo) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +var File_blockindex_indexpb_contractstaking_bucket_proto protoreflect.FileDescriptor + +var file_blockindex_indexpb_contractstaking_bucket_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x73, 0x74, 0x61, + 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x70, 0x62, 0x22, 0x62, 0x0a, 0x0a, 0x42, 0x75, + 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xba, + 0x01, 0x0a, 0x0a, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x79, 0x70, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x09, 0x74, 0x79, 0x70, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, + 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x73, + 0x74, 0x61, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, + 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x6c, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x6c, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x42, 0x37, 0x5a, 0x35, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x2d, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_blockindex_indexpb_contractstaking_bucket_proto_rawDescOnce sync.Once + file_blockindex_indexpb_contractstaking_bucket_proto_rawDescData = file_blockindex_indexpb_contractstaking_bucket_proto_rawDesc +) + +func file_blockindex_indexpb_contractstaking_bucket_proto_rawDescGZIP() []byte { + file_blockindex_indexpb_contractstaking_bucket_proto_rawDescOnce.Do(func() { + file_blockindex_indexpb_contractstaking_bucket_proto_rawDescData = protoimpl.X.CompressGZIP(file_blockindex_indexpb_contractstaking_bucket_proto_rawDescData) + }) + return file_blockindex_indexpb_contractstaking_bucket_proto_rawDescData +} + +var file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_blockindex_indexpb_contractstaking_bucket_proto_goTypes = []interface{}{ + (*BucketType)(nil), // 0: indexpb.BucketType + (*BucketInfo)(nil), // 1: indexpb.BucketInfo +} +var file_blockindex_indexpb_contractstaking_bucket_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_blockindex_indexpb_contractstaking_bucket_proto_init() } +func file_blockindex_indexpb_contractstaking_bucket_proto_init() { + if File_blockindex_indexpb_contractstaking_bucket_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BucketType); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BucketInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_blockindex_indexpb_contractstaking_bucket_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_blockindex_indexpb_contractstaking_bucket_proto_goTypes, + DependencyIndexes: file_blockindex_indexpb_contractstaking_bucket_proto_depIdxs, + MessageInfos: file_blockindex_indexpb_contractstaking_bucket_proto_msgTypes, + }.Build() + File_blockindex_indexpb_contractstaking_bucket_proto = out.File + file_blockindex_indexpb_contractstaking_bucket_proto_rawDesc = nil + file_blockindex_indexpb_contractstaking_bucket_proto_goTypes = nil + file_blockindex_indexpb_contractstaking_bucket_proto_depIdxs = nil +} diff --git a/blockindex/indexpb/contractstaking_bucket.proto b/blockindex/indexpb/contractstaking_bucket.proto new file mode 100644 index 0000000000..2a5a94f7cd --- /dev/null +++ b/blockindex/indexpb/contractstaking_bucket.proto @@ -0,0 +1,25 @@ +// Copyright (c) 2019 IoTeX +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto +syntax = "proto3"; +package indexpb; +option go_package = "github.com/iotexproject/iotex-core/blockindex/indexpb"; + +message BucketType { + string amount = 1; + uint64 duration = 2; + uint64 activatedAt = 3; +} + +message BucketInfo { + uint64 typeIndex = 1; + uint64 createdAt = 2; + uint64 unlockedAt = 3; + uint64 unstakedAt = 4; + string delegate = 5; + string owner = 6; +} diff --git a/blockindex/liquidstaking_bucket.go b/blockindex/liquidstaking_bucket.go new file mode 100644 index 0000000000..771e9c0687 --- /dev/null +++ b/blockindex/liquidstaking_bucket.go @@ -0,0 +1,127 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "math" + "math/big" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/blockindex/indexpb" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" +) + +const ( + maxBlockNumber uint64 = math.MaxUint64 +) + +type ( + // ContractStakingBucketInfo is the bucket information + ContractStakingBucketInfo struct { + TypeIndex uint64 + CreatedAt uint64 + UnlockedAt uint64 + UnstakedAt uint64 + Delegate address.Address // owner address of the delegate + Owner address.Address + } + + // ContractStakingBucketType is the bucket type + ContractStakingBucketType struct { + Amount *big.Int + Duration uint64 + ActivatedAt uint64 + } + + // ContractStakingBucket is the bucket information including bucket type and bucket info + ContractStakingBucket struct { + Index uint64 + Candidate address.Address + Owner address.Address + StakedAmount *big.Int + StakedDurationBlockNumber uint64 + CreateBlockHeight uint64 + StakeBlockHeight uint64 + UnstakeBlockHeight uint64 + AutoStake bool + ContractAddress string + } +) + +func (bt *ContractStakingBucketType) toProto() *indexpb.BucketType { + return &indexpb.BucketType{ + Amount: bt.Amount.String(), + Duration: bt.Duration, + ActivatedAt: bt.ActivatedAt, + } +} + +func (bt *ContractStakingBucketType) loadProto(p *indexpb.BucketType) error { + var ok bool + bt.Amount, ok = big.NewInt(0).SetString(p.Amount, 10) + if !ok { + return errors.New("failed to parse amount") + } + bt.Duration = p.Duration + bt.ActivatedAt = p.ActivatedAt + return nil +} + +func (bt *ContractStakingBucketType) serialize() []byte { + return byteutil.Must(proto.Marshal(bt.toProto())) +} + +func (bt *ContractStakingBucketType) deserialize(b []byte) error { + m := indexpb.BucketType{} + if err := proto.Unmarshal(b, &m); err != nil { + return err + } + return bt.loadProto(&m) +} + +func (bi *ContractStakingBucketInfo) toProto() *indexpb.BucketInfo { + pb := &indexpb.BucketInfo{ + TypeIndex: bi.TypeIndex, + Delegate: bi.Delegate.String(), + CreatedAt: bi.CreatedAt, + Owner: bi.Owner.String(), + UnlockedAt: bi.UnlockedAt, + UnstakedAt: bi.UnstakedAt, + } + return pb +} + +func (bi *ContractStakingBucketInfo) serialize() []byte { + return byteutil.Must(proto.Marshal(bi.toProto())) +} + +func (bi *ContractStakingBucketInfo) deserialize(b []byte) error { + m := indexpb.BucketInfo{} + if err := proto.Unmarshal(b, &m); err != nil { + return err + } + return bi.loadProto(&m) +} + +func (bi *ContractStakingBucketInfo) loadProto(p *indexpb.BucketInfo) error { + var err error + bi.TypeIndex = p.TypeIndex + bi.CreatedAt = p.CreatedAt + bi.UnlockedAt = p.UnlockedAt + bi.UnstakedAt = p.UnstakedAt + bi.Delegate, err = address.FromString(p.Delegate) + if err != nil { + return err + } + bi.Owner, err = address.FromString(p.Owner) + if err != nil { + return err + } + return nil +} diff --git a/blockindex/liquidstaking_cache.go b/blockindex/liquidstaking_cache.go new file mode 100644 index 0000000000..5f27a7da2f --- /dev/null +++ b/blockindex/liquidstaking_cache.go @@ -0,0 +1,217 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "math/big" + + "github.com/iotexproject/iotex-address/address" +) + +type ( + // contractStakingCacheReader is the interface to read contract staking cache + // it serves the purpose of preventing modifications to it. + contractStakingCacheReader interface { + getHeight() uint64 + getTotalBucketCount() uint64 + getTotalBucketTypeCount() uint64 + getBucketTypeIndex(amount *big.Int, duration uint64) (uint64, bool) + getBucketType(id uint64) (*ContractStakingBucketType, bool) + getBucketInfo(id uint64) (*ContractStakingBucketInfo, bool) + mustGetBucketType(id uint64) *ContractStakingBucketType + mustGetBucketInfo(id uint64) *ContractStakingBucketInfo + getAllBucketInfo() map[uint64]*ContractStakingBucketInfo + getActiveBucketType() map[uint64]*ContractStakingBucketType + getCandidateVotes(candidate address.Address) *big.Int + getBucketInfoByCandidate(candidate address.Address) map[uint64]*ContractStakingBucketInfo + } + + // contractStakingCacheManager is the interface to manage contract staking cache + // it's used to hide internal data, ensuring thread safety when used within the package + contractStakingCacheManager interface { + contractStakingCacheReader + merge(delta *contractStakingDelta) error + putHeight(h uint64) + putTotalBucketCount(cnt uint64) + putBucketType(id uint64, bt *ContractStakingBucketType) + putBucketInfo(id uint64, bi *ContractStakingBucketInfo) + deleteBucketInfo(id uint64) + } + + contractStakingCache struct { + idBucketMap map[uint64]*ContractStakingBucketInfo // map[token]BucketInfo + candidateBucketMap map[string]map[uint64]bool // map[candidate]bucket + idBucketTypeMap map[uint64]*ContractStakingBucketType // map[token]BucketType + propertyBucketTypeMap map[int64]map[uint64]uint64 // map[amount][duration]index + height uint64 + totalBucketCount uint64 // total number of buckets including burned buckets + } +) + +func newContractStakingCache() *contractStakingCache { + cache := &contractStakingCache{ + idBucketMap: make(map[uint64]*ContractStakingBucketInfo), + idBucketTypeMap: make(map[uint64]*ContractStakingBucketType), + propertyBucketTypeMap: make(map[int64]map[uint64]uint64), + candidateBucketMap: make(map[string]map[uint64]bool), + } + return cache +} + +func (s *contractStakingCache) putHeight(h uint64) { + s.height = h +} + +func (s *contractStakingCache) getHeight() uint64 { + return s.height +} + +func (s *contractStakingCache) putBucketType(id uint64, bt *ContractStakingBucketType) { + amount := bt.Amount.Int64() + s.idBucketTypeMap[id] = bt + m, ok := s.propertyBucketTypeMap[amount] + if !ok { + s.propertyBucketTypeMap[amount] = make(map[uint64]uint64) + m = s.propertyBucketTypeMap[amount] + } + m[bt.Duration] = id +} + +func (s *contractStakingCache) putBucketInfo(id uint64, bi *ContractStakingBucketInfo) { + s.idBucketMap[id] = bi + if _, ok := s.candidateBucketMap[bi.Delegate.String()]; !ok { + s.candidateBucketMap[bi.Delegate.String()] = make(map[uint64]bool) + } + s.candidateBucketMap[bi.Delegate.String()][id] = true +} + +func (s *contractStakingCache) deleteBucketInfo(id uint64) { + bi, ok := s.idBucketMap[id] + if !ok { + return + } + delete(s.idBucketMap, id) + if _, ok := s.candidateBucketMap[bi.Delegate.String()]; !ok { + return + } + delete(s.candidateBucketMap[bi.Delegate.String()], id) +} + +func (s *contractStakingCache) getBucketTypeIndex(amount *big.Int, duration uint64) (uint64, bool) { + m, ok := s.propertyBucketTypeMap[amount.Int64()] + if !ok { + return 0, false + } + id, ok := m[duration] + return id, ok +} + +func (s *contractStakingCache) getBucketType(id uint64) (*ContractStakingBucketType, bool) { + bt, ok := s.idBucketTypeMap[id] + return bt, ok +} + +func (s *contractStakingCache) mustGetBucketType(id uint64) *ContractStakingBucketType { + bt, ok := s.idBucketTypeMap[id] + if !ok { + panic("bucket type not found") + } + return bt +} + +func (s *contractStakingCache) getBucketInfo(id uint64) (*ContractStakingBucketInfo, bool) { + bi, ok := s.idBucketMap[id] + return bi, ok +} + +func (s *contractStakingCache) mustGetBucketInfo(id uint64) *ContractStakingBucketInfo { + bt, ok := s.idBucketMap[id] + if !ok { + panic("bucket info not found") + } + return bt +} + +func (s *contractStakingCache) getCandidateVotes(candidate address.Address) *big.Int { + votes := big.NewInt(0) + m, ok := s.candidateBucketMap[candidate.String()] + if !ok { + return votes + } + for id, existed := range m { + if !existed { + continue + } + bi, ok := s.idBucketMap[id] + if !ok { + continue + } + if bi.UnstakedAt != maxBlockNumber { + continue + } + bt := s.mustGetBucketType(bi.TypeIndex) + votes.Add(votes, bt.Amount) + } + return votes +} + +func (s *contractStakingCache) putTotalBucketCount(count uint64) { + s.totalBucketCount = count +} + +func (s *contractStakingCache) getTotalBucketCount() uint64 { + return s.totalBucketCount +} + +func (s *contractStakingCache) getTotalBucketTypeCount() uint64 { + return uint64(len(s.idBucketTypeMap)) +} + +func (s *contractStakingCache) getAllBucketInfo() map[uint64]*ContractStakingBucketInfo { + m := make(map[uint64]*ContractStakingBucketInfo) + for k, v := range s.idBucketMap { + m[k] = v + } + return m +} + +func (s *contractStakingCache) getActiveBucketType() map[uint64]*ContractStakingBucketType { + m := make(map[uint64]*ContractStakingBucketType) + for k, v := range s.idBucketTypeMap { + if v.ActivatedAt != maxBlockNumber { + m[k] = v + } + } + return m +} + +func (s *contractStakingCache) getBucketInfoByCandidate(candidate address.Address) map[uint64]*ContractStakingBucketInfo { + m := make(map[uint64]*ContractStakingBucketInfo) + for k, v := range s.candidateBucketMap[candidate.String()] { + if v { + m[k] = s.idBucketMap[k] + } + } + return m +} + +func (s *contractStakingCache) merge(delta *contractStakingDelta) error { + for id, state := range delta.bucketTypeDeltaState { + if state == deltaStateAdded || state == deltaStateModified { + s.putBucketType(id, delta.mustGetBucketType(id)) + } + } + for id, state := range delta.bucketInfoDeltaState { + if state == deltaStateAdded || state == deltaStateModified { + s.putBucketInfo(id, delta.mustGetBucketInfo(id)) + } else if state == deltaStateRemoved { + s.deleteBucketInfo(id) + } + } + s.putHeight(delta.getHeight()) + s.putTotalBucketCount(s.getTotalBucketCount() + delta.addedBucketCnt()) + return nil +} diff --git a/blockindex/liquidstaking_delta.go b/blockindex/liquidstaking_delta.go new file mode 100644 index 0000000000..37d95641dd --- /dev/null +++ b/blockindex/liquidstaking_delta.go @@ -0,0 +1,165 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import "github.com/pkg/errors" + +const ( + deltaStateAdded deltaState = iota + deltaStateRemoved + deltaStateModified + deltaStateReverted + + deltaActionAdd deltaAction = iota + deltaActionRemove + deltaActionModify +) + +type ( + deltaState int + deltaAction int + + contractStakingDelta struct { + contractStakingCacheManager // easy to query buckets + + bucketTypeDeltaState map[uint64]deltaState + bucketInfoDeltaState map[uint64]deltaState + } +) + +var ( + deltaStateTransferMap = map[deltaState]map[deltaAction]deltaState{ + deltaStateAdded: { + deltaActionRemove: deltaStateReverted, + deltaActionModify: deltaStateAdded, + }, + deltaStateRemoved: { + deltaActionAdd: deltaStateModified, + }, + deltaStateModified: { + deltaActionModify: deltaStateModified, + deltaActionRemove: deltaStateRemoved, + }, + deltaStateReverted: { + deltaActionAdd: deltaStateAdded, + }, + } +) + +func (s deltaState) transfer(act deltaAction) (deltaState, error) { + if _, ok := deltaStateTransferMap[s]; !ok { + return s, errors.Errorf("invalid delta state %d", s) + } + if _, ok := deltaStateTransferMap[s][act]; !ok { + return s, errors.Errorf("invalid delta action %d on state %d", act, s) + } + return deltaStateTransferMap[s][act], nil +} + +func newContractStakingDelta() *contractStakingDelta { + return &contractStakingDelta{ + contractStakingCacheManager: newContractStakingCache(), + bucketTypeDeltaState: make(map[uint64]deltaState), + bucketInfoDeltaState: make(map[uint64]deltaState), + } +} + +func (s *contractStakingDelta) addBucketType(id uint64, bt *ContractStakingBucketType) error { + if _, ok := s.bucketTypeDeltaState[id]; !ok { + s.bucketTypeDeltaState[id] = deltaStateAdded + } else { + var err error + s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].transfer(deltaActionAdd) + if err != nil { + return err + } + } + s.contractStakingCacheManager.putBucketType(id, bt) + return nil +} + +func (s *contractStakingDelta) updateBucketType(id uint64, bt *ContractStakingBucketType) error { + if _, ok := s.bucketTypeDeltaState[id]; !ok { + s.bucketTypeDeltaState[id] = deltaStateModified + } else { + var err error + s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].transfer(deltaActionModify) + if err != nil { + return err + } + } + s.contractStakingCacheManager.putBucketType(id, bt) + return nil +} + +func (s *contractStakingDelta) addBucketInfo(id uint64, bi *ContractStakingBucketInfo) error { + var err error + if _, ok := s.bucketInfoDeltaState[id]; !ok { + s.bucketInfoDeltaState[id] = deltaStateAdded + } else { + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].transfer(deltaActionAdd) + if err != nil { + return err + } + } + s.contractStakingCacheManager.putBucketInfo(id, bi) + return nil +} + +func (s *contractStakingDelta) updateBucketInfo(id uint64, bi *ContractStakingBucketInfo) error { + if _, ok := s.bucketInfoDeltaState[id]; !ok { + s.bucketInfoDeltaState[id] = deltaStateModified + } else { + var err error + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].transfer(deltaActionModify) + if err != nil { + return err + } + } + s.contractStakingCacheManager.putBucketInfo(id, bi) + return nil +} + +func (s *contractStakingDelta) deleteBucketInfo(id uint64) error { + if _, ok := s.bucketInfoDeltaState[id]; !ok { + s.bucketInfoDeltaState[id] = deltaStateRemoved + } else { + var err error + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].transfer(deltaActionRemove) + if err != nil { + return err + } + } + s.contractStakingCacheManager.deleteBucketInfo(id) + return nil +} + +func (s *contractStakingDelta) addedBucketCnt() uint64 { + addedBucketCnt := uint64(0) + for _, state := range s.bucketInfoDeltaState { + if state == deltaStateAdded { + addedBucketCnt++ + } + } + return addedBucketCnt +} + +func (s *contractStakingDelta) addedBucketTypeCnt() uint64 { + cnt := uint64(0) + for _, state := range s.bucketTypeDeltaState { + if state == deltaStateAdded { + cnt++ + } + } + return cnt +} + +func (s *contractStakingDelta) isBucketDeleted(id uint64) bool { + if _, ok := s.bucketInfoDeltaState[id]; ok { + return s.bucketInfoDeltaState[id] == deltaStateRemoved + } + return false +} diff --git a/blockindex/liquidstaking_dirty.go b/blockindex/liquidstaking_dirty.go new file mode 100644 index 0000000000..63921eca68 --- /dev/null +++ b/blockindex/liquidstaking_dirty.go @@ -0,0 +1,383 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "math/big" + "sync" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/db/batch" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" +) + +type ( + // contractStakingDirty is the dirty data of contract staking + // main functions: + // 1. update bucket + // 2. get up-to-date bucket + // 3. store delta to merge to clean cache + contractStakingDirty struct { + clean contractStakingCacheReader // clean cache to get buckets of last block + delta *contractStakingDelta // delta for cache to store buckets of current block + batch batch.KVStoreBatch // batch for db to store buckets of current block + tokenOwner map[uint64]address.Address + once sync.Once + } +) + +func newContractStakingDirty(clean contractStakingCacheReader) *contractStakingDirty { + return &contractStakingDirty{ + clean: clean, + delta: newContractStakingDelta(), + batch: batch.NewBatch(), + tokenOwner: make(map[uint64]address.Address), + } +} + +func (dirty *contractStakingDirty) putHeight(h uint64) { + dirty.batch.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(h), "failed to put height") + dirty.delta.putHeight(h) +} + +func (dirty *contractStakingDirty) addBucketType(id uint64, bt *ContractStakingBucketType) error { + dirty.batch.Put(_StakingBucketTypeNS, byteutil.Uint64ToBytesBigEndian(id), bt.serialize(), "failed to put bucket type") + return dirty.delta.addBucketType(id, bt) +} + +func (dirty *contractStakingDirty) updateBucketType(id uint64, bt *ContractStakingBucketType) error { + dirty.batch.Put(_StakingBucketTypeNS, byteutil.Uint64ToBytesBigEndian(id), bt.serialize(), "failed to put bucket type") + return dirty.delta.updateBucketType(id, bt) +} + +func (dirty *contractStakingDirty) addBucketInfo(id uint64, bi *ContractStakingBucketInfo) error { + dirty.batch.Put(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(id), bi.serialize(), "failed to put bucket info") + return dirty.delta.addBucketInfo(id, bi) +} + +func (dirty *contractStakingDirty) updateBucketInfo(id uint64, bi *ContractStakingBucketInfo) error { + dirty.batch.Put(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(id), bi.serialize(), "failed to put bucket info") + return dirty.delta.updateBucketInfo(id, bi) +} + +func (dirty *contractStakingDirty) burnBucket(id uint64) error { + dirty.batch.Delete(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(id), "failed to delete bucket info") + return dirty.delta.deleteBucketInfo(id) +} + +func (dirty *contractStakingDirty) getBucketTypeIndex(amount *big.Int, duration uint64) (uint64, bool) { + id, ok := dirty.delta.getBucketTypeIndex(amount, duration) + if ok { + return id, true + } + id, ok = dirty.clean.getBucketTypeIndex(amount, duration) + return id, ok +} + +func (dirty *contractStakingDirty) getBucketTypeCount() uint64 { + return dirty.clean.getTotalBucketTypeCount() + dirty.delta.addedBucketTypeCnt() +} + +func (dirty *contractStakingDirty) getBucketType(id uint64) (*ContractStakingBucketType, bool) { + bt, ok := dirty.delta.getBucketType(id) + if ok { + return bt, true + } + bt, ok = dirty.clean.getBucketType(id) + return bt, ok +} + +func (dirty *contractStakingDirty) getBucketInfo(id uint64) (*ContractStakingBucketInfo, bool) { + if dirty.delta.isBucketDeleted(id) { + return nil, false + } + bi, ok := dirty.delta.getBucketInfo(id) + if ok { + return bi, true + } + bi, ok = dirty.clean.getBucketInfo(id) + return bi, ok +} + +func (dirty *contractStakingDirty) finalizeBatch() batch.KVStoreBatch { + dirty.once.Do(func() { + total := dirty.clean.getTotalBucketCount() + dirty.delta.addedBucketCnt() + dirty.batch.Put(_StakingNS, _stakingTotalBucketCountKey, byteutil.Uint64ToBytesBigEndian(total), "failed to put total bucket count") + }) + return dirty.batch +} + +func (dirty *contractStakingDirty) finalize() (batch.KVStoreBatch, *contractStakingDelta) { + return dirty.finalizeBatch(), dirty.delta +} + +func (dirty *contractStakingDirty) handleTransferEvent(event eventParam) error { + to, err := event.indexedFieldAddress("to") + if err != nil { + return err + } + tokenID, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + + dirty.tokenOwner[tokenID.Uint64()] = to + return nil +} + +func (dirty *contractStakingDirty) handleBucketTypeActivatedEvent(event eventParam, height uint64) error { + amountParam, err := event.fieldUint256("amount") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + bt := ContractStakingBucketType{ + Amount: amountParam, + Duration: durationParam.Uint64(), + ActivatedAt: height, + } + id, ok := dirty.getBucketTypeIndex(amountParam, bt.Duration) + if !ok { + id = dirty.getBucketTypeCount() + err = dirty.addBucketType(id, &bt) + } else { + err = dirty.updateBucketType(id, &bt) + } + + return err +} + +func (dirty *contractStakingDirty) handleBucketTypeDeactivatedEvent(event eventParam, height uint64) error { + amountParam, err := event.fieldUint256("amount") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + id, ok := dirty.getBucketTypeIndex(amountParam, durationParam.Uint64()) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %d, duration %d", amountParam.Int64(), durationParam.Uint64()) + } + bt, ok := dirty.getBucketType(id) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "id %d", id) + } + bt.ActivatedAt = maxBlockNumber + return dirty.updateBucketType(id, bt) +} + +func (dirty *contractStakingDirty) handleStakedEvent(event eventParam, height uint64) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + delegateParam, err := event.fieldAddress("delegate") + if err != nil { + return err + } + amountParam, err := event.fieldUint256("amount") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + btIdx, ok := dirty.getBucketTypeIndex(amountParam, durationParam.Uint64()) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %d, duration %d", amountParam.Int64(), durationParam.Uint64()) + } + owner, ok := dirty.tokenOwner[tokenIDParam.Uint64()] + if !ok { + return errors.Errorf("no owner for token id %d", tokenIDParam.Uint64()) + } + bucket := ContractStakingBucketInfo{ + TypeIndex: btIdx, + Delegate: delegateParam, + Owner: owner, + CreatedAt: height, + UnlockedAt: maxBlockNumber, + UnstakedAt: maxBlockNumber, + } + return dirty.addBucketInfo(tokenIDParam.Uint64(), &bucket) +} + +func (dirty *contractStakingDirty) handleLockedEvent(event eventParam) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + bt, ok := dirty.getBucketType(b.TypeIndex) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "id %d", b.TypeIndex) + } + newBtIdx, ok := dirty.getBucketTypeIndex(bt.Amount, durationParam.Uint64()) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %v, duration %d", bt.Amount, durationParam.Uint64()) + } + b.TypeIndex = newBtIdx + b.UnlockedAt = maxBlockNumber + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleUnlockedEvent(event eventParam, height uint64) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + b.UnlockedAt = height + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleUnstakedEvent(event eventParam, height uint64) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + b.UnstakedAt = height + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleMergedEvent(event eventParam) error { + tokenIDsParam, err := event.fieldUint256Slice("tokenIds") + if err != nil { + return err + } + amountParam, err := event.fieldUint256("amount") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + // merge to the first bucket + btIdx, ok := dirty.getBucketTypeIndex(amountParam, durationParam.Uint64()) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %d, duration %d", amountParam.Int64(), durationParam.Uint64()) + } + b, ok := dirty.getBucketInfo(tokenIDsParam[0].Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDsParam[0].Uint64()) + } + b.TypeIndex = btIdx + b.UnlockedAt = maxBlockNumber + for i := 1; i < len(tokenIDsParam); i++ { + if err = dirty.burnBucket(tokenIDsParam[i].Uint64()); err != nil { + return err + } + } + return dirty.updateBucketInfo(tokenIDsParam[0].Uint64(), b) +} + +func (dirty *contractStakingDirty) handleDurationExtendedEvent(event eventParam) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + durationParam, err := event.fieldUint256("duration") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + bt, ok := dirty.getBucketType(b.TypeIndex) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "id %d", b.TypeIndex) + } + newBtIdx, ok := dirty.getBucketTypeIndex(bt.Amount, durationParam.Uint64()) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %d, duration %d", bt.Amount.Int64(), durationParam.Uint64()) + } + b.TypeIndex = newBtIdx + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleAmountIncreasedEvent(event eventParam) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + amountParam, err := event.fieldUint256("amount") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + bt, ok := dirty.getBucketType(b.TypeIndex) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "id %d", b.TypeIndex) + } + newBtIdx, ok := dirty.getBucketTypeIndex(amountParam, bt.Duration) + if !ok { + return errors.Wrapf(errBucketTypeNotExist, "amount %d, duration %d", amountParam.Int64(), bt.Duration) + } + b.TypeIndex = newBtIdx + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleDelegateChangedEvent(event eventParam) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + delegateParam, err := event.fieldAddress("newDelegate") + if err != nil { + return err + } + + b, ok := dirty.getBucketInfo(tokenIDParam.Uint64()) + if !ok { + return errors.Wrapf(ErrBucketInfoNotExist, "token id %d", tokenIDParam.Uint64()) + } + b.Delegate = delegateParam + return dirty.updateBucketInfo(tokenIDParam.Uint64(), b) +} + +func (dirty *contractStakingDirty) handleWithdrawalEvent(event eventParam) error { + tokenIDParam, err := event.indexedFieldUint256("tokenId") + if err != nil { + return err + } + + return dirty.burnBucket(tokenIDParam.Uint64()) +} diff --git a/blockindex/liquidstaking_indexer.go b/blockindex/liquidstaking_indexer.go new file mode 100644 index 0000000000..08d60c08e0 --- /dev/null +++ b/blockindex/liquidstaking_indexer.go @@ -0,0 +1,725 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "context" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/blockchain/blockdao" + "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" +) + +const ( + // StakingContractAddress is the address of system staking contract + // TODO (iip-13): replace with the real system staking contract address + StakingContractAddress = "io19ys8f4uhwms6lq6ulexr5fwht9gsjes8mvuugd" + // StakingContractABI is the ABI of system staking contract + StakingContractABI = `[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "AmountIncreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "BucketTypeActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "BucketTypeDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "newDelegate", + "type": "address" + } + ], + "name": "DelegateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "DurationExtended", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Merged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Unlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Unstaked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "Withdrawal", + "type": "event" + } + ]` + + // bucket related namespace in db + _StakingBucketInfoNS = "sbi" + _StakingBucketTypeNS = "sbt" + _StakingNS = "s" +) + +type ( + // ContractStakingIndexer is the interface of contract staking indexer + ContractStakingIndexer interface { + blockdao.BlockIndexer + + // CandidateVotes returns the total votes of a candidate + CandidateVotes(candidate address.Address) *big.Int + // Buckets returns all existed buckets + Buckets() ([]*ContractStakingBucket, error) + // Bucket returns the bucket by id + Bucket(id uint64) (*ContractStakingBucket, error) + // BucketsByIndices returns buckets by indices + BucketsByIndices(indices []uint64) ([]*ContractStakingBucket, error) + // BucketsByCandidate returns buckets by candidate + BucketsByCandidate(candidate address.Address) ([]*ContractStakingBucket, error) + // TotalBucketCount returns the total bucket count including burned buckets + TotalBucketCount() uint64 + // ActiveBucketTypes returns all active bucket types + ActiveBucketTypes() (map[uint64]*ContractStakingBucketType, error) + } + + // contractStakingIndexer is the implementation of ContractStakingIndexer + // Main functions: + // 1. handle contract staking contract events when new block comes to generate index data + // 2. provide query interface for contract staking index data + // Generate index data flow: + // block comes -> new dirty cache -> handle contract events -> update dirty cache -> merge dirty to clean cache + // Main Object: + // kvstore: persistent storage, used to initialize index cache at startup + // cache: in-memory index for clean data, used to query index data + // dirty: the cache to update during event processing, will be merged to clean cache after all events are processed. If errors occur during event processing, dirty cache will be discarded. + contractStakingIndexer struct { + kvstore db.KVStore // persistent storage + cache contractStakingCacheManager // in-memory index for clean data + mutex sync.RWMutex // mutex for multiple reading to cache + } +) + +var ( + _stakingInterface abi.ABI + _stakingHeightKey = []byte("shk") + _stakingTotalBucketCountKey = []byte("stbck") + + errBucketTypeNotExist = errors.New("bucket type does not exist") + + // ErrBucketInfoNotExist is the error when bucket does not exist + ErrBucketInfoNotExist = errors.New("bucket info does not exist") +) + +func init() { + var err error + _stakingInterface, err = abi.JSON(strings.NewReader(StakingContractABI)) + if err != nil { + panic(err) + } +} + +// NewContractStakingIndexer creates a new contract staking indexer +func NewContractStakingIndexer(kvStore db.KVStore) ContractStakingIndexer { + return &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } +} + +// Start starts the indexer +func (s *contractStakingIndexer) Start(ctx context.Context) error { + if err := s.kvstore.Start(ctx); err != nil { + return err + } + return s.loadCache() +} + +// Stop stops the indexer +func (s *contractStakingIndexer) Stop(ctx context.Context) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.kvstore.Stop(ctx); err != nil { + return err + } + s.cache = newContractStakingCache() + return nil +} + +// PutBlock puts a block into indexer +func (s *contractStakingIndexer) PutBlock(ctx context.Context, blk *block.Block) error { + // new dirty cache for this block + // it's not necessary to use thread safe cache here, because only one thread will call this function + // and no update to cache will happen before dirty merge to clean + dirty := newContractStakingDirty(s.cache) + dirty.putHeight(blk.Height()) + + // handle events of block + for _, receipt := range blk.Receipts { + if receipt.Status != uint64(iotextypes.ReceiptStatus_Success) { + continue + } + for _, log := range receipt.Logs() { + if log.Address != StakingContractAddress { + continue + } + if err := s.handleEvent(ctx, dirty, blk, log); err != nil { + return err + } + } + } + + // commit dirty cache + return s.commit(dirty) +} + +// DeleteTipBlock deletes the tip block from indexer +func (s *contractStakingIndexer) DeleteTipBlock(context.Context, *block.Block) error { + return errors.New("not implemented") +} + +// Height returns the tip block height +func (s *contractStakingIndexer) Height() (uint64, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.cache.getHeight(), nil +} + +// CandidateVotes returns the candidate votes +func (s *contractStakingIndexer) CandidateVotes(candidate address.Address) *big.Int { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.cache.getCandidateVotes(candidate) +} + +// Buckets returns the buckets +func (s *contractStakingIndexer) Buckets() ([]*ContractStakingBucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + vbs := []*ContractStakingBucket{} + for id, bi := range s.cache.getAllBucketInfo() { + bt := s.cache.mustGetBucketType(bi.TypeIndex) + vb, err := s.assembleContractStakingBucket(id, bi, bt) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +// Bucket returns the bucket +func (s *contractStakingIndexer) Bucket(id uint64) (*ContractStakingBucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + return s.generateBucket(id) +} + +// BucketsByIndices returns the buckets by indices +func (s *contractStakingIndexer) BucketsByIndices(indices []uint64) ([]*ContractStakingBucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + vbs := make([]*ContractStakingBucket, 0, len(indices)) + for _, id := range indices { + vb, err := s.generateBucket(id) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +func (s *contractStakingIndexer) BucketsByCandidate(candidate address.Address) ([]*ContractStakingBucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + vbs := make([]*ContractStakingBucket, 0) + for id := range s.cache.getBucketInfoByCandidate(candidate) { + vb, err := s.generateBucket(id) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +func (s *contractStakingIndexer) TotalBucketCount() uint64 { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.cache.getTotalBucketCount() +} + +func (s *contractStakingIndexer) ActiveBucketTypes() (map[uint64]*ContractStakingBucketType, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.cache.getActiveBucketType(), nil +} + +func (s *contractStakingIndexer) generateBucket(id uint64) (*ContractStakingBucket, error) { + bi, ok := s.cache.getBucketInfo(id) + if !ok { + return nil, errors.Wrapf(ErrBucketInfoNotExist, "id %d", id) + } + bt := s.cache.mustGetBucketType(bi.TypeIndex) + return s.assembleContractStakingBucket(id, bi, bt) +} + +func (s *contractStakingIndexer) handleEvent(ctx context.Context, dirty *contractStakingDirty, blk *block.Block, log *action.Log) error { + // get event abi + abiEvent, err := _stakingInterface.EventByID(common.Hash(log.Topics[0])) + if err != nil { + return errors.Wrapf(err, "get event abi from topic %v failed", log.Topics[0]) + } + + // unpack event data + event, err := unpackEventParam(abiEvent, log) + if err != nil { + return err + } + + // handle different kinds of event + switch abiEvent.Name { + case "BucketTypeActivated": + return dirty.handleBucketTypeActivatedEvent(event, blk.Height()) + case "BucketTypeDeactivated": + return dirty.handleBucketTypeDeactivatedEvent(event, blk.Height()) + case "Staked": + return dirty.handleStakedEvent(event, blk.Height()) + case "Locked": + return dirty.handleLockedEvent(event) + case "Unlocked": + return dirty.handleUnlockedEvent(event, blk.Height()) + case "Unstaked": + return dirty.handleUnstakedEvent(event, blk.Height()) + case "Merged": + return dirty.handleMergedEvent(event) + case "DurationExtended": + return dirty.handleDurationExtendedEvent(event) + case "AmountIncreased": + return dirty.handleAmountIncreasedEvent(event) + case "DelegateChanged": + return dirty.handleDelegateChangedEvent(event) + case "Withdrawal": + return dirty.handleWithdrawalEvent(event) + case "Transfer": + return dirty.handleTransferEvent(event) + case "Approval", "ApprovalForAll", "OwnershipTransferred", "Paused", "Unpaused": + // not require handling events + return nil + default: + return errors.Errorf("unknown event name %s", abiEvent.Name) + } +} + +func (s *contractStakingIndexer) reloadCache() error { + s.cache = newContractStakingCache() + return s.loadCache() +} + +func (s *contractStakingIndexer) loadCache() error { + delta := newContractStakingDelta() + // load height + var height uint64 + h, err := s.kvstore.Get(_StakingNS, _stakingHeightKey) + if err != nil { + if !errors.Is(err, db.ErrNotExist) { + return err + } + height = 0 + } else { + height = byteutil.BytesToUint64BigEndian(h) + + } + delta.putHeight(height) + + // load total bucket count + var totalBucketCount uint64 + tbc, err := s.kvstore.Get(_StakingNS, _stakingTotalBucketCountKey) + if err != nil { + if !errors.Is(err, db.ErrNotExist) { + return err + } + } else { + totalBucketCount = byteutil.BytesToUint64BigEndian(tbc) + } + delta.putTotalBucketCount(totalBucketCount) + + // load bucket info + ks, vs, err := s.kvstore.Filter(_StakingBucketInfoNS, func(k, v []byte) bool { return true }, nil, nil) + if err != nil && !errors.Is(err, db.ErrBucketNotExist) { + return err + } + for i := range vs { + var b ContractStakingBucketInfo + if err := b.deserialize(vs[i]); err != nil { + return err + } + delta.addBucketInfo(byteutil.BytesToUint64BigEndian(ks[i]), &b) + } + + // load bucket type + ks, vs, err = s.kvstore.Filter(_StakingBucketTypeNS, func(k, v []byte) bool { return true }, nil, nil) + if err != nil && !errors.Is(err, db.ErrBucketNotExist) { + return err + } + for i := range vs { + var b ContractStakingBucketType + if err := b.deserialize(vs[i]); err != nil { + return err + } + delta.addBucketType(byteutil.BytesToUint64BigEndian(ks[i]), &b) + } + return s.cache.merge(delta) +} + +func (s *contractStakingIndexer) commit(dirty *contractStakingDirty) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + batch, delta := dirty.finalize() + if err := s.cache.merge(delta); err != nil { + s.reloadCache() + return err + } + if err := s.kvstore.WriteBatch(batch); err != nil { + s.reloadCache() + return err + } + return nil +} + +func (s *contractStakingIndexer) assembleContractStakingBucket(token uint64, bi *ContractStakingBucketInfo, bt *ContractStakingBucketType) (*ContractStakingBucket, error) { + vb := ContractStakingBucket{ + Index: token, + StakedAmount: bt.Amount, + StakedDurationBlockNumber: bt.Duration, + CreateBlockHeight: bi.CreatedAt, + StakeBlockHeight: bi.CreatedAt, + UnstakeBlockHeight: bi.UnstakedAt, + AutoStake: bi.UnlockedAt == maxBlockNumber, + Candidate: bi.Delegate, + Owner: bi.Owner, + ContractAddress: StakingContractAddress, + } + if bi.UnlockedAt != maxBlockNumber { + vb.StakeBlockHeight = bi.UnlockedAt + } + return &vb, nil +} diff --git a/blockindex/liquidstaking_indexer_test.go b/blockindex/liquidstaking_indexer_test.go new file mode 100644 index 0000000000..1084b18d65 --- /dev/null +++ b/blockindex/liquidstaking_indexer_test.go @@ -0,0 +1,432 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "context" + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/test/identityset" + "github.com/iotexproject/iotex-core/testutil" +) + +func TestContractStakingIndexerLoadCache(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer := &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } + r.NoError(indexer.Start(context.Background())) + + // create a stake + dirty := newContractStakingDirty(indexer.cache) + height := uint64(1) + dirty.putHeight(height) + activateBucketType(r, dirty, 10, 100, height) + owner := identityset.Address(0) + delegate := identityset.Address(1) + stake(r, dirty, owner, delegate, 1, 10, 100, height) + err = indexer.commit(dirty) + r.NoError(err) + buckets, err := indexer.Buckets() + r.NoError(err) + r.NoError(indexer.Stop(context.Background())) + + // load cache from db + newIndexer := &contractStakingIndexer{ + kvstore: db.NewBoltDB(cfg), + cache: newContractStakingCache(), + } + r.NoError(newIndexer.Start(context.Background())) + + // check cache + newBuckets, err := newIndexer.Buckets() + r.NoError(err) + r.Equal(len(buckets), len(newBuckets)) + for i := range buckets { + r.EqualValues(buckets[i], newBuckets[i]) + } + newHeight, err := newIndexer.Height() + r.NoError(err) + r.Equal(height, newHeight) + r.EqualValues(1, newIndexer.TotalBucketCount()) + r.NoError(newIndexer.Stop(context.Background())) +} + +func TestContractStakingIndexerDirty(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer := &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } + r.NoError(indexer.Start(context.Background())) + + // before commit dirty, the cache should be empty + dirty := newContractStakingDirty(indexer.cache) + height := uint64(1) + dirty.putHeight(height) + gotHeight, err := indexer.Height() + r.NoError(err) + r.EqualValues(0, gotHeight) + // after commit dirty, the cache should be updated + err = indexer.commit(dirty) + r.NoError(err) + gotHeight, err = indexer.Height() + r.NoError(err) + r.EqualValues(height, gotHeight) + + r.NoError(indexer.Stop(context.Background())) +} + +func TestContractStakingIndexerThreadSafe(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer := &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } + r.NoError(indexer.Start(context.Background())) + + wait := sync.WaitGroup{} + wait.Add(6) + owner := identityset.Address(0) + delegate := identityset.Address(1) + // read concurrently + for i := 0; i < 5; i++ { + go func() { + defer wait.Done() + for i := 0; i < 1000; i++ { + _, err := indexer.Buckets() + r.NoError(err) + _, err = indexer.ActiveBucketTypes() + r.NoError(err) + _, err = indexer.BucketsByCandidate(delegate) + r.NoError(err) + indexer.CandidateVotes(delegate) + _, err = indexer.Height() + r.NoError(err) + indexer.TotalBucketCount() + } + }() + } + // write + go func() { + defer wait.Done() + // activate bucket type + dirty := newContractStakingDirty(indexer.cache) + activateBucketType(r, dirty, 10, 100, 1) + r.NoError(indexer.commit(dirty)) + for i := 2; i < 1000; i++ { + dirty := newContractStakingDirty(indexer.cache) + height := uint64(i) + dirty.putHeight(height) + stake(r, dirty, owner, delegate, 1, 10, 100, height) + err := indexer.commit(dirty) + r.NoError(err) + } + }() + wait.Wait() + r.NoError(indexer.Stop(context.Background())) + // no panic means thread safe +} + +func TestContractStakingIndexerBucketType(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer := &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } + r.NoError(indexer.Start(context.Background())) + + // activate + bucketTypeData := [][2]int64{ + {10, 10}, + {20, 10}, + {10, 100}, + {20, 100}, + } + height := uint64(1) + dirty := newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + for _, data := range bucketTypeData { + activateBucketType(r, dirty, data[0], data[1], height) + } + err = indexer.commit(dirty) + r.NoError(err) + bucketTypes, err := indexer.ActiveBucketTypes() + r.NoError(err) + r.Equal(len(bucketTypeData), len(bucketTypes)) + for i, data := range bucketTypeData { + r.EqualValues(data[0], bucketTypes[uint64(i)].Amount.Int64()) + r.EqualValues(data[1], bucketTypes[uint64(i)].Duration) + r.EqualValues(height, bucketTypes[uint64(i)].ActivatedAt) + } + // deactivate + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + for i := 0; i < 2; i++ { + data := bucketTypeData[i] + deactivateBucketType(r, dirty, data[0], data[1], height) + } + err = indexer.commit(dirty) + r.NoError(err) + bucketTypes, err = indexer.ActiveBucketTypes() + r.NoError(err) + r.Equal(len(bucketTypeData)-2, len(bucketTypes)) + for i, data := range bucketTypeData { + if i < 2 { + continue + } + r.EqualValues(data[0], bucketTypes[uint64(i)].Amount.Int64()) + r.EqualValues(data[1], bucketTypes[uint64(i)].Duration) + r.EqualValues(1, bucketTypes[uint64(i)].ActivatedAt) + } + // reactivate + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + for i := 0; i < 2; i++ { + data := bucketTypeData[i] + activateBucketType(r, dirty, data[0], data[1], height) + } + err = indexer.commit(dirty) + r.NoError(err) + bucketTypes, err = indexer.ActiveBucketTypes() + r.NoError(err) + r.Equal(len(bucketTypeData), len(bucketTypes)) + for i, data := range bucketTypeData { + r.EqualValues(data[0], bucketTypes[uint64(i)].Amount.Int64()) + r.EqualValues(data[1], bucketTypes[uint64(i)].Duration) + if i < 2 { + r.EqualValues(height, bucketTypes[uint64(i)].ActivatedAt) + } else { + r.EqualValues(1, bucketTypes[uint64(i)].ActivatedAt) + } + } + r.NoError(indexer.Stop(context.Background())) +} + +func TestContractStakingIndexerBucketInfo(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer := &contractStakingIndexer{ + kvstore: kvStore, + cache: newContractStakingCache(), + } + r.NoError(indexer.Start(context.Background())) + + // init bucket type + bucketTypeData := [][2]int64{ + {10, 10}, + {20, 10}, + {10, 100}, + {20, 100}, + } + height := uint64(1) + dirty := newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + for _, data := range bucketTypeData { + activateBucketType(r, dirty, data[0], data[1], height) + } + err = indexer.commit(dirty) + r.NoError(err) + + // stake + owner := identityset.Address(0) + delegate := identityset.Address(1) + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + stake(r, dirty, owner, delegate, 1, 10, 100, height) + r.NoError(err) + r.NoError(indexer.commit(dirty)) + bucket, err := indexer.Bucket(1) + r.NoError(err) + r.EqualValues(1, bucket.Index) + r.EqualValues(owner, bucket.Owner) + r.EqualValues(delegate, bucket.Candidate) + r.EqualValues(10, bucket.StakedAmount.Int64()) + r.EqualValues(100, bucket.StakedDurationBlockNumber) + r.EqualValues(height, bucket.StakeBlockHeight) + r.True(bucket.AutoStake) + r.EqualValues(height, bucket.CreateBlockHeight) + r.EqualValues(maxBlockNumber, bucket.UnstakeBlockHeight) + r.EqualValues(StakingContractAddress, bucket.ContractAddress) + r.EqualValues(10, indexer.CandidateVotes(delegate).Uint64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + // unlock + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + unlock(r, dirty, int64(bucket.Index), height) + r.NoError(indexer.commit(dirty)) + bucket, err = indexer.Bucket(bucket.Index) + r.NoError(err) + r.EqualValues(1, bucket.Index) + r.EqualValues(owner, bucket.Owner) + r.EqualValues(delegate, bucket.Candidate) + r.EqualValues(10, bucket.StakedAmount.Int64()) + r.EqualValues(100, bucket.StakedDurationBlockNumber) + r.EqualValues(height, bucket.StakeBlockHeight) + r.False(bucket.AutoStake) + r.EqualValues(height-1, bucket.CreateBlockHeight) + r.EqualValues(maxBlockNumber, bucket.UnstakeBlockHeight) + r.EqualValues(StakingContractAddress, bucket.ContractAddress) + r.EqualValues(10, indexer.CandidateVotes(delegate).Uint64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + // lock again + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + lock(r, dirty, int64(bucket.Index), int64(10)) + r.NoError(indexer.commit(dirty)) + bucket, err = indexer.Bucket(bucket.Index) + r.NoError(err) + r.EqualValues(1, bucket.Index) + r.EqualValues(owner, bucket.Owner) + r.EqualValues(delegate, bucket.Candidate) + r.EqualValues(10, bucket.StakedAmount.Int64()) + r.EqualValues(10, bucket.StakedDurationBlockNumber) + r.EqualValues(height-2, bucket.StakeBlockHeight) + r.True(bucket.AutoStake) + r.EqualValues(height-2, bucket.CreateBlockHeight) + r.EqualValues(maxBlockNumber, bucket.UnstakeBlockHeight) + r.EqualValues(StakingContractAddress, bucket.ContractAddress) + r.EqualValues(10, indexer.CandidateVotes(delegate).Uint64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + // unstake + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + unlock(r, dirty, int64(bucket.Index), height) + unstake(r, dirty, int64(bucket.Index), height) + r.NoError(indexer.commit(dirty)) + bucket, err = indexer.Bucket(bucket.Index) + r.NoError(err) + r.EqualValues(1, bucket.Index) + r.EqualValues(owner, bucket.Owner) + r.EqualValues(delegate, bucket.Candidate) + r.EqualValues(10, bucket.StakedAmount.Int64()) + r.EqualValues(10, bucket.StakedDurationBlockNumber) + r.EqualValues(height, bucket.StakeBlockHeight) + r.False(bucket.AutoStake) + r.EqualValues(height-3, bucket.CreateBlockHeight) + r.EqualValues(height, bucket.UnstakeBlockHeight) + r.EqualValues(StakingContractAddress, bucket.ContractAddress) + r.EqualValues(0, indexer.CandidateVotes(delegate).Uint64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + // withdraw + height++ + dirty = newContractStakingDirty(indexer.cache) + dirty.putHeight(height) + withdraw(r, dirty, int64(bucket.Index)) + r.NoError(indexer.commit(dirty)) + bucket, err = indexer.Bucket(bucket.Index) + r.ErrorIs(err, ErrBucketInfoNotExist) + r.EqualValues(0, indexer.CandidateVotes(delegate).Uint64()) + r.EqualValues(1, indexer.TotalBucketCount()) +} + +func activateBucketType(r *require.Assertions, dirty *contractStakingDirty, amount, duration int64, height uint64) { + err := dirty.handleBucketTypeActivatedEvent(eventParam{ + "amount": big.NewInt(amount), + "duration": big.NewInt(duration), + }, height) + r.NoError(err) +} + +func deactivateBucketType(r *require.Assertions, dirty *contractStakingDirty, amount, duration int64, height uint64) { + err := dirty.handleBucketTypeDeactivatedEvent(eventParam{ + "amount": big.NewInt(amount), + "duration": big.NewInt(duration), + }, height) + r.NoError(err) +} + +func stake(r *require.Assertions, dirty *contractStakingDirty, owner, candidate address.Address, token, amount, duration int64, height uint64) { + err := dirty.handleTransferEvent(eventParam{ + "to": common.BytesToAddress(owner.Bytes()), + "tokenId": big.NewInt(token), + }) + r.NoError(err) + err = dirty.handleStakedEvent(eventParam{ + "tokenId": big.NewInt(token), + "delegate": common.BytesToAddress(candidate.Bytes()), + "amount": big.NewInt(amount), + "duration": big.NewInt(duration), + }, height) + r.NoError(err) +} + +func unlock(r *require.Assertions, dirty *contractStakingDirty, token int64, height uint64) { + err := dirty.handleUnlockedEvent(eventParam{ + "tokenId": big.NewInt(token), + }, height) + r.NoError(err) +} + +func lock(r *require.Assertions, dirty *contractStakingDirty, token, duration int64) { + err := dirty.handleLockedEvent(eventParam{ + "tokenId": big.NewInt(token), + "duration": big.NewInt(duration), + }) + r.NoError(err) +} + +func unstake(r *require.Assertions, dirty *contractStakingDirty, token int64, height uint64) { + err := dirty.handleUnstakedEvent(eventParam{ + "tokenId": big.NewInt(token), + }, height) + r.NoError(err) +} + +func withdraw(r *require.Assertions, dirty *contractStakingDirty, token int64) { + err := dirty.handleWithdrawalEvent(eventParam{ + "tokenId": big.NewInt(token), + }) + r.NoError(err) +} diff --git a/blockindex/util.go b/blockindex/util.go new file mode 100644 index 0000000000..6202246c56 --- /dev/null +++ b/blockindex/util.go @@ -0,0 +1,99 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package blockindex + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/action" +) + +type ( + // eventParam is a struct to hold smart contract event parameters, which can easily convert a param to go type + eventParam map[string]any +) + +var ( + errInvlidEventParam = errors.New("invalid event param") +) + +func eventField[T any](e eventParam, name string) (T, error) { + field, ok := e[name].(T) + if !ok { + return field, errors.Wrapf(errInvlidEventParam, "field %s got %#v, expect %T", name, e[name], field) + } + return field, nil +} + +func (e eventParam) fieldUint256(name string) (*big.Int, error) { + return eventField[*big.Int](e, name) +} + +func (e eventParam) fieldBytes12(name string) (string, error) { + data, err := eventField[[12]byte](e, name) + if err != nil { + return "", err + } + // remove trailing zeros + tail := len(data) - 1 + for ; tail >= 0 && data[tail] == 0; tail-- { + } + return string(data[:tail+1]), nil +} + +func (e eventParam) fieldUint256Slice(name string) ([]*big.Int, error) { + return eventField[[]*big.Int](e, name) +} + +func (e eventParam) fieldAddress(name string) (address.Address, error) { + commAddr, err := eventField[common.Address](e, name) + if err != nil { + return nil, err + } + return address.FromBytes(commAddr.Bytes()) +} + +func (e eventParam) indexedFieldAddress(name string) (address.Address, error) { + return e.fieldAddress(name) +} + +func (e eventParam) indexedFieldUint256(name string) (*big.Int, error) { + return eventField[*big.Int](e, name) +} + +func unpackEventParam(abiEvent *abi.Event, log *action.Log) (eventParam, error) { + event := make(eventParam) + // unpack non-indexed fields + if len(log.Data) > 0 { + if err := abiEvent.Inputs.UnpackIntoMap(event, log.Data); err != nil { + return nil, errors.Wrap(err, "unpack event data failed") + } + } + // unpack indexed fields + args := make(abi.Arguments, 0) + for _, arg := range abiEvent.Inputs { + if arg.Indexed { + args = append(args, arg) + } + } + topics := make([]common.Hash, 0) + for i, topic := range log.Topics { + if i > 0 { + topics = append(topics, common.Hash(topic)) + } + } + err := abi.ParseTopicsIntoMap(event, args, topics) + if err != nil { + return nil, errors.Wrap(err, "unpack event indexed fields failed") + } + return event, nil +} diff --git a/chainservice/builder.go b/chainservice/builder.go index 228e920df9..5fb87cb876 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -488,8 +488,8 @@ func (builder *Builder) registerStakingProtocol() error { return nil } - // TODO (iip-13): use a real liquid indexer instead - liquidIndexer := staking.NewEmptyLiquidStakingIndexer() + // TODO (iip-13): use a real contract indexer instead + contractStakingIndexer := staking.NewEmptyLiquidStakingIndexer() stakingProtocol, err := staking.NewProtocol( rewarding.DepositGas, @@ -499,7 +499,7 @@ func (builder *Builder) registerStakingProtocol() error { StakingPatchDir: builder.cfg.Chain.StakingPatchDir, }, builder.cs.candBucketsIndexer, - liquidIndexer, + contractStakingIndexer, builder.cfg.Genesis.OkhotskBlockHeight, builder.cfg.Genesis.GreenlandBlockHeight, builder.cfg.Genesis.HawaiiBlockHeight, diff --git a/e2etest/liquid_staking_test.go b/e2etest/liquid_staking_test.go new file mode 100644 index 0000000000..329b6409d9 --- /dev/null +++ b/e2etest/liquid_staking_test.go @@ -0,0 +1,1875 @@ +package e2etest + +import ( + "context" + "encoding/hex" + "math" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/iotexproject/go-pkgs/crypto" + "github.com/iotexproject/go-pkgs/hash" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/action/protocol" + "github.com/iotexproject/iotex-core/action/protocol/account" + accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util" + "github.com/iotexproject/iotex-core/action/protocol/execution" + "github.com/iotexproject/iotex-core/action/protocol/rewarding" + "github.com/iotexproject/iotex-core/action/protocol/rolldpos" + "github.com/iotexproject/iotex-core/actpool" + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/blockchain/blockdao" + "github.com/iotexproject/iotex-core/blockchain/genesis" + "github.com/iotexproject/iotex-core/blockindex" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/state/factory" + "github.com/iotexproject/iotex-core/test/identityset" + "github.com/iotexproject/iotex-core/testutil" +) + +const ( + // _stakingContractByteCode is the byte code of the contract staking contract for testing, which changes the freeze blocks to 10 + _stakingContractByteCode = `60806040523480156200001157600080fd5b5060405180604001604052806009815260200168109d58dad95d13919560ba1b815250604051806040016040528060038152602001621092d560ea1b81525081600090816200006191906200019b565b5060016200007082826200019b565b5050506200008d62000087620000a060201b60201c565b620000a4565b6006805460ff60a01b1916905562000267565b3390565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b634e487b7160e01b600052604160045260246000fd5b600181811c908216806200012157607f821691505b6020821081036200014257634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200019657600081815260208120601f850160051c81016020861015620001715750805b601f850160051c820191505b8181101562000192578281556001016200017d565b5050505b505050565b81516001600160401b03811115620001b757620001b7620000f6565b620001cf81620001c884546200010c565b8462000148565b602080601f831160018114620002075760008415620001ee5750858301515b600019600386901b1c1916600185901b17855562000192565b600085815260208120601f198616915b82811015620002385788860151825594840194600190910190840162000217565b5085821015620002575787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b613dbf80620002776000396000f3fe6080604052600436106102925760003560e01c80637acb77571161015a578063bbe33ea5116100c1578063e449f3411161007a578063e449f341146107e9578063e985e9c514610809578063eb0ffb2e14610852578063eec7ee7314610872578063f0b56b5d14610885578063f2fde38b1461089a57600080fd5b8063bbe33ea514610740578063c87b56dd14610753578063c8e7792314610773578063d0949f9914610793578063d6605fd8146107a9578063e0028ecf146107c957600080fd5b806398ca3b761161011357806398ca3b76146106985780639f7d5b00146106b8578063a22cb465146106cd578063b2383e55146106ed578063b88d4fde14610700578063b8f4bd7b1461072057600080fd5b80637acb7757146105fd5780638456cb59146106105780638da5cb5b1461062557806393b6ef591461064357806395d89b4114610663578063960014bd1461067857600080fd5b806342842e0e116101fe5780636198e339116101b75780636198e339146105485780636352211e1461056857806370a0823114610588578063711563d4146105a8578063715018a6146105bb57806378bfca10146105d057600080fd5b806342842e0e14610458578063431cd92a1461047857806343e06c59146104c95780635c975abb146104e95780635ceb8b5b146105085780635d36598f1461052857600080fd5b80630f5b2ca5116102505780630f5b2ca5146103965780631338736f146103b657806323b872dd146103d65780632e17de78146103f65780633f4ba83a146104165780633fd140df1461042b57600080fd5b8062f714ce1461029757806301ffc9a7146102b957806303459b16146102ee57806306fdde031461031c578063081812fc1461033e578063095ea7b314610376575b600080fd5b3480156102a357600080fd5b506102b76102b23660046134d2565b6108ba565b005b3480156102c557600080fd5b506102d96102d4366004613518565b610981565b60405190151581526020015b60405180910390f35b3480156102fa57600080fd5b5061030e610309366004613535565b6109d3565b6040519081526020016102e5565b34801561032857600080fd5b506103316109f9565b6040516102e5919061359e565b34801561034a57600080fd5b5061035e610359366004613535565b610a8b565b6040516001600160a01b0390911681526020016102e5565b34801561038257600080fd5b506102b76103913660046135b1565b610ab2565b3480156103a257600080fd5b506102b76103b13660046134d2565b610bc7565b3480156103c257600080fd5b506102b76103d13660046135dd565b610c34565b3480156103e257600080fd5b506102b76103f13660046135ff565b610ca7565b34801561040257600080fd5b506102b7610411366004613535565b610cd8565b34801561042257600080fd5b506102b7610d87565b34801561043757600080fd5b5061044b61044636600461368b565b610d99565b6040516102e591906136cc565b34801561046457600080fd5b506102b76104733660046135ff565b610f1b565b34801561048457600080fd5b50610498610493366004613535565b610f36565b6040805195865260208601949094529284019190915260608301526001600160a01b0316608082015260a0016102e5565b3480156104d557600080fd5b506102d96104e43660046135dd565b610fb2565b3480156104f557600080fd5b50600654600160a01b900460ff166102d9565b34801561051457600080fd5b506102b7610523366004613756565b610fcd565b34801561053457600080fd5b506102b761054336600461368b565b611074565b34801561055457600080fd5b506102b7610563366004613535565b61110a565b34801561057457600080fd5b5061035e610583366004613535565b61116c565b34801561059457600080fd5b5061030e6105a33660046137a1565b6111cc565b61030e6105b63660046137be565b611252565b3480156105c757600080fd5b506102b761132b565b3480156105dc57600080fd5b506105f06105eb3660046135dd565b61133d565b6040516102e591906137fd565b61030e61060b3660046134d2565b611472565b34801561061c57600080fd5b506102b76114f6565b34801561063157600080fd5b506006546001600160a01b031661035e565b34801561064f57600080fd5b5061030e61065e366004613535565b611506565b34801561066f57600080fd5b50610331611531565b34801561068457600080fd5b5061044b61069336600461368b565b611540565b3480156106a457600080fd5b506102b76106b3366004613856565b6116ba565b3480156106c457600080fd5b50600b5461030e565b3480156106d957600080fd5b506102b76106e83660046138ac565b611750565b6102b76106fb3660046135dd565b61175f565b34801561070c57600080fd5b506102b761071b366004613925565b611863565b34801561072c57600080fd5b506102b761073b366004613856565b61189b565b6102b761074e366004613756565b61198a565b34801561075f57600080fd5b5061033161076e366004613535565b611b94565b34801561077f57600080fd5b506102b761078e3660046135dd565b611c07565b34801561079f57600080fd5b5061030e60001981565b3480156107b557600080fd5b506102b76107c43660046135dd565b611dac565b3480156107d557600080fd5b506102b76107e43660046135dd565b611e91565b3480156107f557600080fd5b506102b761080436600461368b565b611f05565b34801561081557600080fd5b506102d96108243660046139e8565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b34801561085e57600080fd5b506102b761086d3660046135dd565b611fe1565b61030e610880366004613a16565b612057565b34801561089157600080fd5b5061030e600a81565b3480156108a657600080fd5b506102b76108b53660046137a1565b61215c565b6108c26121d5565b816108cc81612222565b600083815260086020526040902060028101546108e890612277565b156109325760405162461bcd60e51b81526020600482015260156024820152746e6f7420726561647920746f20776974686472617760581b60448201526064015b60405180910390fd5b61093b846122eb565b610945818461238e565b6040516001600160a01b0384169085907fd964a27d45f595739c13d8b1160b57491050cacf3a2e5602207277d6228f64ee90600090a350505050565b60006001600160e01b031982166380ac58cd60e01b14806109b257506001600160e01b03198216635b5e139f60e01b145b806109cd57506301ffc9a760e01b6001600160e01b03198316145b92915050565b60006109de8261244d565b6000828152600860205260409020600201546109cd90612277565b606060008054610a0890613adc565b80601f0160208091040260200160405190810160405280929190818152602001828054610a3490613adc565b8015610a815780601f10610a5657610100808354040283529160200191610a81565b820191906000526020600020905b815481529060010190602001808311610a6457829003601f168201915b5050505050905090565b6000610a968261244d565b506000908152600460205260409020546001600160a01b031690565b6000610abd8261116c565b9050806001600160a01b0316836001600160a01b031603610b2a5760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b6064820152608401610929565b336001600160a01b0382161480610b465750610b468133610824565b610bb85760405162461bcd60e51b815260206004820152603d60248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f7420746f60448201527f6b656e206f776e6572206f7220617070726f76656420666f7220616c6c0000006064820152608401610929565b610bc283836124ac565b505050565b610bcf6121d5565b81610bd981612222565b6000838152600860205260409020610bf1908361251a565b6040516001600160a01b038316815283907f6f08c7e76d830d5f3d0a18fd27f4d8c0049b24a8689ddb39625e0864d894a9c19060200160405180910390a2505050565b610c3c6121d5565b81610c4681612222565b6000838152600860205260409020610c5d81612618565b610c678184612662565b837f907fece23ce39fbcbceb71e515043fe29408353fbb393b25b35eb8a70a4bad0b84604051610c9991815260200190565b60405180910390a250505050565b610cb1338261274f565b610ccd5760405162461bcd60e51b815260040161092990613b16565b610bc28383836127cd565b610ce06121d5565b80610cea81612222565b6000828152600860205260409020610d0181612618565b610d0a8161293e565b15610d4e5760405162461bcd60e51b81526020600482015260146024820152736e6f7420726561647920746f20756e7374616b6560601b6044820152606401610929565b610d57816129e3565b60405183907f11725367022c3ff288940f4b5473aa61c2da6a24af7363a1128ee2401e8983b290600090a2505050565b610d8f612a1a565b610d97612a74565b565b6060816001600160401b03811115610db357610db36138df565b604051908082528060200260200182016040528015610de657816020015b6060815260200190600190039081610dd15790505b5090506000610df4600b5490565b905060005b83811015610f1357816001600160401b03811115610e1957610e196138df565b604051908082528060200260200182016040528015610e42578160200160208202803683370190505b50838281518110610e5557610e55613b63565b60200260200101819052506000600a6000878785818110610e7857610e78613b63565b9050602002016020810190610e8d91906137a1565b6001600160a01b03166001600160a01b03168152602001908152602001600020905060005b83811015610f09576000818152602083905260409020548551869085908110610edd57610edd613b63565b60200260200101518281518110610ef657610ef6613b63565b6020908102919091010152600101610eb2565b5050600101610df9565b505092915050565b610bc283838360405180602001604052806000815250611863565b6000806000806000610f478661244d565b60008681526008602052604081208054600b80549293929091908110610f6f57610f6f613b63565b6000918252602090912060039182020180546001918201549185015460028601549590930154909b919a509198509296506001600160a01b031694509092505050565b6000610fc6610fc18484612ac9565b612b30565b9392505050565b610fd56121d5565b60008060005b8481101561106c57858582818110610ff557610ff5613b63565b90506020020135925061100783612222565b6000838152600860205260409020915061102082612618565b61102a8285612662565b827f907fece23ce39fbcbceb71e515043fe29408353fbb393b25b35eb8a70a4bad0b8560405161105c91815260200190565b60405180910390a2600101610fdb565b505050505050565b61107c6121d5565b60008060005b838110156111035784848281811061109c5761109c613b63565b9050602002013592506110ae83612222565b600083815260086020526040902091506110c782612b61565b6110d082612bab565b60405183907ff27b6ce5b2f5e68ddb2fd95a8a909d4ecf1daaac270935fff052feacb24f184290600090a2600101611082565b5050505050565b6111126121d5565b8061111c81612222565b600082815260086020526040902061113381612b61565b61113c81612bab565b60405183907ff27b6ce5b2f5e68ddb2fd95a8a909d4ecf1daaac270935fff052feacb24f184290600090a2505050565b6000818152600260205260408120546001600160a01b0316806109cd5760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b6044820152606401610929565b60006001600160a01b0382166112365760405162461bcd60e51b815260206004820152602960248201527f4552433732313a2061646472657373207a65726f206973206e6f7420612076616044820152683634b21037bbb732b960b91b6064820152608401610929565b506001600160a01b031660009081526003602052604090205490565b600061125c6121d5565b6000821180156112745750346112728387613b8f565b145b6112905760405162461bcd60e51b815260040161092990613ba6565b600061129c8686612ac9565b90506112a781612bff565b600754600101915060005b83811015611320576112c48286612c4b565b6112ce8184613bd2565b604080516001600160a01b0388168152602081018a90529081018890527f17700ceb1658b18206f427c1578048e87504106b14ec69e9b4586d9a95174a329060600160405180910390a26001016112b2565b50505b949350505050565b611333612a1a565b610d976000612ce5565b606060008211801561135a5750600b546113578385613bd2565b11155b6113765760405162461bcd60e51b815260040161092990613ba6565b816001600160401b0381111561138e5761138e6138df565b6040519080825280602002602001820160405280156113e357816020015b6113d060405180606001604052806000815260200160008152602001600081525090565b8152602001906001900390816113ac5790505b50905060005b8281101561146b57600b8185018154811061140657611406613b63565b9060005260206000209060030201604051806060016040529081600082015481526020016001820154815260200160028201548152505082828151811061144f5761144f613b63565b60200260200101819052506114648160010190565b90506113e9565b5092915050565b600061147c6121d5565b3460006114898286612ac9565b905061149481612bff565b61149e8185612c4b565b600754604080516001600160a01b03871681526020810185905290810187905281907f17700ceb1658b18206f427c1578048e87504106b14ec69e9b4586d9a95174a329060600160405180910390a295945050505050565b6114fe612a1a565b610d97612d37565b60006115118261244d565b600082815260086020526040902061152881612618565b610fc68161293e565b606060018054610a0890613adc565b6060816001600160401b0381111561155a5761155a6138df565b60405190808252806020026020018201604052801561158d57816020015b60608152602001906001900390816115785790505b509050600061159b600b5490565b905060005b83811015610f1357816001600160401b038111156115c0576115c06138df565b6040519080825280602002602001820160405280156115e9578160200160208202803683370190505b508382815181106115fc576115fc613b63565b602002602001018190525060006009600087878581811061161f5761161f613b63565b905060200201602081019061163491906137a1565b6001600160a01b03166001600160a01b03168152602001908152602001600020905060005b838110156116b057600081815260208390526040902054855186908590811061168457611684613b63565b6020026020010151828151811061169d5761169d613b63565b6020908102919091010152600101611659565b50506001016115a0565b6116c26121d5565b6000805b83811015611103578484828181106116e0576116e0613b63565b9050602002013591506116f282612222565b600082815260086020526040902061170a908461251a565b6040516001600160a01b038416815282907f6f08c7e76d830d5f3d0a18fd27f4d8c0049b24a8689ddb39625e0864d894a9c19060200160405180910390a26001016116c6565b61175b338383612d7a565b5050565b6117676121d5565b8161177181612222565b600083815260086020526040902061178881612b61565b8054600b805460009190839081106117a2576117a2613b63565b90600052602060002090600302019050848160000154346117c39190613bd2565b146117e05760405162461bcd60e51b815260040161092990613be5565b60038301546001600160a01b03166000908152600a602090815260408083208584529091529020805460001901905560018101546118219084908790612e48565b857f1d9c4d2b3e13eb9ac08a42625750ac17ec6ca94b4755c49285e9467b4e48c89d8660405161185391815260200190565b60405180910390a2505050505050565b61186d338361274f565b6118895760405162461bcd60e51b815260040161092990613b16565b61189584848484612e94565b50505050565b6118a36121d5565b60008060005b8481101561106c578585828181106118c3576118c3613b63565b9050602002013592506118d583612222565b600083815260086020526040902060028101549092506118f490612277565b156119395760405162461bcd60e51b81526020600482015260156024820152746e6f7420726561647920746f20776974686472617760581b6044820152606401610929565b611942836122eb565b61194c828561238e565b6040516001600160a01b0385169084907fd964a27d45f595739c13d8b1160b57491050cacf3a2e5602207277d6228f64ee90600090a36001016118a9565b6119926121d5565b600182116119d35760405162461bcd60e51b815260206004820152600e60248201526d0d2dcecc2d8d2c840d8cadccee8d60931b6044820152606401610929565b3460008080855b8015611b8a57600019018787828181106119f6576119f6613b63565b905060200201359350611a0884612222565b60008481526008602052604090209250611a2183612618565b82546003840154600b80546001600160a01b039092169183908110611a4857611a48613b63565b906000526020600020906003020193508360010154881015611a9f5760405162461bcd60e51b815260206004820152601060248201526f34b73b30b634b210323ab930ba34b7b760811b6044820152606401610929565b8354611aab9088613bd2565b9650611abd8560010154600019141590565b15611af2576001600160a01b038116600090815260096020908152604080832085845290915290208054600019019055611b1e565b6001600160a01b0381166000908152600a60209081526040808320858452909152902080546000190190555b8215611b3257611b2d866122eb565b611b83565b6000196001860155611b4585888a612e48565b7fb3f4c8ca702dbbd32d9a25ce17b1942a5060284d9d69fc4fcac8fb0397891b128a8a898b604051611b7a9493929190613c10565b60405180910390a15b50506119da565b5050505050505050565b6060611b9f8261244d565b6000611bb660408051602081019091526000815290565b90506000815111611bd65760405180602001604052806000815250610fc6565b80611be084612ec7565b604051602001611bf1929190613c56565b6040516020818303038152906040529392505050565b611c0f612a1a565b81600003611c535760405162461bcd60e51b8152602060048201526011602482015270185b5bdd5b9d081a5cc81a5b9d985b1a59607a1b6044820152606401610929565b6000828152600c6020908152604080832084845290915290205415611cb25760405162461bcd60e51b81526020600482015260156024820152746475706c6963617465206275636b6574207479706560581b6044820152606401610929565b60408051606081018252838152602080820184815243838501908152600b8054600181018255600082815295517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db960039092029182015592517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba84015590517f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb9092019190915554858352600c82528383208584528252918390209190915581518481529081018390527f6b39e3267efcd6611c8d7d2534c4715dcb4824322b90d85540a3a82967b6e7b791015b60405180910390a15050565b611db46121d5565b81611dbe81612222565b6000838152600860205260409020611dd581612b61565b8054600b80546000919083908110611def57611def613b63565b9060005260206000209060030201905080600101548511611e225760405162461bcd60e51b815260040161092990613be5565b60038301546001600160a01b03166000908152600a60209081526040808320858452909152902080546000190190558054611e5f90849087612e48565b857fc599168ac63ff28ec278088a2c424383a36ca26c931eb41af05e014f19252ea48660405161185391815260200190565b611e99612a1a565b43600b611ea68484612ac9565b81548110611eb657611eb6613b63565b9060005260206000209060030201600201819055507f6b39e3267efcd6611c8d7d2534c4715dcb4824322b90d85540a3a82967b6e7b78282604051611da0929190918252602082015260400190565b611f0d6121d5565b60008060005b8381101561110357848482818110611f2d57611f2d613b63565b905060200201359250611f3f83612222565b60008381526008602052604090209150611f5882612618565b611f618261293e565b15611fa55760405162461bcd60e51b81526020600482015260146024820152736e6f7420726561647920746f20756e7374616b6560601b6044820152606401610929565b611fae826129e3565b60405183907f11725367022c3ff288940f4b5473aa61c2da6a24af7363a1128ee2401e8983b290600090a2600101611f13565b611fe9612a1a565b600019600b611ff88484612ac9565b8154811061200857612008613b63565b9060005260206000209060030201600201819055507f099df2bf9247b43481cf1b791a4dd5fa1220c40c62940da539082fbcb30241d68282604051611da0929190918252602082015260400190565b60006120616121d5565b3482518561206f9190613b8f565b1461208c5760405162461bcd60e51b815260040161092990613ba6565b60006120988585612ac9565b90506120a381612bff565b600754600101915060005b8351811015612153576120da828583815181106120cd576120cd613b63565b6020026020010151612c4b565b6120e48184613bd2565b7f17700ceb1658b18206f427c1578048e87504106b14ec69e9b4586d9a95174a3285838151811061211757612117613b63565b602090810291909101810151604080516001600160a01b0390921682529181018a905290810188905260600160405180910390a26001016120ae565b50509392505050565b612164612a1a565b6001600160a01b0381166121c95760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b6064820152608401610929565b6121d281612ce5565b50565b600654600160a01b900460ff1615610d975760405162461bcd60e51b815260206004820152601060248201526f14185d5cd8589b194e881c185d5cd95960821b6044820152606401610929565b61222b8161116c565b6001600160a01b0316336001600160a01b0316146121d25760405162461bcd60e51b81526020600482015260096024820152683737ba1037bbb732b960b91b6044820152606401610929565b600060001982036122c35760405162461bcd60e51b81526020600482015260166024820152751b9bdd08185b881d5b9cdd185ad95908189d58dad95d60521b6044820152606401610929565b60006122d0600a84613bd2565b90504381116122e25750600092915050565b43900392915050565b60006122f68261116c565b9050612306816000846001612f59565b61230f8261116c565b600083815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0385168085526003845282852080546000190190558785526002909352818420805490911690555192935084927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b6000600b8360000154815481106123a7576123a7613b63565b600091825260208220600390910201546040519092506001600160a01b0384169083908381818185875af1925050503d8060008114612402576040519150601f19603f3d011682016040523d82523d6000602084013e612407565b606091505b50509050806118955760405162461bcd60e51b81526020600482015260126024820152713330b4b632b2103a37903a3930b739b332b960711b6044820152606401610929565b6000818152600260205260409020546001600160a01b03166121d25760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b6044820152606401610929565b600081815260046020526040902080546001600160a01b0319166001600160a01b03841690811790915581906124e18261116c565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b61252382612618565b815460038301546001600160a01b0390811690831681036125565760405162461bcd60e51b815260040161092990613be5565b6001840154600019146125ac576001600160a01b038181166000908152600960208181526040808420878552825280842080546000190190559387168352908152828220858352905220805460010190556125f1565b6001600160a01b038181166000908152600a60208181526040808420878552825280842080546000190190559387168352908152828220858352905220805460010190555b505060039190910180546001600160a01b0319166001600160a01b03909216919091179055565b6002810154600019146121d25760405162461bcd60e51b81526020600482015260126024820152713737ba10309039ba30b5b2b2103a37b5b2b760711b6044820152606401610929565b815460038301546001600160a01b031661267b8461293e565b8310156126bd5760405162461bcd60e51b815260206004820152601060248201526f34b73b30b634b210323ab930ba34b7b760811b6044820152606401610929565b60006126ed600b84815481106126d5576126d5613b63565b90600052602060002090600302016000015485612ac9565b90506126f881612bff565b60001960018681018290556001600160a01b039390931660008181526009602090815260408083209783529681528682208054909401909355968390558652600a8152838620918652529220805490920190915550565b60008061275b8361116c565b9050806001600160a01b0316846001600160a01b031614806127a257506001600160a01b0380821660009081526005602090815260408083209388168352929052205460ff165b806113235750836001600160a01b03166127bb84610a8b565b6001600160a01b031614949350505050565b826001600160a01b03166127e08261116c565b6001600160a01b0316146128065760405162461bcd60e51b815260040161092990613c85565b6001600160a01b0382166128685760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b6064820152608401610929565b6128758383836001612f59565b826001600160a01b03166128888261116c565b6001600160a01b0316146128ae5760405162461bcd60e51b815260040161092990613c85565b600081815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0387811680865260038552838620805460001901905590871680865283862080546001019055868652600290945282852080549092168417909155905184937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b600181015460009060001981036129905760405162461bcd60e51b81526020600482015260166024820152751b9bdd08185b881d5b9b1bd8dad95908189d58dad95d60521b6044820152606401610929565b6000600b8460000154815481106129a9576129a9613b63565b906000526020600020906003020160010154826129c69190613bd2565b90504381116129d9575060009392505050565b4390039392505050565b43600282015560038101546001600160a01b0316600090815260096020908152604080832093548352929052208054600019019055565b6006546001600160a01b03163314610d975760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610929565b612a7c613029565b6006805460ff60a01b191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b6000828152600c6020908152604080832084845290915281205480612b265760405162461bcd60e51b8152602060048201526013602482015272696e76616c6964206275636b6574207479706560681b6044820152606401610929565b6000198101611323565b600043600b8381548110612b4657612b46613b63565b90600052602060002090600302016002015411159050919050565b6001810154600019146121d25760405162461bcd60e51b81526020600482015260126024820152713737ba1030903637b1b5b2b2103a37b5b2b760711b6044820152606401610929565b80546003820154436001938401556001600160a01b03166000818152600a60209081526040808320858452825280832080546000190190559282526009815282822093825292909252902080549091019055565b612c0881612b30565b6121d25760405162461bcd60e51b8152602060048201526014602482015273696e616374697665206275636b6574207479706560601b6044820152606401610929565b6007805460019081018083556040805160808101825286815260001960208083018281528385019283526001600160a01b0389811660608601818152600098895260088552878920965187559251868a0155935160028601559051600390940180546001600160a01b03191694909116939093179092558352600a815281832087845290529020805490910190555461175b903390613079565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b612d3f6121d5565b6006805460ff60a01b1916600160a01b1790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258612aac3390565b816001600160a01b0316836001600160a01b031603612ddb5760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152606401610929565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b6000612e548383612ac9565b9050612e5f81612bff565b60038401546001600160a01b03166000908152600a602090815260408083208484529091529020805460010190559092555050565b612e9f8484846127cd565b612eab84848484613093565b6118955760405162461bcd60e51b815260040161092990613cca565b60606000612ed483613191565b60010190506000816001600160401b03811115612ef357612ef36138df565b6040519080825280601f01601f191660200182016040528015612f1d576020820181803683370190505b5090508181016020015b600019016f181899199a1a9b1b9c1cb0b131b232b360811b600a86061a8153600a8504945084612f2757509392505050565b80600114612fa95760405162461bcd60e51b815260206004820152601f60248201527f6261746368207472616e73666572206973206e6f7420737570706f72746564006044820152606401610929565b6001600160a01b0383161580612fd15750600082815260086020526040902060020154600019145b61301d5760405162461bcd60e51b815260206004820152601e60248201527f63616e6e6f74207472616e7366657220756e7374616b656420746f6b656e00006044820152606401610929565b61189584848484613269565b600654600160a01b900460ff16610d975760405162461bcd60e51b815260206004820152601460248201527314185d5cd8589b194e881b9bdd081c185d5cd95960621b6044820152606401610929565b61175b8282604051806020016040528060008152506132f1565b60006001600160a01b0384163b1561318957604051630a85bd0160e11b81526001600160a01b0385169063150b7a02906130d7903390899088908890600401613d1c565b6020604051808303816000875af1925050508015613112575060408051601f3d908101601f1916820190925261310f91810190613d59565b60015b61316f573d808015613140576040519150601f19603f3d011682016040523d82523d6000602084013e613145565b606091505b5080516000036131675760405162461bcd60e51b815260040161092990613cca565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050611323565b506001611323565b60008072184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b83106131d05772184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b830492506040015b6d04ee2d6d415b85acef810000000083106131fc576d04ee2d6d415b85acef8100000000830492506020015b662386f26fc10000831061321a57662386f26fc10000830492506010015b6305f5e1008310613232576305f5e100830492506008015b612710831061324657612710830492506004015b60648310613258576064830492506002015b600a83106109cd5760010192915050565b6001811115611895576001600160a01b038416156132af576001600160a01b038416600090815260036020526040812080548392906132a9908490613d76565b90915550505b6001600160a01b03831615611895576001600160a01b038316600090815260036020526040812080548392906132e6908490613bd2565b909155505050505050565b6132fb8383613324565b6133086000848484613093565b610bc25760405162461bcd60e51b815260040161092990613cca565b6001600160a01b03821661337a5760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152606401610929565b6000818152600260205260409020546001600160a01b0316156133df5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152606401610929565b6133ed600083836001612f59565b6000818152600260205260409020546001600160a01b0316156134525760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152606401610929565b6001600160a01b038216600081815260036020908152604080832080546001019055848352600290915280822080546001600160a01b0319168417905551839291907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b6001600160a01b03811681146121d257600080fd5b600080604083850312156134e557600080fd5b8235915060208301356134f7816134bd565b809150509250929050565b6001600160e01b0319811681146121d257600080fd5b60006020828403121561352a57600080fd5b8135610fc681613502565b60006020828403121561354757600080fd5b5035919050565b60005b83811015613569578181015183820152602001613551565b50506000910152565b6000815180845261358a81602086016020860161354e565b601f01601f19169290920160200192915050565b602081526000610fc66020830184613572565b600080604083850312156135c457600080fd5b82356135cf816134bd565b946020939093013593505050565b600080604083850312156135f057600080fd5b50508035926020909101359150565b60008060006060848603121561361457600080fd5b833561361f816134bd565b9250602084013561362f816134bd565b929592945050506040919091013590565b60008083601f84011261365257600080fd5b5081356001600160401b0381111561366957600080fd5b6020830191508360208260051b850101111561368457600080fd5b9250929050565b6000806020838503121561369e57600080fd5b82356001600160401b038111156136b457600080fd5b6136c085828601613640565b90969095509350505050565b6000602080830181845280855180835260408601915060408160051b87010192508387016000805b8381101561374857888603603f19018552825180518088529088019088880190845b818110156137325783518352928a0192918a0191600101613716565b50909750505093860193918601916001016136f4565b509398975050505050505050565b60008060006040848603121561376b57600080fd5b83356001600160401b0381111561378157600080fd5b61378d86828701613640565b909790965060209590950135949350505050565b6000602082840312156137b357600080fd5b8135610fc6816134bd565b600080600080608085870312156137d457600080fd5b843593506020850135925060408501356137ed816134bd565b9396929550929360600135925050565b602080825282518282018190526000919060409081850190868401855b82811015613849578151805185528681015187860152850151858501526060909301929085019060010161381a565b5091979650505050505050565b60008060006040848603121561386b57600080fd5b83356001600160401b0381111561388157600080fd5b61388d86828701613640565b90945092505060208401356138a1816134bd565b809150509250925092565b600080604083850312156138bf57600080fd5b82356138ca816134bd565b9150602083013580151581146134f757600080fd5b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b038111828210171561391d5761391d6138df565b604052919050565b6000806000806080858703121561393b57600080fd5b8435613946816134bd565b9350602085810135613957816134bd565b93506040860135925060608601356001600160401b038082111561397a57600080fd5b818801915088601f83011261398e57600080fd5b8135818111156139a0576139a06138df565b6139b2601f8201601f191685016138f5565b915080825289848285010111156139c857600080fd5b808484018584013760008482840101525080935050505092959194509250565b600080604083850312156139fb57600080fd5b8235613a06816134bd565b915060208301356134f7816134bd565b600080600060608486031215613a2b57600080fd5b83359250602080850135925060408501356001600160401b0380821115613a5157600080fd5b818701915087601f830112613a6557600080fd5b813581811115613a7757613a776138df565b8060051b9150613a888483016138f5565b818152918301840191848101908a841115613aa257600080fd5b938501935b83851015613acc5784359250613abc836134bd565b8282529385019390850190613aa7565b8096505050505050509250925092565b600181811c90821680613af057607f821691505b602082108103613b1057634e487b7160e01b600052602260045260246000fd5b50919050565b6020808252602d908201527f4552433732313a2063616c6c6572206973206e6f7420746f6b656e206f776e6560408201526c1c881bdc88185c1c1c9bdd9959609a1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b80820281158282048414176109cd576109cd613b79565b602080825260129082015271696e76616c696420706172616d657465727360701b604082015260600190565b808201808211156109cd576109cd613b79565b60208082526011908201527034b73b30b634b21037b832b930ba34b7b760791b604082015260600190565b6060808252810184905260006001600160fb1b03851115613c3057600080fd5b8460051b8087608085013760208301949094525060408101919091520160800192915050565b60008351613c6881846020880161354e565b835190830190613c7c81836020880161354e565b01949350505050565b60208082526025908201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060408201526437bbb732b960d91b606082015260800190565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090613d4f90830184613572565b9695505050505050565b600060208284031215613d6b57600080fd5b8151610fc681613502565b818103818111156109cd576109cd613b7956fea2646970667358221220be917767ef1b31d340fe6087913ba2e72285104d0b6c49192dabe393bb91652864736f6c63430008120033` + _stakingContractABI = `[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "AmountIncreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "BucketTypeActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "BucketTypeDeactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "newDelegate", + "type": "address" + } + ], + "name": "DelegateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "DurationExtended", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Merged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Unlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Unstaked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [], + "name": "UINT256_MAX", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNSTAKE_FREEZE_BLOCKS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "activateBucketType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "addBucketType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "blocksToUnstake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "blocksToWithdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "bucketOf", + "outputs": [ + { + "internalType": "uint256", + "name": "amount_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "duration_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockedAt_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unstakedAt_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "delegate_", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_size", + "type": "uint256" + } + ], + "name": "bucketTypes", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "activatedAt", + "type": "uint256" + } + ], + "internalType": "struct BucketType[]", + "name": "types_", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_delegate", + "type": "address" + } + ], + "name": "changeDelegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_tokenIds", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_delegate", + "type": "address" + } + ], + "name": "changeDelegates", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "deactivateBucketType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_newDuration", + "type": "uint256" + } + ], + "name": "extendDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_newAmount", + "type": "uint256" + } + ], + "name": "increaseAmount", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "isActiveBucketType", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_tokenIds", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_delegates", + "type": "address[]" + } + ], + "name": "lockedVotesTo", + "outputs": [ + { + "internalType": "uint256[][]", + "name": "counts_", + "type": "uint256[][]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "_newDuration", + "type": "uint256" + } + ], + "name": "merge", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "numOfBucketTypes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_delegate", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [ + { + "internalType": "uint256", + "name": "firstTokenId_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_delegate", + "type": "address" + } + ], + "name": "stake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_duration", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "_delegates", + "type": "address[]" + } + ], + "name": "stake", + "outputs": [ + { + "internalType": "uint256", + "name": "firstTokenId_", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_tokenIds", + "type": "uint256[]" + } + ], + "name": "unlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "unlock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_delegates", + "type": "address[]" + } + ], + "name": "unlockedVotesTo", + "outputs": [ + { + "internalType": "uint256[][]", + "name": "counts_", + "type": "uint256[][]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "unstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_tokenIds", + "type": "uint256[]" + } + ], + "name": "unstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "_recipient", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "_tokenIds", + "type": "uint256[]" + }, + { + "internalType": "address payable", + "name": "_recipient", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]` + + _adminID = 22 +) + +var ( + _delegates = []common.Address{ + common.BytesToAddress(identityset.Address(0).Bytes()), + common.BytesToAddress(identityset.Address(1).Bytes()), + common.BytesToAddress(identityset.Address(2).Bytes()), + common.BytesToAddress(identityset.Address(3).Bytes()), + common.BytesToAddress(identityset.Address(4).Bytes()), + common.BytesToAddress(identityset.Address(5).Bytes()), + common.BytesToAddress(identityset.Address(6).Bytes()), + } +) + +func TestContractStaking(t *testing.T) { + r := require.New(t) + // prepare blockchain + adminID := _adminID + ctx := context.Background() + cfg := config.Default + cfg.Chain.ProducerPrivKey = identityset.PrivateKey(adminID).HexString() + cfg.Chain.EnableTrielessStateDB = false + cfg.Genesis.InitBalanceMap[identityset.Address(adminID).String()] = "1000000000000000000000000000" + + bc, sf, dao, ap, indexer := prepareContractStakingBlockchain(ctx, cfg, r) + defer func() { + r.NoError(bc.Stop(ctx)) + }() + ctx = genesis.WithGenesisContext(context.Background(), bc.Genesis()) + + // deploy smart contract + deployAddr := blockindex.StakingContractAddress + param := callParam{ + contractAddr: deployAddr, + bytecode: _stakingContractByteCode, + amount: big.NewInt(0), + gasLimit: 20000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + contractAddresses := deployContracts(bc, sf, dao, ap, ¶m, r) + r.Equal(deployAddr, contractAddresses) + lsdABI, err := abi.JSON(strings.NewReader(_stakingContractABI)) + r.NoError(err) + + // init bucket type + bucketTypes := []struct { + amount int64 + duration int64 + }{ + {10, 100}, + {10, 10}, + {100, 100}, + {100, 10}, + } + params := []*callParam{} + for i := range bucketTypes { + data, err := lsdABI.Pack("addBucketType", big.NewInt(bucketTypes[i].amount), big.NewInt(bucketTypes[i].duration)) + r.NoError(err) + param := callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + params = append(params, ¶m) + } + receipts, _ := writeContract(bc, sf, dao, ap, params, r) + r.Len(receipts, len(params)) + for _, receipt := range receipts { + r.EqualValues(iotextypes.ReceiptStatus_Success, receipt.Status) + } + + simpleStake := func(cand common.Address, amount, duration *big.Int) *blockindex.ContractStakingBucket { + return stake(lsdABI, bc, sf, dao, ap, contractAddresses, indexer, r, cand, amount, duration) + } + + t.Run("stake", func(t *testing.T) { + delegateIdx := 2 + delegate := _delegates[delegateIdx] + data, err := lsdABI.Pack("stake0", big.NewInt(10), delegate) + r.NoError(err) + param := callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(10), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, blk := writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + buckets, err := indexer.Buckets() + r.NoError(err) + slices.SortFunc(buckets, func(i, j *blockindex.ContractStakingBucket) bool { + return i.Index < j.Index + }) + bt := buckets[len(buckets)-1] + tokenID := bt.Index + r.EqualValues(1, bt.Index) + r.True(bt.AutoStake) + r.Equal(identityset.Address(delegateIdx).String(), bt.Candidate.String()) + r.EqualValues(identityset.PrivateKey(adminID).PublicKey().Address().String(), bt.Owner.String()) + r.EqualValues(0, bt.StakedAmount.Cmp(big.NewInt(10))) + r.EqualValues(10, bt.StakedDurationBlockNumber) + r.EqualValues(blk.Height(), bt.CreateBlockHeight) + r.EqualValues(blk.Height(), bt.StakeBlockHeight) + r.True(bt.UnstakeBlockHeight == math.MaxUint64) + r.EqualValues(10, indexer.CandidateVotes(identityset.Address(delegateIdx)).Int64()) + r.EqualValues(1, indexer.TotalBucketCount()) + r.EqualValues(contractAddresses, bt.ContractAddress) + buckets, err = indexer.BucketsByCandidate(identityset.Address(delegateIdx)) + r.NoError(err) + r.Len(buckets, 1) + r.EqualValues(bt, buckets[0]) + + t.Run("unlock", func(t *testing.T) { + data, err = lsdABI.Pack("unlock0", big.NewInt(int64(bt.Index))) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, blk = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err := indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.EqualValues(blk.Height(), bt.StakeBlockHeight) + r.EqualValues(10, indexer.CandidateVotes(identityset.Address(delegateIdx)).Int64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + t.Run("unstake", func(t *testing.T) { + jumpBlocks(bc, 10, r) + data, err = lsdABI.Pack("unstake", big.NewInt(int64(bt.Index))) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, blk = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err := indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.EqualValues(blk.Height(), bt.UnstakeBlockHeight) + r.EqualValues(0, indexer.CandidateVotes(identityset.Address(delegateIdx)).Int64()) + r.EqualValues(1, indexer.TotalBucketCount()) + + t.Run("withdraw", func(t *testing.T) { + // freeze blocks are changed to 10 in test + jumpBlocks(bc, 10, r) + tokenID := bt.Index + + addr := common.BytesToAddress(identityset.PrivateKey(adminID).PublicKey().Bytes()) + data, err := lsdABI.Pack("withdraw", big.NewInt(int64(tokenID)), addr) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err = indexer.Bucket(uint64(tokenID)) + r.ErrorIs(err, blockindex.ErrBucketInfoNotExist) + r.EqualValues(1, indexer.TotalBucketCount()) + }) + }) + }) + }) + + t.Run("lock & unlock", func(t *testing.T) { + bt := simpleStake(_delegates[3], big.NewInt(10), big.NewInt(10)) + tokenID := bt.Index + + data, err := lsdABI.Pack("unlock0", big.NewInt(int64(bt.Index))) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + + data, err = lsdABI.Pack("lock", big.NewInt(int64(bt.Index)), big.NewInt(10)) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ := writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err = indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.True(bt.AutoStake) + }) + t.Run("merge", func(t *testing.T) { + // stake 10 bucket + candName := "delegate3" + params := []*callParam{} + for i := 0; i < 10; i++ { + delegate := [12]byte{} + copy(delegate[:], []byte(candName)) + data, err := lsdABI.Pack("stake0", big.NewInt(10), delegate) + r.NoError(err) + param := callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(10), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + params = append(params, ¶m) + } + receipts, _ := writeContract(bc, sf, dao, ap, params, r) + r.Len(receipts, len(params)) + for _, receipt := range receipts { + r.EqualValues(iotextypes.ReceiptStatus_Success, receipt.Status) + } + buckets, err := indexer.Buckets() + r.NoError(err) + slices.SortFunc(buckets, func(i, j *blockindex.ContractStakingBucket) bool { + return i.Index < j.Index + }) + r.True(len(buckets) >= 10) + // merge + newBuckets := buckets[len(buckets)-10:] + tokens := []*big.Int{} + for _, bucket := range newBuckets { + tokens = append(tokens, big.NewInt(int64(bucket.Index))) + } + data, err := lsdABI.Pack("merge", tokens, big.NewInt(100)) + r.NoError(err) + param := callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + for i := range newBuckets { + if i == 0 { + bt, err := indexer.Bucket(uint64(newBuckets[i].Index)) + r.NoError(err) + r.EqualValues(100, bt.StakedDurationBlockNumber) + } else { + _, err := indexer.Bucket(uint64(newBuckets[i].Index)) + r.ErrorIs(err, blockindex.ErrBucketInfoNotExist) + } + } + }) + + t.Run("extend duration", func(t *testing.T) { + // stake + bt := simpleStake(_delegates[3], big.NewInt(10), big.NewInt(10)) + tokenID := bt.Index + r.EqualValues(10, bt.StakedDurationBlockNumber) + // extend duration + data, err := lsdABI.Pack("extendDuration", big.NewInt(int64(tokenID)), big.NewInt(100)) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err = indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.EqualValues(100, bt.StakedDurationBlockNumber) + }) + + t.Run("increase amount", func(t *testing.T) { + bt := simpleStake(_delegates[4], big.NewInt(10), big.NewInt(10)) + tokenID := bt.Index + + data, err := lsdABI.Pack("increaseAmount", big.NewInt(int64(tokenID)), big.NewInt(100)) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(90), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err = indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.EqualValues(100, bt.StakedAmount.Int64()) + }) + + t.Run("change delegate", func(t *testing.T) { + delegateIdx := 5 + bt := simpleStake(_delegates[delegateIdx], big.NewInt(10), big.NewInt(10)) + tokenID := bt.Index + r.EqualValues(identityset.Address(delegateIdx).String(), bt.Candidate.String()) + + delegateIdx = 6 + delegate := _delegates[delegateIdx] + data, err := lsdABI.Pack("changeDelegate", big.NewInt(int64(tokenID)), delegate) + r.NoError(err) + param = callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: big.NewInt(0), + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(adminID), + } + receipts, _ = writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues("", receipts[0].ExecutionRevertMsg()) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + bt, err = indexer.Bucket(uint64(tokenID)) + r.NoError(err) + r.EqualValues(identityset.Address(delegateIdx).String(), bt.Candidate.String()) + }) + +} + +func prepareContractStakingBlockchain(ctx context.Context, cfg config.Config, r *require.Assertions) (blockchain.Blockchain, factory.Factory, blockdao.BlockDAO, actpool.ActPool, blockindex.ContractStakingIndexer) { + defer func() { + delete(cfg.Plugins, config.GatewayPlugin) + }() + cfg.Plugins[config.GatewayPlugin] = true + cfg.Chain.EnableAsyncIndexWrite = false + cfg.Genesis.EnableGravityChainVoting = false + testTriePath, err := testutil.PathOfTempFile("trie") + r.NoError(err) + defer testutil.CleanupPath(testTriePath) + testContractStakeIndexerPath, err := testutil.PathOfTempFile("contractstakeindexer") + r.NoError(err) + defer testutil.CleanupPath(testContractStakeIndexerPath) + + cfg.Chain.TrieDBPath = testTriePath + cfg.ActPool.MinGasPriceStr = "0" + + cfg.Genesis.Blockchain.AleutianBlockHeight = 0 + cfg.Genesis.Blockchain.BeringBlockHeight = 0 + + cfg.Genesis.HawaiiBlockHeight = 0 + + cfg.Genesis.CookBlockHeight = 0 + cfg.Genesis.DardanellesBlockHeight = 0 + cfg.Genesis.DaytonaBlockHeight = 0 + cfg.Genesis.EasterBlockHeight = 0 + cfg.Genesis.FbkMigrationBlockHeight = 0 + cfg.Genesis.FairbankBlockHeight = 0 + cfg.Genesis.GreenlandBlockHeight = 0 + cfg.Genesis.IcelandBlockHeight = 0 + + // London is enabled at okhotsk height + cfg.Genesis.Blockchain.JutlandBlockHeight = 0 + cfg.Genesis.Blockchain.KamchatkaBlockHeight = 0 + cfg.Genesis.Blockchain.LordHoweBlockHeight = 0 + cfg.Genesis.Blockchain.MidwayBlockHeight = 0 + cfg.Genesis.Blockchain.NewfoundlandBlockHeight = 0 + cfg.Genesis.Blockchain.OkhotskBlockHeight = 0 + + registry := protocol.NewRegistry() + acc := account.NewProtocol(rewarding.DepositGas) + r.NoError(acc.Register(registry)) + rp := rolldpos.NewProtocol(cfg.Genesis.NumCandidateDelegates, cfg.Genesis.NumDelegates, cfg.Genesis.NumSubEpochs) + r.NoError(rp.Register(registry)) + // create state factory + var sf factory.Factory + var daoKV db.KVStore + + factoryCfg := factory.GenerateConfig(cfg.Chain, cfg.Genesis) + if cfg.Chain.EnableTrielessStateDB { + if cfg.Chain.EnableStateDBCaching { + daoKV, err = db.CreateKVStoreWithCache(cfg.DB, cfg.Chain.TrieDBPath, cfg.Chain.StateDBCacheSize) + } else { + daoKV, err = db.CreateKVStore(cfg.DB, cfg.Chain.TrieDBPath) + } + r.NoError(err) + sf, err = factory.NewStateDB(factoryCfg, daoKV, factory.RegistryStateDBOption(registry)) + } else { + sf, err = factory.NewFactory(factoryCfg, db.NewMemKVStore(), factory.RegistryOption(registry)) + } + r.NoError(err) + ap, err := actpool.NewActPool(cfg.Genesis, sf, cfg.ActPool) + r.NoError(err) + // create indexer + indexer, err := blockindex.NewIndexer(db.NewMemKVStore(), cfg.Genesis.Hash()) + r.NoError(err) + cc := cfg.DB + cc.DbPath = testContractStakeIndexerPath + contractStakeIndexer := blockindex.NewContractStakingIndexer(db.NewBoltDB(cc)) + // create BlockDAO + dao := blockdao.NewBlockDAOInMemForTest([]blockdao.BlockIndexer{sf, indexer, contractStakeIndexer}) + r.NotNil(dao) + bc := blockchain.NewBlockchain( + cfg.Chain, + cfg.Genesis, + dao, + factory.NewMinter(sf, ap), + blockchain.BlockValidatorOption(block.NewValidator( + sf, + protocol.NewGenericValidator(sf, accountutil.AccountState), + )), + ) + // reward := rewarding.NewProtocol(cfg.Genesis.Rewarding) + // r.NoError(reward.Register(registry)) + + r.NotNil(bc) + execution := execution.NewProtocol(dao.GetBlockHash, rewarding.DepositGasWithSGD, nil) + r.NoError(execution.Register(registry)) + r.NoError(bc.Start(ctx)) + + return bc, sf, dao, ap, contractStakeIndexer +} + +func deployContracts( + bc blockchain.Blockchain, + sf factory.Factory, + dao blockdao.BlockDAO, + ap actpool.ActPool, + param *callParam, + r *require.Assertions, +) (contractAddresses string) { + sk := param.sk + bytecode, err := hex.DecodeString(param.bytecode) + r.NoError(err) + state, err := accountutil.AccountState(genesis.WithGenesisContext(context.Background(), bc.Genesis()), sf, sk.PublicKey().Address()) + r.NoError(err) + nonce := state.PendingNonce() + amount := param.amount + gasLimit := param.gasLimit + gasPrice := param.gasPrice + exec, err := action.NewExecutionWithAccessList(action.EmptyAddress, nonce, amount, gasLimit, gasPrice, bytecode, nil) + r.NoError(err) + builder := &action.EnvelopeBuilder{} + elp := builder.SetAction(exec). + SetNonce(exec.Nonce()). + SetGasLimit(gasLimit). + SetGasPrice(gasPrice). + Build() + selp, err := action.Sign(elp, sk) + r.NoError(err) + err = ap.Add(context.Background(), selp) + r.NoError(err) + selpHash, err := selp.Hash() + + blk, err := bc.MintNewBlock(testutil.TimestampNow()) + r.NoError(err) + err = bc.CommitBlock(blk) + r.NoError(err) + + receipt, err := dao.GetReceiptByActionHash(selpHash, blk.Height()) + r.NoError(err) + r.NotNil(receipt) + r.Equal(uint64(iotextypes.ReceiptStatus_Success), receipt.Status) + + return receipt.ContractAddress +} + +type callParam struct { + contractAddr string + bytecode string + amount *big.Int + gasLimit uint64 + gasPrice *big.Int + sk crypto.PrivateKey +} + +func writeContract(bc blockchain.Blockchain, + sf factory.Factory, + dao blockdao.BlockDAO, + ap actpool.ActPool, + params []*callParam, + r *require.Assertions, +) ([]*action.Receipt, *block.Block) { + nonces := map[string]uint64{} + hashes := []hash.Hash256{} + for _, param := range params { + nonce := uint64(1) + var ok bool + sk := param.sk + executor := sk.PublicKey().Address() + if nonce, ok = nonces[executor.String()]; !ok { + state, err := accountutil.AccountState(genesis.WithGenesisContext(context.Background(), bc.Genesis()), sf, executor) + r.NoError(err) + nonce = state.PendingNonce() + } else { + nonce++ + } + nonces[executor.String()] = nonce + + amount := param.amount + gasLimit := param.gasLimit + gasPrice := param.gasPrice + bytecode, err := hex.DecodeString(param.bytecode) + r.NoError(err) + exec, err := action.NewExecutionWithAccessList(param.contractAddr, nonce, amount, gasLimit, gasPrice, bytecode, nil) + r.NoError(err) + builder := &action.EnvelopeBuilder{} + elp := builder.SetAction(exec). + SetNonce(exec.Nonce()). + SetGasLimit(gasLimit). + SetGasPrice(gasPrice). + Build() + selp, err := action.Sign(elp, sk) + r.NoError(err) + err = ap.Add(context.Background(), selp) + r.NoError(err) + selpHash, err := selp.Hash() + hashes = append(hashes, selpHash) + } + + blk, err := bc.MintNewBlock(testutil.TimestampNow()) + r.NoError(err) + err = bc.CommitBlock(blk) + r.NoError(err) + + receipts := []*action.Receipt{} + for _, hash := range hashes { + receipt, err := dao.GetReceiptByActionHash(hash, blk.Height()) + r.NoError(err) + receipts = append(receipts, receipt) + } + return receipts, blk +} + +func jumpBlocks(bc blockchain.Blockchain, count int, r *require.Assertions) { + for i := 0; i < count; i++ { + blk, err := bc.MintNewBlock(testutil.TimestampNow()) + r.NoError(err) + err = bc.CommitBlock(blk) + r.NoError(err) + } +} + +func stake(lsdABI abi.ABI, bc blockchain.Blockchain, sf factory.Factory, dao blockdao.BlockDAO, ap actpool.ActPool, contractAddresses string, indexer blockindex.ContractStakingIndexer, r *require.Assertions, cand common.Address, amount, duration *big.Int) *blockindex.ContractStakingBucket { + delegate := cand + data, err := lsdABI.Pack("stake0", duration, delegate) + r.NoError(err) + param := callParam{ + contractAddr: contractAddresses, + bytecode: hex.EncodeToString(data), + amount: amount, + gasLimit: 1000000, + gasPrice: big.NewInt(0), + sk: identityset.PrivateKey(_adminID), + } + receipts, _ := writeContract(bc, sf, dao, ap, []*callParam{¶m}, r) + r.Len(receipts, 1) + r.EqualValues(iotextypes.ReceiptStatus_Success, receipts[0].Status) + buckets, err := indexer.Buckets() + r.NoError(err) + slices.SortFunc(buckets, func(i, j *blockindex.ContractStakingBucket) bool { + return i.Index < j.Index + }) + bt := buckets[len(buckets)-1] + return bt +}