From b355dc30dc3473500755f672ee777795cbcdd2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Thu, 13 Apr 2023 10:53:02 +0200 Subject: [PATCH] [MULTISIG] Storing Alias definitions in Smart Contract state --- contracts/camino_smart_contracts.go | 17 ++++ contracts/camino_smart_contracts_test.go | 25 +++++- plugin/evm/import_tx.go | 45 ++++++++++- plugin/evm/import_tx_test.go | 99 ++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 contracts/camino_smart_contracts.go diff --git a/contracts/camino_smart_contracts.go b/contracts/camino_smart_contracts.go new file mode 100644 index 0000000000..6d70667475 --- /dev/null +++ b/contracts/camino_smart_contracts.go @@ -0,0 +1,17 @@ +package contracts + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func NextSlot(addr common.Hash) common.Hash { + bigAddr := new(big.Int).Add(addr.Big(), common.Big1) + return common.BigToHash(bigAddr) +} + +func EntryAddress(address common.Address, slot int64) common.Hash { + return crypto.Keccak256Hash(address.Hash().Bytes(), common.BigToHash(big.NewInt(slot)).Bytes()) +} diff --git a/contracts/camino_smart_contracts_test.go b/contracts/camino_smart_contracts_test.go index 89b2cdd877..18344e8e77 100644 --- a/contracts/camino_smart_contracts_test.go +++ b/contracts/camino_smart_contracts_test.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/coreth/accounts/abi/bind/backends" "github.com/ava-labs/coreth/accounts/keystore" "github.com/ava-labs/coreth/consensus/dummy" + admin "github.com/ava-labs/coreth/contracts/build_contracts/admin/src" "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/rawdb" "github.com/ava-labs/coreth/core/state" @@ -32,7 +33,7 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/vmerrs" - admin "github.com/ava-labs/coreth/contracts/build_contracts/admin/src" + "github.com/stretchr/testify/require" ) var ( @@ -687,3 +688,25 @@ func addAndVerifyRoles(t *testing.T, adminSession admin.BuildSession, sim *backe return roles } + +func TestStorageSlotIndices(t *testing.T) { + // See article about storing solidity structures https://c4t.atlassian.net/wiki/spaces/TECH/pages/315228161/Extending+Multisig+support+to+other+chains+X+C + const ( + expectedAliasThresholdSlot = "0x02d8eabfe500216f0da458b4ce6732047b68f7e037b3e2d7313765a12e1ff7fc" + expectedAliasCGroupArrayLengthSlot = "0x02d8eabfe500216f0da458b4ce6732047b68f7e037b3e2d7313765a12e1ff7fd" + expectedAliasCGroupArraySlot = "0xba536a6eed5d97c805d767eb1aaffd73a6446dbca7494685f2c6f3bdc1fd2777" + ) + var ( + aliasAdddress = common.HexToAddress("0x010000000000000000000000000000000000000e") + contractMappingSlot = int64(0) + ) + + aliasThresholdSlot := EntryAddress(aliasAdddress, contractMappingSlot) + require.Equal(t, expectedAliasThresholdSlot, aliasThresholdSlot.String()) + + aliasArrayLenSlot := NextSlot(aliasThresholdSlot) + require.Equal(t, expectedAliasCGroupArrayLengthSlot, aliasArrayLenSlot.String()) + + aliasArrauSlot := crypto.Keccak256Hash(aliasArrayLenSlot.Bytes()) + require.Equal(t, expectedAliasCGroupArraySlot, aliasArrauSlot.String()) +} diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 4853f0cdd6..282dd86d27 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -8,6 +8,7 @@ import ( "fmt" "math/big" + "github.com/ava-labs/coreth/contracts" "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/params" @@ -20,18 +21,22 @@ import ( "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/multisig" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" ) var ( - _ UnsignedAtomicTx = &UnsignedImportTx{} - _ secp256k1fx.UnsignedTx = &UnsignedImportTx{} - errImportNonAVAXInputBanff = errors.New("import input cannot contain non-AVAX in Banff") - errImportNonAVAXOutputBanff = errors.New("import output cannot contain non-AVAX in Banff") + _ UnsignedAtomicTx = &UnsignedImportTx{} + _ secp256k1fx.UnsignedTx = &UnsignedImportTx{} + errImportNonAVAXInputBanff = errors.New("import input cannot contain non-AVAX in Banff") + errImportNonAVAXOutputBanff = errors.New("import output cannot contain non-AVAX in Banff") + multisigAliasContractAddress = common.HexToAddress("0x010000000000000000000000000000000000000e") + multisigAliasMappingSlot = int64(0) ) // UnsignedImportTx is an unsigned ImportTx @@ -47,6 +52,8 @@ type UnsignedImportTx struct { ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` // Outputs Outs []EVMOutput `serialize:"true" json:"outputs"` + // MultisigAliases are filled on SemanticVerify where SharedMemory data is fetched and are used in EVMStateTransfer + MultisigAliases []*multisig.AliasWithNonce `serialize:"false"` } // InputUTXOs returns the UTXOIDs of the imported funds @@ -301,6 +308,13 @@ func (utx *UnsignedImportTx) SemanticVerify( } } + utx.MultisigAliases = make([]*multisig.AliasWithNonce, len(*aliasSet)) + i := 0 + for _, alias := range *aliasSet { + utx.MultisigAliases[i] = alias + i++ + } + return vm.conflicts(utx.InputUTXOs(), parent) } @@ -487,5 +501,28 @@ func (utx *UnsignedImportTx) EVMStateTransfer(ctx *snow.Context, state *state.St state.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount) } } + for _, alias := range utx.MultisigAliases { + setAliasInState(state, alias) + } return nil } + +func setAliasInState(state *state.StateDB, alias *multisig.AliasWithNonce) { + aliasIDAddress := common.BytesToAddress(alias.ID.Bytes()) + owners := alias.Owners.(*secp256k1fx.OutputOwners) + + aliasThresholdSlot := contracts.EntryAddress(aliasIDAddress, multisigAliasMappingSlot) + aliasThresholdBig := big.NewInt(int64(owners.Threshold)) + state.SetState(multisigAliasContractAddress, aliasThresholdSlot, common.BigToHash(aliasThresholdBig)) + + aliasArrayLengthSlot := contracts.NextSlot(aliasThresholdSlot) + aliasArrayLengthBig := big.NewInt(int64(len(owners.Addrs))) + state.SetState(multisigAliasContractAddress, aliasArrayLengthSlot, common.BigToHash(aliasArrayLengthBig)) + + aliasArrayElemSlot := ethCrypto.Keccak256Hash(aliasArrayLengthSlot.Bytes()) + for _, owner := range owners.Addrs { + ownerAddress := common.BytesToAddress(owner.Bytes()) + state.SetState(multisigAliasContractAddress, aliasArrayElemSlot, ownerAddress.Hash()) + aliasArrayElemSlot = contracts.NextSlot(aliasArrayElemSlot) + } +} diff --git a/plugin/evm/import_tx_test.go b/plugin/evm/import_tx_test.go index d1bf9549fd..3e867c0b26 100644 --- a/plugin/evm/import_tx_test.go +++ b/plugin/evm/import_tx_test.go @@ -7,6 +7,7 @@ import ( "math/big" "testing" + "github.com/ava-labs/coreth/contracts" "github.com/ava-labs/coreth/params" "github.com/ava-labs/avalanchego/chains/atomic" @@ -21,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" ) // createImportTxOptions adds a UTXO to shared memory and generates a list of import transactions sending this UTXO @@ -1378,6 +1380,103 @@ func TestImportTxEVMStateTransfer(t *testing.T) { } }, }, + "Multisig UTXO": { + setup: func(t *testing.T, vm *VM, sharedMemory *atomic.Memory) *Tx { + txID := ids.GenerateTestID() + amount := uint64(3) + aliasID := ids.ShortID{1, 2, 3, 4, 5} + memberAddr := testKeys[0].Address() + msigAlias := &multisig.AliasWithNonce{ + Alias: multisig.Alias{ + ID: aliasID, + Owners: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{memberAddr}, + }, + }, + } + utxo := &avax.UTXOWithMSig{ + UTXO: avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 0, + }, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{aliasID}, + }, + }, + }, + Aliases: []verify.State{msigAlias}, + } + err := putUTXOToSharedMemory(sharedMemory, vm.ctx.XChainID, vm.ctx.ChainID, [][]byte{}, utxo) + if err != nil { + t.Fatal(err) + } + + tx := &Tx{UnsignedAtomicTx: &UnsignedImportTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + SourceChain: vm.ctx.XChainID, + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: utxo.UTXOID, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: amount, + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + }}, + Outs: []EVMOutput{{ + Address: testEthAddrs[0], + Amount: amount, + AssetID: vm.ctx.AVAXAssetID, + }}, + }} + if err := tx.Sign(vm.codec, [][]*crypto.PrivateKeySECP256K1R{{testKeys[0]}}); err != nil { + t.Fatal(err) + } + return tx + }, + checkState: func(t *testing.T, vm *VM) { + msigContractAddress := common.HexToAddress("0x010000000000000000000000000000000000000e") + aliasAddress := common.HexToAddress("0x0102030405000000000000000000000000000000") + lastAcceptedBlock := vm.LastAcceptedBlockInternal().(*Block) + + sdb, err := vm.blockChain.StateAt(lastAcceptedBlock.ethBlock.Root()) + if err != nil { + t.Fatal(err) + } + + avaxBalance := sdb.GetBalance(testEthAddrs[0]) + expectedBalance := new(big.Int).Mul(big.NewInt( /*amount=*/ 3), x2cRate) + + if avaxBalance.Cmp(expectedBalance) != 0 { + t.Fatalf("Expected AVAX balance to be %d, found balance: %d", x2cRate, avaxBalance) + } + + // check multisig alias is stored in the SC's state + aliasSlot := contracts.EntryAddress(aliasAddress, 0) + threshold := sdb.GetState(msigContractAddress, aliasSlot).Big() + if threshold.Cmp(common.Big1) != 0 { + t.Fatalf("Expected threshold to be 1, found: %d", threshold) + } + lenSlot := contracts.NextSlot(aliasSlot) + length := sdb.GetState(msigContractAddress, lenSlot).Big() + if length.Cmp(common.Big1) != 0 { + t.Fatalf("Expected length to be 1, found: %d", length) + } + + addressSlot := ethCrypto.Keccak256Hash(lenSlot.Bytes()) + address := common.BytesToAddress(sdb.GetState(msigContractAddress, addressSlot).Bytes()) + expectedMemberAddress := common.BytesToAddress(testKeys[0].Address().Bytes()) + if expectedMemberAddress != address { + t.Fatalf("Expected member address to be %s, found: %s", expectedMemberAddress, address) + } + }, + }, } for name, test := range tests {