Skip to content

Commit

Permalink
contracts: added docs to Staking.sol
Browse files Browse the repository at this point in the history
  • Loading branch information
CedarMist committed Oct 17, 2023
1 parent 1e02dea commit e94e677
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 26 deletions.
73 changes: 71 additions & 2 deletions contracts/contracts/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ pragma solidity ^0.8.0;

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

/**
* @title Minimal Staking Implementation
*
* This contract implements delegation and undelegation of the native ROSE token
* with validators. It encompasses the Oasis specific staking logic.
*/
contract Staking {
/// Incremented counter to determine receipt IDs
uint64 private lastReceiptId;
Expand Down Expand Up @@ -40,18 +46,40 @@ contract Staking {
uint128 totalAmount;
}

// -------------------------------------------------------------------------

event OnDelegateStart(address indexed who, StakingAddress to, uint256 amount, uint64 receiptId);

event OnDelegateDone(uint64 indexed receiptId, address who, uint128 shares);

event OnUndelegateStart(uint64 indexed receiptId, address who, uint64 epoch, uint128 shares);

// -------------------------------------------------------------------------

constructor() {
// Due to an oddity in the oasis-cbor package, we start at 2**32
// Otherwise uint64 parsing will fail and the message is rejected
lastReceiptId = 4294967296;
}

/**
* Begin or increase delegation by sending an amount of ROSE to the contract.
*
* Delegation will fail if the minimum per-validator amount has not been
* reached, at the time of writing this is 100 ROSE.
*
* Only one delegation can occur per transaction.
*
* @param to Staking address of validator on the consensus level
*/
function delegate(StakingAddress to) public payable returns (uint64) {
// Whatever is sent to the contract is delegated.
require(msg.value < type(uint128).max);

uint128 amount = uint128(msg.value);

lastReceiptId = lastReceiptId + 1;

uint64 receiptId = lastReceiptId;

Subcall.consensusDelegate(to, amount, receiptId);
Expand All @@ -62,20 +90,40 @@ contract Staking {
amount
);

emit OnDelegateStart(msg.sender, to, msg.value, receiptId);

return receiptId;
}

/**
* Retrieve the number of shares received in return for delegation.
*
* The receipt will only be available after the delegate transaction has
* been included in a block. It is necessary to wait for the message to
* reach the consensus layer and be processed to determine the number of
* shares.
*
* @param receiptId Receipt ID previously emitted/returned by `delegate`.
*/
function delegateDone(uint64 receiptId) public returns (uint128 shares) {
PendingDelegation memory pending = pendingDelegations[receiptId];

require(pending.from != address(0), "unknown receipt");

shares = Subcall.consensusTakeReceiptDelegate(receiptId);

emit OnDelegateDone(receiptId, pending.from, shares);

// Remove pending delegation.
delete pendingDelegations[receiptId];
}

/**
* Begin undelegation of a number of shares
*
* @param from Validator which the shares were staked with
* @param shares Number of shares to debond
*/
function undelegate(StakingAddress from, uint128 shares)
public
returns (uint64)
Expand All @@ -87,7 +135,9 @@ contract Staking {
"must have enough delegated shares"
);

uint64 receiptId = lastReceiptId++;
lastReceiptId = lastReceiptId + 1;

uint64 receiptId = lastReceiptId;

Subcall.consensusUndelegate(from, shares, receiptId);

Expand All @@ -104,6 +154,16 @@ contract Staking {
return receiptId;
}

/**
* Process the undelegation step, which returns the end receipt ID and
* the epoch which debonding will finish.
*
* If multiple undelegations to the same validator are processed within
* the same epoch they will have the same `endReceiptId` as they will finish
* unbonding on the same epoch.
*
* @param receiptId Receipt retuned/emitted from `undelegate`
*/
function undelegateStart(uint64 receiptId) public {
PendingUndelegation storage pending = pendingUndelegations[receiptId];

Expand All @@ -117,8 +177,17 @@ contract Staking {
pending.epoch = epoch;

undelegationPools[endReceipt].totalShares += pending.shares;

emit OnUndelegateStart(receiptId, pending.to, epoch, pending.shares);
}

/**
* Finish the undelegation process, transferring the staked ROSE back.
*
* The
*
* @param receiptId returned/emitted from `undelegateStart`
*/
function undelegateDone(uint64 receiptId) public {
PendingUndelegation memory pending = pendingUndelegations[receiptId];

Expand All @@ -141,7 +210,7 @@ contract Staking {

// Compute how much we get from the pool and transfer the amount.
uint256 transferAmount = (uint256(pending.shares) *
uint256(pool.totalAmount)) / pool.totalShares;
uint256(pool.totalAmount)) / uint256(pool.totalShares);

pending.to.transfer(transferAmount);

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

pragma solidity ^0.8.0;

import {Staking,StakingAddress} from "../Staking.sol";

contract StakingTests {
Staking[] public stakers;

constructor (uint n) {
for( uint i = 0; i < n; i++ ) {
stakers.push(new Staking());
}
}

function delegate(StakingAddress[] memory in_validators)
external payable
{
uint amt = msg.value / in_validators.length;

for( uint i = 0; i < in_validators.length; i++ )
{
stakers[i % stakers.length].delegate{value: amt}(in_validators[i]);
}
}
}
83 changes: 59 additions & 24 deletions contracts/test/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,75 @@ import { expect } from 'chai';
import { getRandomValues } from 'crypto';
import * as oasis from '@oasisprotocol/client';
import { Staking, Staking__factory } from '../typechain-types';
import { parseEther } from 'ethers/lib/utils';
import { StakingTests } from "../typechain-types/contracts/tests";
import { formatEther, parseEther } from 'ethers/lib/utils';

async function randomStakingAddress () {
const secretKey = new Uint8Array(32);
getRandomValues(secretKey);
const alice = oasis.signature.NaclSigner.fromSeed(
ethers.utils.arrayify(secretKey),
'this key is not important',
);
const computedPublicKey = ethers.utils.hexlify(
await oasis.staking.addressFromPublicKey(alice.public()),
);
return computedPublicKey;
}

describe('Staking', () => {
let contract: Staking;
let contract: StakingTests;
let stakers: Staking[] = [];
let delegateTargets: string[] = [];

before(async () => {
const factory = (await ethers.getContractFactory(
'Staking',
)) as Staking__factory;
contract = await factory.deploy();

for (let i = 0; i < 10; i++) {
const secretKey = new Uint8Array(32);
getRandomValues(secretKey);

const alice = oasis.signature.NaclSigner.fromSeed(
ethers.utils.arrayify(secretKey),
'this key is not important',
);
const n = 2;
for (let i = 0; i < n; i++) {
delegateTargets.push(await randomStakingAddress());
}

const computedPublicKey = ethers.utils.hexlify(
await oasis.staking.addressFromPublicKey(alice.public()),
);
const factory = await ethers.getContractFactory('StakingTests');
contract = await factory.deploy(n) as StakingTests;

delegateTargets.push(computedPublicKey);
for( let i = 0; i < n; i++ ) {
const staker = Staking__factory.connect(await contract.stakers(i), factory.signer);
stakers.push(staker);
}
});

it('Delegate', async () => {
const tx = await contract.delegate(delegateTargets[0], {
value: parseEther('101'),
it.skip('Delegate', async () => {
// Submit the first delegation
let tx = await stakers[0].delegate(delegateTargets[0], {
value: parseEther('100'),
});
const receipt = await tx.wait();
console.log(receipt);
let receipt = await tx.wait();
let bal = await stakers[0].provider.getBalance(contract.address);
expect(bal).eq(0n);
let args = receipt.events![0].args!;
const firstReceiptId = Number.parseInt(args.receiptId);

// Submit the second delegation
tx = await stakers[0].delegate(delegateTargets[0], {
value: parseEther('100'),
});
receipt = await tx.wait();
bal = await stakers[0].provider.getBalance(contract.address);
expect(bal).eq(0n);

// Verify receipt IDs increment
args = receipt.events![0].args!;
const secondReceiptId = Number.parseInt(args.receiptId);
expect(secondReceiptId).eq(firstReceiptId + 1);
console.log(args);
});

// Note: this will not work!
// multiple delegations in same tx is not supported!
it('Multi Delegate', async () => {
const m = 2;
const d = delegateTargets.slice(0, m);
const tx = await contract.delegate(d, {value: parseEther('200')});
//const receipt = ;
expect(tx.wait()).revertedWithoutReason();
});
});

0 comments on commit e94e677

Please sign in to comment.