Skip to content

Commit ab97e33

Browse files
authored
Merge pull request #14 from rsksmart/stRifOperations
DAO468 & DAO469: depositAndDelegate & transferAndDelegate functions with tests
2 parents 23dbdb0 + feb7305 commit ab97e33

File tree

4 files changed

+379
-0
lines changed

4 files changed

+379
-0
lines changed

contracts/StRIFToken.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,58 @@ contract StRIFToken is
3434
__UUPSUpgradeable_init();
3535
}
3636

37+
/**
38+
* @dev Allows token holder to transfer tokens to another account, after which
39+
* the recipient automatically delegates votes to themselves if they do
40+
* not already have a delegate.
41+
* Transfer and delegation happen within one transaction.
42+
* @param to The address of the recipient of the token transfer
43+
* @param value The amount of tokens being transferred
44+
*/
45+
function transferAndDelegate(address to, uint256 value) public virtual {
46+
transfer(to, value);
47+
_autoDelegate(to, value);
48+
}
49+
50+
/**
51+
* @dev Allows a token holder to transfer tokens from one account to another account,
52+
* after which the recipient automatically delegates votes to themselves if they do
53+
* not already have a delegate. This function is analogous to `transferAndDelegate` and
54+
* exists as a counterpart to the `transferFrom` function from the ERC-20 standard.
55+
*
56+
* @param from The address of the account to transfer tokens from
57+
* @param to The address of the recipient of the token transfer
58+
* @param value The amount of tokens being transferred
59+
*/
60+
function transferFromAndDelegate(address from, address to, uint256 value) public virtual {
61+
transferFrom(from, to, value);
62+
_autoDelegate(to, value);
63+
}
64+
65+
/**
66+
* @dev Allows to mint stRIFs from underlying RIF tokens (stake)
67+
* and delegate gained voting power to a provided address
68+
* @param to a target address for minting and delegation
69+
* @param value amount of RIF tokens to stake
70+
*/
71+
function depositAndDelegate(address to, uint256 value) public virtual {
72+
depositFor(to, value);
73+
_autoDelegate(to, value);
74+
}
75+
76+
/**
77+
* @dev Internal function to automatically delegate votes to the recipient
78+
* after a token transfer, if the recipient does not already have a delegate.
79+
* Delegation only occurs if the transfer amount is greater than zero.
80+
*
81+
* @param to The address of the recipient of the token transfer.
82+
* @param value The amount of tokens being transferred.
83+
*/
84+
function _autoDelegate(address to, uint256 value) internal virtual {
85+
if (value == 0 || delegates(to) != address(0)) return;
86+
_delegate(to, to);
87+
}
88+
3789
// The following functions are overrides required by Solidity.
3890

