Skip to content

Commit 76d8d10

Browse files
authored
fix(precompiles): return data for revert (#224)
* fix(vm/keeper): add return data of ApplyMessageWithConfig for ErrExecutionReverted * fix(precompile): modify return data of distribution precompile for revert error * test: remove redundant test case * chore(precompiles/staking): modify description for integration test case * chore: fix lint * fix(precompiles): modify return data of precompiles for revert error * fix: broken test cases after modifying precompile err to revert err * refactor:(precompiles) convert error that precompile.Run returns to ErrExecutionReverted * wip: test(precompiles/staking): fix test cases * chore: compile latest test contracts * fix(precompiles/staking): check revert reason or integration test cases * test(precompiles/staking): fix unit test * test(precompiles/distribution): improve integration test * test(precompiles/erc20): improve integration test * test(precompiles/ics20): improve integration test * test(precompiles/slashing): add slashing integration test for proof of audit issue fix * chore: fix lint * chore: fix lint
1 parent e6fe094 commit 76d8d10

File tree

39 files changed

+783
-325
lines changed

39 files changed

+783
-325
lines changed

contracts/solidity/precompiles/gov/IGov.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ enum VoteOption {
2121
Abstain,
2222
// No defines a no vote option.
2323
No,
24-
// NoWithWeto defines a no with veto vote option.
25-
NoWithWeto
24+
// NoWithVeto defines a no with veto vote option.
25+
NoWithVeto
2626
}
2727
/// @dev WeightedVote represents a vote on a governance proposal
2828
struct WeightedVote {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity >=0.8.17;
3+
4+
import "../ISlashing.sol" as slashing;
5+
6+
contract SlashingCaller {
7+
event TestResult(string message, bool success);
8+
9+
function testUnjail(address validatorAddr) public returns (bool success) {
10+
return slashing.SLASHING_CONTRACT.unjail(validatorAddr);
11+
}
12+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity >=0.8.17;
3+
4+
contract DelegationManager {
5+
6+
/// The delegation mapping is used to associate the EOA address that
7+
/// actually made the delegate request with its corresponding delegation information.
8+
mapping(address => mapping(string => uint256)) public delegations;
9+
10+
/// The unbonding queue is used to store the unbonding operations that are in progress.
11+
mapping(address => UnbondingDelegation[]) public unbondingDelegations;
12+
13+
/// The unbonding entry struct represents an unbonding operation that is in progress.
14+
/// It contains information about the validator and the amount of tokens that are being unbonded.
15+
struct UnbondingDelegation {
16+
/// @dev The validator address is the address of the validator that is being unbonded.
17+
string validator;
18+
/// @dev The amount of tokens that are being unbonded.
19+
uint256 amount;
20+
/// @dev The creation height is the height at which the unbonding operation was created.
21+
uint256 creationHeight;
22+
/// @dev The completion time is the time at which the unbonding operation will complete.
23+
int64 completionTime;
24+
}
25+
26+
function _increaseAmount(address _delegator, string memory _validator, uint256 _amount) internal {
27+
delegations[_delegator][_validator] += _amount;
28+
}
29+
30+
function _decreaseAmount(address _delegator, string memory _validator, uint256 _amount) internal {
31+
require(delegations[_delegator][_validator] >= _amount, "Insufficient delegation amount");
32+
delegations[_delegator][_validator] -= _amount;
33+
}
34+
35+
function _undelegate(string memory _validatorAddr, uint256 _amount, int64 completionTime) internal {
36+
unbondingDelegations[msg.sender].push(UnbondingDelegation({
37+
validator: _validatorAddr,
38+
amount: _amount,
39+
creationHeight: block.number,
40+
completionTime: completionTime
41+
}));
42+
}
43+
44+
/// @dev This function is used to dequeue unbonding entries that have expired.
45+
///
46+
/// @notice StakingCaller acts as the delegator and manages delegation/unbonding state per EoA.
47+
/// Reflecting x/staking unbondingDelegations changes in real-time would require event listening.
48+
/// To simplify unbonding entry processing, this function is called during delegate/undelegate calls.
49+
/// Although updating unbondingDelegations state isn't tested in the staking precompile integration tests,
50+
/// it is included for the completeness of the contract.
51+
function _dequeueUnbondingDelegation() internal {
52+
for (uint256 i = 0; i < unbondingDelegations[msg.sender].length; i++) {
53+
UnbondingDelegation storage entry = unbondingDelegations[msg.sender][i];
54+
if (uint256(int256(entry.completionTime)) <= block.timestamp) {
55+
delete unbondingDelegations[msg.sender][i];
56+
delegations[msg.sender][entry.validator] -= entry.amount;
57+
}
58+
}
59+
}
60+
61+
/// @dev This function is used to cancel unbonding entries that have been cancelled.
62+
/// @param _creationHeight The creation height of the unbonding entry to cancel.
63+
/// @param _amount The amount to cancel.
64+
function _cancelUnbonding(uint256 _creationHeight, uint256 _amount) internal {
65+
UnbondingDelegation[] storage entries = unbondingDelegations[msg.sender];
66+
67+
for (uint256 i = 0; i < entries.length; i++) {
68+
UnbondingDelegation storage entry = entries[i];
69+
70+
if (entry.creationHeight != _creationHeight) { continue; }
71+
72+
require(entry.amount >= _amount, "amount exceeds unbonding entry amount");
73+
entry.amount -= _amount;
74+
75+
// If the amount is now 0, remove the entry
76+
if (entry.amount == 0) { delete entries[i]; }
77+
78+
// Only cancel one entry per call
79+
break;
80+
}
81+
}
82+
83+
function _checkDelegation(string memory _validatorAddr, uint256 _delegateAmount) internal view {
84+
require(
85+
delegations[msg.sender][_validatorAddr] >= _delegateAmount,
86+
"Delegation does not exist or insufficient delegation amount"
87+
);
88+
}
89+
90+
function _checkUnbondingDelegation(address _delegatorAddr, string memory _validatorAddr) internal view {
91+
bool found;
92+
for (uint256 i = 0; i < unbondingDelegations[_delegatorAddr].length; i++) {
93+
UnbondingDelegation storage entry = unbondingDelegations[_delegatorAddr][i];
94+
if (
95+
_equalStrings(entry.validator, _validatorAddr) &&
96+
uint256(int256(entry.completionTime)) > block.timestamp
97+
) {
98+
found = true;
99+
break;
100+
}
101+
}
102+
require(found == true, "Unbonding delegation does not exist");
103+
}
104+
105+
function _equalStrings(string memory a, string memory b) internal pure returns (bool) {
106+
return keccak256(bytes(a)) == keccak256(bytes(b));
107+
}
108+
}

contracts/solidity/precompiles/staking/testdata/StakingCaller.sol

Lines changed: 21 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,17 @@
22
pragma solidity >=0.8.17;
33

44
import "../StakingI.sol" as staking;
5+
import "./DelegationManager.sol";
56

67
/// @title StakingCaller
78
/// @author Evmos Core Team
89
/// @dev This contract is used to test external contract calls to the staking precompile.
9-
contract StakingCaller {
10+
contract StakingCaller is DelegationManager{
1011
/// counter is used to test the state persistence bug, when EVM and Cosmos state were both
1112
/// changed in the same function.
1213
uint256 public counter;
1314
string[] private delegateMethod = [staking.MSG_DELEGATE];
1415

15-
/// The delegation mapping is used to associate the EOA address that
16-
/// actually made the delegate request with its corresponding delegation information.
17-
mapping(address => mapping(string => uint256)) public delegation;
18-
19-
/// The unbonding entry struct represents an unbonding operation that is in progress.
20-
/// It contains information about the validator and the amount of tokens that are being unbonded.
21-
struct UnbondingEntry {
22-
/// @dev The validator address is the address of the validator that is being unbonded.
23-
string validator;
24-
/// @dev The amount of tokens that are being unbonded.
25-
uint256 amount;
26-
/// @dev The creation height is the height at which the unbonding operation was created.
27-
uint256 creationHeight;
28-
/// @dev The completion time is the time at which the unbonding operation will complete.
29-
int64 completionTime;
30-
}
31-
32-
/// The unbonding queue is used to store the unbonding operations that are in progress.
33-
mapping(address => UnbondingEntry[]) public unbondingQueue;
34-
3516
/// @dev This function calls the staking precompile's create validator method
3617
/// using the msg.sender as the validator's operator address.
3718
/// @param _descr The initial description
@@ -91,15 +72,14 @@ contract StakingCaller {
9172
function testDelegate(
9273
string memory _validatorAddr
9374
) public payable {
94-
_dequeueUnbondingEntry();
95-
75+
_dequeueUnbondingDelegation();
9676
bool success = staking.STAKING_CONTRACT.delegate(
9777
address(this),
9878
_validatorAddr,
9979
msg.value
10080
);
10181
require(success, "delegate failed");
102-
delegation[msg.sender][_validatorAddr] += msg.value;
82+
_increaseAmount(msg.sender, _validatorAddr, msg.value);
10383
}
10484

10585
/// @dev This function calls the staking precompile's undelegate method.
@@ -109,20 +89,11 @@ contract StakingCaller {
10989
string memory _validatorAddr,
11090
uint256 _amount
11191
) public {
112-
_dequeueUnbondingEntry();
113-
114-
require(delegation[msg.sender][_validatorAddr] >= _amount, "Insufficient delegation");
115-
92+
_checkDelegation(_validatorAddr, _amount);
93+
_dequeueUnbondingDelegation();
11694
int64 completionTime = staking.STAKING_CONTRACT.undelegate(address(this), _validatorAddr, _amount);
11795
require(completionTime > 0, "Failed to undelegate");
118-
119-
uint256 creationHeight = block.number;
120-
unbondingQueue[msg.sender].push(UnbondingEntry({
121-
validator: _validatorAddr,
122-
amount: _amount,
123-
creationHeight: creationHeight,
124-
completionTime: completionTime
125-
}));
96+
_undelegate(_validatorAddr, _amount, completionTime);
12697
}
12798

12899
/// @dev This function calls the staking precompile's redelegate method.
@@ -133,16 +104,17 @@ contract StakingCaller {
133104
string memory _validatorSrcAddr,
134105
string memory _validatorDstAddr,
135106
uint256 _amount
136-
) public {
107+
) public {
108+
_checkDelegation(_validatorSrcAddr, _amount);
137109
int64 completionTime = staking.STAKING_CONTRACT.redelegate(
138110
address(this),
139111
_validatorSrcAddr,
140112
_validatorDstAddr,
141113
_amount
142114
);
143115
require(completionTime > 0, "Failed to redelegate");
144-
delegation[msg.sender][_validatorSrcAddr] -= _amount;
145-
delegation[msg.sender][_validatorDstAddr] += _amount;
116+
_decreaseAmount(msg.sender, _validatorSrcAddr, _amount);
117+
_increaseAmount(msg.sender, _validatorDstAddr, _amount);
146118
}
147119

148120
/// @dev This function calls the staking precompile's cancel unbonding delegation method.
@@ -154,16 +126,15 @@ contract StakingCaller {
154126
uint256 _amount,
155127
uint256 _creationHeight
156128
) public {
157-
_dequeueUnbondingEntry();
158-
129+
_dequeueUnbondingDelegation();
130+
_checkUnbondingDelegation(msg.sender, _validatorAddr);
159131
bool success = staking.STAKING_CONTRACT.cancelUnbondingDelegation(
160132
address(this),
161133
_validatorAddr,
162134
_amount,
163135
_creationHeight
164136
);
165137
require(success, "Failed to cancel unbonding");
166-
167138
_cancelUnbonding(_creationHeight, _amount);
168139
}
169140

@@ -280,8 +251,7 @@ contract StakingCaller {
280251
uint256 _amount,
281252
string memory _calltype
282253
) public {
283-
_dequeueUnbondingEntry();
284-
254+
_dequeueUnbondingDelegation();
285255
address calledContractAddress = staking.STAKING_PRECOMPILE_ADDRESS;
286256
bytes memory payload = abi.encodeWithSignature(
287257
"undelegate(address,string,uint256)",
@@ -331,14 +301,7 @@ contract StakingCaller {
331301
} else {
332302
revert("invalid calltype");
333303
}
334-
335-
uint256 creationHeight = block.number;
336-
unbondingQueue[msg.sender].push(UnbondingEntry({
337-
validator: _validatorAddr,
338-
amount: _amount,
339-
creationHeight: creationHeight,
340-
completionTime: completionTime
341-
}));
304+
_undelegate(_validatorAddr, _amount, completionTime);
342305
}
343306

344307
/// @dev This function is used to test the behaviour when executing queries using special function calling opcodes,
@@ -452,15 +415,14 @@ contract StakingCaller {
452415
function testDelegateIncrementCounter(
453416
string memory _validatorAddr
454417
) public payable {
455-
_dequeueUnbondingEntry();
456-
418+
_dequeueUnbondingDelegation();
457419
bool success = staking.STAKING_CONTRACT.delegate(
458420
address(this),
459421
_validatorAddr,
460422
msg.value
461423
);
462424
require(success, "delegate failed");
463-
delegation[msg.sender][_validatorAddr] += msg.value;
425+
_increaseAmount(msg.sender, _validatorAddr, msg.value);
464426
counter += 1;
465427
}
466428

@@ -469,15 +431,14 @@ contract StakingCaller {
469431
function testDelegateAndFailCustomLogic(
470432
string memory _validatorAddr
471433
) public payable {
472-
_dequeueUnbondingEntry();
473-
434+
_dequeueUnbondingDelegation();
474435
bool success = staking.STAKING_CONTRACT.delegate(
475436
address(this),
476437
_validatorAddr,
477438
msg.value
478439
);
479440
require(success, "delegate failed");
480-
delegation[msg.sender][_validatorAddr] += msg.value;
441+
_increaseAmount(msg.sender, _validatorAddr, msg.value);
481442

482443
// This should fail since the balance is already spent in the previous call
483444
payable(msg.sender).transfer(msg.value);
@@ -496,8 +457,7 @@ contract StakingCaller {
496457
string memory _validatorAddr,
497458
uint256 _amount
498459
) public payable {
499-
_dequeueUnbondingEntry();
500-
460+
_dequeueUnbondingDelegation();
501461
(bool success, ) = _contract.call(
502462
abi.encodeWithSignature(
503463
"transfer(address,uint256)",
@@ -506,53 +466,8 @@ contract StakingCaller {
506466
)
507467
);
508468
require(success, "transfer failed");
509-
510469
success = staking.STAKING_CONTRACT.delegate(address(this), _validatorAddr, msg.value);
511470
require(success, "delegate failed");
512-
delegation[msg.sender][_validatorAddr] += msg.value;
513-
}
514-
515-
/// @dev This function is used to dequeue unbonding entries that have expired.
516-
///
517-
/// @notice StakingCaller acts as the delegator and manages delegation/unbonding state per EoA.
518-
/// Reflecting x/staking unbondingQueue changes in real-time would require event listening.
519-
/// To simplify unbonding entry processing, this function is called during delegate/undelegate calls.
520-
/// Although updating unbondingQueue state isn't tested in the staking precompile integration tests,
521-
/// it is included for the completeness of the contract.
522-
function _dequeueUnbondingEntry() private {
523-
524-
for (uint256 i = 0; i < unbondingQueue[msg.sender].length; i++) {
525-
UnbondingEntry storage entry = unbondingQueue[msg.sender][i];
526-
if (uint256(int256(entry.completionTime)) <= block.timestamp) {
527-
delete unbondingQueue[msg.sender][i];
528-
delegation[msg.sender][entry.validator] -= entry.amount;
529-
}
530-
}
531-
}
532-
533-
/// @dev This function is used to cancel unbonding entries that have been cancelled.
534-
/// @param _creationHeight The creation height of the unbonding entry to cancel.
535-
/// @param _amount The amount to cancel.
536-
function _cancelUnbonding(uint256 _creationHeight, uint256 _amount) private {
537-
UnbondingEntry[] storage entries = unbondingQueue[msg.sender];
538-
539-
for (uint256 i = 0; i < entries.length; i++) {
540-
UnbondingEntry storage entry = entries[i];
541-
542-
if (entry.creationHeight != _creationHeight) {
543-
continue;
544-
}
545-
546-
require(entry.amount >= _amount, "amount exceeds unbonding entry amount");
547-
entry.amount -= _amount;
548-
549-
// If the amount is now 0, remove the entry
550-
if (entry.amount == 0) {
551-
delete entries[i];
552-
}
553-
554-
// Only cancel one entry per call
555-
break;
556-
}
471+
_increaseAmount(msg.sender, _validatorAddr, msg.value);
557472
}
558473
}

contracts/solidity/precompiles/testutil/contracts/StakingReverter.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ contract StakingReverter {
3131
}
3232
}
3333

34+
/// @dev callPrecompileBeforeAndAfterRevert tests whether precompile calls that occur
35+
/// before and after an intentionally ignored revert correctly modify the state.
36+
/// This method assumes that the StakingReverter.sol contract holds a native balance.
37+
/// Therefore, in order to call this method, the contract must be funded with a balance in advance.
3438
function callPrecompileBeforeAndAfterRevert(uint numTimes, string calldata validatorAddress) external {
3539
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
3640

0 commit comments

Comments
 (0)