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

subcall wrapper + unit tests, and staking address generation #170

Merged
merged 15 commits into from
Sep 18, 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
93 changes: 93 additions & 0 deletions contracts/contracts/ConsensusUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import {sha512_256, Sapphire} from "./Sapphire.sol";

/// 21 byte version-prefixed address (1 byte version, 20 bytes truncated digest).
type StakingAddress is bytes21;

/// 32 byte secret key.
type StakingSecretKey is bytes32;

/**
* @title Consensus-level utilities
* @dev Generate Oasis wallets for use with staking at the consensus level
*/
library ConsensusUtils {
/// The unique context for v0 staking account addresses.
/// https://github.com/oasisprotocol/oasis-core/blob/master/go/staking/api/address.go#L16
string private constant ADDRESS_V0_CONTEXT_IDENTIFIER =
CedarMist marked this conversation as resolved.
Show resolved Hide resolved
"oasis-core/address: staking";
uint8 private constant ADDRESS_V0_CONTEXT_VERSION = 0;

/**
* @dev Generate a random Ed25519 wallet for Oasis consensus-layer staking
* @param personalization Optional user-specified entropy
* @return publicAddress Public address of the keypair
* @return secretKey Secret key for the keypair
*/
function generateStakingAddress(bytes memory personalization)
internal
view
returns (StakingAddress publicAddress, StakingSecretKey secretKey)
{
bytes memory sk = Sapphire.randomBytes(32, personalization);

(bytes memory pk, ) = Sapphire.generateSigningKeyPair(
Sapphire.SigningAlg.Ed25519Oasis,
sk
);

publicAddress = StakingAddress.wrap(
_stakingAddressFromPublicKey(bytes32(pk))
);

secretKey = StakingSecretKey.wrap(bytes32(sk));
}

/**
* @dev Derive the staking address from the public key
* @param ed25519publicKey Ed25519 public key
*/
function _stakingAddressFromPublicKey(bytes32 ed25519publicKey)
internal
view
returns (bytes21)
{
return
_addressFromData(
ADDRESS_V0_CONTEXT_IDENTIFIER,
ADDRESS_V0_CONTEXT_VERSION,
abi.encodePacked(ed25519publicKey)
);
}

/**
* @dev Derive an Oasis-style address
* @param contextIdentifier Domain separator
* @param contextVersion Domain version
* @param data Public point of the keypair
*/
function _addressFromData(
string memory contextIdentifier,
uint8 contextVersion,
bytes memory data
) internal view returns (bytes21) {
return
bytes21(
abi.encodePacked(
contextVersion,
bytes20(
sha512_256(
abi.encodePacked(
contextIdentifier,
contextVersion,
data
)
)
)
)
);
}
}
25 changes: 0 additions & 25 deletions contracts/contracts/Sapphire.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ library Sapphire {
0x0100000000000000000000000000000000000101;
address internal constant SHA512 =
0x0100000000000000000000000000000000000102;
address internal constant SUBCALL =
0x0100000000000000000000000000000000000103;

type Curve25519PublicKey is bytes32;
type Curve25519SecretKey is bytes32;
Expand Down Expand Up @@ -223,29 +221,6 @@ library Sapphire {
require(success, "verify: failed");
return abi.decode(v, (bool));
}

/**
* Submit a native message to the Oasis runtime layer
*
* Messages which re-enter the EVM module are forbidden: evm.*
*
* @param method Native message type
* @param body CBOR encoded body
* @return status_code Result of call
* @return data CBOR encoded result
*/
function subcall(string memory method, bytes memory body)
internal
returns (uint64 status_code, bytes memory data)
{
(bool success, bytes memory tmp) = SUBCALL.call(
abi.encode(method, body)
);

require(success, "subcall");

(status_code, data) = abi.decode(tmp, (uint64, bytes));
}
}

