Skip to content

Commit

Permalink
feat: pay for slashing by burning operator's self-delegation (#657)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtakalai authored Oct 2, 2023
1 parent a3a609c commit 7af3930
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,24 @@ contract Operator is Initializable, ERC2771ContextUpgradeable, IERC677Receiver,
if (indexOfSponsorships[sponsorship] == 0) {
revert NotMyStakedSponsorship();
}

// operator pays for slashing by undelegating worth slashing
uint amountOperatorTokens = moduleCall(address(exchangeRatePolicy), abi.encodeWithSelector(exchangeRatePolicy.operatorTokenToDataInverse.selector, amountSlashed));
_burn(owner, min(balanceOf(owner), amountOperatorTokens));
emit BalanceUpdate(owner, balanceOf(owner), totalSupply());

// operator value is decreased by the slashed amount => exchange rate doesn't change (unless the operator ran out of tokens)
slashedIn[sponsorship] += amountSlashed;
totalSlashedInSponsorshipsWei += amountSlashed;

emit StakeUpdate(sponsorship, stakedInto[sponsorship] - slashedIn[sponsorship]);
emit OperatorValueUpdate(totalStakedIntoSponsorshipsWei - totalSlashedInSponsorshipsWei, token.balanceOf(address(this)));
}

function min(uint a, uint b) internal pure returns (uint) {
return a < b ? a : b;
}

function onKick(uint, uint receivedPayoutWei) external {
Sponsorship sponsorship = Sponsorship(_msgSender());
if (indexOfSponsorships[sponsorship] == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract DefaultDelegationPolicy is IDelegationPolicy, Operator {
* @dev - the first delegation must be self-delegation since at first balance(owner) == 0
* @dev - if minimumSelfDelegationFraction == 0, then any delegations are fine, AS LONG AS the owner has some tokens
* @param delegator The address of the delegator
*/
*/
function onDelegate(address delegator) external {
// owner can always add delegation, even if for some reason the self-delegation requirement is violated (possibly the limit was changed)
if (delegator == owner) { return; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,72 @@ describe("Operator contract", (): void => {

describe("Kick/slash handler", () => {

it("burns operator's tokens on slashing", async function(): Promise<void> {
const { token } = sharedContracts
await setTokens(operatorWallet, "1000")
await setTokens(delegator, "1000")

const { operator } = await deployOperator(operatorWallet)
await (await token.connect(operatorWallet).transferAndCall(operator.address, parseEther("1000"), "0x")).wait()
await (await token.connect(delegator).transferAndCall(operator.address, parseEther("1000"), "0x")).wait()
const sponsorship = await deploySponsorship(sharedContracts, {}, [], [], undefined, undefined, testKickPolicy)

await (await operator.stake(sponsorship.address, parseEther("1000"))).wait()

const balanceBefore = await operator.balanceOf(operatorWallet.address)
const balanceInDataBefore = await operator.balanceInData(operatorWallet.address)
const delegationInDataBefore = await operator.balanceInData(delegator.address)
await (await sponsorship.connect(admin).flag(operator.address, "")).wait() // TestKickPolicy slashes 10 ether without kicking
const balanceAfter = await operator.balanceOf(operatorWallet.address)
const balanceInDataAfter = await operator.balanceInData(operatorWallet.address)
const delegationInDataAfter = await operator.balanceInData(delegator.address)

// operator's tokens are burned
expect(balanceBefore).to.equal(parseEther("1000"))
expect(balanceInDataBefore).to.equal(parseEther("1000"))
expect(balanceAfter).to.equal(parseEther("990"))
expect(balanceInDataAfter).to.equal(parseEther("990"))

// DATA value held by delegator doesn't change
expect(delegationInDataBefore).to.equal(parseEther("1000"))
expect(delegationInDataAfter).to.equal(parseEther("1000"))
})

it("if operator runs out of tokens, slashing will reduce the delegator' value", async function(): Promise<void> {
const { token } = sharedContracts
await setTokens(operatorWallet, "1000")
await setTokens(delegator, "1000")

const { operator } = await deployOperator(operatorWallet)
await (await token.connect(operatorWallet).transferAndCall(operator.address, parseEther("1"), "0x")).wait()
await (await token.connect(delegator).transferAndCall(operator.address, parseEther("1000"), "0x")).wait()
const sponsorship = await deploySponsorship(sharedContracts, {}, [], [], undefined, undefined, testKickPolicy)

await (await operator.stake(sponsorship.address, parseEther("1000"))).wait()

const balanceBefore = await operator.balanceOf(operatorWallet.address)
const balanceInDataBefore = await operator.balanceInData(operatorWallet.address)
const delegationBefore = await operator.balanceOf(delegator.address)
const delegationInDataBefore = await operator.balanceInData(delegator.address)
await (await sponsorship.connect(admin).flag(operator.address, "")).wait() // TestKickPolicy slashes 10 ether without kicking
const balanceAfter = await operator.balanceOf(operatorWallet.address)
const balanceInDataAfter = await operator.balanceInData(operatorWallet.address)
const delegationAfter = await operator.balanceOf(delegator.address)
const delegationInDataAfter = await operator.balanceInData(delegator.address)

// operator's tokens are burned: loses 1 DATA
expect(balanceBefore).to.equal(parseEther("1"))
expect(balanceInDataBefore).to.equal(parseEther("1"))
expect(balanceAfter).to.equal(parseEther("0"))
expect(balanceInDataAfter).to.equal(parseEther("0"))

// delegator loses value worth the remaining 9 DATA (although token amount doesn't change)
expect(delegationBefore).to.equal(parseEther("1000"))
expect(delegationAfter).to.equal(parseEther("1000"))
expect(delegationInDataBefore).to.equal(parseEther("1000"))
expect(delegationInDataAfter).to.equal(parseEther("991"))
})

it("reduces operator value when it gets slashed without kicking (IOperator interface)", async function(): Promise<void> {
const { token } = sharedContracts
await setTokens(operatorWallet, "1000")
Expand Down

0 comments on commit 7af3930

Please sign in to comment.