-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into golang-1.21.12
- Loading branch information
Showing
11 changed files
with
1,456 additions
and
14 deletions.
There are no files selected for viewing
570 changes: 570 additions & 0 deletions
570
abi-bindings/go/Utilities/ValidatorSetSig/ValidatorSetSig.go
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// (c) 2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package validatorsetsig | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/ava-labs/subnet-evm/accounts/abi" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
var validatorSetSigMessageType abi.Type | ||
|
||
func init() { | ||
// Create an ABI binding for ValidatorSetSigMessage, defined in ValidatorSetSig.sol | ||
// abigen does not support ABI bindings for standalone structs, only methods and events, | ||
// so we must manually keep this up-to-date with the struct defined in the contract. | ||
var err error | ||
validatorSetSigMessageType, err = abi.NewType("tuple", "struct Overloader.F", []abi.ArgumentMarshaling{ | ||
{Name: "targetBlockchainID", Type: "bytes32"}, | ||
{Name: "validatorSetSigAddress", Type: "address"}, | ||
{Name: "targetContractAddress", Type: "address"}, | ||
{Name: "nonce", Type: "uint256"}, | ||
{Name: "payload", Type: "bytes"}, | ||
}) | ||
if err != nil { | ||
panic(fmt.Sprintf("failed to create ValidatorSetSigMessage ABI type: %v", err)) | ||
} | ||
} | ||
|
||
func PackValidatorSetSigWarpPayload(message ValidatorSetSigMessage) ([]byte, error) { | ||
args := abi.Arguments{ | ||
{ | ||
Name: "validatorSetSigMessage", | ||
Type: validatorSetSigMessageType, | ||
}, | ||
} | ||
return args.Pack(message) | ||
} | ||
|
||
func UnpackValidatorSetSigWarpPayload(messageBytes []byte) (ValidatorSetSigMessage, error) { | ||
args := abi.Arguments{ | ||
{ | ||
Name: "validatorSetSigMessage", | ||
Type: validatorSetSigMessageType, | ||
}, | ||
} | ||
unpacked, err := args.Unpack(messageBytes) | ||
fmt.Println("unpacked: ", unpacked) | ||
if err != nil { | ||
return ValidatorSetSigMessage{}, fmt.Errorf("failed to unpack to ValidatorSetSigMessage with err: %v", err) | ||
} | ||
type validatorSetSigWarpPayload struct { | ||
ValidatorSetSigMessage ValidatorSetSigMessage `json:"validatorSetSigMessage"` | ||
} | ||
var payload validatorSetSigWarpPayload | ||
err = args.Copy(&payload, unpacked) | ||
if err != nil { | ||
return ValidatorSetSigMessage{}, err | ||
} | ||
return payload.ValidatorSetSigMessage, nil | ||
} | ||
|
||
// PackExecuteCall packs the input to form a call to the executeCall function | ||
func PackExecuteCall(messageIndex uint32) ([]byte, error) { | ||
abi, err := ValidatorSetSigMetaData.GetAbi() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to get abi") | ||
} | ||
return abi.Pack("executeCall", messageIndex) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// (c) 2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package validatorsetsig | ||
|
||
import ( | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/ava-labs/avalanchego/ids" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestPackUnpackValidatorSetSigMessage(t *testing.T) { | ||
msg := ValidatorSetSigMessage{ | ||
TargetBlockchainID: ids.ID{1, 2, 3, 4}, | ||
ValidatorSetSigAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), | ||
TargetContractAddress: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234568"), | ||
Nonce: big.NewInt(1), | ||
Payload: []byte{1, 2, 3, 4}, | ||
} | ||
b, err := PackValidatorSetSigWarpPayload(msg) | ||
require.NoError(t, err) | ||
|
||
unpackedMsg, err := UnpackValidatorSetSigWarpPayload(b) | ||
require.NoError(t, err) | ||
|
||
require.Equal(t, msg, unpackedMsg) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
## Utility Contracts | ||
|
||
### ValidatorSetSig Contract | ||
|
||
This contract provides an alternative to traditional multi-signature contracts where instead of requiring signatures from `K` of `N` pre-specified signers, an aggregate signature is required from a quorum of the current validators for a given blockchain. | ||
|
||
The contract leverages off-chain [Avalanche Warp Messages](https://docs.avax.network/build/cross-chain/awm/overview), which are manually approved for signing by a chain's validators. It requires these messages to have the source address set to the zero address to enforce this, since on-chain warp messages cannot have zero source address. | ||
|
||
Note: | ||
1. The blockchain validating the message may or may not be the same chain where the target contract and the `ValidatorSetSig` contract are deployed. | ||
2. [Off-Chain Warp messages](https://github.com/ava-labs/subnet-evm/issues/729) are Warp messages that validators can include in their config to indicate that they are willing to sign them even though they are not a result of on-chain activity. | ||
|
||
#### Creating a valid Off-chain Warp Message for interaction with the ValidatorSetSig Contract | ||
|
||
1. ABI-encode a `ValidatorSetSigMessage` as defined in `ValidatorSetSig.sol` | ||
2. OPTIONAL: call `validateMessage` view method of the intended contract to confirm that the message has correct fields, including the nonce. | ||
3. Pack that as the payload of an [AddressedCall](https://github.com/ava-labs/avalanchego/blob/0c4efd743e1d737f4e8970d0e0ebf229ea44406c/vms/platformvm/warp/payload/addressed_call.go#L15) Warp Message format. Note that the `SourceAddress` field has to be set to the zero address. | ||
4. Pack the `AddressedCall` as the payload of the [UnsignedWarpMessage](https://github.com/ava-labs/avalanchego/blob/f17ea6a7ab4036c41b693e47b94d8f0c81cb69ec/vms/platformvm/warp/unsigned_message.go#L14). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// (c) 2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
// SPDX-License-Identifier: Ecosystem | ||
|
||
pragma solidity 0.8.18; | ||
|
||
import { | ||
WarpMessage, | ||
IWarpMessenger | ||
} from "@avalabs/[email protected]/contracts/interfaces/IWarpMessenger.sol"; | ||
import {ReentrancyGuard} from "@openzeppelin/[email protected]/security/ReentrancyGuard.sol"; | ||
|
||
/** | ||
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. | ||
* DO NOT USE THIS CODE IN PRODUCTION. | ||
*/ | ||
|
||
/** | ||
* @dev Message format for the WarpMessage payload to be forwarded to the target contract | ||
* | ||
* targetBlockchainID: Blockchain ID of the chain the message is intended for | ||
* validatorSetSigAddress: Address of the ValidatorSetSig contract this message is intended for | ||
* targetContractAddress: Address of the contract that the payload should be forwarded to | ||
* nonce: Unique nonce for the target contract address to provide replay protection | ||
* payload: Payload to be forwarded to the target contract. Usually ABI encoded function call with parameters. | ||
*/ | ||
struct ValidatorSetSigMessage { | ||
bytes32 targetBlockchainID; | ||
address validatorSetSigAddress; | ||
address targetContractAddress; | ||
uint256 nonce; | ||
bytes payload; | ||
} | ||
|
||
/** | ||
* @dev Contract that verifies that a set threshold of validators from a given blockchainID | ||
* have signed an off-chain Warp message and forwards the payload to the target contract specified in the ValidatorSetSigMessage. | ||
* The threshold itself is set by the validator themselves in their Warp configs: | ||
* https://github.com/ava-labs/subnet-evm/blob/6c018f89339f3d381909e02013f002f234dc7ae3/precompile/contracts/warp/config.go#L50 | ||
* | ||
* This is intended to be used for safe off-chain governance of enabled contracts. An example use case would be | ||
* to deploy an `Ownable` target contract that is owned by an instance of this contract and adding the | ||
* `onlyOwner` modifier to the functions that should be governed. | ||
* | ||
* @custom:security-contact https://github.com/ava-labs/teleporter/blob/main/SECURITY.md | ||
*/ | ||
contract ValidatorSetSig is ReentrancyGuard { | ||
/** | ||
* @notice A blockchain ID whose validators need to sign the message for it to be considered valid. | ||
*/ | ||
bytes32 public immutable validatorBlockchainID; | ||
|
||
/** | ||
* @dev Tracks nonces for messages sent to a specific contract address to provide replay protection. | ||
* The target contract address is used as the key instead of keeping a global nonce to prevent | ||
* one party from consuming a global nonce that another was intending to use | ||
* in case a single instance of ValidatorSetSig contract is used to manage multiple downstream contracts. | ||
*/ | ||
mapping(address targetContractAddress => uint256 nonce) public nonces; | ||
|
||
/** | ||
* @notice Address that the off-chain Warp message sets as the "source" address. | ||
* @dev The address is not owned by any EOA or smart contract account, so it | ||
* cannot possibly be the source address of any other Warp message emitted by the VM. | ||
*/ | ||
address public constant VALIDATORS_SOURCE_ADDRESS = address(0); | ||
|
||
/** | ||
* @notice The blockchain ID of the chain the contract is deployed on. | ||
*/ | ||
bytes32 public immutable blockchainID; | ||
|
||
/** | ||
* @notice Warp precompile used for sending and receiving Warp messages. | ||
*/ | ||
IWarpMessenger public constant WARP_MESSENGER = | ||
IWarpMessenger(0x0200000000000000000000000000000000000005); | ||
|
||
/** | ||
* @notice Emited when the payload is successfully delivered to the target contract. | ||
*/ | ||
event Delivered(address indexed targetContractAddress, uint256 indexed nonce); | ||
|
||
constructor(bytes32 validatorBlockchainID_) { | ||
validatorBlockchainID = validatorBlockchainID_; | ||
blockchainID = WARP_MESSENGER.getBlockchainID(); | ||
} | ||
|
||
function executeCall(uint32 messageIndex) external nonReentrant { | ||
// Get the WarpMessage from the WarpMessenger precompile and verify that it is valid | ||
(WarpMessage memory message, bool valid) = | ||
WARP_MESSENGER.getVerifiedWarpMessage(messageIndex); | ||
require(valid, "ValidatorSetSig: invalid warp message"); | ||
|
||
// Ensure that the sourceChainID of the validator set that signed the message is the authorized one | ||
require( | ||
message.sourceChainID == validatorBlockchainID, "ValidatorSetSig: invalid sourceChainID" | ||
); | ||
// Ensure that the originSenderAddress is the zero address to prevent on-chain messages from being forwarded | ||
require( | ||
message.originSenderAddress == VALIDATORS_SOURCE_ADDRESS, | ||
"ValidatorSetSig: non-zero originSenderAddress" | ||
); | ||
|
||
ValidatorSetSigMessage memory validatorSetSigMessage = | ||
abi.decode(message.payload, (ValidatorSetSigMessage)); | ||
|
||
validateMessage(validatorSetSigMessage); | ||
|
||
nonces[validatorSetSigMessage.targetContractAddress] = validatorSetSigMessage.nonce + 1; | ||
|
||
// We don't need to protect against return bomb vectors below here since the caller is expected to have full control over the contract called. | ||
(bool success,) = | ||
// solhint-disable-next-line avoid-low-level-calls | ||
validatorSetSigMessage.targetContractAddress.call(validatorSetSigMessage.payload); | ||
|
||
// Use require to revert the transaction if the call fails. This is to prevent consuming the nonce if the call fails due to out of gas | ||
// and requiring re-signing of the message with a new nonce. | ||
require(success, "ValidatorSetSig: call failed"); | ||
emit Delivered(validatorSetSigMessage.targetContractAddress, validatorSetSigMessage.nonce); | ||
} | ||
|
||
function validateMessage(ValidatorSetSigMessage memory message) public view { | ||
require( | ||
message.targetBlockchainID == blockchainID, | ||
"ValidatorSetSig: invalid targetBlockchainID" | ||
); | ||
require( | ||
message.validatorSetSigAddress == address(this), | ||
"ValidatorSetSig: invalid validatorSetSigAddress" | ||
); | ||
require( | ||
nonces[message.targetContractAddress] == message.nonce, "ValidatorSetSig: invalid nonce" | ||
); | ||
} | ||
} |
Oops, something went wrong.