3991
//solhint-disable-next-line no-empty-blocks
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
2+
import { expect } from 'chai'
3+
import { ethers } from 'hardhat'
4+
import { RIFToken, StRIFToken } from '../typechain-types'
5+
import { ContractTransactionResponse, parseEther } from 'ethers'
6+
import { deployContracts } from './deployContracts'
7+
8+
describe('stRIF token: Function depositAndDelegate', () => {
9+
let deployer: SignerWithAddress
10+
let alice: SignerWithAddress
11+
let bob: SignerWithAddress
12+
let rif: RIFToken
13+
let stRif: StRIFToken
14+
let stRifAddress: string
15+
let mintDelegateTx: ContractTransactionResponse
16+
const votingPower = parseEther('100') // 100 RIF tokens
17+
18+
before(async () => {
19+
/*
20+
Deployment of RIF token and transferring a certain amount to Alice.
21+
See the relevant cases in StRIFToken tests
22+
*/
23+
;[deployer, alice, bob] = await ethers.getSigners()
24+
;({ rif, stRif, stRifAddress } = await deployContracts(deployer))
25+
await (await rif.transfer(alice.address, votingPower)).wait()
26+
})
27+
28+
describe('Approving RIFs', () => {
29+
it('Alice should own 100 RIF tokens', async () => {
30+
const rifBalance = await rif.balanceOf(alice.address)
31+
expect(rifBalance).to.equal(votingPower)
32+
})
33+
34+
it('Alice should set allowance for stRif contract to spend her RIFs', async () => {
35+
const tx = await rif.connect(alice).approve(stRifAddress, votingPower)
36+
await expect(tx).to.emit(rif, 'Approval').withArgs(alice, stRifAddress, votingPower)
37+
})
38+
})
39+
40+
describe('Before stRif minting / delegation', () => {
41+
it('approval should be set on RIF', async () => {
42+
expect(await rif.allowance(alice.address, stRifAddress)).to.equal(votingPower)
43+
})
44+
45+
it('Alice should NOT have any stRIF tokens on her balance', async () => {
46+
expect(await stRif.balanceOf(alice.address)).to.equal(0n)
47+
})
48+
49+
it('Alice should NOT have delegate set', async () => {
50+
expect(await stRif.delegates(alice.address)).to.equal(ethers.ZeroAddress)
51+
})
52+
53+
it('Alice should NOT have voting power', async () => {
54+
expect(await stRif.getVotes(alice.address)).to.equal(0n)
55+
})
56+
57+
it('Alice should NOT have checkpoints', async () => {
58+
expect(await stRif.numCheckpoints(alice.address)).to.equal(0n)
59+
})
60+
})
61+
62+
describe('Minting / delegation in one Tx', () => {
63+
describe('Sad path', () => {
64+
it('Bob should not be able to mint stRifs because he has no RIFs', async () => {
65+
const tx = stRif.connect(bob).depositAndDelegate(bob.address, votingPower)
66+
await expect(tx)
67+
// proxy error
68+
.to.be.revertedWithCustomError(stRif, 'FailedInnerCall')
69+
})
70+
71+
it('Alice should not mint more stRifs than her RIF balance', async () => {
72+
const tx = stRif.connect(alice).depositAndDelegate(alice.address, votingPower + 1n)
73+
// proxy error
74+
await expect(tx).to.be.revertedWithCustomError(stRif, 'FailedInnerCall')
75+
})
76+
77+
it('Alice should not mint zero stRifs', async () => {
78+
const tx = stRif.connect(alice).depositAndDelegate(alice.address, 0)
79+
await expect(() => tx).to.changeTokenBalance(stRif, alice, 0)
80+
})
81+
82+
it('Alice should not mint to zero address', async () => {
83+
const tx = stRif.connect(alice).depositAndDelegate(ethers.ZeroAddress, votingPower)
84+
await expect(tx)
85+
.to.be.revertedWithCustomError(stRif, 'ERC20InvalidReceiver')
86+
.withArgs(ethers.ZeroAddress)
87+
})
88+
89+
it('Alice should not mint to stRif contract address', async () => {
90+
const tx = stRif.connect(alice).depositAndDelegate(stRifAddress, votingPower)
91+
await expect(tx).to.be.revertedWithCustomError(stRif, 'ERC20InvalidReceiver').withArgs(stRifAddress)
92+
})
93+
})
94+
95+
describe('Happy path', () => {
96+
before(async () => {
97+
mintDelegateTx = await stRif.connect(alice).depositAndDelegate(alice.address, votingPower)
98+
})
99+
100+
it('Alice should stake her RIFs and mint stRIFs', async () => {
101+
await expect(mintDelegateTx)
102+
.to.emit(stRif, 'Transfer')
103+
.withArgs(ethers.ZeroAddress, alice.address, votingPower)
104+
})
105+
106+
it('Alice should delegate voting power in the SAME transaction', async () => {
107+
await expect(mintDelegateTx)
108+
.to.emit(stRif, 'DelegateChanged')
109+
.withArgs(alice.address, ethers.ZeroAddress, alice.address)
110+
})
111+
})
112+
})
113+
114+
describe('After minting / delegation', () => {
115+
it('approval(allowance) for stRif should NO LONGER be set on RIF', async () => {
116+
expect(await rif.allowance(alice.address, stRifAddress)).to.equal(0n)
117+
})
118+
119+
it('Alice should have newly minted stRIF tokens on her balance', async () => {
120+
expect(await stRif.balanceOf(alice.address)).to.equal(votingPower)
121+
})
122+
123+
it('Alice should now be the delegate of herself', async () => {
124+
expect(await stRif.delegates(alice.address)).to.equal(alice.address)
125+
})
126+
127+
it('Alice should have voting power', async () => {
128+
expect(await stRif.getVotes(alice.address)).to.equal(votingPower)
129+
})
130+
131+
it('Alice should have 1 checkpoint', async () => {
132+
expect(await stRif.numCheckpoints(alice.address)).to.equal(1n)
133+
})
134+
135+
it('block number and voting power should be recorded (snapshot) at the checkpoint', async () => {
136+
const checkpointIndex = 0n
137+
const [blockNumAtCheckpoint, votePowerAtCheckpoint] = await stRif.checkpoints(
138+
alice.address,
139+
checkpointIndex,
140+
)
141+
expect(blockNumAtCheckpoint).to.equal(mintDelegateTx.blockNumber)
142+
expect(votePowerAtCheckpoint).to.equal(votingPower)
143+
})
144+
})
145+
})
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
2+
import { expect } from 'chai'
3+
import { ethers } from 'hardhat'
4+
import { RIFToken, StRIFToken } from '../typechain-types'
5+
import { ContractTransactionResponse, parseEther } from 'ethers'
6+
import { deployContracts } from './deployContracts'
7+
8+
describe('stRIF token: Function transferAndDelegate', () => {
9+
let deployer: SignerWithAddress
10+
let alice: SignerWithAddress
11+
let bob: SignerWithAddress
12+
let john: SignerWithAddress
13+
let rif: RIFToken
14+
let stRif: StRIFToken
15+
let transferAndDelegateTx: ContractTransactionResponse
16+
const votingPower = parseEther('100') // RIF tokens
17+
18+
const enfranchiseUser = async (user: SignerWithAddress, amount: bigint) => {
19+
await (await rif.transfer(user.address, amount)).wait()
20+
await (await rif.connect(user).approve(await stRif.getAddress(), amount)).wait()
21+
await (await stRif.connect(user).depositAndDelegate(user.address, amount)).wait()
22+
}
23+
24+
before(async () => {
25+
;[deployer, alice, bob, john] = await ethers.getSigners()
26+
;({ rif, stRif } = await deployContracts(deployer))
27+
await enfranchiseUser(alice, votingPower)
28+
await enfranchiseUser(john, votingPower)
29+
})
30+
31+
describe('Before transfer', () => {
32+
it('Alice should own 100 stRIFs tokens', async () => {
33+
expect(await stRif.balanceOf(alice.address)).to.equal(votingPower)
34+
})
35+
it('Bob should not have stRIFs tokens', async () => {
36+
expect(await stRif.balanceOf(alice.address)).to.equal(votingPower)
37+
})
38+
it('Alice should be her own delegate', async () => {
39+
expect(await stRif.delegates(alice.address)).to.equal(alice.address)
40+
})
41+
it('Bob shouldn`t have delegates', async () => {
42+
expect(await stRif.delegates(bob.address)).to.equal(ethers.ZeroAddress)
43+
})
44+
it('Alice should have voting power', async () => {
45+
expect(await stRif.getVotes(alice.address)).to.equal(votingPower)
46+
})
47+
it('Bob shouldn`t have voting power', async () => {
48+
expect(await stRif.getVotes(bob.address)).to.equal(0n)
49+
})
50+
})
51+
52+
describe('Transfer and delegate', () => {
53+
describe('Sad path', () => {
54+
it('should not change balances after transfer of zero tokens', async () => {
55+
const tx = await stRif.connect(alice).transferAndDelegate(bob.address, 0n)
56+
await expect(() => tx).to.changeTokenBalances(stRif, [alice, bob], [0n, 0n])
57+
})
58+
it('Bob should`t be delegated to vote after transfer of 0 tokens', async () => {
59+
expect(await stRif.delegates(bob.address)).to.equal(ethers.ZeroAddress)
60+
})
61+
it('Alice shouldn`t be able to transfer and delegate to zero address', async () => {
62+
const tx = stRif.connect(alice).transferAndDelegate(ethers.ZeroAddress, votingPower)
63+
await expect(tx)
64+
.to.be.revertedWithCustomError(stRif, 'ERC20InvalidReceiver')
65+
.withArgs(ethers.ZeroAddress)
66+
})
67+
})
68+
describe('Happy path', () => {
69+
before(async () => {
70+
transferAndDelegateTx = await stRif.connect(alice).transferAndDelegate(bob.address, parseEther('25'))
71+
})
72+
it('Alice should use `transferAndDelegate` function to transfer 1/4 tokens to Bob', async () => {
73+
await expect(transferAndDelegateTx)
74+
.to.emit(stRif, 'Transfer')
75+
.withArgs(alice.address, bob.address, parseEther('25'))
76+
})
77+
it('Voting power should be delegated to Bob within the SAME transaction', async () => {
78+
await expect(transferAndDelegateTx)
79+
.to.emit(stRif, 'DelegateChanged')
80+
.withArgs(bob.address, ethers.ZeroAddress, bob.address)
81+
})
82+
})
83+
})
84+
85+
describe('After transfer', () => {
86+
it('Alice should own 3/4 tokens', async () => {
87+
expect(await stRif.balanceOf(alice.address)).to.equal(parseEther('75'))
88+
})
89+
it('Bob should own 1/4 tokens', async () => {
90+
expect(await stRif.balanceOf(bob.address)).to.equal(parseEther('25'))
91+
})
92+
it('Alice should STILL be her own delegate', async () => {
93+
expect(await stRif.delegates(alice.address)).to.equal(alice.address)
94+
})
95+
it('Bob should be his own delegate (the delegation was from Bob to Bob)', async () => {
96+
expect(await stRif.delegates(bob.address)).to.equal(bob.address)
97+
})
98+
it('Alice`s voting power should equal her balance (3/4)', async () => {
99+
expect(await stRif.getVotes(alice.address)).to.equal(parseEther('75'))
100+
})
101+
it('Bob`s voting power should equal his balance (1/4)', async () => {
102+
expect(await stRif.getVotes(bob.address)).to.equal(parseEther('25'))
103+
})
104+
})
105+
106+
describe('Receiving tokens from a third source after the delegation', () => {
107+
let johnsTransferTx: ContractTransactionResponse
108+
109+
it('John should own 100 stRIFs tokens', async () => {
110+
expect(await stRif.balanceOf(john.address)).to.equal(votingPower)
111+
})
112+
it('John should transfer his tokens to Bob', async () => {
113+
johnsTransferTx = await stRif.connect(john).transferAndDelegate(bob.address, votingPower)
114+
await expect(() => johnsTransferTx).to.changeTokenBalances(
115+
stRif,
116+
[john, bob],
117+
[-votingPower, votingPower],
118+
)
119+
})
120+
it('John`s transfer tx should NOT initiate another delegation', async () => {
121+
await expect(johnsTransferTx).not.to.emit(stRif, 'DelegateChanged')
122+
})
123+
it('Bob should remain his own delegate', async () => {
124+
expect(await stRif.delegates(bob.address)).to.equal(bob.address)
125+
})
126+
it('Bob`s voting power should include Alice`s and John`s tokens', async () => {
127+
expect(await stRif.getVotes(bob.address)).to.equal(parseEther('25') + votingPower)
128+
})
129+
})
130+
131+
describe('transferFromAndDelegate: a variation with approval', () => {
132+
const amount = parseEther('25')
133+
let transferTx: ContractTransactionResponse
134+
135+
it('should reset Bob`s delegate back to zero address', async () => {
136+
const tx = await stRif.connect(bob).delegate(ethers.ZeroAddress)
137+
await expect(tx)
138+
.to.emit(stRif, 'DelegateChanged')
139+
.withArgs(bob.address, bob.address, ethers.ZeroAddress)
140+
})
141+
it('Bob should no longer have delegate', async () => {
142+
expect(await stRif.delegates(bob.address)).to.equal(ethers.ZeroAddress)
143+
})
144+
it('Alice should approve Bob to transfer 25 stRIFs', async () => {
145+
const tx = await stRif.connect(alice).approve(bob.address, amount)
146+
await expect(tx).to.emit(stRif, 'Approval').withArgs(alice.address, bob.address, amount)
147+
})
148+
it('Bob now has approval to transfer stRIFs from Alice`s balance', async () => {
149+
expect(await stRif.allowance(alice.address, bob.address)).to.equal(amount)
150+
})
151+
it('Bob should transfer Alice`s stRIFs to himself ', async () => {
152+
transferTx = await stRif.connect(bob).transferFromAndDelegate(alice.address, bob.address, amount)
153+
await expect(() => transferTx).to.changeTokenBalances(stRif, [bob, alice], [amount, -amount])
154+
})
155+
it('Bob`s tx should emit Transfer event', async () => {
156+
await expect(transferTx).to.emit(stRif, 'Transfer').withArgs(alice.address, bob.address, amount)
157+
})
158+
it('Bob should become his own delegate within the same tx', async () => {
159+
await expect(transferTx)
160+
.to.emit(stRif, 'DelegateChanged')
161+
.withArgs(bob.address, ethers.ZeroAddress, bob.address)
162+
})
163+
it('Bob should be his own delegate', async () => {
164+
expect(await stRif.delegates(bob.address)).to.equal(bob.address)
165+
})
166+
})
167+
})

test/deployContracts.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
2+
import { ethers } from 'hardhat'
3+
import { deployStRIF } from '../scripts/deploy-stRIF'
4+
5+
export const deployContracts = async (admin: SignerWithAddress) => {
6+
const rif = await (await ethers.deployContract('RIFToken', { signer: admin })).waitForDeployment()
7+
const rifAddress = await rif.getAddress()
8+
await (await rif.setAuthorizedManagerContract(admin.address)).wait()
9+
const latestBlock = await ethers.provider.getBlock('latest')
10+
if (!latestBlock) throw new Error('Latest block not found')
11+
await (await rif.closeTokenDistribution(latestBlock.timestamp)).wait()
12+
const stRif = await deployStRIF(rifAddress, admin.address)
13+
const stRifAddress = await stRif.getAddress()
14+
return { rif, rifAddress, stRif, stRifAddress }
15+
}

0 commit comments

Comments
 (0)