Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: precompile redelegate #151

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Ref: https://keepachangelog.com/en/1.0.0/

# Change log

## [Unreleased]

### Features

* Precompile staking redelegate

## [v5.0.0]

### Features
Expand Down
173 changes: 171 additions & 2 deletions contract/IStaking.go

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions solidity/contracts/staking/Decode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ library Decode {
return (amount, reward, completionTime);
}

function redelegate(
bytes memory data
) internal pure returns (uint256, uint256, uint256) {
(uint256 amount, uint256 reward, uint256 completionTime) = abi.decode(
data,
(uint256, uint256, uint256)
);
return (amount, reward, completionTime);
}

function withdraw(bytes memory data) internal pure returns (uint256) {
uint256 reward = abi.decode(data, (uint256));
return reward;
Expand Down
14 changes: 14 additions & 0 deletions solidity/contracts/staking/Encode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ library Encode {
);
}

function redelegate(
string memory _valSrc,
string memory _valDst,
uint256 _shares
) internal pure returns (bytes memory) {
return
abi.encodeWithSignature(
"redelegate(string,string,uint256)",
_valSrc,
_valDst,
_shares
);
}

function withdraw(
string memory _validator
) internal pure returns (bytes memory) {
Expand Down
17 changes: 17 additions & 0 deletions solidity/contracts/staking/IStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ interface IStaking {
external
returns (uint256 _amount, uint256 _reward, uint256 _completionTime);

function redelegate(
string memory _valSrc,
string memory _valDst,
uint256 _shares
)
external
returns (uint256 _amount, uint256 _reward, uint256 _completionTime);

function withdraw(string memory _val) external returns (uint256 _reward);

function approveShares(
Expand Down Expand Up @@ -66,6 +74,15 @@ interface IStaking {
uint256 completionTime
);

event Redelegate(
address indexed sender,
string valSrc,
string valDst,
uint256 shares,
uint256 amount,
uint256 completionTime
);

event Withdraw(address indexed sender, string validator, uint256 reward);

event ApproveShares(
Expand Down
13 changes: 13 additions & 0 deletions solidity/contracts/staking/StakingCall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ library StakingCall {
return Decode.undelegate(data);
}

function redelegate(
string memory _valSrc,
string memory _valDst,
uint256 _shares
) internal returns (uint256, uint256, uint256) {
// solhint-disable-next-line avoid-low-level-calls
(bool result, bytes memory data) = STAKING_ADDRESS.call(
Encode.redelegate(_valSrc, _valDst, _shares)
);
Decode.ok(result, data, "redelegate failed");
return Decode.redelegate(data);
}

function withdraw(string memory _val) internal returns (uint256) {
// solhint-disable-next-line avoid-low-level-calls
(bool result, bytes memory data) = STAKING_ADDRESS.call(
Expand Down
12 changes: 12 additions & 0 deletions solidity/contracts/test/StakingTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ contract StakingTest is IStaking {
return (amount, reward, completionTime);
}

function redelegate(
string memory _valSrc,
string memory _valDst,
uint256 _shares
) external override returns (uint256, uint256, uint256) {
(uint256 amount, uint256 reward, uint256 completionTime) = StakingCall
.redelegate(_valSrc, _valDst, _shares);
validatorShares[_valSrc] -= _shares;
validatorShares[_valDst] += _shares;
return (amount, reward, completionTime);
}

function withdraw(string memory _val) external override returns (uint256) {
uint256 amount = StakingCall.withdraw(_val);
return amount;
Expand Down
175 changes: 172 additions & 3 deletions tests/contract/StakingTest.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func (suite *IntegrationTest) TestRun() {
suite.StakingContractTest()
suite.StakingSharesTest()
suite.StakingSharesContractTest()
suite.StakingPrecompileRedelegateTest()
suite.StakingPrecompileRedelegateByContractTest()

suite.MigrateTestDelegate()
suite.MigrateTestUnDelegate()
Expand Down
39 changes: 36 additions & 3 deletions tests/staking_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,20 @@ func (suite *StakingSuite) DelegationRewards(delAddr, valAddr string) sdk.DecCoi
return response.Rewards
}

func (suite *StakingSuite) SetWithdrawAddress(delAddr, withdrawAddr sdk.AccAddress) {
func (suite *StakingSuite) SetWithdrawAddress(withdrawAddr sdk.AccAddress) {
suite.SetWithdrawAddressWithResponse(suite.privKey, withdrawAddr)
}

func (suite *StakingSuite) SetWithdrawAddressWithResponse(privKey cryptotypes.PrivKey, withdrawAddr sdk.AccAddress) *sdk.TxResponse {
delAddr := sdk.AccAddress(privKey.PubKey().Address())
setWithdrawAddress := distrtypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr)
txResponse := suite.BroadcastTx(suite.privKey, setWithdrawAddress)
txResponse := suite.BroadcastTx(privKey, setWithdrawAddress)
suite.Require().True(txResponse.Code == 0)
response, err := suite.GRPCClient().DistrQuery().DelegatorWithdrawAddress(suite.ctx, &distrtypes.QueryDelegatorWithdrawAddressRequest{DelegatorAddress: delAddr.String()})
response, err := suite.GRPCClient().DistrQuery().DelegatorWithdrawAddress(suite.ctx,
&distrtypes.QueryDelegatorWithdrawAddressRequest{DelegatorAddress: delAddr.String()})
suite.Require().NoError(err)
suite.Require().EqualValues(response.WithdrawAddress, withdrawAddr.String())
return txResponse
}

func (suite *StakingSuite) Delegate(privateKey cryptotypes.PrivKey, valAddr string, delAmount *big.Int) *ethtypes.Receipt {
Expand All @@ -105,6 +112,15 @@ func (suite *StakingSuite) Delegate(privateKey cryptotypes.PrivKey, valAddr stri
return suite.SendTransaction(transaction)
}

func (suite *StakingSuite) Redelegate(privateKey cryptotypes.PrivKey, valSrc, valDst string, shares *big.Int) *ethtypes.Receipt {
stakingContract := precompilesstaking.GetAddress()
pack, err := precompilesstaking.GetABI().Pack("redelegate", valSrc, valDst, shares)
suite.Require().NoError(err)
transaction, err := client.BuildEthTransaction(suite.ctx, suite.EthClient(), privateKey, &stakingContract, big.NewInt(0), pack)
suite.Require().NoError(err)
return suite.SendTransaction(transaction)
}

func (suite *StakingSuite) DelegateByContract(privateKey cryptotypes.PrivKey, contract common.Address, valAddr string, delAmount *big.Int) *ethtypes.Receipt {
stakingContract, err := testscontract.NewStakingTest(contract, suite.EthClient())
suite.Require().NoError(err)
Expand Down Expand Up @@ -157,6 +173,23 @@ func (suite *StakingSuite) UndelegateByContract(privateKey cryptotypes.PrivKey,
return receipt
}

func (suite *StakingSuite) RedelegateByContract(privateKey cryptotypes.PrivKey, contract common.Address, valSrc, valDst string, shares *big.Int) *ethtypes.Receipt {
stakingContract, err := testscontract.NewStakingTest(contract, suite.EthClient())
suite.Require().NoError(err)

auth := suite.TransactionOpts(privateKey)

tx, err := stakingContract.Redelegate(auth, valSrc, valDst, shares)
suite.Require().NoError(err)

ctx, cancel := context.WithTimeout(suite.ctx, 5*time.Second)
defer cancel()
receipt, err := bind.WaitMined(ctx, suite.EthClient(), tx)
suite.Require().NoError(err)
suite.Require().Equal(receipt.Status, ethtypes.ReceiptStatusSuccessful)
return receipt
}

func (suite *StakingSuite) UnDelegate(privateKey cryptotypes.PrivKey, valAddr string, shares *big.Int) {
stakingContract := precompilesstaking.GetAddress()
pack, err := precompilesstaking.GetABI().Pack(precompilesstaking.UndelegateMethodName, valAddr, shares)
Expand Down
101 changes: 100 additions & 1 deletion tests/staking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/common"
"google.golang.org/grpc/metadata"

"github.com/functionx/fx-core/v6/app"
Expand Down Expand Up @@ -46,7 +47,7 @@ func (suite *IntegrationTest) StakingTest() {

// set WithdrawAddress
rewardAddress := sdk.AccAddress(helpers.NewEthPrivKey().Bytes())
suite.staking.SetWithdrawAddress(delAddr, rewardAddress)
suite.staking.SetWithdrawAddress(rewardAddress)

// delegation rewards
rewards := suite.staking.Rewards(valAddr.String(), suite.staking.Address())
Expand Down Expand Up @@ -434,6 +435,104 @@ func (suite *IntegrationTest) StakingSharesContractTest() {
suite.Require().Equal(contractBal.AmountOf(fxtypes.DefaultDenom).String(), totalReward.String())
}

func (suite *IntegrationTest) StakingPrecompileRedelegateTest() {
var (
delSigner = helpers.NewSigner(helpers.NewEthPrivKey())
valAddr = suite.staking.GetFirstValAddr()
valNew = helpers.NewSigner(helpers.NewEthPrivKey())
initBalance = sdkmath.NewInt(2000).MulRaw(1e18)
delBalance = sdkmath.NewInt(1000).MulRaw(1e18)
)

suite.Send(delSigner.AccAddress(), sdk.NewCoin(fxtypes.DefaultDenom, initBalance))
suite.Send(valNew.AccAddress(), sdk.NewCoin(fxtypes.DefaultDenom, initBalance))

// delegate
receipt := suite.staking.Delegate(delSigner.PrivKey(), valAddr.String(), delBalance.BigInt())
txFee1 := suite.evm.TxFee(receipt.TxHash)

delBal := suite.QueryBalances(delSigner.AccAddress())
total := delBalance.Add(sdkmath.NewIntFromBigInt(txFee1)).Add(delBal.AmountOf(fxtypes.DefaultDenom))
suite.Equal(initBalance.String(), total.String())

// set WithdrawAddress
rewardAddress := sdk.AccAddress(helpers.NewEthPrivKey().PubKey().Address().Bytes())
txRsp := suite.staking.SetWithdrawAddressWithResponse(delSigner.PrivKey(), rewardAddress)
gasPrice, err := sdk.ParseCoinNormalized(suite.network.Config.MinGasPrices)
suite.NoError(err)
gasFee := gasPrice.Amount.Mul(sdkmath.NewInt(txRsp.GasWanted))

hexAddr := common.BytesToAddress(delSigner.AccAddress().Bytes())
// query delegate
valAddrShares1, _ := suite.staking.Delegation(valAddr.String(), hexAddr)

resp := suite.staking.CreateValidator(valNew.PrivKey())
suite.Equal(resp.Code, uint32(0))

receipt = suite.staking.Redelegate(delSigner.PrivKey(), valAddr.String(), sdk.ValAddress(valNew.AccAddress()).String(), valAddrShares1)
txFee2 := suite.evm.TxFee(receipt.TxHash)

valAddrShares2, _ := suite.staking.Delegation(valAddr.String(), hexAddr)
suite.Equal(big.NewInt(0).String(), valAddrShares2.String())

valNewShares, _ := suite.staking.Delegation(sdk.ValAddress(valNew.AccAddress()).String(), hexAddr)
suite.Equal(valAddrShares1, valNewShares)

delBal = suite.QueryBalances(delSigner.AccAddress())
total = delBalance.Add(sdkmath.NewIntFromBigInt(txFee1)).
Add(gasFee).
Add(sdkmath.NewIntFromBigInt(txFee2)).
Add(delBal.AmountOf(fxtypes.DefaultDenom))
suite.Equal(initBalance.String(), total.String())
}

func (suite *IntegrationTest) StakingPrecompileRedelegateByContractTest() {
var (
delSigner = helpers.NewSigner(helpers.NewEthPrivKey())
valAddr = suite.staking.GetFirstValAddr()
valNew = helpers.NewSigner(helpers.NewEthPrivKey())
initBalance = sdkmath.NewInt(2000).MulRaw(1e18)
delBalance = sdkmath.NewInt(1000).MulRaw(1e18)
)

suite.Send(delSigner.AccAddress(), sdk.NewCoin(fxtypes.DefaultDenom, initBalance))
suite.Send(valNew.AccAddress(), sdk.NewCoin(fxtypes.DefaultDenom, initBalance))

// deploy contract to staking
contract, txHash := suite.staking.DeployStakingContract(delSigner.PrivKey())
txFee1 := suite.evm.TxFee(txHash)

// delegate by contract
receipt := suite.staking.DelegateByContract(delSigner.PrivKey(), contract, valAddr.String(), delBalance.BigInt())
txFee2 := suite.evm.TxFee(receipt.TxHash)

delBal := suite.QueryBalances(delSigner.AccAddress())
total := delBalance.Add(sdkmath.NewIntFromBigInt(txFee1)).Add(sdkmath.NewIntFromBigInt(txFee2)).Add(delBal.AmountOf(fxtypes.DefaultDenom))
suite.Equal(initBalance.String(), total.String())

// query delegate
valAddrShares1, _ := suite.staking.Delegation(valAddr.String(), contract)

resp := suite.staking.CreateValidator(valNew.PrivKey())
suite.Equal(resp.Code, uint32(0))

receipt = suite.staking.RedelegateByContract(delSigner.PrivKey(), contract, valAddr.String(), sdk.ValAddress(valNew.AccAddress()).String(), valAddrShares1)
txFee3 := suite.evm.TxFee(receipt.TxHash)

valAddrShares2, _ := suite.staking.Delegation(valAddr.String(), contract)
suite.Equal(big.NewInt(0).String(), valAddrShares2.String())

valNewShares, _ := suite.staking.Delegation(sdk.ValAddress(valNew.AccAddress()).String(), contract)
suite.Equal(valAddrShares1, valNewShares)

delBal = suite.QueryBalances(delSigner.AccAddress())
total = delBalance.Add(sdkmath.NewIntFromBigInt(txFee1)).
Add(sdkmath.NewIntFromBigInt(txFee2)).
Add(sdkmath.NewIntFromBigInt(txFee3)).
Add(delBal.AmountOf(fxtypes.DefaultDenom))
suite.Equal(initBalance.String(), total.String())
}

func (suite *IntegrationMultiNodeTest) StakingGrantPrivilege() {
initBalance := sdkmath.NewInt(2000).MulRaw(1e18)
suite.Send(suite.staking.AccAddress(), sdk.NewCoin(fxtypes.DefaultDenom, initBalance))
Expand Down
4 changes: 4 additions & 0 deletions x/evm/precompiles/staking/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func (c *Contract) RequiredGas(input []byte) uint64 {
return DelegateGas
case string(UndelegateMethod.ID):
return UndelegateGas
case string(RedelegateMethod.ID):
return RedelegateGas
case string(WithdrawMethod.ID):
return WithdrawGas
case string(DelegationMethod.ID):
Expand Down Expand Up @@ -86,6 +88,8 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) (ret [
ret, err = c.Delegate(cacheCtx, evm, contract, readonly)
case string(UndelegateMethod.ID):
ret, err = c.Undelegate(cacheCtx, evm, contract, readonly)
case string(RedelegateMethod.ID):
ret, err = c.Redelegation(cacheCtx, evm, contract, readonly)
case string(WithdrawMethod.ID):
ret, err = c.Withdraw(cacheCtx, evm, contract, readonly)
case string(DelegationMethod.ID):
Expand Down
3 changes: 2 additions & 1 deletion x/evm/precompiles/staking/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
const (
StakingTestDelegateName = "delegate"
StakingTestUndelegateName = "undelegate"
StakingTestRedelegateName = "redelegate"
StakingTestWithdrawName = "withdraw"
StakingTestDelegationName = "delegation"
StakingTestDelegationRewardsName = "delegationRewards"
Expand Down Expand Up @@ -64,7 +65,7 @@ func (suite *PrecompileTestSuite) SetupTest() {
require.NoError(suite.T(), err)
suite.signer = helpers.NewSigner(priv)

set, accs, balances := helpers.GenerateGenesisValidator(tmrand.Intn(10)+2, nil)
set, accs, balances := helpers.GenerateGenesisValidator(tmrand.Intn(10)+3, nil)
suite.app = helpers.SetupWithGenesisValSet(suite.T(), set, accs, balances...)

suite.ctx = suite.app.NewContext(false, tmproto.Header{
Expand Down
14 changes: 14 additions & 0 deletions x/evm/precompiles/staking/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,18 @@ var (
abi.Argument{Name: "reward", Type: types.TypeUint256, Indexed: false},
},
)

RedelegateEvent = abi.NewEvent(
RedelegateEventName,
RedelegateEventName,
false,
abi.Arguments{
abi.Argument{Name: "sender", Type: types.TypeAddress, Indexed: true},
abi.Argument{Name: "valSrc", Type: types.TypeString, Indexed: false},
abi.Argument{Name: "valDst", Type: types.TypeString, Indexed: false},
abi.Argument{Name: "shares", Type: types.TypeUint256, Indexed: false},
abi.Argument{Name: "amount", Type: types.TypeUint256, Indexed: false},
abi.Argument{Name: "completionTime", Type: types.TypeUint256, Indexed: false},
},
)
)
3 changes: 3 additions & 0 deletions x/evm/precompiles/staking/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type StakingKeeper interface {
GetAllowance(ctx sdk.Context, valAddr sdk.ValAddress, owner, spender sdk.AccAddress) *big.Int
SetAllowance(ctx sdk.Context, valAddr sdk.ValAddress, owner, spender sdk.AccAddress, shares *big.Int)
HasReceivingRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, valDstAddr sdk.ValAddress) bool
BeginRedelegation(
ctx sdk.Context, delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress, sharesAmount sdk.Dec,
) (completionTime time.Time, err error)
}

type DistrKeeper interface {
Expand Down
Loading