Skip to content

Commit

Permalink
[MULTISIG] Storing Alias definitions in Smart Contract state
Browse files Browse the repository at this point in the history
  • Loading branch information
Paweł Nowosielski committed Apr 13, 2023
1 parent 656452d commit b355dc3
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 5 deletions.
17 changes: 17 additions & 0 deletions contracts/camino_smart_contracts.go
Original file line number Diff line number Diff line change
@@ -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())
}
25 changes: 24 additions & 1 deletion contracts/camino_smart_contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 (
Expand Down Expand Up @@ -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())
}
45 changes: 41 additions & 4 deletions plugin/evm/import_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}
99 changes: 99 additions & 0 deletions plugin/evm/import_tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit b355dc3

Please sign in to comment.