From 46ac37b921aa6202dc64575268f01687766962ed Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Fri, 5 Nov 2021 20:21:28 +0100 Subject: [PATCH 1/7] Add payment delegation info --- .../protocol/contracts/common/Accounts.sol | 38 ++++++++++++++ packages/protocol/test/common/accounts.ts | 52 ++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 5406584e211..f01a7bdb93e 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -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"; @@ -19,6 +20,7 @@ contract Accounts is Initializable, UsingRegistry { + using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; struct Signers { @@ -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; @@ -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")); @@ -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 @@ -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.lt(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 diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 29ae6129ad3..30602efe624 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -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' @@ -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 () => { From 3da49139a505cf7542cc4ee3ebb0cc69a82cab8c Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Fri, 19 Nov 2021 16:58:19 +0100 Subject: [PATCH 2/7] Add new functions to Accounts interface --- packages/protocol/contracts/common/interfaces/IAccounts.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index 3f10095a796..b32d7ac98bb 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -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); } From 91a03918247c623dc9f31efa3363f88eff7894e8 Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Fri, 19 Nov 2021 16:59:16 +0100 Subject: [PATCH 3/7] Transfer fraction of payment to delegatee --- .../contracts/governance/Validators.sol | 8 ++++- .../test/governance/validators/validators.ts | 35 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 361c1e392ec..e8f5b358dbe 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -515,10 +515,16 @@ 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"); + require(stableToken.mint(beneficiary, delegatedPayment), "mint failed to delegatee"); emit ValidatorEpochPaymentDistributed(account, validatorPayment, group, groupPayment); return totalPayment.fromFixed(); } else { diff --git a/packages/protocol/test/governance/validators/validators.ts b/packages/protocol/test/governance/validators/validators.ts index 1fd084741c8..0894eccc29a 100644 --- a/packages/protocol/test/governance/validators/validators.ts +++ b/packages/protocol/test/governance/validators/validators.ts @@ -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 () => { @@ -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', () => { @@ -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) @@ -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)) }) @@ -2250,6 +2260,10 @@ 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) }) @@ -2259,6 +2273,7 @@ contract('Validators', (accounts: string[]) => { let halfExpectedTotalPayment: BigNumber let halfExpectedGroupPayment: BigNumber let halfExpectedValidatorPayment: BigNumber + let halfExpectedDelegatedPayment: BigNumber beforeEach(async () => { halfExpectedTotalPayment = expectedScore @@ -2268,7 +2283,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) @@ -2283,6 +2302,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) }) @@ -2306,6 +2329,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) }) @@ -2329,6 +2356,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) }) From f857af33e5202ef99b5e31760b44fe248c5933c0 Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Mon, 29 Nov 2021 16:35:59 +0100 Subject: [PATCH 4/7] Add setPaymentDelegation to non privileged operations --- packages/protocol/specs/accountsPrivileged.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/specs/accountsPrivileged.spec b/packages/protocol/specs/accountsPrivileged.spec index a8122fd7b21..4719f2a9690 100644 --- a/packages/protocol/specs/accountsPrivileged.spec +++ b/packages/protocol/specs/accountsPrivileged.spec @@ -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().selector ; rule privilegedOperation(method f, address privileged) From d234055cfc5811680203c766f5fa484841a5277f Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Thu, 2 Dec 2021 17:23:00 +0100 Subject: [PATCH 5/7] Fix case when there is no delegated payment --- .../contracts/governance/Validators.sol | 4 +++- .../stability/test/MockStableToken.sol | 1 + .../test/governance/validators/validators.ts | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index e8f5b358dbe..5c5d9381cb7 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -524,7 +524,9 @@ contract Validators is IStableToken stableToken = getStableToken(); require(stableToken.mint(group, groupPayment), "mint failed to validator group"); require(stableToken.mint(account, validatorPayment), "mint failed to validator account"); - require(stableToken.mint(beneficiary, delegatedPayment), "mint failed to delegatee"); + if (fraction != 0) { + require(stableToken.mint(beneficiary, delegatedPayment), "mint failed to delegatee"); + } emit ValidatorEpochPaymentDistributed(account, validatorPayment, group, groupPayment); return totalPayment.fromFixed(); } else { diff --git a/packages/protocol/contracts/stability/test/MockStableToken.sol b/packages/protocol/contracts/stability/test/MockStableToken.sol index 8200d36d299..fe5e1244b70 100644 --- a/packages/protocol/contracts/stability/test/MockStableToken.sol +++ b/packages/protocol/contracts/stability/test/MockStableToken.sol @@ -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; diff --git a/packages/protocol/test/governance/validators/validators.ts b/packages/protocol/test/governance/validators/validators.ts index 0894eccc29a..cacefe887f4 100644 --- a/packages/protocol/test/governance/validators/validators.ts +++ b/packages/protocol/test/governance/validators/validators.ts @@ -2269,6 +2269,30 @@ contract('Validators', (accounts: string[]) => { }) }) + 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) + }) + }) + describe('when slashing multiplier is halved', () => { let halfExpectedTotalPayment: BigNumber let halfExpectedGroupPayment: BigNumber From e248bd845910600fafe91004a919b79d67c19a4f Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Fri, 3 Dec 2021 01:50:15 +0100 Subject: [PATCH 6/7] Fix function signature --- packages/protocol/specs/accountsPrivileged.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/specs/accountsPrivileged.spec b/packages/protocol/specs/accountsPrivileged.spec index 4719f2a9690..7331007ccb5 100644 --- a/packages/protocol/specs/accountsPrivileged.spec +++ b/packages/protocol/specs/accountsPrivileged.spec @@ -24,7 +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().selector + || f.selector == setPaymentDelegation(address,uint256).selector ; rule privilegedOperation(method f, address privileged) From 0fea04630b9c7d4920b0fbe0ffe53e3438a0e9db Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Thu, 6 Jan 2022 21:31:38 +0100 Subject: [PATCH 7/7] Change lt to lte --- packages/protocol/contracts/common/Accounts.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index f01a7bdb93e..28d3c604baf 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -327,7 +327,7 @@ contract Accounts is function setPaymentDelegation(address beneficiary, uint256 fraction) public { require(isAccount(msg.sender), "Not an account"); FixidityLib.Fraction memory f = FixidityLib.wrap(fraction); - require(f.lt(FixidityLib.fixed1()), "Fraction must not be greater than 1"); + require(f.lte(FixidityLib.fixed1()), "Fraction must not be greater than 1"); paymentDelegations[msg.sender] = PaymentDelegation(beneficiary, f); emit PaymentDelegationSet(beneficiary, fraction); }