Skip to content

Commit

Permalink
feat: precompile redelegate (#151)
Browse files Browse the repository at this point in the history
Co-authored-by: nulnut <[email protected]>
Co-authored-by: fx0x55 <[email protected]>
  • Loading branch information
3 people authored Dec 6, 2023
1 parent ba06308 commit dd699f9
Show file tree
Hide file tree
Showing 19 changed files with 1,109 additions and 10 deletions.
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

0 comments on commit dd699f9

Please sign in to comment.