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

Allow validators to delegate part of their epoch payment #8993

Merged
merged 12 commits into from
Jan 6, 2022
38 changes: 38 additions & 0 deletions packages/protocol/contracts/common/Accounts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

import "./interfaces/IAccounts.sol";

import "../common/FixidityLib.sol";
import "../common/Initializable.sol";
import "../common/interfaces/ICeloVersionedContract.sol";
import "../common/Signatures.sol";
Expand All @@ -19,6 +20,7 @@ contract Accounts is
Initializable,
UsingRegistry
{
using FixidityLib for FixidityLib.Fraction;
using SafeMath for uint256;

struct Signers {
Expand Down Expand Up @@ -57,6 +59,13 @@ contract Accounts is
string metadataURL;
}

struct PaymentDelegation {
// Address that should receive a fraction of validator payments.
address beneficiary;
// Fraction of payment to delegate to `beneficiary`.
FixidityLib.Fraction fraction;
}

mapping(address => Account) internal accounts;
// Maps authorized signers to the account that provided the authorization.
mapping(address => address) public authorizedBy;
Expand All @@ -74,6 +83,9 @@ contract Accounts is
// A per-account list of CIP8 storage roots, bypassing CIP3.
mapping(address => bytes[]) public offchainStorageRoots;

// Optional per-account validator payment delegation information.
mapping(address => PaymentDelegation) internal paymentDelegations;

bytes32 constant ValidatorSigner = keccak256(abi.encodePacked("celo.org/core/validator"));
bytes32 constant AttestationSigner = keccak256(abi.encodePacked("celo.org/core/attestation"));
bytes32 constant VoteSigner = keccak256(abi.encodePacked("celo.org/core/vote"));
Expand Down Expand Up @@ -101,6 +113,7 @@ contract Accounts is
event AccountCreated(address indexed account);
event OffchainStorageRootAdded(address indexed account, bytes url);
event OffchainStorageRootRemoved(address indexed account, bytes url, uint256 index);
event PaymentDelegationSet(address indexed beneficiary, uint256 fraction);

/**
* @notice Sets initialized == true on implementation contracts
Expand Down Expand Up @@ -303,6 +316,31 @@ contract Accounts is
return (concatenated, lengths);
}

/**
* @notice Sets validator payment delegation settings.
* @param beneficiary The address that should receive a portion of vaidator
* payments.
* @param fraction The fraction of the validator's payment that should be
* diverted to `beneficiary` every epoch, given as FixidyLib value. Must not
* be greater than 1.
*/
function setPaymentDelegation(address beneficiary, uint256 fraction) public {
require(isAccount(msg.sender), "Not an account");
FixidityLib.Fraction memory f = FixidityLib.wrap(fraction);
require(f.lte(FixidityLib.fixed1()), "Fraction must not be greater than 1");
paymentDelegations[msg.sender] = PaymentDelegation(beneficiary, f);
emit PaymentDelegationSet(beneficiary, fraction);
}

/**
* @notice Gets validator payment delegation settings.
* @return Beneficiary address and fraction of payment delegated.
*/
function getPaymentDelegation(address account) external view returns (address, uint256) {
PaymentDelegation storage delegation = paymentDelegations[account];
return (delegation.beneficiary, delegation.fraction.unwrap());
}

/**
* @notice Set the indexed signer for a specific role
* @param signer the address to set as default
Expand Down
3 changes: 3 additions & 0 deletions packages/protocol/contracts/common/interfaces/IAccounts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ interface IAccounts {
) external;
function authorizeAttestationSigner(address, uint8, bytes32, bytes32) external;
function createAccount() external returns (bool);

function setPaymentDelegation(address, uint256) external;
function getPaymentDelegation(address) external view returns (address, uint256);
}
10 changes: 9 additions & 1 deletion packages/protocol/contracts/governance/Validators.sol
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,18 @@ contract Validators is
.multiply(validators[account].score)
.multiply(groups[group].slashInfo.multiplier);
uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed();
uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment);
FixidityLib.Fraction memory remainingPayment = FixidityLib.newFixed(
totalPayment.fromFixed().sub(groupPayment)
);
(address beneficiary, uint256 fraction) = getAccounts().getPaymentDelegation(account);
uint256 delegatedPayment = remainingPayment.multiply(FixidityLib.wrap(fraction)).fromFixed();
uint256 validatorPayment = remainingPayment.fromFixed().sub(delegatedPayment);
IStableToken stableToken = getStableToken();
require(stableToken.mint(group, groupPayment), "mint failed to validator group");
require(stableToken.mint(account, validatorPayment), "mint failed to validator account");
if (fraction != 0) {
require(stableToken.mint(beneficiary, delegatedPayment), "mint failed to delegatee");
}
m-chrzan marked this conversation as resolved.
Show resolved Hide resolved
emit ValidatorEpochPaymentDistributed(account, validatorPayment, group, groupPayment);
return totalPayment.fromFixed();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ contract MockStableToken {
}

function mint(address to, uint256 value) external returns (bool) {
require(to != address(0), "0 is a reserved address");
balances[to] = balances[to].add(valueToUnits(value));
_totalSupply = _totalSupply.add(value);
return true;
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/specs/accountsPrivileged.spec
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ definition knownAsNonPrivileged(method f) returns bool = false
|| f.selector == removeDefaultSigner(bytes32).selector
|| f.selector == removeSigner(address,bytes32).selector
|| f.selector == setEip712DomainSeparator().selector
|| f.selector == setPaymentDelegation(address,uint256).selector
;

rule privilegedOperation(method f, address privileged)
Expand Down
52 changes: 51 additions & 1 deletion packages/protocol/test/common/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Address, ensureLeading0x, NULL_ADDRESS } from '@celo/base/lib/address'
import { CeloContractName } from '@celo/protocol/lib/registry-utils'
import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils'
import { assertLogMatches, assertLogMatches2, assertRevert } from '@celo/protocol/lib/test-utils'
import {
assertEqualBN,
assertLogMatches,
assertLogMatches2,
assertRevert,
assertRevertWithReason,
} from '@celo/protocol/lib/test-utils'
import { toFixed } from '@celo/utils/lib/fixidity'
import { parseSolidityStringArray } from '@celo/utils/lib/parsing'
import { authorizeSigner as buildAuthorizeSignerTypedData } from '@celo/utils/lib/typed-data-constructors'
import { generateTypedDataHash } from '@celo/utils/src/sign-typed-data-utils'
Expand Down Expand Up @@ -447,6 +454,49 @@ contract('Accounts', (accounts: string[]) => {
})
})

describe('#setPaymentDelegation', () => {
const beneficiary = accounts[1]
const fraction = toFixed(0.2)
const badFraction = toFixed(1.2)

it('should not be callable by a non-account', async () => {
await assertRevertWithReason(
accountsInstance.setPaymentDelegation(beneficiary, fraction),
'Not an account'
)
})

describe('when an account has been created', () => {
beforeEach(async () => {
await accountsInstance.createAccount()
})

it('should set an address and a fraction', async () => {
await accountsInstance.setPaymentDelegation(beneficiary, fraction)
const [realBeneficiary, realFraction] = await accountsInstance.getPaymentDelegation.call(
accounts[0]
)
assert.equal(realBeneficiary, beneficiary)
assertEqualBN(realFraction, fraction)
})

it('should not allow a fraction greater than 1', async () => {
await assertRevertWithReason(
accountsInstance.setPaymentDelegation(beneficiary, badFraction),
'Fraction must not be greater than 1'
)
})

it('emits a PaymentDelegationSet event', async () => {
const resp = await accountsInstance.setPaymentDelegation(beneficiary, fraction)
assertLogMatches2(resp.logs[0], {
event: 'PaymentDelegationSet',
args: { beneficiary, fraction },
})
})
})
})

describe('#setName', () => {
describe('when the account has not been created', () => {
it('should revert', async () => {
Expand Down
59 changes: 57 additions & 2 deletions packages/protocol/test/governance/validators/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,9 @@ contract('Validators', (accounts: string[]) => {
describe('#distributeEpochPaymentsFromSigner', () => {
const validator = accounts[0]
const group = accounts[1]
const delegatee = accounts[2]
const delegatedFraction = toFixed(0.1)

const maxPayment = new BigNumber(20122394876)
let mockStableToken: MockStableTokenInstance
beforeEach(async () => {
Expand All @@ -2211,6 +2214,8 @@ contract('Validators', (accounts: string[]) => {
// Fast-forward to the next epoch, so that the getMembershipInLastEpoch(validator) == group
await mineToNextEpoch(web3)
await mockLockedGold.addSlasher(accounts[2])

await accountsInstance.setPaymentDelegation(delegatee, delegatedFraction)
})

describe('when the validator score is non-zero', () => {
Expand All @@ -2219,6 +2224,7 @@ contract('Validators', (accounts: string[]) => {
let expectedTotalPayment: BigNumber
let expectedGroupPayment: BigNumber
let expectedValidatorPayment: BigNumber
let expectedDelegatedPayment: BigNumber

beforeEach(async () => {
const uptime = new BigNumber(0.99)
Expand All @@ -2231,7 +2237,11 @@ contract('Validators', (accounts: string[]) => {
expectedGroupPayment = expectedTotalPayment
.times(fromFixed(commission))
.dp(0, BigNumber.ROUND_FLOOR)
expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment)
const remainingPayment = expectedTotalPayment.minus(expectedGroupPayment)
expectedDelegatedPayment = remainingPayment
.times(fromFixed(delegatedFraction))
.dp(0, BigNumber.ROUND_FLOOR)
expectedValidatorPayment = remainingPayment.minus(expectedDelegatedPayment)

await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime))
})
Expand All @@ -2250,6 +2260,34 @@ contract('Validators', (accounts: string[]) => {
assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment)
})

it('should pay the delegatee', async () => {
assertEqualBN(await mockStableToken.balanceOf(delegatee), expectedDelegatedPayment)
})

it('should return the expected total payment', async () => {
assertEqualBN(ret, expectedTotalPayment)
})
})

describe('when the validator and group meet the balance requirements and no payment is delegated', async () => {
beforeEach(async () => {
expectedDelegatedPayment = new BigNumber(0)
expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment)

await accountsInstance.setPaymentDelegation(NULL_ADDRESS, toFixed(0))

ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment)
await validators.distributeEpochPaymentsFromSigner(validator, maxPayment)
})

it('should pay the validator', async () => {
assertEqualBN(await mockStableToken.balanceOf(validator), expectedValidatorPayment)
})

it('should pay the group', async () => {
assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment)
})

it('should return the expected total payment', async () => {
assertEqualBN(ret, expectedTotalPayment)
})
Expand All @@ -2259,6 +2297,7 @@ contract('Validators', (accounts: string[]) => {
let halfExpectedTotalPayment: BigNumber
let halfExpectedGroupPayment: BigNumber
let halfExpectedValidatorPayment: BigNumber
let halfExpectedDelegatedPayment: BigNumber

beforeEach(async () => {
halfExpectedTotalPayment = expectedScore
Expand All @@ -2268,7 +2307,11 @@ contract('Validators', (accounts: string[]) => {
halfExpectedGroupPayment = halfExpectedTotalPayment
.times(fromFixed(commission))
.dp(0, BigNumber.ROUND_FLOOR)
halfExpectedValidatorPayment = halfExpectedTotalPayment.minus(halfExpectedGroupPayment)
const remainingPayment = halfExpectedTotalPayment.minus(halfExpectedGroupPayment)
halfExpectedDelegatedPayment = remainingPayment
.times(fromFixed(delegatedFraction))
.dp(0, BigNumber.ROUND_FLOOR)
halfExpectedValidatorPayment = remainingPayment.minus(halfExpectedDelegatedPayment)

await validators.halveSlashingMultiplier(group, { from: accounts[2] })
ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment)
Expand All @@ -2283,6 +2326,10 @@ contract('Validators', (accounts: string[]) => {
assertEqualBN(await mockStableToken.balanceOf(group), halfExpectedGroupPayment)
})

it('should pay the delegatee only half', async () => {
assertEqualBN(await mockStableToken.balanceOf(delegatee), halfExpectedDelegatedPayment)
})

it('should return the expected total payment', async () => {
assertEqualBN(ret, halfExpectedTotalPayment)
})
Expand All @@ -2306,6 +2353,10 @@ contract('Validators', (accounts: string[]) => {
assertEqualBN(await mockStableToken.balanceOf(group), 0)
})

it('should not pay the delegatee', async () => {
assertEqualBN(await mockStableToken.balanceOf(delegatee), 0)
})

it('should return zero', async () => {
assertEqualBN(ret, 0)
})
Expand All @@ -2329,6 +2380,10 @@ contract('Validators', (accounts: string[]) => {
assertEqualBN(await mockStableToken.balanceOf(group), 0)
})

it('should not pay the delegatee', async () => {
assertEqualBN(await mockStableToken.balanceOf(delegatee), 0)
})

it('should return zero', async () => {
assertEqualBN(ret, 0)
})
Expand Down