/**
Expand Down
168 changes: 168 additions & 0 deletions contracts/contracts/Subcall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import {StakingAddress, StakingSecretKey} from "./ConsensusUtils.sol";

/**
* @title SDK Subcall wrappers
* @dev Interact with Oasis Runtime SDK modules from Sapphire.
*/
library Subcall {
string private constant CONSENSUS_DELEGATE = "consensus.Delegate";
string private constant CONSENSUS_UNDELEGATE = "consensus.Undelegate";
string private constant CONSENSUS_WITHDRAW = "consensus.Withdraw";
string private constant ACCOUNTS_TRANSFER = "accounts.Transfer";

/// Address of the SUBCALL precompile
address internal constant SUBCALL =
0x0100000000000000000000000000000000000103;
CedarMist marked this conversation as resolved.
Show resolved Hide resolved

/// Raised if the underlying subcall precompile does not succeed
error SubcallError();

error ConsensusUndelegateError(uint64 status, string data);

error ConsensusDelegateError(uint64 status, string data);

error ConsensusWithdrawError(uint64 status, string data);

error AccountsTransferError(uint64 status, string data);

/**
* Submit a native message to the Oasis runtime layer.
*
* Messages which re-enter the EVM module are forbidden: evm.*
*
* @param method Native message type
* @param body CBOR encoded body
* @return status Result of call
* @return data CBOR encoded result
*/
function subcall(string memory method, bytes memory body)
internal
returns (uint64 status, bytes memory data)
{
(bool success, bytes memory tmp) = SUBCALL.call(
abi.encode(method, body)
);

if (!success) {
revert SubcallError();
}

(status, data) = abi.decode(tmp, (uint64, bytes));
}

/**
* @dev Generic method to call `{to:address, amount:uint128}`
* @param method Runtime SDK method name ('module.Action')
* @param to Destination address
* @param value Amount specified
* @return status Non-zero on error
* @return data Module name on error
*/
function _subcallWithToAndAmount(
string memory method,
StakingAddress to,
uint128 value
) internal returns (uint64 status, bytes memory data) {
(status, data) = subcall(
method,
abi.encodePacked(
hex"a262",
"to",
hex"55",
to,
hex"66",
"amount",
hex"8250",
value,
hex"40"
)
);
}

/**
* Start the undelegation process of the given number of shares from
* consensus staking account to runtime account.
*
* @param from Consensus address which shares were delegated to
* @param shares Number of shares to withdraw back to us
*/
function consensusUndelegate(StakingAddress from, uint128 shares) internal {
(uint64 status, bytes memory data) = subcall(
CONSENSUS_UNDELEGATE,
abi.encodePacked(
hex"a264",
"from",
hex"55",
from,
hex"66",
"shares",
hex"50",
shares
)
);

if (status != 0) {
revert ConsensusUndelegateError(status, string(data));
}
}

/**
* Delegate native token to consensus level.
*
* @param to Consensus address shares are delegated to
* @param value Native token amount (in wei)
*/
function consensusDelegate(StakingAddress to, uint128 value) internal {
(uint64 status, bytes memory data) = _subcallWithToAndAmount(
CONSENSUS_DELEGATE,
to,
value
);

if (status != 0) {
revert ConsensusDelegateError(status, string(data));
}
}

/**
* Transfer from an account in this runtime to a consensus staking account.
*
* @param to Consensus address which gets the tokens
* @param value Native token amount (in wei)
*/
function consensusWithdraw(StakingAddress to, uint128 value) internal {
(uint64 status, bytes memory data) = _subcallWithToAndAmount(
CONSENSUS_WITHDRAW,
to,
value
);

if (status != 0) {
revert ConsensusWithdrawError(status, string(data));
}
}

/**
* Perform a transfer to another account.
*
* This is equivalent of `payable(to).transfer(value);`
*
* @param to Destination account
* @param value native token amount (in wei)
*/
function accountsTransfer(address to, uint128 value) internal {
(uint64 status, bytes memory data) = _subcallWithToAndAmount(
ACCOUNTS_TRANSFER,
StakingAddress.wrap(bytes21(abi.encodePacked(uint8(0x00), to))),
value
);

if (status != 0) {
revert AccountsTransferError(status, string(data));
}
}
}
35 changes: 33 additions & 2 deletions contracts/contracts/tests/SubcallTests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,47 @@

pragma solidity ^0.8.0;

import {Sapphire} from "../Sapphire.sol";
import {ConsensusUtils, StakingAddress, StakingSecretKey} from "../ConsensusUtils.sol";
import {Subcall} from "../Subcall.sol";

contract SubcallTests {
event SubcallResult(uint64 status, bytes data);

constructor() payable {}

receive() external payable {}

function generateRandomAddress()
external
view
returns (StakingAddress publicKey, StakingSecretKey secretKey)
{
return ConsensusUtils.generateStakingAddress("");
}

function testSubcall(string memory method, bytes memory data) external {
uint64 status;

(status, data) = Sapphire.subcall(method, data);
(status, data) = Subcall.subcall(method, data);

emit SubcallResult(status, data);
}

function testAccountsTransfer(address to, uint128 value) external {
Subcall.accountsTransfer(to, value);
}

function testConsensusDelegate(StakingAddress to, uint128 value) external {
Subcall.consensusDelegate(to, value);
}

function testConsensusUndelegate(StakingAddress to, uint128 value)
external
{
Subcall.consensusUndelegate(to, value);
}

function testConsensusWithdraw(StakingAddress to, uint128 value) external {
Subcall.consensusWithdraw(to, value);
}
}
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nomicfoundation/hardhat-chai-matchers": "^1.0.5",
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@oasisprotocol/sapphire-hardhat": "workspace:^",
"@oasisprotocol/client": "^0.1.1-alpha.2",
"@openzeppelin/contracts": "^4.7.3",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat": "^6.1.3",
Expand All @@ -35,7 +36,6 @@
"@types/node": "^18.7.18",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"bech32": "^2.0.0",
"cborg": "^1.9.5",
"chai": "^4.3.6",
"eslint": "^8.23.1",
Expand Down
Loading
Loading